Skip to content

v13.32.0-beta

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 12:34
· 266 commits to main since this release

Pollora now follows Laravel's version numbers: this release requires Laravel 13.32.

Added

  • Translation diagnostics in pollora:status and the admin dashboard — reports whether the __() override is the one actually installed, alongside the WordPress and Laravel locales. laravel/framework declares __() behind the same function_exists() guard as pollora/helper-overrider and Composer emits autoload.files in dependency order, so whichever loads first wins; when Laravel wins nothing errors, WordPress catalogues simply stop resolving and every core, theme and plugin string silently renders untranslated. Detection compares the file declaring __() against the one declaring pollora_translation_resolver() — they ship in the same helpers.php, so matching paths prove the package won the race whatever the install layout
  • WordPress Abilities API support through the new pollora/abilities package (requires WordPress 6.9)
    • Ability facade for fluent declaration: Ability::define('acme/get-posts')->description(…)->category(…)->can(…)->using(…)
    • #[Ability] attribute on classes implementing AbilityHandler, discovered automatically
    • Ability::category() for the categories abilities file under, declared for you when an ability names one nobody registered
    • Behaviour annotations (reads/creates/updates/deletes) published to AI clients as readOnlyHint, destructiveHint and idempotentHint
    • Typed SchemaBuilder for input and output JSON Schema, and a defensive Input reader
    • Permission checks receive the same input as the body, allowing per-object capability checks; they default to refusing
  • #[SkipDiscovery] attribute to exclude classes from the discovery engine
    • Classes annotated with #[SkipDiscovery] are completely invisible to all discoveries — no reflection loaded, no attributes scanned
    • except parameter for selective exclusion: #[SkipDiscovery(except: [HookDiscovery::class])] skips all discoveries except the listed ones
  • Publishable config/discovery.php with skip_classes and skip_paths arrays for config-level exclusions (third-party packages, vendor classes)
  • DiscoveryRegistrar — automatically registers Discovery classes from the service container, eliminating manual addDiscovery() calls in ServiceProviders
  • pollora_register() helper with ModuleType enum for simplified theme/plugin registration
    • Themes: pollora_register(ModuleType::Theme) — 3 lines in functions.php, auto-detects theme name and path
    • Plugins: pollora_register(ModuleType::Plugin, 'my-plugin', __DIR__) — replaces manual PluginRegistrar wiring
  • #[Ajax] attribute for declarative AJAX action registration with security-by-default
  • Discovery system performance benchmark suite (tests/Benchmark/) with realistic attribute complexity and multi-location DDD scenarios
  • Public method caching in ReflectionCache::getPublicMethods() — avoids redundant getMethods(IS_PUBLIC) reflection calls

