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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- An `AssetEnqueuer` destroyed after its application, as in a test, no longer throws while it reports a failure of its deferred enqueue
- The typed meta classes that will leave for `pollora/meta` no longer reach for Laravel: `MetaUiDrivers` and `MetaAccessor::extend()` take a PSR-11 `ContainerInterface` (the Laravel container is one, a driver closure receives the same object), and the key of a data object's property is derived by `Pollora\Attributes\Support\Naming` instead of `Str::snake()`. What needs Laravel moved to `Pollora\Meta\Infrastructure\Laravel`: `MetaServiceProvider`, `LaravelMetaValidator`, `TypedMetaCheck`, `MetaListCommand` and `MetaAuditCommand`. The rendered pages, the REST responses and the output of the `pollora:meta:*`, `pollora:binding:list`, `pollora:roles:*` and `pollora:doctor` commands of a Pollora site are unchanged
- The roles classes that will leave for `pollora/roles` no longer reach for Laravel: the label of a `#[Role]` without one comes from `Naming::headline()` (the same words as `Str::headline()`) and `Naming::classBasename()`. What needs Laravel moved to `Pollora\Role\Infrastructure\Laravel`: `RoleServiceProvider`, the `role` middleware `EnsureUserHasRole`, the `@role` directive `RoleDirective`, the `RolesCheck` of `pollora:doctor` and the `pollora:roles:*` and `pollora:make:role` commands with their stubs. Roles, capabilities and command outputs of a Pollora site are unchanged
- The Block Bindings classes that will leave for `pollora/block-bindings` no longer reach for Laravel: the resolver takes a PSR-11 container, and three contracts carry what Laravel did for it, `MethodInvokerInterface` (the method of a `#[BlockBinding]` source gets the services its typed parameters name; `LaravelMethodInvoker` calls through Laravel's container, `ReflectionMethodInvoker` by reflection) `BindingEventsInterface` (`BindingResolved` reaches Laravel's dispatcher through `LaravelBindingEvents`) and `PostFinderInterface` (`BindingContext::post()` loads the framework's `Pollora\Models\Post` through `LaravelPostFinder`, Colt's `Post` without it, and is typed as Colt's `Post`, which the framework's extends). The options a project lists in `block-bindings.options` reach `OptionSource` and the editor data as an array, read from the config by the provider. Labels and names come from `Naming`, as `Str` writes them. What needs Laravel moved to `Pollora\BlockBinding\Infrastructure\Laravel`: the provider, the doctor check, `pollora:binding:list` and `pollora:make:binding` with their stub. Pages, REST responses and command outputs of a Pollora site are unchanged

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

Expand Down
39 changes: 22 additions & 17 deletions src/BlockBinding/Application/Services/BindingResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@

namespace Pollora\BlockBinding\Application\Services;

use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Support\Htmlable;
use Pollora\BlockBinding\Domain\Contracts\BindingEventsInterface;
use Pollora\BlockBinding\Domain\Contracts\ContentVisibilityInterface;
use Pollora\BlockBinding\Domain\Contracts\MethodInvokerInterface;
use Pollora\BlockBinding\Domain\Contracts\PostFinderInterface;
use Pollora\BlockBinding\Domain\Contracts\ValuePresenterInterface;
use Pollora\BlockBinding\Domain\Enums\BindingFieldType;
use Pollora\BlockBinding\Domain\Events\BindingResolved;
use Pollora\BlockBinding\Domain\Models\BindingContext;
use Pollora\BlockBinding\Domain\Models\BindingFieldDefinition;
use Pollora\BlockBinding\Domain\Models\BindingSource;
use Pollora\BlockBinding\Infrastructure\Adapters\ReflectionMethodInvoker;
use Pollora\Meta\Application\Services\MetaAccessor;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Stringable;
use Throwable;
Expand Down Expand Up @@ -54,14 +57,21 @@ final class BindingResolver
*/
private array $instances = [];

private readonly MethodInvokerInterface $invoker;

public function __construct(
private readonly Container $container,
private readonly ContainerInterface $container,
private readonly ContentVisibilityInterface $visibility,
private readonly ValuePresenterInterface $presenter,
private readonly ?MetaAccessor $metaAccessor = null,
private readonly bool $debug = false,
private readonly ?LoggerInterface $logger = null,
) {}
?MethodInvokerInterface $invoker = null,
private readonly ?BindingEventsInterface $events = null,
private readonly ?PostFinderInterface $posts = null,
) {
$this->invoker = $invoker ?? new ReflectionMethodInvoker($container);
}

