v13.32.0-beta
Pre-releasePollora now follows Laravel's version numbers: this release requires Laravel 13.32.
Added
- Translation diagnostics in
pollora:statusand the admin dashboard — reports whether the__()override is the one actually installed, alongside the WordPress and Laravel locales.laravel/frameworkdeclares__()behind the samefunction_exists()guard aspollora/helper-overriderand Composer emitsautoload.filesin 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 declaringpollora_translation_resolver()— they ship in the samehelpers.php, so matching paths prove the package won the race whatever the install layout - WordPress Abilities API support through the new
pollora/abilitiespackage (requires WordPress 6.9)Abilityfacade for fluent declaration:Ability::define('acme/get-posts')->description(…)->category(…)->can(…)->using(…)#[Ability]attribute on classes implementingAbilityHandler, discovered automaticallyAbility::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 asreadOnlyHint,destructiveHintandidempotentHint - Typed
SchemaBuilderfor input and output JSON Schema, and a defensiveInputreader - 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 exceptparameter for selective exclusion:#[SkipDiscovery(except: [HookDiscovery::class])]skips all discoveries except the listed ones
- Classes annotated with
- Publishable
config/discovery.phpwithskip_classesandskip_pathsarrays for config-level exclusions (third-party packages, vendor classes) DiscoveryRegistrar— automatically registers Discovery classes from the service container, eliminating manualaddDiscovery()calls in ServiceProviderspollora_register()helper withModuleTypeenum for simplified theme/plugin registration- Themes:
pollora_register(ModuleType::Theme)— 3 lines infunctions.php, auto-detects theme name and path - Plugins:
pollora_register(ModuleType::Plugin, 'my-plugin', __DIR__)— replaces manualPluginRegistrarwiring
- Themes:
#[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 redundantgetMethods(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\Translaternow requires its$domainargument. The old'wordpress'default existed only to pair with thewordpress.key prefix thatpollora/helper-overrider1.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 (SidebarandMenusboth 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 unanchoredstr_replace(), so a value such as'Go to menus.example'came back as'Go to example', andtranslateItem()was typedstringunderdeclare(strict_types=1), so a wildcardtranslate(['*'])over a config array holding an int or bool raised aTypeError - BREAKING: Ajax module extracted to
pollora/ajaxpackage - BREAKING: Option module extracted to
pollora/optionpackage - BREAKING: Hook domain and adapters extracted to
pollora/hookpackage - 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 byDiscoveryRegistrarauto-registration - Empty
boot()methods removed fromLaravelPluginModuleandLaravelThemeModule(RectorRemoveParentDelegatingClassMethodRector) - Artisan command signatures renamed to Laravel colon convention (e.g.
pollora:make:theme) DiscoveryItems::all()optimized — replaced spread-in-loop witharray_merge- Redundant reflection pre-loading removed from
DiscoveryEngine
Fixed
WordPressHeadersmiddleware now respects theDONOTCACHEPAGEconstant 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'])raisedTypeError: Cannot access offset of type array in isset or emptywheneverLang::has()answered no: the guard normalised only the empty array to thedefaulttext domain, so a non-empty replacement array travelled into WordPress'stranslate()as the domain and reachedisset($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 noen_US.jsonexists at all, taking down every string with a placeholder. Requirespollora/helper-overrider1.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 unguardedLang::has()that raisedA facade root has not been set.wherever__()runs before or without an application, awordpress.prefix strip that rewrote the substring anywhere it appeared (__('Go to wordpress.org')returnedGo to org), Laravel group keys returning an array to callers typed against WordPress'sstring, and a missing base-language locale fallback that kept afr_FRsite from ever readinglang/fr.json- The
LoopandQueryfacade aliases no longer crash a page that uses them. Both were still declared inextra.laravel.aliasesafter their facade classes had been deleted —Loopsince9a50cac(2026-04-21),Querysince9c0cb42(2024-08-05). Laravel registers an alias without checking its target and only callsclass_alias()the first time the short name is used, so the package installed and booted cleanly and then threwClass "Pollora\Support\Facades\Loop" not foundon whichever page happened to call it. Present in every v13.4.x release. See the migration note below for what replacesLoop. PluginManagerTestisolation — realContainerwith dynamicWP_PLUGIN_DIRreplaces fragileContainerInterfacemock- WooCommerce hooks test updated for
ComingSoonHandlerdependency - PHPStan dead catches widened, redundant
@return $thisdocblocks removed OptionServicenamespace 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
LoopandQueryentries inextra.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()andterms()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-madeclass="…"attribute string, where
get_post_class()returns an array.content()andexcerpt()appliedthe_content/the_excerptthemselves;
dropping the filter silently strips whatever other plugins add there.tags()andterms()normalisedfalseto 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 byDiscoveryRegistrar)