Skip to content

Commit fa85b0b

Browse files
committed
Improve errors when trying to find a writable property
1 parent eab7611 commit fa85b0b

File tree

6 files changed

+200
-31
lines changed

6 files changed

+200
-31
lines changed

src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -635,14 +635,24 @@ private function getWriteAccessInfo(string $class, string $property, $value): ar
635635
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
636636
$camelized = $this->camelize($property);
637637
$singulars = (array) Inflector::singularize($camelized);
638+
$errors = [];
638639

639640
if ($useAdderAndRemover) {
640-
$methods = $this->findAdderAndRemover($reflClass, $singulars);
641+
foreach ($this->findAdderAndRemover($reflClass, $singulars) as $methods) {
642+
if (3 === \count($methods)) {
643+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
644+
$access[self::ACCESS_ADDER] = $methods[self::ACCESS_ADDER];
645+
$access[self::ACCESS_REMOVER] = $methods[self::ACCESS_REMOVER];
646+
break;
647+
}
648+
649+
if (isset($methods[self::ACCESS_ADDER])) {
650+
$errors[] = sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found', $methods['methods'][self::ACCESS_ADDER], $reflClass->name,$methods['methods'][self::ACCESS_REMOVER]);
651+
}
641652

642-
if (null !== $methods) {
643-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
644-
$access[self::ACCESS_ADDER] = $methods[0];
645-
$access[self::ACCESS_REMOVER] = $methods[1];
653+
if (isset($methods[self::ACCESS_REMOVER])) {
654+
$errors[] = sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found', $methods['methods'][self::ACCESS_REMOVER], $reflClass->name, $methods['methods'][self::ACCESS_ADDER]);
655+
}
646656
}
647657
}
648658

@@ -666,30 +676,69 @@ private function getWriteAccessInfo(string $class, string $property, $value): ar
666676
// we call the getter and hope the __call do the job
667677
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
668678
$access[self::ACCESS_NAME] = $setter;
669-
} elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) {
670-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
671-
$access[self::ACCESS_NAME] = sprintf(
672-
'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
673-
'the new value must be an array or an instance of \Traversable, '.
674-
'"%s" given.',
675-
$property,
676-
$reflClass->name,
677-
implode('()", "', $methods),
678-
\is_object($value) ? \get_class($value) : \gettype($value)
679-
);
680679
} else {
681-
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
682-
$access[self::ACCESS_NAME] = sprintf(
683-
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
684-
'"__set()" or "__call()" exist and have public access in class "%s".',
685-
$property,
686-
implode('', array_map(function ($singular) {
687-
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
688-
}, $singulars)),
689-
$setter,
690-
$getsetter,
691-
$reflClass->name
692-
);
680+
foreach ($this->findAdderAndRemover($reflClass, $singulars) as $methods) {
681+
if (3 === \count($methods)) {
682+
$errors[] = sprintf(
683+
'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
684+
'the new value must be an array or an instance of \Traversable, '.
685+
'"%s" given.',
686+
$property,
687+
$reflClass->name,
688+
implode('()", "', [$methods[self::ACCESS_ADDER], $methods[self::ACCESS_REMOVER]]),
689+
\is_object($value) ? \get_class($value) : \gettype($value)
690+
);
691+
}
692+
}
693+
694+
if (!isset($access[self::ACCESS_NAME])) {
695+
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
696+
697+
$triedMethods = [
698+
$setter => 1,
699+
$getsetter => 1,
700+
'__set' => 2,
701+
'__call' => 2,
702+
];
703+
704+
foreach ($singulars as $singular) {
705+
$triedMethods['add'.$singular] = 1;
706+
$triedMethods['remove'.$singular] = 1;
707+
}
708+
709+
foreach ($triedMethods as $methodName => $parameters) {
710+
if (!$reflClass->hasMethod($methodName)) {
711+
continue;
712+
}
713+
714+
$method = $reflClass->getMethod($methodName);
715+
716+
if (!$method->isPublic()) {
717+
$errors[] = sprintf('The method "%s" in class "%s" was found but do not have public access', $methodName, $reflClass->name);
718+
continue;
719+
}
720+
721+
if ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) {
722+
$errors[] = sprintf('The method "%s" in class "%s" requires at least "%d" parameters, "%d" found', $methodName, $reflClass->name, $parameters, $method->getNumberOfRequiredParameters());
723+
}
724+
}
725+
726+
if (\count($errors)) {
727+
$access[self::ACCESS_NAME] = implode('. ', $errors).'.';
728+
} else {
729+
$access[self::ACCESS_NAME] = sprintf(
730+
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
731+
'"__set()" or "__call()" exist and have public access in class "%s".',
732+
$property,
733+
implode('', array_map(function ($singular) {
734+
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
735+
}, $singulars)),
736+
$setter,
737+
$getsetter,
738+
$reflClass->name
739+
);
740+
}
741+
}
693742
}
694743
}
695744

