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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/Pollora/framework/compare/v13.36.1...develop)

### Added
- A `Role` facade declares roles and capabilities the fluent way, besides `#[Role]` and `#[ModifyRole]`: `Role::make('event_manager', 'Event manager')->inherits('author')->grantsPostType(Event::class, Access::Editor)->grants('export_attendees')->without('publish_posts')`, `Role::modify('editor')` and `Role::capabilitySet(EventCap::class)`. The names are those of the attributes and of their arguments, and the same code checks both, so a mistake is refused with the same reason, naming the call (`Role::make('x') at file:line`). There is nothing to register: the declarations are checked when WordPress builds its roles. It comes with `pollora/roles` 0.2

### Changed
- `RoleServiceProvider` binds `RoleRegistry` with its `RoleDefinitionBuilder` and adds `RoleDeclarer`, the service the facade forwards to

## [v13.36.1](https://github.com/Pollora/framework/compare/v13.36.0...v13.36.1) - 2026-10-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"pollora/meta": "^0.1",
"pollora/hook": "^2.0.1",
"pollora/option": "^1.1",
"pollora/roles": "^0.1",
"pollora/roles": "^0.2",
"pollora/schedule": "^1.0",
"pollora/wp-cli": "^0.1 || ^1.0",
"pollora/wp-rest": "^0.1 || ^1.0",
Expand Down
15 changes: 14 additions & 1 deletion src/Role/Infrastructure/Laravel/RoleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
use Pollora\Hook\Domain\Contract\Action;
use Pollora\Hook\Domain\Contract\Filter;
use Pollora\Role\Application\Services\CapabilityOwnerReader;
use Pollora\Role\Application\Services\RoleDeclarer;
use Pollora\Role\Application\Services\RoleDefinitionBuilder;
use Pollora\Role\Application\Services\RoleRegistry;
use Pollora\Role\Domain\Contracts\RoleStoreInterface;
use Pollora\Role\Domain\Contracts\RoleUsageInterface;
use Pollora\Role\Domain\Services\PostTypeCapabilityMap;
use Pollora\Role\Domain\Services\RoleCompiler;
use Pollora\Role\Infrastructure\Adapters\WordPressLateRefresh;
use Pollora\Role\Infrastructure\Adapters\WordPressRoleInjector;
use Pollora\Role\Infrastructure\Adapters\WordPressRoleLabelTranslator;
use Pollora\Role\Infrastructure\Adapters\WordPressRoleStore;
Expand Down Expand Up @@ -48,7 +50,6 @@ public function register(): void
$this->app->singleton(CapabilityOwnerReader::class);
$this->app->singleton(PostTypeCapabilityMap::class);
$this->app->singleton(RoleCompiler::class);
$this->app->singleton(RoleRegistry::class);
$this->app->singleton(WordPressRoleLabelTranslator::class);
$this->app->singleton(RoleUsageInterface::class, WordPressRoleUsage::class);
$this->app->singleton(RoleStoreInterface::class, WordPressRoleStore::class);
Expand All @@ -58,6 +59,18 @@ public function register(): void
$this->superRoles(),
));

// Explicit: the registry checks the fluent declarations with this builder.
$this->app->singleton(RoleRegistry::class, fn (Application $app): RoleRegistry => new RoleRegistry(
$app->make(CapabilityOwnerReader::class),
$app->make(PostTypeCapabilityMap::class),
$app->make(RoleDefinitionBuilder::class),
));

$this->app->singleton(RoleDeclarer::class, fn (Application $app): RoleDeclarer => new RoleDeclarer(
$app->make(RoleRegistry::class),
(new WordPressLateRefresh($app->make(WordPressRoleInjector::class)))->__invoke(...),
));

