diff --git a/src/Type/AbstractType.php b/src/Type/AbstractType.php index 5f3620d..e8e5102 100644 --- a/src/Type/AbstractType.php +++ b/src/Type/AbstractType.php @@ -7,8 +7,10 @@ use Palmtree\ArgParser\ArgParser; use Palmtree\Form\Constraint\ConstraintInterface; use Palmtree\Form\Constraint\NotBlank; +use Palmtree\Form\Exception\InvalidTypeException; use Palmtree\Form\Exception\OutOfBoundsException; use Palmtree\Form\Form; +use Palmtree\Form\TypeLocator; use Palmtree\Html\Element; use Palmtree\NameConverter\SnakeCaseToCamelCaseNameConverter; use Palmtree\NameConverter\SnakeCaseToHumanNameConverter; @@ -45,6 +47,8 @@ abstract class AbstractType implements TypeInterface protected $constraints = []; /** @var SnakeCaseToHumanNameConverter */ protected $nameConverter; + /** @var TypeLocator */ + protected $typeLocator; /** @var bool */ protected $mapped = true; /** @var array */ @@ -56,6 +60,7 @@ abstract class AbstractType implements TypeInterface public function __construct(array $args = []) { $this->nameConverter = new SnakeCaseToHumanNameConverter(); + $this->typeLocator = new TypeLocator(); $this->args = $this->parseArgs($args); @@ -384,8 +389,13 @@ public function addChild(TypeInterface $child): TypeInterface public function add(string $name, string $class, array $options = []): TypeInterface { + if (!$class = $this->typeLocator->getTypeClass($class)) { + throw new InvalidTypeException('Type could not be found'); + } + /** @var TypeInterface $type */ $type = new $class($options); + $type->setName($name); if ($type->getLabel() === null) { diff --git a/src/Type/CollectionType.php b/src/Type/CollectionType.php index dec91f6..2ef5cd2 100644 --- a/src/Type/CollectionType.php +++ b/src/Type/CollectionType.php @@ -4,6 +4,7 @@ namespace Palmtree\Form\Type; +use Palmtree\Form\Exception\InvalidTypeException; use Palmtree\Html\Element; class CollectionType extends AbstractType @@ -61,11 +62,15 @@ public function addChild(TypeInterface $child): TypeInterface } /** - * @psalm-param class-string $entryType + * @psalm-param class-string|string $entryType */ public function setEntryType(string $entryType): void { - $this->entryType = $entryType; + if (!$class = $this->typeLocator->getTypeClass($entryType)) { + throw new InvalidTypeException('Type could not be found'); + } + + $this->entryType = $class; } /** @@ -141,6 +146,14 @@ private function buildEntry(int $position = 0, $data = null): TypeInterface if (\func_num_args() > 0) { $entry->setData($data); + + if (\is_array($data)) { + foreach ($entry->all() as $child) { + if (isset($data[$child->getName()])) { + $child->setData($data[$child->getName()]); + } + } + } } return $entry; diff --git a/src/TypeLocator.php b/src/TypeLocator.php index 4955614..087d3f2 100644 --- a/src/TypeLocator.php +++ b/src/TypeLocator.php @@ -9,7 +9,7 @@ class TypeLocator { - /** @var array Map of types where key is the shorthand name e.g 'text' and value is the FCQN. */ + /** @var array> Map of types where key is the shorthand name e.g 'text' and value is the FCQN. */ private static $types = []; private const TYPE_KEYS = [ @@ -21,11 +21,18 @@ public function __construct() { if (empty(self::$types)) { foreach (glob(__DIR__ . '/Type/*Type.php', \GLOB_NOSORT) ?: [] as $file) { - self::$types[strtolower(basename($file, 'Type.php'))] = __NAMESPACE__ . '\\Type\\' . basename($file, '.php'); + /** @var class-string $class */ + $class = __NAMESPACE__ . '\\Type\\' . basename($file, '.php'); + self::$types[strtolower(basename($file, 'Type.php'))] = $class; } } } + /** + * @param string|class-string $type + * + * @return class-string|null + */ public function getTypeClass(string $type): ?string { if (isset(self::$types[$type])) { @@ -33,6 +40,7 @@ public function getTypeClass(string $type): ?string } if (class_exists($type)) { + /** @var class-string */ return $type; } diff --git a/tests/DataBindingTest.php b/tests/DataBindingTest.php index 8daf536..3763882 100644 --- a/tests/DataBindingTest.php +++ b/tests/DataBindingTest.php @@ -8,6 +8,8 @@ use Palmtree\Form\Form; use Palmtree\Form\FormBuilder; use Palmtree\Form\Test\Fixtures\Person; +use Palmtree\Form\Test\Fixtures\PersonType; +use Palmtree\Form\Type\CollectionType; use PHPUnit\Framework\TestCase; class DataBindingTest extends TestCase @@ -168,6 +170,83 @@ public function testArrayAccessDataMapperThrowsOutOfBoundsException(): void ]); } + public function testCollectionOneWayDataBinding(): void + { + $data = [ + 'people' => [ + [ + 'name' => 'John Smith', + 'emailAddress' => 'john.smith@example.org', + 'age' => 42, + 'favouriteConsole' => 'PlayStation', + 'interests' => ['football', 'gaming'], + 'signup' => false, + 'pets' => [], + ], + ], + ]; + + $builder = new FormBuilder('test', $data); + + $builder->add('people', CollectionType::class, [ + 'entry_type' => PersonType::class, + ]); + + $builder->add('send_message', 'submit'); + + $form = $builder->getForm(); + $form->render(); + + $formPeople = $form->get('people')->getData(); + + self::assertSame($data['people'], $formPeople); + } + + public function testCollectionTwoWayDataBinding(): void + { + $data = new \ArrayObject([ + 'people' => [ + [ + 'name' => 'John Smith', + 'emailAddress' => 'john.smith@example.org', + 'age' => 42, + 'favouriteConsole' => 'PlayStation', + 'interests' => ['football', 'gaming'], + 'signup' => false, + 'pets' => [], + ], + ], + ]); + + $builder = new FormBuilder('test', $data); + + $builder->add('people', CollectionType::class, [ + 'entry_type' => PersonType::class, + ]); + + $builder->add('send_message', 'submit'); + + $form = $builder->getForm(); + + $form->submit([ + 'people' => [ + [ + 'name' => 'Bob Smith', + 'emailAddress' => 'bob.smith@example.org', + 'age' => 45, + 'favouriteConsole' => 'Xbox', + 'interests' => ['guitar'], + ], + ], + ]); + + self::assertSame($data['people'][0]['name'], 'Bob Smith'); + self::assertSame($data['people'][0]['emailAddress'], 'bob.smith@example.org'); + self::assertSame($data['people'][0]['age'], 45); + self::assertSame($data['people'][0]['favouriteConsole'], 'Xbox'); + self::assertSame($data['people'][0]['interests'], ['guitar']); + } + /** * @param Person|array|\ArrayAccess|\stdClass $person */ @@ -201,7 +280,8 @@ private function buildForm($person): Form ]) ->add('pets', 'collection', [ 'entry_type' => 'text', - ]); + ]) + ; return $builder->getForm(); }