diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5336320..270cd2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.1.0] - 2025-12-27
+
+### Added
+- Twig extension `has_permission()` function to check permissions in templates (returns boolean)
+- Twig extension `permission()` function to get detailed permission decision with violations
+- PermissionDecisionInterface and PermissionDecision to represent permission validation results
+
## [1.0.0] - 2025-12-19
### Added
diff --git a/README.md b/README.md
index e7c1a2b..7349d50 100644
--- a/README.md
+++ b/README.md
@@ -187,6 +187,51 @@ if ($violations->count() > 0) {
}
```
+### Method 3: With Twig Extensions
+
+The bundle provides two Twig functions to check permissions directly in your templates.
+
+#### Function `has_permission()`
+
+This function returns a boolean indicating whether the permission is granted or not.
+
+**Usage**:
+```twig
+{% if has_permission('App\\Security\\Permission\\EditArticlePermission', articleId, userId) %}
+
+{% else %}
+
You are not authorized to edit this article.
+{% endif %}
+```
+
+The function takes:
+- First argument: The fully qualified class name of the permission (as a string)
+- Remaining arguments: The arguments to pass to the permission's constructor
+
+#### Function `permission()`
+
+This function returns a decision object containing both the granted status and the violations list.
+
+**Usage**:
+```twig
+{% set decision = permission('App\\Security\\Permission\\EditArticlePermission', articleId, userId) %}
+
+{% if decision.granted %}
+
+{% else %}
+
Access denied. Reasons:
+
+ {% for violation in decision.violations %}
+
{{ violation.message }}
+ {% endfor %}
+
+{% endif %}
+```
+
+The returned object has:
+- `granted` (bool): Whether the permission is granted
+- `violations` (ConstraintViolationListInterface): The list of violations if the permission was denied
+
## Complete example
Here's a complete example with a (little) more complex permission:
diff --git a/ROADMAP.md b/ROADMAP.md
index 1cad26a..9d1d050 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -4,9 +4,8 @@ This document outlines planned features and improvements for the Permission Bund
## Planned Features
-### v1.1.0 (Planned)
+### v1.2.0 (Planned)
- **PHP Attribute HasPermission** : A PHP 8 attribute to simplify permission checks in controllers
-- **Twig Extension has_permission** : A Twig function to check permissions directly in templates
## Ideas for Future Versions
diff --git a/composer.json b/composer.json
index 14e12e3..cdf5a27 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
"symfony/dependency-injection": "^7.1",
"symfony/http-kernel": "^7.1",
"symfony/security-core": "^7.1",
+ "symfony/twig-bundle": "^7.1",
"symfony/validator": "^7.1"
},
"require-dev": {
diff --git a/src/Decision/PermissionDecision.php b/src/Decision/PermissionDecision.php
new file mode 100644
index 0000000..9d1ce55
--- /dev/null
+++ b/src/Decision/PermissionDecision.php
@@ -0,0 +1,26 @@
+granted;
+ }
+
+ public function getViolations(): ConstraintViolationListInterface
+ {
+ return $this->violations;
+ }
+}
diff --git a/src/Decision/PermissionDecisionFactory.php b/src/Decision/PermissionDecisionFactory.php
new file mode 100644
index 0000000..ab1cd24
--- /dev/null
+++ b/src/Decision/PermissionDecisionFactory.php
@@ -0,0 +1,20 @@
+addTag('security.voter'),
);
+ /** @see PermissionDecisionFactory */
+ $container->setDefinition(
+ 'sybio_permission.result_factory',
+ (new Definition(PermissionDecisionFactory::class))
+ ->setPublic(false),
+ );
+
+ /** @see HasPermissionTwigFunction */
+ $container->setDefinition(
+ 'sybio_permission.twig.has_permission',
+ (new Definition(HasPermissionTwigFunction::class))
+ ->setArgument(0, new Reference('security.authorization_checker'))
+ ->setArgument(1, $container->getParameter('sybio_permission.security_attribute'))
+ ->addTag('twig.extension')
+ ->setPublic(false),
+ );
+
+ /** @see PermissionTwigFunction */
+ $container->setDefinition(
+ 'sybio_permission.twig.permission',
+ (new Definition(PermissionTwigFunction::class))
+ ->setArgument(0, new Reference('security.authorization_checker'))
+ ->setArgument(1, new Reference('sybio_permission.validator'))
+ ->setArgument(2, new Reference('sybio_permission.result_factory'))
+ ->addTag('twig.extension')
+ ->setPublic(false),
+ );
+
$container->setAlias(PermissionConstraintValidator::class, 'sybio_permission.constraint_validator');
$container->setAlias(PermissionValidatorInterface::class, 'sybio_permission.validator');
$container->setAlias(PermissionValidator::class, 'sybio_permission.validator');
$container->setAlias(PermissionVoter::class, 'sybio_permission.voter');
+ $container->setAlias(PermissionDecisionFactoryInterface::class, 'sybio_permission.result_factory');
}
public function getAlias(): string
diff --git a/src/Twig/HasPermissionTwigFunction.php b/src/Twig/HasPermissionTwigFunction.php
new file mode 100644
index 0000000..0ab59cf
--- /dev/null
+++ b/src/Twig/HasPermissionTwigFunction.php
@@ -0,0 +1,40 @@
+hasPermission(...)),
+ ];
+ }
+
+ /**
+ * @param class-string $permissionClass
+ */
+ public function hasPermission(
+ string $permissionClass,
+ mixed ...$arguments,
+ ): bool {
+ $permission = new $permissionClass(...$arguments);
+
+ return $this->authorizationChecker->isGranted(
+ $permissionClass,
+ $permission,
+ );
+ }
+}
diff --git a/src/Twig/PermissionTwigFunction.php b/src/Twig/PermissionTwigFunction.php
new file mode 100644
index 0000000..941025b
--- /dev/null
+++ b/src/Twig/PermissionTwigFunction.php
@@ -0,0 +1,51 @@
+permission(...)),
+ ];
+ }
+
+ /**
+ * @param class-string $permissionClass
+ */
+ public function permission(
+ string $permissionClass,
+ mixed ...$arguments,
+ ): PermissionDecisionInterface {
+ $permission = new $permissionClass(...$arguments);
+
+ $isGranted = $this->authorizationChecker->isGranted(
+ $permissionClass,
+ $permission,
+ );
+ $violations = $this->permissionValidator->getLastViolations();
+
+ return $this->permissionDecisionFactory->createDecision(
+ $isGranted,
+ $violations,
+ );
+ }
+}