$this->app->singleton(WordPressRoleInjector::class, fn (Application $app): WordPressRoleInjector => new WordPressRoleInjector(
$app->make(RoleRegistry::class),
$app->make(RoleCompiler::class),
Expand Down
42 changes: 42 additions & 0 deletions src/Support/Facades/Role.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Pollora\Support\Facades;

use BackedEnum;
use Illuminate\Support\Facades\Facade;
use Pollora\Role\Application\Services\RoleDeclaration;
use Pollora\Role\Application\Services\RoleDeclarer;

/**
* Facade for declaring roles and capabilities in code, the fluent way.
*
* The same roles as the `#[Role]` and `#[ModifyRole]` attributes, checked by
* the same code. There is nothing to register: a declaration is checked and
* handed to WordPress when it builds its roles.
*
* ```php
* Role::make('event_manager', 'Event manager')
* ->inherits('author')
* ->grantsPostType(Event::class, Access::Editor)
* ->grants('export_attendees')
* ->without('publish_posts');
* ```
*
* @method static RoleDeclaration make(string $slug, string $label) Declare a role
* @method static RoleDeclaration modify(string $slug) Adjust a role WordPress already has
* @method static void capabilitySet(class-string<BackedEnum>|list<string> $capabilities) Declare capabilities of the project
*
* @see RoleDeclarer
*/
class Role extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return RoleDeclarer::class;
}
}
88 changes: 88 additions & 0 deletions tests/Feature/Extraction/RolesContractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,32 @@
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Application;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Facade;
use Illuminate\View\Compilers\BladeCompiler;
use Pollora\Hook\Domain\Contract\Action;
use Pollora\Hook\Domain\Contract\Filter;
use Pollora\Role\Application\Services\CapabilityOwnerReader;
use Pollora\Role\Application\Services\RoleDeclarer;
use Pollora\Role\Application\Services\RoleDefinitionBuilder;
use Pollora\Role\Application\Services\RoleRegistry;
use Pollora\Role\Domain\Enums\Access;
use Pollora\Role\Domain\Services\PostTypeCapabilityMap;
use Pollora\Role\Domain\Services\RoleCompiler;
use Pollora\Role\Infrastructure\Adapters\WordPressRoleInjector;
use Pollora\Role\Infrastructure\Laravel\EnsureUserHasRole;
use Pollora\Role\Infrastructure\Laravel\RoleServiceProvider;
use Pollora\Support\Facades\Role as RoleFacade;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Tests\Unit\Role\Fixtures\EditorAdjustments;
use Tests\Unit\Role\Fixtures\Event;
use Tests\Unit\Role\Fixtures\EventCap;
use Tests\Unit\Role\Fixtures\EventManager;
use Tests\Unit\Role\Fixtures\Genre;
use Tests\Unit\Role\Fixtures\Venue;

require_once dirname(__DIR__, 2).'/Unit/Role/Fixtures/wordpress-roles.php';

/**
* Roles declared in code, from the attributes to the capabilities WordPress stores, with the real provider, the
* real Laravel container and the real compiler; only WordPress's own role storage is out of the picture (the
Expand Down Expand Up @@ -96,3 +106,81 @@
expect($router->getMiddleware()['role'] ?? null)->toBe(EnsureUserHasRole::class)
->and($blade->compileString("@role('editor') shown @endrole"))->toContain('endif');
});

it('declares roles with the fluent API through the container and the facade, to the same roles as the attributes', function (): void {
Facade::clearResolvedInstances();
Facade::setFacadeApplication($this->app);

try {
$registry = $this->app->make(RoleRegistry::class);
$registry->addPostType(Event::class);
$registry->addPostType(Venue::class);
$registry->addTaxonomy(Genre::class);

RoleFacade::make('event_manager', 'Event manager')
->inherits('author')
->grantsPostType(Event::class, Access::Editor)
->grantsPostType('venue', Access::Author)
->grantsTaxonomy(Genre::class)
->grants(EventCap::ExportAttendees, 'scan_tickets')
->grants('upload_files')
->without('publish_posts');
RoleFacade::modify('editor')
->grantsPostType(Event::class, Access::Contributor)
->grants(EventCap::ScanTickets)
->without('edit_theme_options');

$declarer = $this->app->make(RoleDeclarer::class);
$viaFacade = $this->app->make(RoleCompiler::class)->compile(
$this->stored,
$registry->definitions(),
$registry->modifications(),
['administrator'],
$registry->declaredCapabilities(),
);

$other = new RoleRegistry(new CapabilityOwnerReader, new PostTypeCapabilityMap);
$other->addPostType(Event::class);
$other->addPostType(Venue::class);
$other->addTaxonomy(Genre::class);
$builder = $this->app->make(RoleDefinitionBuilder::class);
$other->addRole($builder->build(EventManager::class));
$other->addModification($builder->build(EditorAdjustments::class));
$viaAttributes = $this->app->make(RoleCompiler::class)->compile($this->stored, $other->definitions(), $other->modifications(), ['administrator'], $other->declaredCapabilities());

expect($viaFacade)->toBe($viaAttributes)
->and($declarer)->toBe($this->app->make(RoleDeclarer::class))
->and($registry->refusals())->toBe([]);
} finally {
Facade::clearResolvedInstances();
Facade::setFacadeApplication(null);
}
});

it('names the call in the facade that declared a role in what it refuses', function (): void {
Facade::clearResolvedInstances();
Facade::setFacadeApplication($this->app);

try {
RoleFacade::make('editor', 'Editor');

expect($this->app->make(RoleRegistry::class)->refusals()[0])->toStartWith("Role::make('editor') at ".__FILE__.':');
} finally {
Facade::clearResolvedInstances();
Facade::setFacadeApplication(null);
}
});

it('checks fluent declarations in the registry the injector compiles', function (): void {
$this->app->make(RoleDeclarer::class)->make('meddler', 'Meddler')->grants('edit_users');
$this->app->make(RoleDeclarer::class)->make('usher', 'Usher')->inherits('subscriber');

$wpRoles = new WP_Roles;
$wpRoles->roles = $this->stored;

$injector = $this->app->make(WordPressRoleInjector::class);
$injector->inject($wpRoles);

expect($wpRoles->roles)->toHaveKey('usher')->not->toHaveKey('meddler')
->and($injector->warnings()[0])->toContain('allowSensitive: true');
});
Loading