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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Discovery lives in its own package, [`pollora/discovery`](https://github.com/Pollora/discovery), which the framework requires: the engine, its contracts, `DiscoveryManager`, the cache and the `#[SkipDiscovery]` attribute, with their tests and benchmarks. Class names do not change. The framework keeps what needs Laravel: the service provider, `discovery:run` / `discovery:clear`, `DiscoveryRegistrar`, `ServiceProviderDiscovery`, the Laravel adapters, and `HasConfiguringSupport`, which builds post types and taxonomies
- Discovery can run without Laravel's application, the first step towards `pollora/discovery`: `new DiscoveryEngine()` works with no argument. Two contracts let an environment plug in — `InstanceResolverInterface` builds the classes discovery needs (Laravel's container in a Pollora application, `ReflectionInstanceResolver` otherwise) and `DiscoveryRegistrarInterface` adds the environment's discoveries before each run (`DiscoveryRegistrar` keeps scanning the container's `*Discovery` bindings). `Psr16DiscoverCacheDriver` caches scans in any PSR-16 cache
- Post type and taxonomy attributes say what they register: each implements `ConfiguresArguments`, a pure function that returns the registration arguments with its own contribution (`#[HasArchive('library')]` sets `has_archive`, `#[Labels]` merges into the generated labels), and the method attributes (`#[AdminCol]`, `#[RegisterMetaBoxCb]`, `#[MetaBoxCb]`, `#[MetaBoxSanitizeCb]`, `#[UpdateCountCallback]`) implement `ConfiguresArgumentsWithMethod`, the discovery handing them the method as a callback. The `Pollora\Attributes` namespace no longer reaches Laravel or a framework service, which an architecture test keeps: the first step towards `pollora/attributes`, a package to declare attributes without the framework

### Changed
- `DiscoveryEngine`, `DiscoveryCacheManager`, `InstancePool` and `ReflectionCache` take their dependencies instead of reaching for `app()` and `config()`; reading `structure-discoverer.cache` moved to `LaravelDiscoveryCacheManagerFactory`. `DiscoveryRegistrar::registerFromContainer()` is now `registerInto()`. These are infrastructure classes, outside the public contracts (`Pollora\Discovery\Domain\*`), which do not change
- One `DiscoveryCacheManager` is shared by the engine and the `pollora:doctor` discovery cache check, which built its own
- The discovery benchmarks measure memory as PHP allocates it, from a reset peak: they read 2 MB whatever the load
- `#[WpRestRoute\Method]` no longer registers its route: `WpRestRouteRegistrar`, in the WpRest module, does, with the same arguments, permissions and request injection. Default slugs, meta keys and command names come from `Pollora\Attributes\Support\Naming`, which gives the same results as `Str::kebab()` and `Str::snake()`; `Pollora\Support\Slug` uses it

### Deprecated
- `DiscoveryCacheInterface` and `LaravelDiscoveryCache`, removed in 15.0: nothing reads this cache, scans are cached by the Spatie driver given to `DiscoveryCacheManager`
- `Pollora\Attributes\PostType\PostTypeAttribute` and `Pollora\Attributes\Taxonomy\TaxonomyAttribute`, removed in 15.0: no built-in attribute extends them any more; implement `ConfiguresArguments`

### Removed
- `handle()` on `#[Action]`, `#[Filter]`, `#[WpRestRoute]` and `#[WpRestRoute\Method]`, and `Hook` no longer implements `HandlesAttributes`: `HookDiscovery` and `WpRestDiscovery` register hooks and routes themselves. Only the bug below reached `Action::handle()`, and `WpRestRoute::handle()` wrote to a read-only wrapper

### Fixed
- A public `#[Action]` or `#[Filter]` method in a `#[PostType]` or `#[Taxonomy]` class is registered once: the post type and taxonomy discoveries also handed it to `Action::handle()`, which registered it a second time on the discovery's configuration object, so the page firing that hook failed with `call_user_func(): Argument #1 ($callback) must be a valid callback, class Pollora\PostType\Infrastructure\Services\PostTypeConfiguration does not have a method …`

## [v13.35.6](https://github.com/Pollora/framework/compare/v13.35.5...v13.35.6) - 2026-10-12

Expand Down
18 changes: 0 additions & 18 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,6 @@ parameters:
count: 1
path: src/Asset/Infrastructure/Services/ViteManager.php

-
message: '#^Access to an undefined property Pollora\\Attributes\\Attributable\:\:\$classPermission\.$#'
identifier: property.notFound
count: 1
path: src/Attributes/WpRestRoute/Method.php

-
message: '#^Access to an undefined property Pollora\\Attributes\\Attributable\:\:\$namespace\.$#'
identifier: property.notFound
count: 1
path: src/Attributes/WpRestRoute/Method.php

-
message: '#^Access to an undefined property Pollora\\Attributes\\Attributable\:\:\$route\.$#'
identifier: property.notFound
count: 2
path: src/Attributes/WpRestRoute/Method.php

-
message: '#^Constant AUTH_COOKIE not found\.$#'
identifier: constant.notFound
Expand Down
10 changes: 3 additions & 7 deletions src/Attributes/Ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@
namespace Pollora\Attributes;

