diff --git a/src/StimulusBundle/src/Builder/StimulusAttributeBuilder.php b/src/StimulusBundle/src/Builder/StimulusAttributeBuilder.php new file mode 100644 index 00000000000..24553b174dc --- /dev/null +++ b/src/StimulusBundle/src/Builder/StimulusAttributeBuilder.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\StimulusBundle\Builder; + +use Symfony\UX\StimulusBundle\Helper\StimulusSyntaxHelper; + +class StimulusAttributeBuilder +{ + private string $controllerName; + private array $actions = []; + private array $values = []; + private array $classes = []; + private array $targets = []; + private array $outlets = []; + private array $attributes = []; + + public static function controller(string $controllerName): self + { + $builder = new self(); + $builder->controllerName = $controllerName; + + return $builder; + } + + public function action(string $actionName, ?string $eventName = null, array $parameters = []): self + { + $this->actions[] = [ + 'actionName' => $actionName, + 'eventName' => $eventName, + 'parameters' => $parameters, + ]; + + return $this; + } + + public function value(string $key, mixed $value): self + { + $this->values[$key] = $value; + + return $this; + } + + public function class(string $key, mixed $value): self + { + $this->classes[$key] = $value; + + return $this; + } + + public function target(string $key, mixed $value): self + { + $this->targets[$key] = $value; + + return $this; + } + + public function outlet(string $identifier, string $outlet, mixed $selector): self + { + $this->outlets[] = [ + 'identifier' => $identifier, + 'outlet' => $outlet, + 'selector' => $selector, + ]; + + return $this; + } + + public function build(): array + { + $syntaxHelper = new StimulusSyntaxHelper(); + $controllerName = $syntaxHelper->normalizeControllerName($this->controllerName); + + $this->attributes['data-controller'] = $controllerName; + + // Actions + $this->attributes = array_merge( + $this->attributes, + ...array_map(function (array $actionData) use ($controllerName): array { + $actionName = htmlspecialchars($actionData['actionName']); + $eventName = $actionData['eventName']; + + $action = \sprintf('%s#%s', $controllerName, $actionName); + if (null !== $eventName) { + $action = \sprintf('%s->%s', htmlspecialchars($eventName), $action); + } + + return ['data-action' => $action]; + }, $this->actions) + ); + + // Action Parameters + $this->attributes = array_merge( + $this->attributes, + ...array_map( + function (array $actionData) use ($controllerName, $syntaxHelper): array { + $parameters = []; + + foreach ($actionData['parameters'] as $name => $value) { + $key = $syntaxHelper->normalizeKeyName($name); + $value = $syntaxHelper->getFormattedValue($value); + + $parameters[\sprintf( + 'data-%s-%s-param', + $controllerName, + $key + )] = htmlspecialchars($value); + } + + return $parameters; + }, + $this->actions + ) + ); + + // Values + foreach ($this->values as $key => $value) { + if (null === $value) { + continue; + } + + $key = $syntaxHelper->normalizeKeyName($key); + $value = $syntaxHelper->getFormattedValue($value); + + $this->attributes[\sprintf('data-%s-%s-value', $controllerName, $key)] = $value; + } + + // Classes + foreach ($this->classes as $key => $class) { + $key = $syntaxHelper->normalizeKeyName($key); + + $this->attributes[\sprintf('data-%s-%s-class', $controllerName, $key)] = $class; + } + + // Outlets + foreach ($this->outlets as $outletItem) { + $identifier = $syntaxHelper->normalizeControllerName($outletItem['identifier']); + $outlet = $syntaxHelper->normalizeKeyName($outletItem['outlet']); + + $this->attributes[\sprintf('data-%s-%s-outlet', $identifier, $outlet)] = htmlspecialchars( + $outletItem['selector'] + ); + } + + // Targets + foreach ($this->targets as $key => $target) { + $key = $syntaxHelper->normalizeKeyName($key); + + $this->attributes[\sprintf('data-%s-target', $key)] = htmlspecialchars($target); + } + + return $this->attributes; + } +} diff --git a/src/StimulusBundle/src/Helper/StimulusSyntaxHelper.php b/src/StimulusBundle/src/Helper/StimulusSyntaxHelper.php new file mode 100644 index 00000000000..f5237bd452f --- /dev/null +++ b/src/StimulusBundle/src/Helper/StimulusSyntaxHelper.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\StimulusBundle\Helper; + +class StimulusSyntaxHelper +{ + public function normalizeControllerName(string $controllerName): string + { + return preg_replace('/^@/', '', str_replace('_', '-', str_replace('/', '--', $controllerName))); + } + + public function normalizeKeyName(string $str): string + { + // Adapted from ByteString::camel + $str = ucfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $str)))); + + // Adapted from ByteString::snake + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1-\2', $str)); + } + + public function getFormattedValue(mixed $value): string + { + if ($value instanceof \Stringable || (\is_object($value) && \is_callable([$value, '__toString']))) { + $value = (string) $value; + } elseif (!\is_scalar($value)) { + $value = json_encode($value); + } elseif (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + return (string) $value; + } +}