diff --git a/config/services.php b/config/services.php
index 9533f90..cf58801 100644
--- a/config/services.php
+++ b/config/services.php
@@ -123,8 +123,8 @@
->tag(
'kernel.event_listener',
[
- 'event' => 'kernel.controller',
- 'method' => 'onKernelController',
+ 'event' => 'kernel.controller_arguments',
+ 'method' => 'onKernelControllerArguments',
'priority' => -1
]
)
@@ -138,8 +138,8 @@
->tag(
'kernel.event_listener',
[
- 'event' => 'kernel.controller',
- 'method' => 'onKernelController',
+ 'event' => 'kernel.controller_arguments',
+ 'method' => 'onKernelControllerArguments',
'priority' => -1
]
)
diff --git a/docs/development/breadcrumb.md b/docs/development/breadcrumb.md
index 350c03a..a7c778e 100644
--- a/docs/development/breadcrumb.md
+++ b/docs/development/breadcrumb.md
@@ -1,193 +1,171 @@
# Using the breadcrumb
-The breadcrumb is a nice way to indicate where a user is in the application.
+## Basics
-## Manual breadcrumbs
-Adding a breadcrumb is as simple as adding a `#[Breadcrumb()]` attribute.
+Add a `#[Breadcrumb]` attribute to a controller method to register a crumb. The attribute is repeatable — each one
+appends a crumb to the trail in declaration order.
-Example: single
```php
-#[Route('single', name:'single')]
-#[Breadcrumb('single')]
-public function __invoke(): Response
-{
-}
+use SumoCoders\FrameworkCoreBundle\Attribute\Breadcrumb;
```
-Example: attributes on the class
+Single crumb:
+
```php
-#[Route('single', name:'single')]
-#[Breadcrumb('single')]
-class Controller
+#[Route('/books', name: 'books_overview')]
+#[Breadcrumb('books')]
+public function __invoke(): Response
{
- public function __invoke(): Response
- {
- }
}
```
-Adding multiple attributes will chain the breadcrumb.
+Chained crumbs on one controller:
-Example: single > multiple
```php
-#[Route('/single/multiple', name:'multiple')]
-#[Breadcrumb('single')]
-#[Breadcrumb('multiple')]
+#[Route('/books/genres', name: 'genres_overview')]
+#[Breadcrumb('books')]
+#[Breadcrumb('genres')]
public function __invoke(): Response
{
}
```
-When needed a route can be added to the breadcrumb.
+## Class-level attributes
+
+`#[Breadcrumb]` can be placed on the class instead of the method. Class attributes are only picked up for `__invoke`
+controllers, or for named methods that also have at least one `#[Breadcrumb]` attribute of their own.
-Example: [single](#) > multiple
```php
-#[Route('/single/multiple', name:'multiple')]
-#[Breadcrumb('single', route:['name' => 'single'])]
-#[Breadcrumb('multiple')]
-public function __invoke(): Response
+#[Breadcrumb('books')]
+class BooksController
{
+ public function __invoke(): Response
+ {
+ }
}
```
-Route parameters are also supported, but can only accept fixed values so are less useful to build dynamic breadcrumbs.
+
+## Linked breadcrumbs (`route:`)
+
+Pass `route:` to make the crumb a link. Required route parameters are automatically resolved from the current
+controller's named arguments and request attributes — you do not need to specify them manually.
+
```php
-#[Route('/single/{foo}', name:'multiple')]
-#[Breadcrumb('single', route:['name' => 'single', 'parameters' => ['foo' => 'bar']])]
-public function __invoke(string $foo): Response
+#[Route('/books/genres', name: 'genres_overview')]
+#[Breadcrumb('books', route: ['name' => 'books_overview'])]
+#[Breadcrumb('genres')]
+public function __invoke(): Response
{
}
```
-## Automated breadcrumbs
-A breadcrumb trail can be generated by passing the name of another route to the attribute.
+When a parameter cannot be resolved automatically, you can supply a fixed value. This is rarely needed and ties the
+breadcrumb to a hardcoded value:
+
+```php
+#[Breadcrumb('section', route: ['name' => 'section_detail', 'parameters' => ['id' => 42]])]
+```
-The parent route **must have a breadcrumb attribute as well**, as the chain is built by parsing and resolving the attribute in the given parent route.
+## Parent route chaining (`parent:`)
-Parents are always rendered as a link. Parameters of the parent route are copied from the base route and the current request.
+Pass `parent:` to automatically prepend the full breadcrumb trail of another route. The parent route must have its own
+`#[Breadcrumb]` attribute. Parameters are resolved from the current request.
-Example: [books](#) > genres
```php
-#[Route('/books', name:'books_overview')]
+#[Route('/books', name: 'books_overview')]
#[Breadcrumb('books')]
public function __invoke(): Response
{
}
-#[Route('/books/genres', name:'genres_overview')]
-#[Breadcrumb('genres', parent:['name' => 'books_overview'])]
+#[Route('/books/genres', name: 'genres_overview')]
+#[Breadcrumb('genres', parent: ['name' => 'books_overview'])]
public function __invoke(): Response
{
}
```
-## Rendering parameters in the breadcrumb
-You can reference the passed parameter in your breadcrumb by putting it between curly brackets in the breadcrumb title.
+The chain is resolved recursively, so parents of parents work as long as each route in the chain has `#[Breadcrumb]`.
-Example: Authors > whatever-you-pass-as-$name
-```php
-#[Route('/author/{name}')]
-#[Breadcrumb('authors')]
-#[Breadcrumb('{name}')]
-public function __invoke(string $name): Response
-{
-}
-```
-When ParamConverters are used it's possible to use a property of the object as breadcrumb value.
+## Dynamic titles from object properties
+
+Use `{object.property}` to read a value from a controller argument at request time:
-Example: Books > Harry Potter
```php
-#[Route('/book/{book}')]
-#[Breadcrumb('books')]
+#[Route('/books/{book}', name: 'book_detail')]
+#[Breadcrumb('books', route: ['name' => 'books_overview'])]
#[Breadcrumb('{book.title}')]
public function __invoke(Book $book): Response
{
}
```
-This also works with chained parent attributes, as long as the parameter names are still present in the route.
-
-Example: J.K. Rowling > Harry Potter
-```php
-#[Route('/{author}', name:'author_detail')]
-#[Breadcrumb('{author.name}')]
-public function __invoke(Author $author): Response
-{
-}
-
-// ! If we'd omit the /{author} from this route, this example would no longer work !
-#[Route('/{author}/{book}', name:'book_detail')]
-#[Breadcrumb('{book.title}', parent:['name' => 'author_detail'])]
-public function __invoke(Author $author, Book $book): Response
-{
-}
-```
-If you reference a parent route with required parameters, and the required parameter cannot be resolved from the current request, an error will be thrown.
+This also works when combined with `parent:`, as long as the required route parameters are present in the URL:
-You can also supply parameters yourself, just like you would with a route, but this has the same drawbacks. You can only pass fixed values, which kind of defeats the purpose of building dynamic breadcrumbs. It is strongly recommended to **match your breadcrumbs to your URL structure**.
```php
-#[Route('/{book}', name:'book_detail')]
-#[Breadcrumb('{book.title}', parent:['name' => 'author_detail', 'parameters' => ['author' => 4]])]
+// ! /{author} must be in the route for parameter resolution to work
+#[Route('/{author}/{book}', name: 'book_detail')]
+#[Breadcrumb('{book.title}', parent: ['name' => 'author_detail'])]
public function __invoke(Author $author, Book $book): Response
{
}
```
-Custom ParamConverters are **not** supported. Only basic Doctrine Entity -> ID will work out of the box.
+> Scalar parameters (e.g. `string $name`) cannot be used with the `{name}` syntax — only objects with a property path
+> are supported. Using a scalar silently omits the breadcrumb.
## Translations
-It's possible to use translations as these get translated in the template.
-Example: Authors
+All breadcrumb titles pass through the `|trans` Twig filter when rendered. Translation keys work out of the box:
+
```yaml
-breadcrumb.authors: 'Authors'
+# translations/messages.en.yaml
+breadcrumb.books: 'Books'
```
```php
-#[Route('/authors/{author}', name:'authors')]
-#[Breadcrumb('breadcrumb.authors')]
-public function book(Author $author): Response
-{
-}
+#[Breadcrumb('breadcrumb.books')]
```
-
-It's also possible to add paramters to the translation.
+For parameterized translations, pass `parameters:` as an array where keys are the translation placeholders and values
+are `object.property` paths resolved from the current named arguments:
```yaml
-breadcrumb.authors: 'Author: %name%'
+breadcrumb.author_detail: 'Author: %name%'
```
```php
-#[Route('/authors/{author}', name:'authors')]
-#[Breadcrumb('breadcrumb.authors', parameters: ['%name%' => 'author.name
-public function book(Author $author): Response
+#[Route('/author/{author}', name: 'author_detail')]
+#[Breadcrumb('breadcrumb.author_detail', parameters: ['%name%' => 'author.name'])]
+public function __invoke(Author $author): Response
{
}
```
## Full example
-Auteurs > J.K. Rowling > Harry Potter
+
+Trail: Authors > J.K. Rowling > Harry Potter
```php
-#[Route('/author', name:'author_overview')]
-#[Breadcrumb('authors')]
+#[Route('/authors', name: 'author_overview')]
+#[Breadcrumb('breadcrumb.authors')]
public function __invoke(): Response
{
}
-#[Route('/author/{author}', name:'author_detail')]
-#[Breadcrumb('{author.name}', parent:['name' => 'author_overview']))]
+#[Route('/authors/{author}', name: 'author_detail')]
+#[Breadcrumb('{author.name}', parent: ['name' => 'author_overview'])]
public function __invoke(Author $author): Response
{
}
-#[Route('/{author}/{book}', name:'book_detail')]
-#[Breadcrumb('{book.title}', parent:['name' => 'author_detail'])]
+#[Route('/authors/{author}/{book}', name: 'book_detail')]
+#[Breadcrumb('{book.title}', parent: ['name' => 'author_detail'])]
public function __invoke(Author $author, Book $book): Response
{
}
```
```yaml
-authors: 'Auteurs'
+breadcrumb.authors: 'Authors'
```
diff --git a/docs/development/title.md b/docs/development/title.md
index cfe71d3..786a55b 100644
--- a/docs/development/title.md
+++ b/docs/development/title.md
@@ -1,57 +1,132 @@
-# The page title
-By default no page title should be set. This is because by default the `fallback.site_title` is used. This is a configuration value that can be set in the `services.yaml` file.
-In case there are breadcrumbs configured, the reverse order of the breadcrumbs will be used as the page title.
+# Page title
-## The `Title` attribute
+## Resolution order
-The `Title` attribute is a custom attribute used in the framework. It is used to set the title of a page dynamically based on the controller method that is being executed. Here's a step-by-step guide on how to use it:
+The `PageTitle` service resolves the title in this order:
-1. Import the `Title` attribute at the top of your controller file:
+1. A title set explicitly via `#[Title]` on the controller method.
+2. The breadcrumb trail in reverse order, joined with ` - `, appended with the site title.
+3. The `fallback.site_title` value alone if no breadcrumbs are present.
+
+## Configuring the site title
+
+Set `fallback.site_title` in your `services.yaml`:
+
+```yaml
+parameters:
+ fallbacks:
+ site_title: 'My Application'
+```
+
+## The `#[Title]` attribute
```php
use SumoCoders\FrameworkCoreBundle\Attribute\Title;
```
-2. Apply the `Title` attribute to a controller method. The `Title` attribute takes a string as its first argument, which is the title you want to set for the page when this method is executed.
+### Basic usage
```php
#[Title('My Page Title')]
-public function myMethod()
+public function __invoke(): Response
+{
+ // ...
+}
+```
+
+Output: `My Page Title - My Application`
+
+The title string is passed through the translator, so translation keys work:
+
+```yaml
+# translations/messages.en.yaml
+page.my_page: 'My Page Title'
+```
+
+```php
+#[Title('page.my_page')]
+public function __invoke(): Response
+{
+ // ...
+}
+```
+
+### With a parent route
+
+Pass `['name' => 'route_name']` to append the parent route's title to the chain:
+
+```php
+#[Title('Detail', ['name' => 'overview_route'])]
+public function __invoke(): Response
{
- // Your code here
+ // ...
}
```
-3. If you want the title to be extended with the parent's title, you can pass a second argument to the `Title` attribute. This argument should be an array with a `name` key that corresponds to the route name of the parent.
+Output: `Detail - Overview - My Application`
+
+The parent chain is resolved recursively: if the parent route also has a `#[Title]` with its own parent, that is included too.
+
+### Dynamic titles
+
+Reference a controller argument by name using `{param}`:
```php
-#[Title('My Page Title', ['name' => 'parent_route'])]
-public function myMethod()
+#[Title('Edit {name}')]
+public function __invoke(string $name): Response
{
- // Your code here
+ // ...
+}
+```
+
+Access a property of an object argument using `{object.property}`:
+
+```php
+#[Title('{blog.title}')]
+public function __invoke(
+ #[MapEntity(mapping: ['slug' => 'slug'])]
+ Blog $blog,
+): Response {
+ // ...
}
```
-4. If you want to prevent the title from being extended with the parent's title, you can pass a third argument to the `Title` attribute. This argument should be a boolean that indicates whether the title should be extended (`true`) or not (`false`).
+Dynamic parameters are resolved from the named controller arguments. If a placeholder is not found, an exception is thrown.
+
+### Disable automatic appending
+
+Pass `extend: false` to set the title verbatim, with no translation, no parent chain, and no site title appended:
```php
-#[Title('My Page Title', ['name' => 'parent_route'], false)]
-public function myMethod()
+#[Title('Exact Title', extend: false)]
+public function __invoke(): Response
{
- // Your code here
+ // ...
}
```
-5. The `Title` attribute can also handle dynamic titles. If you want to include a parameter in the title, you can do so by including it in curly braces `{}` in the title string. The parameter should be available in the request attributes.
+Output: `Exact Title`
+
+## Using `PageTitle` directly
+
+Inject `PageTitle` to set or get the title from a service or Twig template:
```php
-#[Title('My Page Title for {id}')]
-public function myMethod($id)
+use SumoCoders\FrameworkCoreBundle\Service\PageTitle;
+
+class MyService
{
- // Your code here
+ public function __construct(private PageTitle $pageTitle) {}
+
+ public function doSomething(): void
+ {
+ $this->pageTitle->setTitle('Custom Title');
+ }
}
```
-6. The `TitleListener` class will automatically handle the `Title` attribute. It listens to the kernel controller event, fetches the `Title` attribute from the controller method being executed, and sets the page title accordingly.
+In Twig, `PageTitle` is available as a string (via `__toString`):
-Remember to clear the Symfony cache after adding or changing attributes, as Symfony compiles and caches the attributes when the cache is built. You can clear the cache by running `bin/console cache:clear` in your terminal.
+```twig
+
{{ pageTitle }}
+```
diff --git a/src/EventListener/BreadcrumbListener.php b/src/EventListener/BreadcrumbListener.php
index cef5fd6..a5dc1a8 100644
--- a/src/EventListener/BreadcrumbListener.php
+++ b/src/EventListener/BreadcrumbListener.php
@@ -2,86 +2,65 @@
namespace SumoCoders\FrameworkCoreBundle\EventListener;
-use Doctrine\ORM\EntityManagerInterface;
use SumoCoders\FrameworkCoreBundle\Exception\Breadcrumb\EntityNotFoundException;
use SumoCoders\FrameworkCoreBundle\ValueObject\Route;
-use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use SumoCoders\FrameworkCoreBundle\Service\BreadcrumbTrail;
use SumoCoders\FrameworkCoreBundle\ValueObject\Breadcrumb;
use SumoCoders\FrameworkCoreBundle\Attribute\Breadcrumb as BreadcrumbAttribute;
-use Symfony\Component\HttpKernel\Event\KernelEvent;
-use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
+use Symfony\Component\Routing\RouterInterface;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Contracts\Translation\TranslatorInterface;
class BreadcrumbListener
{
- private RouterInterface $router;
- private PropertyAccessorInterface $propertyAccess;
- private BreadcrumbTrail $breadcrumbTrail;
- private Request $request;
- private EntityManagerInterface $manager;
- private TranslatorInterface $translator;
-
public function __construct(
- RouterInterface $router,
- PropertyAccessorInterface $propertyAccess,
- BreadcrumbTrail $breadcrumbTrail,
- EntityManagerInterface $manager,
- TranslatorInterface $translator
+ private readonly RouterInterface $router,
+ private readonly PropertyAccessorInterface $propertyAccess,
+ private readonly BreadcrumbTrail $breadcrumbTrail,
+ private readonly TranslatorInterface $translator,
) {
- $this->router = $router;
- $this->propertyAccess = $propertyAccess;
- $this->breadcrumbTrail = $breadcrumbTrail;
- $this->manager = $manager;
- $this->translator = $translator;
}
- public function onKernelController(KernelEvent $event): void
+ public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
{
- $controller = $event->getController();
- $this->request = $event->getRequest();
-
- if (is_array($controller)) {
- $controller = $controller[0];
- }
-
if ($event->isMainRequest()) {
$this->breadcrumbTrail->reset();
}
- $this->processBreadcrumbs($controller);
- }
+ $controller = $event->getController();
- private function processBreadcrumbs(object $controller): void
- {
- // Build a new ReflectionClass instance of our controller
- $class = new \ReflectionClass($controller);
+ if (is_array($controller)) {
+ [$object, $methodName] = $controller;
+ $class = new \ReflectionClass($object);
+ $method = $class->getMethod($methodName);
+ } elseif (is_object($controller) && !$controller instanceof \Closure) {
+ $class = new \ReflectionClass($controller);
+ $method = $class->getMethod('__invoke');
+ } else {
+ return;
+ }
if ($class->isAbstract()) {
throw new InvalidArgumentException(
- sprintf(
- 'Attributes from class "%s" cannot be read as it is abstract.',
- $class
- )
+ sprintf('Attributes from class "%s" cannot be read as it is abstract.', $class->getName())
);
}
- $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
-
- foreach ($methods as $method) {
- $this->processAttributeFromMethod($method, $class);
- }
+ $this->processAttributeFromMethod($method, $class, $event->getNamedArguments(), $event->getRequest());
}
+ /** @param array $namedArguments */
private function processAttributeFromMethod(
- \Reflectionmethod $method,
+ \ReflectionMethod $method,
\ReflectionClass $class,
- ?Route $route = null
+ array $namedArguments,
+ Request $request,
+ ?Route $route = null,
): void {
$attributes = $method->getAttributes(BreadcrumbAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);
if ($method->name === '__invoke' || $attributes !== []) {
@@ -97,30 +76,29 @@ private function processAttributeFromMethod(
}
if ($attributeInstance->hasParent()) {
- $this->addBreadcrumbsForParent($attributeInstance->getParent());
+ $this->addBreadcrumbsForParent($attributeInstance->getParent(), $namedArguments, $request);
}
try {
$this->breadcrumbTrail->add(
- $this->generateBreadcrumb(
- $attributeInstance,
- $method
- )
+ $this->generateBreadcrumb($attributeInstance, $namedArguments, $request)
);
} catch (EntityNotFoundException $e) {
-
}
}
}
+ /**
+ * @param array $namedArguments
+ */
private function generateBreadcrumb(
BreadcrumbAttribute $breadcrumb,
- \Reflectionmethod $method
+ array $namedArguments,
+ Request $request,
): Breadcrumb {
$title = $breadcrumb->getTitle();
$parameters = $breadcrumb->getParameters();
- // We're dealing with an expression, e.g. {item.name}
if ($title[0] === '{' && $title[-1] === '}') {
$expression = substr($title, 1, strlen($title) - 2);
@@ -132,44 +110,18 @@ private function generateBreadcrumb(
$attributeName = $expression;
}
- if (!$this->request->attributes->has($attributeName)) {
+ if (!array_key_exists($attributeName, $namedArguments)) {
throw new RuntimeException(
'You tried to use {' . $attributeName . '} as a breadcrumb parameter, but there is no ' .
'parameter with that name in the route.'
);
}
- $attributeId = $this->request->attributes->get($attributeName);
-
- $name = null;
- $mapping = null;
- foreach ($method->getParameters() as $parameter) {
- if ($parameter->name === $attributeName) {
- $name = $parameter->getType()->getName();
- foreach ($parameter->getAttributes() as $attribute) {
- if ($attribute->getName() === MapEntity::class) {
- $mapping = $attribute->newInstance()->mapping;
- }
- }
- }
- }
-
- if ($name === null) {
- throw new RuntimeException(
- 'You tried to use {' . $attributeName . '} as a breadcrumb parameter, but there is no ' .
- 'parameter with that name in the route.'
- );
- }
-
- if ($mapping !== null && isset($mapping[$attributeName])) {
- $attribute = $this->manager->getRepository($name)->findOneBy([$mapping[$attributeName] => $attributeId]);
- } else {
- $attribute = $this->manager->getRepository($name)->find($attributeId);
- }
+ $attribute = $namedArguments[$attributeName];
if (!is_object($attribute)) {
throw new EntityNotFoundException(
- 'Could not resolve entity ' . $name . ' with ID ' . $attributeId
+ 'Could not resolve entity for parameter ' . $attributeName
);
}
@@ -184,7 +136,7 @@ private function generateBreadcrumb(
}
if ($breadcrumb->hasRoute()) {
- $this->resolveRouteParameters($breadcrumb);
+ $this->resolveRouteParameters($breadcrumb, $namedArguments, $request);
return new Breadcrumb(
$title,
@@ -206,34 +158,18 @@ private function generateBreadcrumb(
$attributeName = $parameterValue;
}
- if (!$this->request->attributes->has($attributeName)) {
- throw new RuntimeException(
- 'You tried to use {' . $attributeName . '} as a breadcrumb parameter, but there is no ' .
- 'parameter with that name in the route.'
- );
- }
-
- $attributeId = $this->request->attributes->get($attributeName);
-
- $name = null;
- foreach ($method->getParameters() as $parameter) {
- if ($parameter->name === $attributeName) {
- $name = $parameter->getType()->getName();
- }
- }
-
- if ($name === null) {
+ if (!array_key_exists($attributeName, $namedArguments)) {
throw new RuntimeException(
'You tried to use {' . $attributeName . '} as a breadcrumb parameter, but there is no ' .
'parameter with that name in the route.'
);
}
- $attribute = $this->manager->getRepository($name)->find($attributeId);
+ $attribute = $namedArguments[$attributeName];
if (!is_object($attribute)) {
throw new RuntimeException(
- 'Could not resolve entity ' . $name . ' with ID ' . $attributeId
+ 'Could not resolve entity for parameter ' . $attributeName
);
}
@@ -250,11 +186,11 @@ private function generateBreadcrumb(
$title = $this->translator->trans($title, $parameters);
}
- // Just a simple string
return new Breadcrumb($title);
}
- private function addBreadcrumbsForParent(Route $parent): void
+ /** @param array $namedArguments */
+ private function addBreadcrumbsForParent(Route $parent, array $namedArguments, Request $request): void
{
$routeName = $parent->getName();
$routeInformation = $this->getRouteInformation($routeName);
@@ -265,59 +201,36 @@ private function addBreadcrumbsForParent(Route $parent): void
);
}
- // If class contains :: in the name, we're dealing with a static method
- if (strpos($routeInformation['controller'], '::') > 0) {
- $parts = explode('::', $routeInformation['controller']);
- $class = new \ReflectionClass($parts[0]);
-
- $method = $class->getMethod($parts[1]);
- } else {
- $class = new \ReflectionClass($routeInformation['controller']);
- $method = $class->getMethod($routeInformation['method']);
- }
+ $class = new \ReflectionClass($routeInformation['controller']);
+ $method = $class->getMethod($routeInformation['method']);
- $this->processAttributeFromMethod($method, $class, new Route($routeName));
+ $this->processAttributeFromMethod($method, $class, $namedArguments, $request, new Route($routeName));
}
+ /** @return array|null */
private function getRouteInformation(string $name): ?array
{
- // Get all the routes defined in the entire application
$routes = $this->router->getRouteCollection()->all();
foreach ($routes as $key => $route) {
- // Get our canonical (without a locale prefixed) route name
if ($route->getDefault('_canonical_route') !== $name && $key !== $name) {
continue;
}
- /*
- * In the case of multiple methods defined per controller,
- * explode the controller name and method
- */
- if (strpos($route->getDefault('_controller'), '::') > 0) {
- $chunk = explode('::', $route->getDefault('_controller'));
- $controller = $chunk[0];
- $method = $chunk[1];
- } else {
- $controller = $route->getDefault('_controller');
- }
+ $controller = $route->getDefault('_controller');
+ $hasMethod = strpos($controller, '::') > 0;
+ $controllerClass = $hasMethod ? explode('::', $controller)[0] : $controller;
+ $method = $hasMethod ? explode('::', $controller)[1] : '__invoke';
- // Compile the route to access the parameters
$compiledRoute = $route->compile();
- $parameters = $compiledRoute->getVariables();
-
- // Loop each parameter and check if a default exists for it
- $requiredParameters = [];
- foreach ($parameters as $parameter) {
- if ($route->getDefault($parameter) === null) {
- $requiredParameters[] = $parameter;
- }
- }
+ $requiredParameters = array_filter(
+ $compiledRoute->getVariables(),
+ fn($parameter) => $route->getDefault($parameter) === null
+ );
- // Return the controller, method and required parameters
return [
- 'controller' => $controller,
- 'method' => $method ?? '__invoke',
+ 'controller' => $controllerClass,
+ 'method' => $method,
'parameters' => $requiredParameters,
];
}
@@ -325,38 +238,27 @@ private function getRouteInformation(string $name): ?array
return null;
}
- private function resolveRouteParameters(BreadcrumbAttribute $breadcrumb): void
+ /** @param array $namedArguments */
+ private function resolveRouteParameters(BreadcrumbAttribute $breadcrumb, array $namedArguments, Request $request): void
{
$route = $breadcrumb->getRoute();
$routeInformation = $this->getRouteInformation($route->getName());
$requiredParameters = $routeInformation['parameters'];
$parentParameters = [];
- $currentAttributes = $this->request->attributes->all();
foreach ($requiredParameters as $requiredParentParameter) {
- /*
- * In real world scenario's, the parent is often present
- * in the same URI as the request. Take for example:
- * /{item}/{child}
- * If we're currently in the child route, we can check the URI
- * for the author parameter and already fill it in.
- */
- if (\array_key_exists($requiredParentParameter, $currentAttributes)) {
- if (is_object($currentAttributes[$requiredParentParameter])) {
- $parentParameters[$requiredParentParameter] = $currentAttributes[$requiredParentParameter]->getId();
- } else {
- $parentParameters[$requiredParentParameter] = $currentAttributes[$requiredParentParameter];
- }
+ if (array_key_exists($requiredParentParameter, $namedArguments)) {
+ $value = $namedArguments[$requiredParentParameter];
+ $parentParameters[$requiredParentParameter] = is_object($value) ? $value->getId() : $value;
+ } elseif ($request->attributes->has($requiredParentParameter)) {
+ $parentParameters[$requiredParentParameter] = $request->attributes->get($requiredParentParameter);
}
}
$route->addParameters($parentParameters);
- if (
- count($routeInformation['parameters']) > 0
- && !$route->getParameters()
- ) {
+ if (count($routeInformation['parameters']) > 0 && !$route->getParameters()) {
throw new RuntimeException(
'Your breadcrumb route is missing required parameters: ' .
implode($routeInformation['parameters'])
@@ -364,7 +266,7 @@ private function resolveRouteParameters(BreadcrumbAttribute $breadcrumb): void
}
foreach ($routeInformation['parameters'] as $requiredParameter) {
- if (!\array_key_exists($requiredParameter, $route->getParameters())) {
+ if (!array_key_exists($requiredParameter, $route->getParameters())) {
throw new RuntimeException(
'Your breadcrumb route is missing required parameters: ' . $requiredParameter
);
diff --git a/src/EventListener/TitleListener.php b/src/EventListener/TitleListener.php
index 30305d2..57c2811 100644
--- a/src/EventListener/TitleListener.php
+++ b/src/EventListener/TitleListener.php
@@ -2,24 +2,16 @@
namespace SumoCoders\FrameworkCoreBundle\EventListener;
-use ReflectionClass;
+use ReflectionMethod;
use SumoCoders\FrameworkCoreBundle\Attribute\Title;
use SumoCoders\FrameworkCoreBundle\Service\Fallbacks;
use SumoCoders\FrameworkCoreBundle\Service\PageTitle;
use SumoCoders\FrameworkCoreBundle\ValueObject\Route;
-use Symfony\Bridge\Doctrine\Attribute\MapEntity;
-use Symfony\Component\HttpKernel\Event\KernelEvent;
+use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
-use Doctrine\ORM\EntityManagerInterface;
-
-/**
- * Class TitleListener
- *
- * This class is responsible for handling the title of the page.
- * It listens to the kernel controller event and sets the title based on the Title attribute.
- */
+
class TitleListener
{
public function __construct(
@@ -27,85 +19,53 @@ public function __construct(
private Fallbacks $fallbacks,
private RouterInterface $router,
private TranslatorInterface $translator,
- private EntityManagerInterface $manager,
private PropertyAccessorInterface $propertyAccess,
) {
}
- /**
- * Event listener for the kernel controller event.
- *
- * @param KernelEvent $event
- */
- public function onKernelController(KernelEvent $event): void
+ public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
{
- // Get the controller and its methods
- $controller = is_array($event->getController()) ? $event->getController()[0] : $event->getController();
- $methods = (new ReflectionClass($controller))->getMethods();
-
- // Loop through the methods and process the Title attributes
- foreach ($methods as $method) {
- $attributes = $method->getAttributes(Title::class, \ReflectionAttribute::IS_INSTANCEOF);
-
- if (empty($attributes)) {
- continue;
- }
+ $method = $this->resolveMethod($event->getController());
+ if ($method === null) {
+ return;
+ }
- // Process the parameters of the method
- $parameters = $this->processParameters($method->getParameters(), $event->getRequest()->attributes->all());
+ $attributes = $method->getAttributes(Title::class, \ReflectionAttribute::IS_INSTANCEOF);
+ if (empty($attributes)) {
+ return;
+ }
- // Loop through the Title attributes and set the page title
- foreach ($attributes as $attribute) {
- $titleAttribute = $attribute->newInstance();
+ $parameters = $event->getNamedArguments();
- if (!$titleAttribute->isExtend()) {
- $this->pageTitleService->setTitle($titleAttribute->getTitle());
- return;
- }
+ foreach ($attributes as $attribute) {
+ $titleAttribute = $attribute->newInstance();
- $title = $this->processTitle($titleAttribute->getTitle(), $parameters);
+ if (!$titleAttribute->isExtend()) {
+ $this->pageTitleService->setTitle($titleAttribute->getTitle());
+ return;
+ }
- if ($titleAttribute->hasParent()) {
- $title .= $this->getTitleFromParent($titleAttribute->getParent(), $parameters);
- }
+ $title = $this->processTitle($titleAttribute->getTitle(), $parameters);
- $this->pageTitleService->setTitle($title . ' - ' . $this->fallbacks->get('site_title'));
+ if ($titleAttribute->hasParent()) {
+ $title .= $this->getTitleFromParent($titleAttribute->getParent(), $parameters);
}
+
+ $this->pageTitleService->setTitle($title . ' - ' . $this->fallbacks->get('site_title'));
}
}
- /**
- * Process the parameters of a method.
- *
- * @param array<\ReflectionParameter> $reflextionParameters
- * @param array $parameters
- * @return array
- */
- private function processParameters(array $reflextionParameters, array $parameters): array
+ private function resolveMethod(mixed $controller): ?ReflectionMethod
{
- // Loop through the reflection parameters and process the MapEntity attributes
- foreach ($reflextionParameters as $reflextionParameter) {
- $parameterName = $reflextionParameter->getName();
-
- if (!array_key_exists($parameterName, $parameters)) {
- continue;
- }
-
- $parameterAttributes = $reflextionParameter->getAttributes(MapEntity::class);
- if (empty($parameterAttributes)) {
- continue;
- }
-
- // Get the mapping and value of the parameter
- $mapping = $parameterAttributes[0]->getArguments()['mapping'] ?? null;
- $value = $mapping !== null && isset($parameters[$parameterName])
- ? $this->manager->getRepository($reflextionParameter->getType()->getName())->findOneBy([$mapping[$parameterName] => $parameters[$parameterName]])
- : $this->manager->getRepository($reflextionParameter->getType()->getName())->find($parameters[$parameterName]);
+ if (is_array($controller)) {
+ return new ReflectionMethod($controller[0], $controller[1]);
+ }
- $parameters[$parameterName] = $value;
+ if (is_object($controller) && !$controller instanceof \Closure) {
+ return new ReflectionMethod($controller, '__invoke');
}
- return $parameters;
+ return null;
}
/**
@@ -182,13 +142,15 @@ private function getRouteInformation(string $name): ?array
// Get the controller and method of the route
$controller = $route->getDefault('_controller');
- $method = strpos($controller, '::') > 0 ? explode('::', $controller)[1] : '__invoke';
+ $hasMethod = strpos($controller, '::') > 0;
+ $controllerClass = $hasMethod ? explode('::', $controller)[0] : $controller;
+ $method = $hasMethod ? explode('::', $controller)[1] : '__invoke';
// Get the required parameters of the route
$requiredParameters = array_filter($route->compile()->getVariables(), fn($parameter) => $route->getDefault($parameter) === null);
return [
- 'controller' => $controller,
+ 'controller' => $controllerClass,
'method' => $method,
'parameters' => $requiredParameters,
];
diff --git a/templates/base.html.twig b/templates/base.html.twig
index db683a9..0a3577d 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -7,10 +7,65 @@
or has_header_actions_center|trim is not empty
or has_header_actions_right|trim is not empty
%}
+
+
+
+
+
+
+ {% block title %}
+ {{ page_title }}
+ {% endblock %}
+
-{% block head %}
- {{ include('@SumoCodersFrameworkCore/head.html.twig', {title: block('title') is defined ? block('title') : page_title}) }}
-{% endblock %}
+ {% block meta %}
+ {# Call both script and style so both nonces are generated #}
+ {% set csp_nonce = csp_nonce('script') %}
+ {% set csp_nonce = csp_nonce('style') %}
+
+
+ {% endblock %}
+
+ {% block stylesheets %}
+
+ {% endblock %}
+
+ {% block javascripts %}
+ {{ importmap('app', {'nonce': csp_nonce('script')}) }}
+ {% endblock %}
+
+ {% block icons %}
+
+
+
+ {% endblock %}
+
+ {% block favicon %}
+
+
+ {% endblock %}
+
+ {% block windows_tile %}
+ {# Windows 8 tile #}
+
+
+
+ {% endblock %}
+
+ {% block og %}
+
+ {% endblock %}
+
+ {% block twitter %}
+ {# Twitter #}
+
+
+
+ {% endblock %}
+
+ {% block end_head %}
+ {% endblock %}
+
{{ include('@SumoCodersFrameworkCore/settheme.html.twig') }}
diff --git a/templates/base_error.html.twig b/templates/base_error.html.twig
index 3b7af41..440ec4c 100644
--- a/templates/base_error.html.twig
+++ b/templates/base_error.html.twig
@@ -1,11 +1,63 @@
-{% block head %}
- {% include '@SumoCodersFrameworkCore/head.html.twig' with {title: block('title') is defined ? block('title') : page_title} %}
+
+
+
+
+
+
+
+ {% block title %}
+ {{ page_title }}
+ {% endblock %}
+
+
+ {% block meta %}
+ {# Call both script and style so both nonces are generated #}
+ {% set csp_nonce = csp_nonce('script') %}
+ {% set csp_nonce = csp_nonce('style') %}
+
+
+ {% endblock %}
{% block stylesheets %}
{% endblock %}
-{% endblock %}
+ {% block javascripts %}
+ {{ importmap('app', {'nonce': csp_nonce('script')}) }}
+ {% endblock %}
+
+ {% block icons %}
+
+
+
+ {% endblock %}
+
+ {% block favicon %}
+
+
+ {% endblock %}
+
+ {% block windows_tile %}
+ {# Windows 8 tile #}
+
+
+
+ {% endblock %}
+
+ {% block og %}
+
+ {% endblock %}
+
+ {% block twitter %}
+ {# Twitter #}
+
+
+
+ {% endblock %}
+
+ {% block end_head %}
+ {% endblock %}
+
{{ include('@SumoCodersFrameworkCore/settheme.html.twig') }}
diff --git a/templates/base_no_sidebar.html.twig b/templates/base_no_sidebar.html.twig
index 7571149..076429d 100644
--- a/templates/base_no_sidebar.html.twig
+++ b/templates/base_no_sidebar.html.twig
@@ -1,6 +1,63 @@
-{% block head %}
- {% include '@SumoCodersFrameworkCore/head.html.twig' with {title: block('title') is defined ? block('title') : page_title} %}
-{% endblock %}
+
+
+
+
+
+
+
+ {% block title %}
+ {{ page_title }}
+ {% endblock %}
+
+
+ {% block meta %}
+ {# Call both script and style so both nonces are generated #}
+ {% set csp_nonce = csp_nonce('script') %}
+ {% set csp_nonce = csp_nonce('style') %}
+
+
+ {% endblock %}
+
+ {% block stylesheets %}
+
+ {% endblock %}
+
+ {% block javascripts %}
+ {{ importmap('app', {'nonce': csp_nonce('script')}) }}
+ {% endblock %}
+
+ {% block icons %}
+
+
+
+ {% endblock %}
+
+ {% block favicon %}
+
+
+ {% endblock %}
+
+ {% block windows_tile %}
+ {# Windows 8 tile #}
+
+
+
+ {% endblock %}
+
+ {% block og %}
+
+ {% endblock %}
+
+ {% block twitter %}
+ {# Twitter #}
+
+
+
+ {% endblock %}
+
+ {% block end_head %}
+ {% endblock %}
+
{{ include('@SumoCodersFrameworkCore/settheme.html.twig') }}
diff --git a/templates/empty.html.twig b/templates/empty.html.twig
index f71af79..b14c326 100644
--- a/templates/empty.html.twig
+++ b/templates/empty.html.twig
@@ -1,6 +1,63 @@
-{% block head %}
- {% include '@SumoCodersFrameworkCore/head.html.twig' with {title: block('title') is defined ? block('title') : page_title} %}
-{% endblock %}
+
+
+
+
+
+
+
+ {% block title %}
+ {{ page_title }}
+ {% endblock %}
+
+
+ {% block meta %}
+ {# Call both script and style so both nonces are generated #}
+ {% set csp_nonce = csp_nonce('script') %}
+ {% set csp_nonce = csp_nonce('style') %}
+
+
+ {% endblock %}
+
+ {% block stylesheets %}
+
+ {% endblock %}
+
+ {% block javascripts %}
+ {{ importmap('app', {'nonce': csp_nonce('script')}) }}
+ {% endblock %}
+
+ {% block icons %}
+
+
+
+ {% endblock %}
+
+ {% block favicon %}
+
+
+ {% endblock %}
+
+ {% block windows_tile %}
+ {# Windows 8 tile #}
+
+
+
+ {% endblock %}
+
+ {% block og %}
+
+ {% endblock %}
+
+ {% block twitter %}
+ {# Twitter #}
+
+
+
+ {% endblock %}
+
+ {% block end_head %}
+ {% endblock %}
+
{{ include('@SumoCodersFrameworkCore/settheme.html.twig') }}
diff --git a/templates/head.html.twig b/templates/head.html.twig
deleted file mode 100644
index c177e97..0000000
--- a/templates/head.html.twig
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
- {{ title }}
-
- {% block meta %}
- {# Call both script and style so both nonces are generated #}
- {% set csp_nonce = csp_nonce('script') %}
- {% set csp_nonce = csp_nonce('style') %}
-
-
- {% endblock %}
-
- {% block stylesheets %}
-
- {% endblock %}
-
- {% block javascripts %}
- {{ importmap('app', {'nonce': csp_nonce('script')}) }}
- {% endblock %}
-
- {% block icons %}
-
-
-
- {% endblock %}
-
- {% block favicon %}
-
-
- {% endblock %}
-
- {% block windows_tile %}
- {# Windows 8 tile #}
-
-
-
- {% endblock %}
-
- {% block og %}
-
- {% endblock %}
-
- {% block twitter %}
- {# Twitter #}
-
-
-
- {% endblock %}
-
- {% block end_head %}
- {% endblock %}
-
diff --git a/templates/user.html.twig b/templates/user.html.twig
index 1a673e1..dc90962 100644
--- a/templates/user.html.twig
+++ b/templates/user.html.twig
@@ -1,6 +1,63 @@
-{% block head %}
- {% include '@SumoCodersFrameworkCore/head.html.twig' with {title: block('title') is defined ? block('title') : page_title} %}
-{% endblock %}
+
+
+
+
+
+
+
+ {% block title %}
+ {{ page_title }}
+ {% endblock %}
+
+
+ {% block meta %}
+ {# Call both script and style so both nonces are generated #}
+ {% set csp_nonce = csp_nonce('script') %}
+ {% set csp_nonce = csp_nonce('style') %}
+
+
+ {% endblock %}
+
+ {% block stylesheets %}
+
+ {% endblock %}
+
+ {% block javascripts %}
+ {{ importmap('app', {'nonce': csp_nonce('script')}) }}
+ {% endblock %}
+
+ {% block icons %}
+
+
+
+ {% endblock %}
+
+ {% block favicon %}
+
+
+ {% endblock %}
+
+ {% block windows_tile %}
+ {# Windows 8 tile #}
+
+
+
+ {% endblock %}
+
+ {% block og %}
+
+ {% endblock %}
+
+ {% block twitter %}
+ {# Twitter #}
+
+
+
+ {% endblock %}
+
+ {% block end_head %}
+ {% endblock %}
+
{{ include('@SumoCodersFrameworkCore/settheme.html.twig') }}