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 @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Typed meta for every object (**experimental**): `#[PostMeta('product')]` or `#[PostMeta(['post', 'page'])]` for post types the project does not declare, `#[TermMeta('category')]`, `#[UserMeta]` and `#[CommentMeta]` (every comment type: WordPress has no per-type comment meta). `Meta::of()` reads and writes them by object ID; a key two classes declare for the same objects is refused at discovery
- Typed meta on the Eloquent models (**experimental**): `Pollora\Models\Post`, `Page`, `Term`, `User` and `Comment` read the `#[Meta]` their object carries as attributes, with their PHP type (`$event->capacity`, or by key `$event->sold_out`), check a write at once and store it through `update_metadata()` when the model is saved; `whereMeta('capacity', '>=', 100)` compares numbers as numbers. A post model with `protected $postType = 'event'` is bound to its post type at discovery, so `Post::find()` returns it. A class known to carry typed meta stops eager loading the `meta` relation and primes WordPress's meta cache once per collection (one query instead of the relation); other models and undeclared keys keep Colt's behaviour
- Roles in Laravel (**experimental**), a role being named by its slug or by the class of a `#[Role]`: `hasRole()`, `assignRole()`, `removeRole()` and `roles()` on `Pollora\Models\User` (trait `HasRoles`; writes go through `WP_User`, an unknown role is refused); the `role:` route middleware (`role:event_manager,editor`, or `EnsureUserHasRole::using(EventManager::class)`), which refuses with a 403 a user who has none of the roles, an alias the application already uses being kept; `@role` accepts role classes (`@role(EventManager::class)`) and keeps the behaviour of Sage Directives' `@role` for slugs
- REST permission `Can` for `#[WpRestRoute]` and `#[Method]`: `permissionCallback: new Can('edit_posts')`, a `#[CapabilitySet]` enum case, or `new Can('edit_post', parameter: 'id')` to check a meta capability on the object in the request. `permissionCallback` now accepts a `Permission` instance as well as a class name; a refusal answers 401 to a guest and 403 to a logged-in user
- Translated role labels: `#[Role(…, textDomain: 'my-theme')]` translates the label with that domain wherever WordPress shows role names (`translate_user_role()`: users list, role dropdowns). Translation happens when the admin displays the role, through `gettext_with_context_default`, so no translation is loaded early; a label WordPress already translates is left alone
Expand Down
6 changes: 6 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Rector\Config\RectorConfig;
use Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ScalarParamTypeByMethodCallTypeRector;
use RectorLaravel\Rector\ArrayDimFetch\ServerVariableToRequestFacadeRector;
use RectorLaravel\Rector\MethodCall\ContainerBindConcreteWithClosureOnlyRector;
use RectorLaravel\Set\LaravelLevelSetList;
Expand All @@ -16,6 +17,11 @@
])
->withSkip([
AddOverrideAttributeToOverriddenMethodsRector::class,
// getAttribute($key) and setAttribute($key) override Eloquent's untyped
// signatures: typing $key would be a fatal incompatible declaration.
ScalarParamTypeByMethodCallTypeRector::class => [
__DIR__.'/src/Models/Concerns/HasTypedMeta.php',
],
ContainerBindConcreteWithClosureOnlyRector::class => [
__DIR__.'/src/Hook/Infrastructure/Providers/HookServiceProvider.php',
],
Expand Down
9 changes: 8 additions & 1 deletion src/Meta/Application/Services/MetaAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Pollora\Meta\Domain\Exceptions\InvalidMetaValueException;
use Pollora\Meta\Domain\Models\MetaDefinition;
use Pollora\Meta\Domain\Models\MetaRecord;
use Pollora\Meta\Domain\Models\MetaSchema;
use Pollora\Meta\Domain\Services\MetaValueCaster;
use Psr\Log\LoggerInterface;

Expand Down Expand Up @@ -35,8 +36,14 @@ public function __construct(
*/
public function of(string $class, int $objectId): MetaRecord
{
$schema = $this->schemas->forClass($class) ?? $this->builder->build($class);
return $this->record($this->schemas->forClass($class) ?? $this->builder->build($class), $objectId);
}

/**
* The typed meta of a schema on one object.
*/
public function record(MetaSchema $schema, int $objectId): MetaRecord
{
return new MetaRecord($schema, $objectId, $this->store, $this->caster, $this->handleUnreadable(...));
}

Expand Down
34 changes: 34 additions & 0 deletions src/Meta/Application/Services/MetaSchemaRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Pollora\Meta\Application\Services;

use Pollora\Meta\Domain\Enums\MetaObjectType;
use Pollora\Meta\Domain\Exceptions\InvalidMetaDefinitionException;
use Pollora\Meta\Domain\Models\MetaDefinition;
use Pollora\Meta\Domain\Models\MetaSchema;
Expand All @@ -18,6 +19,11 @@ final class MetaSchemaRepository
*/
private array $schemas = [];

/**
* @var array<string, array<string, true>> Property names and keys, by object type
*/
private array $names = [];

/**
* @throws InvalidMetaDefinitionException When another class already declares one of its keys on the same object
*/
Expand All @@ -36,6 +42,11 @@ public function add(MetaSchema $schema): void
}

$this->schemas[$schema->declaringClass] = $schema;

foreach ($schema->definitions as $definition) {
$this->names[$schema->objectType->value][$definition->property] = true;
$this->names[$schema->objectType->value][$definition->key] = true;
}
}

/**
Expand All @@ -46,6 +57,29 @@ public function forClass(string $class): ?MetaSchema
return $this->schemas[$class] ?? null;
}

/**
* The schemas whose meta an object can carry: those of its type covering every
* object, and those listing its subtype.
*
* @return list<MetaSchema>
*/
public function forObject(MetaObjectType $objectType, ?string $subtype): array
{
return array_values(array_filter(
$this->schemas,
static fn (MetaSchema $schema): bool => $schema->objectType === $objectType
&& ($schema->subtypes === [] || ($subtype !== null && in_array($subtype, $schema->subtypes, true)))
));
}

/**
* Whether a schema of this object type declares a meta under that property name or key.
*/
public function declares(MetaObjectType $objectType, string $propertyOrKey): bool
{
return isset($this->names[$objectType->value][$propertyOrKey]);
}

/**
* @return list<MetaSchema>
*/
Expand Down
36 changes: 33 additions & 3 deletions src/Meta/Infrastructure/Services/MetaDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Pollora\Attributes\Taxonomy;
use Pollora\Attributes\TermMeta;
use Pollora\Attributes\UserMeta;
use Pollora\Colt\Model\Post as ColtPost;
use Pollora\Discovery\Domain\Contracts\DiscoveryInterface;
use Pollora\Discovery\Domain\Contracts\DiscoveryLocationInterface;
use Pollora\Discovery\Domain\Contracts\ReflectionCacheInterface;
Expand All @@ -18,16 +19,18 @@
use Pollora\Meta\Application\Services\MetaSchemaRepository;
use Pollora\Meta\Domain\Contracts\MetaRegistryInterface;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use Spatie\StructureDiscoverer\Data\DiscoveredClass;
use Spatie\StructureDiscoverer\Data\DiscoveredStructure;

/**
* Discovers the `#[Meta]` properties of `#[PostType]`, `#[Taxonomy]`, `#[PostMeta]`,
* `#[TermMeta]`, `#[UserMeta]` and `#[CommentMeta]` classes.
*
* 1. **discover()** — keeps the classes carrying one of those attributes.
* 1. **discover()** — keeps the classes carrying one of those attributes, and the
* post models (classes extending a Colt post model).
* 2. **apply()** — builds each class's schema, stores it for `Meta::of()` and
* queues it for `register_meta()`.
* queues it for `register_meta()`; binds each post model to its `$postType`.
*
* A declaration that cannot be registered is logged with the class and property
* named, and the other classes still register.
Expand All @@ -51,13 +54,19 @@ public function discover(DiscoveryLocationInterface $location, DiscoveredStructu
return;
}

$class = $structure->namespace.'\\'.$structure->name;

foreach ($structure->attributes as $attribute) {
if (in_array($attribute->class, self::OWNER_ATTRIBUTES, true)) {
$this->getItems()->add($location, ['class' => $structure->namespace.'\\'.$structure->name]);
$this->getItems()->add($location, ['class' => $class]);

return;
}
}

if ($structure->extends !== null && is_subclass_of($class, ColtPost::class)) {
$this->getItems()->add($location, ['class' => $class, 'model' => true]);
}
}

public function apply(): void
Expand All @@ -66,6 +75,12 @@ public function apply(): void
/** @var class-string $class */
$class = $item['class'];

if ($item['model'] ?? false) {
$this->registerPostModel($class);

continue;
}

try {
$schema = $this->builder->build($class);

Expand All @@ -81,6 +96,21 @@ public function apply(): void
}
}

/**
* Makes `Post::find()` return the model bound to the post type
* (`protected $postType = 'event'`), so its typed meta are there.
*
* @param class-string $class
*/
private function registerPostModel(string $class): void
{
$postType = (new ReflectionClass($class))->getDefaultProperties()['postType'] ?? null;

if (is_string($postType) && $postType !== '') {
ColtPost::registerPostType($postType, $class);
}
}

public function getIdentifier(): string
{
return 'meta';
Expand Down
7 changes: 6 additions & 1 deletion src/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

namespace Pollora\Models;

use Pollora\Models\Concerns\HasTypedMeta;

/**
* Class Comment
*/
class Comment extends \Pollora\Colt\Model\Comment {}
class Comment extends \Pollora\Colt\Model\Comment
{
use HasTypedMeta;
}
Loading
Loading