Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(twig): functions has_permission() and permission()
  • Loading branch information
Sybio committed Dec 27, 2025
commit 2c4f01829bef696166a9e4028eef923472bc4916
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) %}
<button>Edit Article</button>
{% else %}
<p>You are not authorized to edit this article.</p>
{% 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 %}
<button>Edit Article</button>
{% else %}
<p>Access denied. Reasons:</p>
<ul>
{% for violation in decision.violations %}
<li>{{ violation.message }}</li>
{% endfor %}
</ul>
{% 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:
Expand Down
3 changes: 1 addition & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
26 changes: 26 additions & 0 deletions src/Decision/PermissionDecision.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Sybio\PermissionBundle\Decision;

use Symfony\Component\Validator\ConstraintViolationListInterface;

final readonly class PermissionDecision implements PermissionDecisionInterface
{
public function __construct(
private bool $granted,
private ConstraintViolationListInterface $violations,
) {
}

public function isGranted(): bool
{
return $this->granted;
}

public function getViolations(): ConstraintViolationListInterface
{
return $this->violations;
}
}
20 changes: 20 additions & 0 deletions src/Decision/PermissionDecisionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Sybio\PermissionBundle\Decision;

use Symfony\Component\Validator\ConstraintViolationListInterface;

final class PermissionDecisionFactory implements PermissionDecisionFactoryInterface
{
public function createDecision(
bool $granted,
ConstraintViolationListInterface $violations,
): PermissionDecisionInterface {
return new PermissionDecision(
$granted,
$violations,
);
}
}
15 changes: 15 additions & 0 deletions src/Decision/PermissionDecisionFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Sybio\PermissionBundle\Decision;

use Symfony\Component\Validator\ConstraintViolationListInterface;

interface PermissionDecisionFactoryInterface
{
public function createDecision(
bool $granted,
ConstraintViolationListInterface $violations,
): PermissionDecisionInterface;
}
14 changes: 14 additions & 0 deletions src/Decision/PermissionDecisionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Sybio\PermissionBundle\Decision;

use Symfony\Component\Validator\ConstraintViolationListInterface;

interface PermissionDecisionInterface
{
public function isGranted(): bool;

public function getViolations(): ConstraintViolationListInterface;
}
33 changes: 33 additions & 0 deletions src/DependencyInjection/PermissionBundleConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@

namespace Sybio\PermissionBundle\DependencyInjection;

use Sybio\PermissionBundle\Decision\PermissionDecisionFactory;
use Sybio\PermissionBundle\Decision\PermissionDecisionFactoryInterface;
use Sybio\PermissionBundle\PermissionCheckerInterface;
use Sybio\PermissionBundle\Security\PermissionVoter;
use Sybio\PermissionBundle\Twig\HasPermissionTwigFunction;
use Sybio\PermissionBundle\Twig\PermissionTwigFunction;
use Sybio\PermissionBundle\Validation\PermissionConstraintValidator;
use Sybio\PermissionBundle\Validation\PermissionValidator;
use Sybio\PermissionBundle\Validation\PermissionValidatorInterface;
Expand Down Expand Up @@ -70,10 +74,39 @@ public function load(
->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
Expand Down
40 changes: 40 additions & 0 deletions src/Twig/HasPermissionTwigFunction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Sybio\PermissionBundle\Twig;

use Sybio\PermissionBundle\PermissionInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class HasPermissionTwigFunction extends AbstractExtension
{
public function __construct(
private readonly AuthorizationCheckerInterface $authorizationChecker,
) {
}

public function getFunctions(): array
{
return [
new TwigFunction('has_permission', $this->hasPermission(...)),
];
}

/**
* @param class-string<PermissionInterface> $permissionClass
*/
public function hasPermission(
string $permissionClass,
mixed ...$arguments,
): bool {
$permission = new $permissionClass(...$arguments);

return $this->authorizationChecker->isGranted(
$permissionClass,
$permission,
);
}
}
51 changes: 51 additions & 0 deletions src/Twig/PermissionTwigFunction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Sybio\PermissionBundle\Twig;

use Sybio\PermissionBundle\Decision\PermissionDecisionFactoryInterface;
use Sybio\PermissionBundle\Decision\PermissionDecisionInterface;
use Sybio\PermissionBundle\PermissionInterface;
use Sybio\PermissionBundle\Validation\PermissionValidatorInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class PermissionTwigFunction extends AbstractExtension
{
public function __construct(
private readonly AuthorizationCheckerInterface $authorizationChecker,
private readonly PermissionValidatorInterface $permissionValidator,
private readonly PermissionDecisionFactoryInterface $permissionDecisionFactory,
) {
}

public function getFunctions(): array
{
return [
new TwigFunction('permission', $this->permission(...)),
];
}

/**
* @param class-string<PermissionInterface> $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,
);
}
}