@@ -753,13 +802,21 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula
753802
foreach ($singulars as $singular) {
754803
$addMethod = 'add'.$singular;
755804
$removeMethod = 'remove'.$singular;
805+
$result = ['methods' => [self::ACCESS_ADDER => $addMethod, self::ACCESS_REMOVER => $removeMethod]];
756806

757807
$addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1);
808+
809+
if ($addMethodFound) {
810+
$result[self::ACCESS_ADDER] = $addMethod;
811+
}
812+
758813
$removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1);
759814

760-
if ($addMethodFound && $removeMethodFound) {
761-
return [$addMethod, $removeMethod];
815+
if ($removeMethodFound) {
816+
$result[self::ACCESS_REMOVER] = $removeMethod;
762817
}
818+
819+
yield $result;
763820
}
764821
}
765822

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
13+
14+
class TestAdderRemoverInvalidArgumentLength
15+
{
16+
public function addFoo()
17+
{
18+
19+
}
20+
21+
public function removeFoo($var1, $var2)
22+
{
23+
24+
}
25+
26+
public function setBar($var1, $var2)
27+
{
28+
29+
}
30+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
13+
14+
class TestAdderRemoverInvalidMethods
15+
{
16+
public function addFoo($foo)
17+
{
18+
19+
}
20+
21+
public function removeBar($foo)
22+
{
23+
24+
}
25+
}

src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassSetValue.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ public function __construct($value)
2929
{
3030
$this->value = $value;
3131
}
32+
33+
private function setFoo()
34+
{
35+
36+
}
3237
}

src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists()
189189

190190
/**
191191
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
192-
* expectedExceptionMessageRegExp /The property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis()", "removeAxis()" but the new value must be an array or an instance of \Traversable, "string" given./
192+
* @expectedExceptionMessageRegExp /Could not determine access type for property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*": The property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis\(\)", "removeAxis\(\)" but the new value must be an array or an instance of \\Traversable, "string" given./
193193
*/
194194
public function testSetValueFailsIfAdderAndRemoverExistButValueIsNotTraversable()
195195
{

src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Symfony\Component\PropertyAccess\PropertyAccess;
1818
use Symfony\Component\PropertyAccess\PropertyAccessor;
1919
use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped;
20+
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidArgumentLength;
21+
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidMethods;
2022
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
2123
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable;
2224
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall;
@@ -762,4 +764,54 @@ public function testAdderAndRemoverArePreferredOverSetterForSameSingularAndPlura
762764

763765
$this->assertEquals(['aeroplane'], $object->getAircraft());
764766
}
767+
768+
/**
769+
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
770+
* @expectedExceptionMessageRegExp /.*The add method "addFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidMethods" was found, but the corresponding remove method "removeFoo" was not found\./
771+
*/
772+
public function testAdderWithoutRemover()
773+
{
774+
$object = new TestAdderRemoverInvalidMethods();
775+
$this->propertyAccessor->setValue($object, 'foos', [1, 2]);
776+
}
777+
778+
/**
779+
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
780+
* @expectedExceptionMessageRegExp /.*The remove method "removeBar" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidMethods" was found, but the corresponding add method "addBar" was not found\./
781+
*/
782+
public function testRemoverWithoutAdder()
783+
{
784+
$object = new TestAdderRemoverInvalidMethods();
785+
$this->propertyAccessor->setValue($object, 'bars', [1, 2]);
786+
}
787+
788+
/**
789+
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
790+
* @expectedExceptionMessageRegExp /.*The method "addFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidArgumentLength" requires at least "1" parameters, "0" found. The method "removeFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidArgumentLength" requires at least "1" parameters, "2" found\./
791+
*/
792+
public function testAdderAndRemoveNeedsTheExactParametersDefined()
793+
{
794+
$object = new TestAdderRemoverInvalidArgumentLength();
795+
$this->propertyAccessor->setValue($object, 'foo', [1, 2]);
796+
}
797+
798+
/**
799+
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
800+
* @expectedExceptionMessageRegExp /.*The method "setBar" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidArgumentLength" requires at least "1" parameters, "2" found./
801+
*/
802+
public function testSetterNeedsTheExactParametersDefined()
803+
{
804+
$object = new TestAdderRemoverInvalidArgumentLength();
805+
$this->propertyAccessor->setValue($object, 'bar', [1, 2]);
806+
}
807+
808+
/**
809+
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
810+
* @expectedExceptionMessageRegExp /.*The method "setFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestClassSetValue" was found but do not have public access./
811+
*/
812+
public function testSetterNeedsPublicAccess()
813+
{
814+
$object = new TestClassSetValue(0);
815+
$this->propertyAccessor->setValue($object, 'foo', 1);
816+
}
765817
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy