Installation:
composer require bartlomiejbeta/api-scope-bundle
Register the bundle in config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 3):
BartB\APIScopeBundle\APIScopeBundle::class => ['all' => true],
Basic Configuration:
Add to config/packages/api_scope.yaml:
api_scope:
scopes:
api.get_item: # Route name
always_included:
- 'group1'
- 'group2'
supported_key_map:
external1: { internal_name: 'scope.internal_name1' }
First Use Case:
Annotate your API controller method with @ScopeConverter() and @Rest\Route:
use BartB\APIScopeBundle\Annotation\ScopeConverter;
use Nelmio\ApiDocBundle\Annotation\Rest;
/**
* @ScopeConverter()
* @Rest\Route("/items", name="api.get_items")
*/
public function getItems(Request $request, ScopeCollection $scopeCollection): Response
{
// $scopeCollection now contains merged scopes from query params and config
return $this->json($this->serializer->serialize($items, 'json', $scopeCollection));
}
Trigger Scopes via Query: Call your API with query params like:
/items?external1=true
This will add scope.internal_name1 to the serialization groups.
Dynamic Serialization Groups:
Use supported_key_map to map query params to Doctrine serialization groups. Example:
supported_key_map:
user_details: { internal_name: 'user.details' }
admin_data: { internal_name: 'admin.data', security: 'ROLE_ADMIN' }
Call with ?user_details=true&admin_data=true to include both groups.
Security-Constrained Scopes: Leverage Symfony’s security voters to restrict sensitive scopes:
admin_data:
internal_name: 'admin.data'
security: 'can_access_admin_data' # Custom voter
The bundle checks voters before applying the scope.
Always-Included Groups: Define groups that are always included, regardless of query params:
always_included:
- 'public.metadata'
Integration with Serializer:
Inject ScopeCollection into your controller/action and pass it to the serializer:
$data = $this->serializer->serialize($entity, 'json', $scopeCollection);
Route-Specific Scopes:
Configure scopes per route in api_scope.scopes. Example for nested routes:
api.get_user:
always_included: ['user.basic']
supported_key_map:
full_profile: { internal_name: 'user.full_profile' }
user-details) for consistency.always_included for mandatory groups (e.g., ['public.id']).ScopeCollection contents in your controller to verify merged scopes:
dump($scopeCollection->getAll());
Route Name Mismatch:
@Rest\Route names match your config.yaml keys.php bin/console debug:router.Security Voter Failures:
security: 'ROLE_ADMIN'), the bundle silently ignores it if the voter fails.$token = $this->get('security.token_storage')->getToken();
$voter = $this->get('security.authorization_checker');
$voter->isGranted('can_access_admin_data', $entity);
Query Param Case Sensitivity:
?External1=true won’t match external1).supported_key_map or use a router event listener.Serializer Group Conflicts:
internal_name in supported_key_map conflicts with existing groups, the bundle appends them without merging.user.details.v1).Legacy Symfony Versions:
symfony/serializer, symfony/security).Log Scope Merging:
Override the ScopeCollection service to log merged scopes:
# config/services.yaml
BartB\APIScopeBundle\Scope\ScopeCollection:
class: App\Service\DebugScopeCollection
decorates: 'bartb_api_scope.scope_collection'
// src/Service/DebugScopeCollection.php
class DebugScopeCollection extends ScopeCollection
{
public function add($scope)
{
$this->logger->info('Adding scope: '.$scope);
parent::add($scope);
}
}
Validate Config: Use Symfony’s config validator to catch YAML errors early:
php bin/console config:validate api_scope
Custom Scope Providers:
Extend the ScopeProviderInterface to add dynamic scopes (e.g., from headers or JWT claims):
class HeaderScopeProvider implements ScopeProviderInterface
{
public function getScopes(Request $request): array
{
return ['header.group' => $request->headers->get('X-Group')];
}
}
Register as a service and tag it with bartb_api_scope.scope_provider.
Override Scope Merging:
Decorate the ScopeCollection service to modify merging logic:
// src/Service/CustomScopeCollection.php
class CustomScopeCollection extends ScopeCollection
{
public function merge(array $scopes)
{
// Custom logic (e.g., prioritize certain groups)
parent::merge($scopes);
}
}
Add Security Voters: Create a voter for scope-specific permissions:
class CanAccessAdminScopeVoter implements VoterInterface
{
public function vote(TokenInterface $token, $scope, array $attributes)
{
return $token->getUser()->hasRole('ROLE_ADMIN');
}
}
Reference it in supported_key_map:
admin_scope:
internal_name: 'admin.data'
security: 'can_access_admin_scope'
Event Listeners:
Listen to api_scope.scopes_merged to react to scope changes:
// src/EventListener/ScopeListener.php
class ScopeListener
{
public function onScopesMerged(ScopesMergedEvent $event)
{
if ($event->hasScope('user.details')) {
$event->addScope('user.metadata'); // Auto-add related groups
}
}
}
Tag the listener with kernel.event_listener and set the event to bartb_api_scope.scopes_merged.
How can I help you explore Laravel packages today?