Skip to content

Commit 5677db1

Browse files
weaverryankbond
andcommitted
Initial commit of symfony/ux-twig-component
Co-authored-by: Kevin Bond <[email protected]>
1 parent ddd7953 commit 5677db1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2811
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\LiveComponent\Attribute;
13+
14+
/**
15+
* Call a method before re-rendering.
16+
*
17+
* This hook ONLY happens when rendering via HTTP: it does
18+
* not happen during the initial render of a component.
19+
*
20+
* @Annotation
21+
* @Target("METHOD")
22+
*
23+
* @experimental
24+
*/
25+
final class BeforeReRender
26+
{
27+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\LiveComponent\Attribute;
13+
14+
/**
15+
* @Annotation
16+
* @Target("METHOD")
17+
*
18+
* @experimental
19+
*/
20+
final class LiveAction
21+
{
22+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\LiveComponent\Attribute;
13+
14+
use Symfony\UX\LiveComponent\LiveComponentInterface;
15+
16+
/**
17+
* @Annotation
18+
* @Target("PROPERTY")
19+
*
20+
* @experimental
21+
*/
22+
final class LiveProp
23+
{
24+
/** @var bool */
25+
private $writable = false;
26+
27+
/** @var string[] */
28+
private $exposed = [];
29+
30+
/** @var string|null */
31+
private $hydrateWith = null;
32+
33+
/** @var string|null */
34+
private $dehydrateWith = null;
35+
36+
/**
37+
*The "frontend" field name that should be used for this property.
38+
*
39+
* This can be used, for example, to have a property called "foo", which actually
40+
* maps to a frontend data model called "bar".
41+
*
42+
* If you pass a string that ends in () - like "getFieldName()" - that
43+
* method on the component will be called to determine this.
44+
*
45+
* @var string|null
46+
*/
47+
private $fieldName = null;
48+
49+
public function __construct(array $values)
50+
{
51+
$validOptions = ['writable', 'exposed', 'hydrateWith', 'dehydrateWith', 'fieldName'];
52+
53+
foreach ($values as $name => $value) {
54+
if (!in_array($name, $validOptions)) {
55+
throw new \InvalidArgumentException(sprintf('Unknown option "%s" passed to LiveProp. Valid options are: %s.', $name, implode(', ', $validOptions)));
56+
}
57+
58+
$this->$name = $value;
59+
}
60+
}
61+
62+
public function isReadonly(): bool
63+
{
64+
return !$this->writable;
65+
}
66+
67+
public function exposed(): array
68+
{
69+
return $this->exposed;
70+
}
71+
72+
public function hydrateMethod(): ?string
73+
{
74+
return $this->hydrateWith ? trim($this->hydrateWith, '()') : null;
75+
}
76+
77+
public function dehydrateMethod(): ?string
78+
{
79+
return $this->dehydrateWith ? trim($this->dehydrateWith, '()') : null;
80+
}
81+
82+
public function calculateFieldName(LiveComponentInterface $component, string $fallback): string
83+
{
84+
if (!$this->fieldName) {
85+
return $fallback;
86+
}
87+
88+
if (str_ends_with($this->fieldName, '()')) {
89+
return $component->{trim($this->fieldName, '()')}();
90+
}
91+
92+
return $this->fieldName;
93+
}
94+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\LiveComponent\Attribute;
13+
14+
/**
15+
* @Annotation
16+
* @Target("METHOD")
17+
*
18+
* @experimental
19+
*/
20+
final class PostHydrate
21+
{
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\LiveComponent\Attribute;
13+
14+
/**
15+
* @Annotation
16+
* @Target("METHOD")
17+
*
18+
* @experimental
19+
*/
20+
final class PreDehydrate
21+
{
22+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\LiveComponent;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
16+
use Symfony\Component\Validator\ConstraintViolation;
17+
use Symfony\Component\Validator\Validator\ValidatorInterface;
18+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
19+
20+
/**
21+
* @author Ryan Weaver <[email protected]>
22+
*
23+
* @experimental
24+
* @internal
25+
*/
26+
class ComponentValidator implements ComponentValidatorInterface, ServiceSubscriberInterface
27+
{
28+
/** @var ContainerInterface */
29+
private $container;
30+
31+
public function __construct(ContainerInterface $container)
32+
{
33+
$this->container = $container;
34+
}
35+
36+
/**
37+
* @return ConstraintViolation[][]
38+
*/
39+
public function validate(object $component): array
40+
{
41+
$errors = $this->getValidator()->validate($component);
42+
43+
$validationErrors = [];
44+
foreach ($errors as $error) {
45+
/** @var ConstraintViolation $error */
46+
$property = $error->getPropertyPath();
47+
if (!isset($validationErrors[$property])) {
48+
$validationErrors[$property] = [];
49+
}
50+
51+
$validationErrors[$property][] = $error;
52+
}
53+
54+
return $validationErrors;
55+
}
56+
57+
/**
58+
* Validates a single field.
59+
*
60+
* If a property path - like post.title - is passed, this will
61+
* validate the *entire* "post" property. It will then loop
62+
* over all the errors and collect only those for "post.title".
63+
*
64+
* @return ConstraintViolation[]
65+
*/
66+
public function validateField(object $component, string $propertyPath): array
67+
{
68+
$propertyParts = explode('.', $propertyPath);
69+
$propertyName = $propertyParts[0];
70+
71+
/** @var $errors */
72+
$errors = $this->getValidator()->validateProperty($component, $propertyName);
73+
74+
$errorsForPath = [];
75+
foreach ($errors as $error) {
76+
/** @var ConstraintViolation $error */
77+
if ($error->getPropertyPath() === $propertyPath) {
78+
$errorsForPath[] = $error;
79+
}
80+
}
81+
82+
return $errorsForPath;
83+
}
84+
85+
private function getValidator(): ValidatorInterface
86+
{
87+
return $this->container->get('validator');
88+
}
89+
90+
private function getPropertyAccessor(): PropertyAccessorInterface
91+
{
92+
return $this->container->get('property_accessor');
93+
}
94+
95+
public static function getSubscribedServices(): array
96+
{
97+
return [
98+
'validator' => ValidatorInterface::class,
99+
'property_accessor' => PropertyAccessorInterface::class,
100+
];
101+
}
102+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\LiveComponent;
13+
14+
use Symfony\Component\Validator\ConstraintViolation;
15+
16+
/**
17+
* @author Ryan Weaver <[email protected]>
18+
*
19+
* @experimental
20+
*/
21+
interface ComponentValidatorInterface
22+
{
23+
/**
24+
* Returns an array - keyed by the property path - containing
25+
* another array of validation errors.
26+
*
27+
* For example:
28+
*
29+
* [
30+
* 'firstName' => [ConstraintViolation, ConstraintViolation],
31+
* ]
32+
*
33+
* @return ConstraintViolation[][]
34+
*/
35+
public function validate(object $component): array;
36+
37+
/**
38+
* Returns an array of violations for this one specific property.
39+
*
40+
* @return ConstraintViolation[]
41+
*/
42+
public function validateField(object $component, string $propertyName): array;
43+
}

0 commit comments

Comments
 (0)