Changed

  • BREAKING: requires Laravel 13.32 (illuminate/* ^13.32). The framework version now tracks the Laravel release it targets, hence the jump from v13.4.3
  • BREAKING: Pollora\Services\Translater now requires its $domain argument. The old 'wordpress' default existed only to pair with the wordpress. key prefix that pollora/helper-overrider 1.2.0 removed — it prefixed every lookup as __('wordpress.'.$value) and relied on the resolver stripping it before the gettext call, so with 1.2.0 installed it silently returned values untranslated. Nothing in the framework used it (Sidebar and Menus both pass 'sidebars'/'menus' explicitly); pass __($value, 'default') if you want WordPress's core catalogue. Two latent bugs went with it: the prefix was stripped with an unanchored str_replace(), so a value such as 'Go to menus.example' came back as 'Go to example', and translateItem() was typed string under declare(strict_types=1), so a wildcard translate(['*']) over a config array holding an int or bool raised a TypeError
  • BREAKING: Ajax module extracted to pollora/ajax package
  • BREAKING: Option module extracted to pollora/option package
  • BREAKING: Hook domain and adapters extracted to pollora/hook package
  • Domain layer paths restored for hexagonal architecture consistency
  • Public API types exposed for Theme and Option modules
  • All manual addDiscovery() calls removed from ServiceProviders — replaced by DiscoveryRegistrar auto-registration
  • Empty boot() methods removed from LaravelPluginModule and LaravelThemeModule (Rector RemoveParentDelegatingClassMethodRector)
  • Artisan command signatures renamed to Laravel colon convention (e.g. pollora:make:theme)
  • DiscoveryItems::all() optimized — replaced spread-in-loop with array_merge
  • Redundant reflection pre-loading removed from DiscoveryEngine

Fixed

  • WordPressHeaders middleware now respects the DONOTCACHEPAGE constant set by WooCommerce and compatible cache plugins, preventing public cache headers on cart, checkout, and account pages
  • __() no longer fatals on a translation with replacements whose key is absent from the current locale. __('Shipping :brand', ['brand' => 'Test']) raised TypeError: Cannot access offset of type array in isset or empty whenever Lang::has() answered no: the guard normalised only the empty array to the default text domain, so a non-empty replacement array travelled into WordPress's translate() as the domain and reached isset($l10n[$domain]). It hit every __($key, [...]) call whose key was not in the locale's catalogue — typically a site whose sources and language are both English, where no en_US.json exists at all, taking down every string with a placeholder. Requires pollora/helper-overrider 1.2, which routes on the caller's intent — a string second argument is a WordPress text domain, a non-empty array is a Laravel call — instead of on whether Laravel happens to hold the key. The same release fixes an unguarded Lang::has() that raised A facade root has not been set. wherever __() runs before or without an application, a wordpress. prefix strip that rewrote the substring anywhere it appeared (__('Go to wordpress.org') returned Go to org), Laravel group keys returning an array to callers typed against WordPress's string, and a missing base-language locale fallback that kept a fr_FR site from ever reading lang/fr.json
  • The Loop and Query facade aliases no longer crash a page that uses them. Both were still declared in extra.laravel.aliases after their facade classes had been deleted — Loop since 9a50cac (2026-04-21), Query since 9c0cb42 (2024-08-05). Laravel registers an alias without checking its target and only calls class_alias() the first time the short name is used, so the package installed and booted cleanly and then threw Class "Pollora\Support\Facades\Loop" not found on whichever page happened to call it. Present in every v13.4.x release. See the migration note below for what replaces Loop.
  • PluginManagerTest isolation — real Container with dynamic WP_PLUGIN_DIR replaces fragile ContainerInterface mock
  • WooCommerce hooks test updated for ComingSoonHandler dependency
  • PHPStan dead catches widened, redundant @return $this docblocks removed
  • OptionService namespace aligned with extracted package
  • GitHub releases are created again on tag push — the Deploy workflow lacked contents: write; suffixed tags (-beta, -rc.1) are now published as pre-releases

Removed

  • The Loop and Query entries in extra.laravel.aliases, whose facade classes no longer exist

Migrating from Loop

Loop:: was removed in 9a50cac as a breaking change, but the alias stayed
behind and the removal was never written down — so a project only found out when
a page 500'd. This is that note, late.

In Blade, the replacement is Sage Directives
(log1x/sage-directives, registered by PolloraServiceProvider): @title,
@content, @excerpt, @permalink, @published, @posts and the rest.

In PHP, Pollora\View\Loop was only ever a proxy over WordPress functions. The
full mapping, read off the deleted source rather than reconstructed:

Removed Replacement
Loop::id() get_the_ID()
Loop::title($post) get_the_title($post)
Loop::author() get_the_author()
Loop::authorMeta($field, $userId) get_the_author_meta($field, $userId)
Loop::content($moreText, $stripTeaser) apply_filters('the_content', get_the_content($moreText, $stripTeaser)), then str_replace(']]>', ']]>', …)
Loop::excerpt($post) apply_filters('the_excerpt', get_the_excerpt($post))
Loop::thumbnail($size, $attr, $post) get_the_post_thumbnail($post, $size, $attr)
Loop::thumbnailUrl($size, $icon) wp_get_attachment_image_src(get_post_thumbnail_id(), $size, $icon)[0] ?? null
Loop::link($post, $leavename) get_permalink($post, $leavename)
Loop::category($id) get_the_category($id)
Loop::tags($id) get_the_tags($id) ?: []
Loop::terms($taxonomy, $post) get_the_terms($post, $taxonomy) ?: []
Loop::date($format, $post) get_the_date($format, $post)
Loop::postClass($class, $postId) 'class="'.implode(' ', get_post_class($class, $postId)).'"'
Loop::nextPage($label, $maxPage) get_next_posts_link($label, $maxPage)
Loop::previousPage($label) get_previous_posts_link($label)
Loop::paginate($args) paginate_links($args)

Four of these are not straight renames, and a blind substitution breaks them:

  • thumbnail() and terms() reorder their arguments — the post moves from
    last to first — and both defaulted the post to the one in the loop.
  • postClass() returned a ready-made class="…" attribute string, where
    get_post_class() returns an array.
  • content() and excerpt() applied the_content / the_excerpt themselves;
    dropping the filter silently strips whatever other plugins add there.
  • tags() and terms() normalised false to an empty array.

Query:: has no replacement to document — the facade was already gone before
v13, and the alias outlived it by two years.

  • Unfinished Admin Pages module
  • Manual addDiscovery() boot logic in HookServiceProvider, PostTypeServiceProvider, TaxonomyServiceProvider, WpRestAttributeServiceProvider, SchedulerDiscoveryServiceProvider
  • RegisterScheduleDiscoveryUseCase (superseded by DiscoveryRegistrar)