use Attribute;
use Pollora\Abilities\Domain\Contracts\AbilityHandler;
use Pollora\Abilities\Domain\Model\Behaviour;
use Pollora\Ability\Infrastructure\Services\AbilityDiscovery;

/**
* Attribute for declarative WordPress ability registration.
*
* Place this attribute on a class implementing {@see AbilityHandler}. The class
* is discovered and registered with the WordPress Abilities API through the
* {@see AbilityDiscovery} system, and published to AI clients — an MCP server,
* Place this attribute on a class implementing `AbilityHandler` (pollora/abilities).
* The class is discovered and registered with the WordPress Abilities API, and published to AI clients — an MCP server,
* the core abilities REST controllers — by whatever consumes them.
*
* The attribute carries what the ability *is*; the handler carries what it
Expand All @@ -33,8 +30,7 @@
* The category must exist. Declare it once — in a service provider, through the
* `Ability` facade — before the abilities that file under it.
*
* @see AbilityHandler The interface the decorated class implements.
* @see Behaviour How the ability's effect on the site is described to clients.
* @see Behaviour How the ability's effect on the site is described to clients.
*/
#[Attribute(Attribute::TARGET_CLASS)]
class Ability
Expand Down
66 changes: 1 addition & 65 deletions src/Attributes/Action.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@
namespace Pollora\Attributes;

use Attribute;
use Pollora\Hook\Application\Services\AsyncDeclarationFailures;
use Pollora\Hook\Domain\Contract\Action as ActionService;
use Pollora\Hook\Infrastructure\Services\AsyncAttributeRegistrar;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use ReflectionMethod;

/**
* Class Action
Expand All @@ -19,62 +13,4 @@
* This class is used to define an action hook in WordPress.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Action extends Hook
{
/**
* Handle the attribute processing.
*
* @param object $serviceLocator Service locator used to resolve dependencies
* @param object $instance The instance being processed
* @param ReflectionMethod|ReflectionClass $context The reflection context
* @param object $attribute The attribute instance
*/
public function handle(
$serviceLocator,
object $instance,
ReflectionMethod|ReflectionClass $context,
object $attribute,
): void {
// Retrieve the Action service from the locator
$actionService = $serviceLocator->get(ActionService::class);
if (! $actionService) {
return;
}

// #[Action] targets methods only
if (! $context instanceof ReflectionMethod) {
return;
}

// Asynchronously when the method or its class carries #[Async]
$logger = $this->optional($serviceLocator, LoggerInterface::class);
$failures = $this->optional($serviceLocator, AsyncDeclarationFailures::class);

(new AsyncAttributeRegistrar($actionService, $logger, $failures))->register(
$attribute->hook,
$instance,
$context,
$attribute->priority,
$context->getNumberOfParameters()
);
}

/**
* A service the service locator may not provide: the logger, the record of ignored #[Async].
*
* @template T of object
*
* @param class-string<T> $class
* @return T|null
*/
private function optional(object $serviceLocator, string $class): ?object
{
try {
$service = $serviceLocator->get($class);
} catch (\Throwable) {
return null;
}

return $service instanceof $class ? $service : null;
}
}
class Action extends Hook {}
4 changes: 1 addition & 3 deletions src/Attributes/Ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@

use Attribute;
use Pollora\Ajax\Domain\Model\AjaxAccess;
use Pollora\Ajax\Infrastructure\Services\AjaxDiscovery;

/**
* Attribute for declarative WordPress AJAX action registration.
*
* Place this attribute on a public method to register it as a WordPress AJAX
* handler. The method is automatically discovered and wired via the
* {@see AjaxDiscovery} system.
* handler. The method is discovered and wired automatically.
*
* By default, the action is restricted to **logged-in users** (security-by-default).
* Use the `access` parameter to change targeting.
Expand Down
24 changes: 24 additions & 0 deletions src/Attributes/Contracts/ConfiguresArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Pollora\Attributes\Contracts;

/**
* An attribute that contributes to the arguments a post type or a taxonomy is
* registered with.
*
* The contribution is a pure function of the attribute's own values: the
* discovery hands it the arguments gathered so far and registers what it
* returns, so reading the attribute alone tells what it changes.
*/
interface ConfiguresArguments
{
/**
* Return the registration arguments with this attribute's contribution.
*
* @param array<string, mixed> $arguments The arguments gathered so far
* @return array<string, mixed>
*/
public function configureArguments(array $arguments): array;
}
24 changes: 24 additions & 0 deletions src/Attributes/Contracts/ConfiguresArgumentsWithMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Pollora\Attributes\Contracts;

/**
* A method attribute that contributes to the arguments a post type or a
* taxonomy is registered with, using the method it decorates as a callback.
*
* The discovery builds the callback, an instance of the declaring class and
* the method name, and the attribute says where it goes.
*/
interface ConfiguresArgumentsWithMethod
{
/**
* Return the registration arguments with this attribute's contribution.
*
* @param array<string, mixed> $arguments The arguments gathered so far
* @param callable $method The decorated method, bound to an instance of its class
* @return array<string, mixed>
*/
public function configureArguments(array $arguments, callable $method): array;
}
34 changes: 1 addition & 33 deletions src/Attributes/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
namespace Pollora\Attributes;