/**
* The value of a bound attribute, or null to keep what the block holds.
Expand Down Expand Up @@ -133,17 +143,11 @@ private function resolveIn(BindingSource $source, array $args, array $blockConte
*/
private function announce(BindingSource $source, BindingFieldDefinition $field, BindingContext $context, float $milliseconds, bool $cached, bool $hasValue): void
{
if (! $this->container->bound('events')) {
return;
}

$events = $this->container->make('events');

if (! $events->hasListeners(BindingResolved::class)) {
if (! $this->events instanceof BindingEventsInterface || ! $this->events->isListening()) {
return;
}

$events->dispatch(new BindingResolved(
$this->events->dispatch(new BindingResolved(
source: $source->name,
field: $field->name,
attribute: $context->attribute,
Expand All @@ -169,6 +173,7 @@ private function context(array $args, array $blockContext, ?\WP_Block $block, st
taxonomy: is_string($blockContext['taxonomy'] ?? null) ? $blockContext['taxonomy'] : null,
block: $block,
metaAccessor: $this->metaAccessor,
postFinder: $this->posts,
);
}

Expand All @@ -184,14 +189,14 @@ private function isVisible(BindingContext $context): bool
private function call(BindingSource $source, BindingFieldDefinition $field, BindingContext $context): mixed
{
try {
$instance = $this->instances[$source->class] ??= $this->container->make($source->class);
$instance = $this->instances[$source->class] ??= $this->container->get($source->class);

if (! $this->debug) {
return $this->container->call([$instance, $field->method], [BindingContext::class => $context, 'context' => $context]);
return $this->invoker->call($instance, $field->method, [BindingContext::class => $context, 'context' => $context]);
}

$start = hrtime(true);
$value = $this->container->call([$instance, $field->method], [BindingContext::class => $context, 'context' => $context]);
$value = $this->invoker->call($instance, $field->method, [BindingContext::class => $context, 'context' => $context]);
$this->reportSlow($source, $field, $context, (hrtime(true) - $start) / 1_000_000);

return $value;
Expand Down Expand Up @@ -239,8 +244,8 @@ private function present(mixed $value, BindingFieldType $type, ?string $attribut

$inHtml = in_array($attributeSource, self::HTML_SOURCES, true);

if ($value instanceof Htmlable) {
return $this->presenter->richText($value->toHtml());
if (is_object($value) && method_exists($value, 'toHtml')) {
return $this->presenter->richText((string) $value->toHtml());
}

if (is_bool($value)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace Pollora\BlockBinding\Application\Services;

use Illuminate\Support\Str;
use Pollora\Attributes\BlockBinding;
use Pollora\Attributes\BlockBinding\BindingField;
use Pollora\Attributes\Support\Naming;
use Pollora\BlockBinding\Domain\Enums\BindingFieldType;
use Pollora\BlockBinding\Domain\Exceptions\InvalidBindingSourceException;
use Pollora\BlockBinding\Domain\Models\BindingFieldDefinition;
Expand Down Expand Up @@ -75,7 +75,7 @@ public function build(string $class): BindingSource

return new BindingSource(
name: $attribute->name,
label: $attribute->label ?? Str::headline($reflection->getShortName()),
label: $attribute->label ?? Naming::headline($reflection->getShortName()),
class: $class,
usesContext: $attribute->usesContext,
postTypes: $attribute->postTypes,
Expand Down Expand Up @@ -105,7 +105,7 @@ private function buildFields(ReflectionClass $reflection): array

$this->assertReturnType($class, $method);

$name = $attribute->name ?? Str::snake($method->getName());
$name = $attribute->name ?? Naming::snake($method->getName());

if (isset($fields[$name])) {
throw InvalidBindingSourceException::forField($class, $method->getName(), sprintf(
Expand All @@ -126,7 +126,7 @@ private function buildFields(ReflectionClass $reflection): array

$fields[$name] = new BindingFieldDefinition(
name: $name,
label: $attribute->label ?? Str::headline($method->getName()),
label: $attribute->label ?? Naming::headline($method->getName()),
type: $type,
method: $method->getName(),
);
Expand Down
21 changes: 21 additions & 0 deletions src/BlockBinding/Domain/Contracts/BindingEventsInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Pollora\BlockBinding\Domain\Contracts;

use Pollora\BlockBinding\Domain\Events\BindingResolved;

/**
* Where the resolver tells debugging tools what it resolved. In a Pollora
* application it is Laravel's event dispatcher; without one, nothing listens.
*/
interface BindingEventsInterface
{
/**
* Whether anything listens to BindingResolved: the event is built only then.
*/
public function isListening(): bool;

public function dispatch(BindingResolved $event): void;
}
19 changes: 19 additions & 0 deletions src/BlockBinding/Domain/Contracts/MethodInvokerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Pollora\BlockBinding\Domain\Contracts;

/**
* Calls the method of a binding source, giving it the parameters it asks for:
* the binding's context by its type or by the name `$context`, and the services
* its other typed parameters name. In a Pollora application it is Laravel's
* container; without Laravel, the reflection invoker of the package.
*/
interface MethodInvokerInterface
{
/**
* @param array<string, mixed> $parameters Values given by parameter name or by class name
*/
public function call(object $instance, string $method, array $parameters): mixed;
}
18 changes: 18 additions & 0 deletions src/BlockBinding/Domain/Contracts/PostFinderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Pollora\BlockBinding\Domain\Contracts;

use Pollora\Colt\Model\Post;

/**
* Loads the post of a block context as a model, for `BindingContext::post()`.
* In a Pollora application it is the framework's `Pollora\Models\Post` (the
* class bound to the post type, with its typed meta accessors); without it,
* Colt's `Post`.
*/
interface PostFinderInterface
{
public function find(int $postId): ?Post;
}
12 changes: 7 additions & 5 deletions src/BlockBinding/Domain/Models/BindingContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
namespace Pollora\BlockBinding\Domain\Models;

use LogicException;
use Pollora\BlockBinding\Domain\Contracts\PostFinderInterface;
use Pollora\Colt\Model\Post;
use Pollora\Meta\Application\Services\MetaAccessor;
use Pollora\Meta\Domain\Enums\MetaObjectType;
use Pollora\Meta\Domain\Models\MetaRecord;
use Pollora\Models\Post;

/**
* What a binding field knows about the block it fills: the post or term of the
Expand Down Expand Up @@ -37,6 +38,7 @@ public function __construct(
public readonly ?string $taxonomy = null,
public readonly ?\WP_Block $block = null,
private readonly ?MetaAccessor $metaAccessor = null,
private readonly ?PostFinderInterface $postFinder = null,
) {}

/**
Expand All @@ -48,12 +50,12 @@ public function arg(string $name, mixed $default = null): mixed
}

/**
* The post of the block context, as a Pollora model (the class bound to its
* post type, when there is one).
* The post of the block context, as a model (the class bound to its post type, when
* there is one; in a Pollora application, the framework's Post model).
*/
public function post(): ?Post
{
$this->post ??= $this->postId === null ? false : (Post::query()->find($this->postId) ?? false);
$this->post ??= $this->postId === null ? false : (($this->postFinder instanceof PostFinderInterface ? $this->postFinder->find($this->postId) : Post::query()->find($this->postId)) ?? false);

return $this->post ?: null;
}
Expand Down Expand Up @@ -84,7 +86,7 @@ private function objectIdFor(MetaObjectType $objectType): ?int

private function authorId(): ?int
{
$author = (int) $this->post()?->post_author;
$author = (int) $this->post()?->getAttribute('post_author');

return $author > 0 ? $author : null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Pollora\BlockBinding\Infrastructure\Adapters;

use InvalidArgumentException;
use Pollora\BlockBinding\Domain\Contracts\MethodInvokerInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;

/**
* Calls a source's method by reflection, for a project without Laravel's
* container. A parameter takes the value given under its name or under its
* class name; another class-typed parameter is asked of the container, if
* there is one, or built when it needs no argument; else its default value, or
* null when it allows it.
*/
final readonly class ReflectionMethodInvoker implements MethodInvokerInterface
{
public function __construct(private ?ContainerInterface $container = null) {}

public function call(object $instance, string $method, array $parameters): mixed
{
$reflection = new ReflectionMethod($instance, $method);
$arguments = [];

foreach ($reflection->getParameters() as $parameter) {
$type = $parameter->getType();
$class = $type instanceof ReflectionNamedType && ! $type->isBuiltin() ? $type->getName() : null;

$arguments[] = match (true) {
array_key_exists($parameter->getName(), $parameters) => $parameters[$parameter->getName()],
$class !== null && array_key_exists($class, $parameters) => $parameters[$class],
$class !== null && ($service = $this->service($class)) !== null => $service,
$parameter->isDefaultValueAvailable() => $parameter->getDefaultValue(),
$type?->allowsNull() === true => null,
default => throw new InvalidArgumentException(sprintf('Unable to resolve the parameter $%s of %s::%s().', $parameter->getName(), $instance::class, $method)),
};
}

return $reflection->invokeArgs($instance, $arguments);
}

/**
* A service the container gives, or an instance of a class that needs
* nothing to be built.
*/
private function service(string $class): ?object
{
if ($this->container?->has($class) === true) {
return $this->container->get($class);
}

try {
return $this->container?->get($class);
} catch (ContainerExceptionInterface) {
// not a service of this container: a class with no required argument is built below
}

$constructor = class_exists($class) ? (new ReflectionClass($class))->getConstructor() : null;

return class_exists($class) && ($constructor === null || $constructor->getNumberOfRequiredParameters() === 0) ? new $class : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Pollora\BlockBinding\Infrastructure\Providers;
namespace Pollora\BlockBinding\Infrastructure\Laravel;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
Expand All @@ -16,7 +16,8 @@
use Pollora\BlockBinding\Infrastructure\Adapters\WordPressBindingRegistry;
use Pollora\BlockBinding\Infrastructure\Adapters\WordPressContentVisibility;
use Pollora\BlockBinding\Infrastructure\Adapters\WordPressValuePresenter;
use Pollora\BlockBinding\Infrastructure\Checks\BlockBindingsCheck;
use Pollora\BlockBinding\Infrastructure\Laravel\Console\BindingListCommand;
use Pollora\BlockBinding\Infrastructure\Laravel\Console\MakeBindingCommand;
use Pollora\BlockBinding\Infrastructure\Services\BindingEditorData;
use Pollora\BlockBinding\Infrastructure\Services\BindingFormatter;
use Pollora\BlockBinding\Infrastructure\Services\BlockBindingDiscovery;
Expand All @@ -25,8 +26,6 @@
use Pollora\BlockBinding\Infrastructure\Sources\PostMetaSource;
use Pollora\BlockBinding\Infrastructure\Sources\TermMetaSource;
use Pollora\BlockBinding\Infrastructure\Sources\TypedMetaReader;
use Pollora\BlockBinding\UI\Console\BindingListCommand;
use Pollora\BlockBinding\UI\Console\MakeBindingCommand;
use Pollora\BlockBinding\UI\Http\ResolveBindingsController;
use Pollora\Doctor\Infrastructure\Providers\DoctorServiceProvider;
use Pollora\Hook\Domain\Contract\Action;
Expand Down Expand Up @@ -63,14 +62,19 @@ public function register(): void
$this->app->singleton(ValuePresenterInterface::class, WordPressValuePresenter::class);
$this->app->singleton(BindingFormatter::class);
$this->app->singleton(TypedMetaReader::class);
$this->app->singleton(BindingEditorData::class);
$this->app->singleton(BindingEditorData::class, fn (Application $app): BindingEditorData => new BindingEditorData(
$app->make(BindingSourceRegistry::class),
$app->make(MetaSchemaRepository::class),
$this->options(),
));
$this->app->singleton(OptionSource::class, fn (): OptionSource => new OptionSource($this->options()));
$this->app->singleton(WordPressBindingEditorScript::class);
$this->app->singleton(ResolveBindingsController::class);

$this->app->singleton(BindingReferenceInspector::class, fn (Application $app): BindingReferenceInspector => new BindingReferenceInspector(
$app->make(BindingSourceRegistry::class),
$app->make(MetaSchemaRepository::class),
array_values(array_filter((array) $app->make('config')->get('block-bindings.options', []), is_string(...))),
$this->options(),
));

$this->app->singleton(BindingResolver::class, fn (Application $app): BindingResolver => new BindingResolver(
Expand All @@ -80,6 +84,9 @@ public function register(): void
$app->make(MetaAccessor::class),
(bool) $app->make('config')->get('app.debug', false),
$app->make(LoggerInterface::class),
$app->make(LaravelMethodInvoker::class),
$app->make(LaravelBindingEvents::class),
$app->make(LaravelPostFinder::class),
));

$this->app->singleton(WordPressBindingRegistry::class, fn (Application $app): WordPressBindingRegistry => new WordPressBindingRegistry(
Expand Down Expand Up @@ -118,4 +125,14 @@ public function boot(): void
$action->add('enqueue_block_editor_assets', fn () => $this->app->make(WordPressBindingEditorScript::class)->enqueue());
$action->add('rest_api_init', fn () => $this->app->make(ResolveBindingsController::class)->register());
}

/**
* The options a project lists as bindable.
*
* @return list<string>
*/
private function options(): array
{
return array_values(array_filter((array) $this->app->make('config')->get('block-bindings.options', []), is_string(...)));
}
}
Loading
Loading