Skip to content

Commit 92baab0

Browse files
committed
feat(twig): functions has_permission() and permission()
1 parent b5d6489 commit 92baab0

11 files changed

+253
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.1.0] - 2025-12-27
11+
12+
### Added
13+
- Twig extension `has_permission()` function to check permissions in templates (returns boolean)
14+
- Twig extension `permission()` function to get detailed permission decision with violations
15+
- PermissionDecisionInterface and PermissionDecision to represent permission validation results
16+
1017
## [1.0.0] - 2025-12-19
1118

1219
### Added

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,51 @@ if ($violations->count() > 0) {
187187
}
188188
```
189189

190+
### Method 3: With Twig Extensions
191+
192+
The bundle provides two Twig functions to check permissions directly in your templates.
193+
194+
#### Function `has_permission()`
195+
196+
This function returns a boolean indicating whether the permission is granted or not.
197+
198+
**Usage**:
199+
```twig
200+
{% if has_permission('App\\Security\\Permission\\EditArticlePermission', articleId, userId) %}
201+
<button>Edit Article</button>
202+
{% else %}
203+
<p>You are not authorized to edit this article.</p>
204+
{% endif %}
205+
```
206+
207+
The function takes:
208+
- First argument: The fully qualified class name of the permission (as a string)
209+
- Remaining arguments: The arguments to pass to the permission's constructor
210+
211+
#### Function `permission()`
212+
213+
This function returns a decision object containing both the granted status and the violations list.
214+
215+
**Usage**:
216+
```twig
217+
{% set decision = permission('App\\Security\\Permission\\EditArticlePermission', articleId, userId) %}
218+
219+
{% if decision.granted %}
220+
<button>Edit Article</button>
221+
{% else %}
222+
<p>Access denied. Reasons:</p>
223+
<ul>
224+
{% for violation in decision.violations %}
225+
<li>{{ violation.message }}</li>
226+
{% endfor %}
227+
</ul>
228+
{% endif %}
229+
```
230+
231+
The returned object has:
232+
- `granted` (bool): Whether the permission is granted
233+
- `violations` (ConstraintViolationListInterface): The list of violations if the permission was denied
234+
190235
## Complete example
191236

192237
Here's a complete example with a (little) more complex permission:

ROADMAP.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ This document outlines planned features and improvements for the Permission Bund
44

55
## Planned Features
66

7-
### v1.1.0 (Planned)
7+
### v1.2.0 (Planned)
88
- **PHP Attribute HasPermission** : A PHP 8 attribute to simplify permission checks in controllers
9-
- **Twig Extension has_permission** : A Twig function to check permissions directly in templates
109

1110
## Ideas for Future Versions
1211

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"symfony/dependency-injection": "^7.1",
1818
"symfony/http-kernel": "^7.1",
1919
"symfony/security-core": "^7.1",
20+
"symfony/twig-bundle": "^7.1",
2021
"symfony/validator": "^7.1"
2122
},
2223
"require-dev": {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sybio\PermissionBundle\Decision;
6+
7+
use Symfony\Component\Validator\ConstraintViolationListInterface;
8+
9+
final readonly class PermissionDecision implements PermissionDecisionInterface
10+
{
11+
public function __construct(
12+
private bool $granted,
13+
private ConstraintViolationListInterface $violations,
14+
) {
15+
}
16+
17+
public function isGranted(): bool
18+
{
19+
return $this->granted;
20+
}
21+
22+
public function getViolations(): ConstraintViolationListInterface
23+
{
24+
return $this->violations;
25+
}
26+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sybio\PermissionBundle\Decision;
6+
7+
use Symfony\Component\Validator\ConstraintViolationListInterface;
8+
9+
class PermissionDecisionFactory implements PermissionDecisionFactoryInterface
10+
{
11+
public function createDecision(
12+
bool $granted,
13+
ConstraintViolationListInterface $violations,
14+
): PermissionDecisionInterface {
15+
return new PermissionDecision(
16+
$granted,
17+
$violations,
18+
);
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sybio\PermissionBundle\Decision;
6+
7+
use Symfony\Component\Validator\ConstraintViolationListInterface;
8+
9+
interface PermissionDecisionFactoryInterface
10+
{
11+
public function createDecision(
12+
bool $granted,
13+
ConstraintViolationListInterface $violations,
14+
): PermissionDecisionInterface;
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sybio\PermissionBundle\Decision;
6+
7+
use Symfony\Component\Validator\ConstraintViolationListInterface;
8+
9+
interface PermissionDecisionInterface
10+
{
11+
public function isGranted(): bool;
12+
13+
public function getViolations(): ConstraintViolationListInterface;
14+
}

src/DependencyInjection/PermissionBundleConfiguration.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44

55
namespace Sybio\PermissionBundle\DependencyInjection;
66

7+
use Sybio\PermissionBundle\Decision\PermissionDecisionFactory;
8+
use Sybio\PermissionBundle\Decision\PermissionDecisionFactoryInterface;
79
use Sybio\PermissionBundle\PermissionCheckerInterface;
810
use Sybio\PermissionBundle\Security\PermissionVoter;
11+
use Sybio\PermissionBundle\Twig\HasPermissionTwigFunction;
12+
use Sybio\PermissionBundle\Twig\PermissionTwigFunction;
913
use Sybio\PermissionBundle\Validation\PermissionConstraintValidator;
1014
use Sybio\PermissionBundle\Validation\PermissionValidator;
1115
use Sybio\PermissionBundle\Validation\PermissionValidatorInterface;
@@ -70,10 +74,39 @@ public function load(
7074
->addTag('security.voter'),
7175
);
7276

77+
/** @see PermissionDecisionFactory */
78+
$container->setDefinition(
79+
'sybio_permission.result_factory',
80+
(new Definition(PermissionDecisionFactory::class))
81+
->setPublic(false),
82+
);
83+
84+
/** @see HasPermissionTwigFunction */
85+
$container->setDefinition(
86+
'sybio_permission.twig.has_permission',
87+
(new Definition(HasPermissionTwigFunction::class))
88+
->setArgument(0, new Reference('security.authorization_checker'))
89+
->setArgument(1, $container->getParameter('sybio_permission.security_attribute'))
90+
->addTag('twig.extension')
91+
->setPublic(false),
92+
);
93+
94+
/** @see PermissionTwigFunction */
95+
$container->setDefinition(
96+
'sybio_permission.twig.permission',
97+
(new Definition(PermissionTwigFunction::class))
98+
->setArgument(0, new Reference('security.authorization_checker'))
99+
->setArgument(1, new Reference('sybio_permission.validator'))
100+
->setArgument(2, new Reference('sybio_permission.result_factory'))
101+
->addTag('twig.extension')
102+
->setPublic(false),
103+
);
104+
73105
$container->setAlias(PermissionConstraintValidator::class, 'sybio_permission.constraint_validator');
74106
$container->setAlias(PermissionValidatorInterface::class, 'sybio_permission.validator');
75107
$container->setAlias(PermissionValidator::class, 'sybio_permission.validator');
76108
$container->setAlias(PermissionVoter::class, 'sybio_permission.voter');
109+
$container->setAlias(PermissionDecisionFactoryInterface::class, 'sybio_permission.result_factory');
77110
}
78111

79112
public function getAlias(): string
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sybio\PermissionBundle\Twig;
6+
7+
use Sybio\PermissionBundle\PermissionInterface;
8+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
9+
use Twig\Extension\AbstractExtension;
10+
use Twig\TwigFunction;
11+
12+
class HasPermissionTwigFunction extends AbstractExtension
13+
{
14+
public function __construct(
15+
private readonly AuthorizationCheckerInterface $authorizationChecker,
16+
) {
17+
}
18+
19+
public function getFunctions(): array
20+
{
21+
return [
22+
new TwigFunction('has_permission', $this->hasPermission(...)),
23+
];
24+
}
25+
26+
/**
27+
* @param class-string<PermissionInterface> $permissionClass
28+
*/
29+
public function hasPermission(
30+
string $permissionClass,
31+
mixed ...$arguments,
32+
): bool {
33+
$permission = new $permissionClass(...$arguments);
34+
35+
return $this->authorizationChecker->isGranted(
36+
$permissionClass,
37+
$permission,
38+
);
39+
}
40+
}

0 commit comments

Comments
 (0)