use Attribute;
use Pollora\Hook\Domain\Contract\Filter as FilterService;
use ReflectionClass;
use ReflectionMethod;

/**
* Class Filter
Expand All @@ -16,33 +13,4 @@
* This class is used to define a filter hook in WordPress.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Filter extends Hook
{
/**
* Handle the attribute processing.
*
* @param object $serviceLocator Service locator used to resolve dependencies
* @param object $instance The instance being processed
* @param ReflectionMethod|ReflectionClass $context The reflection context
* @param object $attribute The attribute instance
*/
public function handle(
$serviceLocator,
object $instance,
ReflectionMethod|ReflectionClass $context,
object $attribute
): void {
// Retrieve the Filter service from the locator
$filterService = $serviceLocator->get(FilterService::class);
if (! $filterService) {
return;
}

$filterService->add(
$attribute->hook,
[$instance, $context->getName()],
$attribute->priority,
$context->getNumberOfParameters()
);
}
}
class Filter extends Hook {}
4 changes: 1 addition & 3 deletions src/Attributes/Hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@

namespace Pollora\Attributes;

use Pollora\Attributes\Contracts\HandlesAttributes;

/**
* Class Hook
*
* Abstract base class for WordPress hook attributes.
* Provides common properties for hook attributes.
*/
abstract class Hook implements HandlesAttributes
abstract class Hook
{
/**
* Constructor for the Hook class.
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Pollora\Attributes;

use Attribute;
use Illuminate\Support\Str;
use Pollora\Attributes\Support\Naming;
use Pollora\Meta\Domain\Enums\Control;

/**
Expand Down Expand Up @@ -71,6 +71,6 @@ public function __construct(
*/
public function resolveKey(string $property): string
{
return $this->key ?? Str::snake($property);
return $this->key ?? Naming::snake($property);
}
}
4 changes: 2 additions & 2 deletions src/Attributes/PostType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Pollora\Attributes;

use Attribute;
use Illuminate\Support\Str;
use Pollora\Attributes\Support\Naming;

/**
* PostType Attribute
Expand Down Expand Up @@ -51,6 +51,6 @@ public function __construct(
*/
public function resolveSlug(string $className): string
{
return $this->slug ?? substr(Str::kebab(class_basename($className)), 0, 20);
return $this->slug ?? substr(Naming::kebab(Naming::classBasename($className)), 0, 20);
}
}
37 changes: 11 additions & 26 deletions src/Attributes/PostType/AdminCol.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
namespace Pollora\Attributes\PostType;

use Attribute;
use Pollora\Attributes\Contracts\HandlesAttributes;
use Pollora\PostType\Domain\Contracts\PostTypeAttributeInterface;
use ReflectionClass;
use ReflectionMethod;
use Pollora\Attributes\Contracts\ConfiguresArgumentsWithMethod;

/**
* Attribute to define an admin column for a post type.
Expand All @@ -19,7 +16,7 @@
* @Attribute
*/
#[Attribute(Attribute::TARGET_METHOD)]
class AdminCol implements HandlesAttributes
class AdminCol implements ConfiguresArgumentsWithMethod
{
/**
* Constructor.
Expand Down Expand Up @@ -57,32 +54,20 @@ public function __construct(
) {}

/**
* Handle the attribute processing.
* Add this column to the admin_cols argument, rendered by the decorated method.
*
* @param object $container The container instance
* @param object $instance The instance being processed
* @param ReflectionMethod|ReflectionClass $context The method the attribute is applied to
* @param object $attribute The attribute instance
* @param array<string, mixed> $arguments The registration arguments gathered so far
* @param callable $method The decorated method
* @return array<string, mixed>
*/
public function handle($container, object $instance, ReflectionMethod|ReflectionClass $context, object $attribute): void
public function configureArguments(array $arguments, callable $method): array
{
if (! $instance instanceof PostTypeAttributeInterface) {
return;
}

// Initialize admin_cols array if it doesn't exist
$adminCols = $instance->getArg('admin_cols', []);

// Build column configuration based on extended-cpts options
$columnConfig = [
'title' => $this->title,
'function' => $method,
];

// Add custom function callback if this is a method-level attribute
if ($context instanceof ReflectionMethod) {
$columnConfig['function'] = [new $context->class, $context->getName()];
}

// Add optional configuration based on extended-cpts features
if ($this->titleIcon !== null) {
$columnConfig['title_icon'] = $this->titleIcon;
Expand Down Expand Up @@ -133,8 +118,8 @@ public function handle($container, object $instance, ReflectionMethod|Reflection
$columnConfig['sortable'] = $this->sortable;
}

// Add the column to the admin_cols array
$adminCols[$this->key] = $columnConfig;
$instance->setArg('admin_cols', $adminCols);
$arguments['admin_cols'] = [...$arguments['admin_cols'] ?? [], $this->key => $columnConfig];

return $arguments;
}
}
Loading
Loading