diff --git a/.claude/skills/hyva-alpine-component b/.claude/skills/hyva-alpine-component
new file mode 120000
index 00000000..cce7ec53
--- /dev/null
+++ b/.claude/skills/hyva-alpine-component
@@ -0,0 +1 @@
+/Users/willem/hyva-ai-tools/skills/hyva-alpine-component
\ No newline at end of file
diff --git a/.claude/skills/hyva-internal-coding-standard b/.claude/skills/hyva-internal-coding-standard
new file mode 120000
index 00000000..46bb644d
--- /dev/null
+++ b/.claude/skills/hyva-internal-coding-standard
@@ -0,0 +1 @@
+/Users/willem/hyva-internal-skills/skills/hyva-internal-coding-standard
\ No newline at end of file
diff --git a/.claude/skills/hyva-internal-skill-security-review b/.claude/skills/hyva-internal-skill-security-review
new file mode 120000
index 00000000..e3a8e569
--- /dev/null
+++ b/.claude/skills/hyva-internal-skill-security-review
@@ -0,0 +1 @@
+/Users/willem/hyva-internal-skills/skills/hyva-internal-skill-security-review
\ No newline at end of file
diff --git a/.claude/skills/hyva-playwright-test b/.claude/skills/hyva-playwright-test
new file mode 120000
index 00000000..e5b22c7d
--- /dev/null
+++ b/.claude/skills/hyva-playwright-test
@@ -0,0 +1 @@
+/Users/willem/hyva-ai-tools/skills/hyva-playwright-test
\ No newline at end of file
diff --git a/.claude/skills/magewire-architecture/SKILL.md b/.claude/skills/magewire-architecture/SKILL.md
new file mode 100644
index 00000000..9df5e171
--- /dev/null
+++ b/.claude/skills/magewire-architecture/SKILL.md
@@ -0,0 +1,596 @@
+---
+name: magewire-architecture
+description: "Internals and extension guide for the Magewire framework: directory layout (src/lib/dist/portman), Mechanisms vs Features, area-scoped DI (frontend/adminhtml), snapshot/state flow, layout containers, JS extension points, and Facades. Use when extending Magewire, creating custom Features or Mechanisms, debugging the framework itself, or understanding how the codebase is structured."
+license: MIT
+metadata:
+ author: Willem Poortman
+---
+
+# Magewire Architecture
+
+Magewire's codebase is split into two layers: an upstream Livewire core (PHP, ported and maintained via Portman) and a Magento integration layer (controllers, blocks, DI, layout, templates). Understanding this split is essential for knowing where to add or change things.
+
+---
+
+## Repository Layout
+
+```
+vendor/magewirephp/magewire/
+├── src/ # Magento module structure (hand-written, must live here for Magento)
+│ ├── Component.php # Base component class (public API)
+│ ├── MagewireServiceProvider.php
+│ ├── Controller/ # Route controllers (magewire/update)
+│ ├── Model/ # Magento-specific models, view utils, fragment handling
+│ ├── Observer/ # Magento event observers
+│ ├── Plugin/ # Magento plugin interceptors
+│ ├── ViewModel/ # MagewireViewModel (utilities facade)
+│ ├── Exceptions/
+│ ├── etc/ # module.xml, di.xml, events.xml, routes.xml
+│ └── view/ # Layout XML, PHTML templates, JS/CSS assets
+│
+├── lib/ # Hand-written custom code + downloaded Livewire source
+│ ├── Livewire/ # Downloaded upstream Livewire source (Portman input cache)
+│ ├── Magewire/ # Hand-written Magewire core (mechanisms, features, enums, runtime)
+│ ├── MagewireBc/ # Hand-written backwards compatibility layer
+│ ├── Magento/ # Hand-written Magento framework integrations
+│ └── Symfony/ # Hand-written Symfony adaptations
+│
+├── portman/ # Portman augmentation files
+│ └── Livewire/ # Augmentations: what to change/extend in upstream Livewire
+│
+├── dist/ # Portman BUILD OUTPUT — do not edit directly
+│ │ # (Livewire source + augmentations, namespace-transformed)
+│ ├── ComponentHook.php
+│ ├── Mechanisms/
+│ └── Features/
+│
+├── themes/ # Theme support sub-modules (e.g. Hyva, Luma)
+├── tests/ # Playwright + other test types
+└── portman.config.php # Controls Portman: source paths, output dir, transformations
+```
+
+**Rule of thumb**: `src/` and `lib/` (excluding `lib/Livewire/`) are yours to edit freely. `dist/` is Portman-generated — changes belong in `portman/Livewire/` (augmentations) or `portman.config.php`. `lib/Livewire/` is the downloaded upstream source cache — don't edit it either.
+
+---
+
+## Bootstrap & Runtime
+
+Magewire boots through `MagewireServiceProvider`, which is triggered by Magento's DI during the request lifecycle. It registers three types of services in order:
+
+1. **Containers** — Internal DI-like containers (MagewireManager, etc.)
+2. **Mechanisms** — Non-optional core infrastructure (sorted, loaded in order)
+3. **Features** — Optional lifecycle hooks (sorted, loaded in order)
+
+Boot modes per service item (`ServiceTypeItemBootMode`):
+- `LAZY` (10) — Boot only when needed
+- `PERSISTENT` (20) — Boot during setup phase, persists across request modes
+- `ALWAYS` (30) — Boot on every request (default)
+
+Runtime state progression: `UNINITIALIZED → SETUP → BOOTING → BOOTED` (or `FAILED / STOPPED`)
+
+Request modes: `PRECEDING` (initial page load) and `SUBSEQUENT` (AJAX update).
+
+---
+
+## Mechanisms
+
+Mechanisms are the non-optional core pipeline. They live in `lib/Magewire/Mechanisms/` (hand-written, Magento-specific) and `dist/Mechanisms/` (ported from Livewire), and run in `sort_order` sequence:
+
+| Sort | Mechanism | Role |
+|------|-----------|------|
+| 1000 | `ResolveComponents` | Discovers and instantiates components from layout blocks |
+| 1050 | `PersistentMiddleware` | Carries persistent data between requests |
+| 1100 | `HandleComponents` | Manages snapshot, properties, synthesizers |
+| 1200 | `HandleRequests` | Orchestrates the full update cycle |
+| 1250 | `DataStore` | Global request-scoped data storage |
+| 1400 | `FrontendAssets` | Serves the JS bundle and manifest |
+
+Mechanisms are registered in `src/etc/frontend/di.xml` and `src/etc/adminhtml/di.xml` separately — never in the global `di.xml`. See the note on area-scoped DI below.
+
+To add a custom mechanism, add an entry to the DI config pointing to a class that extends the mechanism base and implements its contract. Mechanisms cannot be skipped — if you only need optional behavior, use a Feature instead.
+
+---
+
+## Features
+
+Features are optional lifecycle extensions. Each Feature is a `ComponentHook` subclass that hooks into the component lifecycle via `provide()`. They live in `lib/Magewire/Features/` (hand-written, Magewire-specific) or `dist/Features/` (ported from Livewire via Portman).
+
+Key built-in features and their sort orders:
+
+| Sort | Feature | Role |
+|------|---------|------|
+| 700 | `SupportMagewireViewModel` | Auto-injects view_model on blocks |
+| 800 | `SupportMagewireExceptionHandling` | Exception management |
+| 900 | `SupportMagewireRateLimiting` | Rate limiting |
+| 1000 | `SupportNestingComponents` | Parent-child relationships |
+| 1200 | `SupportAttributes` | PHP attribute handling |
+| 1300 | `SupportRedirects` | Redirect effects |
+| 1500 | `SupportLocales` | Locale/i18n support |
+| 1600 | `SupportEvents` | Inter-component events |
+| 5000 | `SupportMagentoLayouts` | Magento layout rendering |
+| 5100 | `SupportMagentoFlashMessages` | Magento session flash messages |
+| 5200 | `SupportMagewireLoaders` | Loader state management |
+| 5200 | `SupportMagewireNotifications` | Toast notification system |
+| 99000 | `SupportLifecycleHooks` | Calls lifecycle methods on components |
+| 99100 | `SupportMagewireBackwardsCompatibility` | Legacy API support |
+
+### Naming Conventions
+
+- `SupportMagewire` prefix — Magewire-specific features (e.g. `SupportMagewireLoaders`)
+- `SupportMagento` prefix — Magento compatibility features (e.g. `SupportMagentoLayouts`)
+
+### The Hook System
+
+Features hook into component lifecycle events using `on()` / `trigger()`. A hook callback must return either another callback (which receives the result) or the original value unchanged.
+
+```php
+use function Magewirephp\Magewire\on;
+
+class SupportMyFeature extends \Magewirephp\Magewire\ComponentHook
+{
+ public static function provide(): void
+ {
+ on('magewire:construct', function () {
+ // Return a callback — it fires AFTER the trigger resolves
+ return function (\Magento\Framework\View\Element\AbstractBlock $block) {
+ // Do something with the block
+ return $block; // Must return the value
+ };
+ });
+ }
+}
+```
+
+Hooks are also observable via Magento's native event system through the `SupportMagentoObserverEvents` feature (sort order 99400). It maps 30+ lifecycle hooks to Magento events with the pattern `magewire_on_{eventname}` (special characters replaced with underscores). This means third-party modules can observe Magewire lifecycle events using standard Magento `events.xml` observers without needing to create a Feature.
+
+### Middleware-Style Hooks
+
+Some hooks support a middleware pattern: if your callback returns a callable, that callable runs **after** the operation completes. This is not available on all hooks — only those that support before/after semantics:
+
+- **`update`** — return a callback that runs after the property is updated (used for `updated*` hooks)
+- **`call`** — return a callback that runs after method execution
+- **`render`** — return a callback that runs after rendering completes (used for `rendered` hook)
+
+```php
+on('update', function ($propertyName, $fullPath, $newValue) {
+ // Runs BEFORE the property update
+
+ return function () use ($fullPath) {
+ // Runs AFTER the property update
+ };
+});
+```
+
+Hooks that do **not** support middleware (e.g., `mount`, `hydrate`, `dehydrate`, `exception`) simply run their callback inline — returning a callable from these has no effect.
+
+### Adding a Custom Feature
+
+1. Create a class extending `Magewirephp\Magewire\ComponentHook`:
+
+```php
+namespace Vendor\Module\Magewire\Features;
+
+use Magewirephp\Magewire\ComponentHook;
+use function Magewirephp\Magewire\on;
+
+class SupportMyFeature extends ComponentHook
+{
+ public static function provide(): void
+ {
+ on('mount', function ($params) {
+ // Runs during mount — no middleware support
+ });
+
+ on('hydrate', function ($memo) { });
+
+ on('dehydrate', function ($context) { });
+
+ on('update', function ($propertyName, $fullPath, $newValue) {
+ // Before update
+ return function () {
+ // After update (middleware pattern)
+ };
+ });
+ }
+}
+```
+
+2. Register in `etc/frontend/di.xml` **and** `etc/adminhtml/di.xml` as needed — never in the global `etc/di.xml`. See the note on area-scoped DI below.
+
+```xml
+
+
+
+ -
+
- Vendor\Module\Magewire\Features\SupportMyFeature
+ - 5050
+
+
+
+
+```
+
+Place feature PHP code in `src/Features/SupportMyFeature/` and any JS in its corresponding `view/` subfolder.
+
+**Feature templates live at:** `view/{area}/templates/magewire-features/{feature-name}/` — the same convention applies to core framework features (in the Magewire module) and to theme compatibility features (in theme modules like Hyvä). Keeping JS, UI, and other feature-related PHTMLs co-located in a single folder per feature makes ownership and overrides easy to reason about.
+
+---
+
+## Area-Scoped DI: Why `frontend/di.xml` and `adminhtml/di.xml`
+
+Features and Mechanisms **must** be registered in area-specific DI files (`etc/frontend/di.xml`, `etc/adminhtml/di.xml`) rather than the global `etc/di.xml`.
+
+**Why:** Magento's DI system merges global config into every area. If a Feature or Mechanism were declared in the global `di.xml`, it would be impossible for another module to register a different set for frontend vs adminhtml — the global declaration would apply everywhere and couldn't be selectively overridden per area.
+
+By keeping registrations area-scoped:
+- A feature active on the frontend can be absent in the admin (or replaced with a different implementation)
+- Third-party compatibility modules (e.g. `MagewireCompatibilityWithHyva`) can add their own features to `frontend/di.xml` without affecting the admin
+- The admin panel (`adminhtml/di.xml`) can carry a different, leaner or admin-tailored set of features and mechanisms
+
+**Rule:** When adding a Feature or Mechanism to your own module, always register it in `etc/frontend/di.xml`, `etc/adminhtml/di.xml`, or both — depending on which areas it should be active in. Never use `etc/di.xml` for this.
+
+---
+
+## Component Resolvers & Arguments
+
+The `ResolveComponents` mechanism is Magewire-specific (not ported from Livewire) and is one of the most powerful parts of the framework. It determines **how** a Magento block becomes a Magewire component — and it's designed to be extensible, so that components can come from layout XML, widgets, API calls, or any custom source.
+
+### How Resolution Works
+
+When Magewire encounters a block during rendering, the `ComponentResolverManager` iterates registered resolvers (sorted by DI `sortOrder`) and calls `complies()` on each until one matches. The winning resolver then handles construction (initial page load) and reconstruction (AJAX updates).
+
+Resolvers are registered in area-scoped DI:
+
+```xml
+
+
+
+
+ -
+ Magewirephp\Magewire\Mechanisms\ResolveComponents\ComponentResolver\LayoutResolver
+
+
+
+
+```
+
+The resolver's DI item name must match its `$accessor` property exactly. The accessor is stored in the component's snapshot memo, so subsequent requests can find the right resolver to reconstruct the component.
+
+### The Resolver Contract
+
+Every resolver extends `ComponentResolver` and implements:
+
+| Method | Purpose |
+|--------|---------|
+| `complies($block, $magewire)` | Lightweight check: does this block belong to this resolver? |
+| `construct($block)` | Initial page load: bind a `Component` instance to the block's `magewire` data key, set `name` and `id` |
+| `reconstruct($request)` | AJAX update: rebuild the block + component from snapshot data (typically calls `construct()` internally) |
+| `arguments()` | Return a `MagewireArguments` subclass for this resolver |
+| `assemble($block, $component)` | Final assembly after construct/reconstruct — sets name, id, alias on the component |
+| `remember()` | Whether this resolver should be cached (default `true`; set `false` for dynamic resolution) |
+
+```php
+namespace Vendor\Module\Mechanisms\ResolveComponents\ComponentResolver;
+
+use Magewirephp\Magewire\Mechanisms\ResolveComponents\ComponentResolver\ComponentResolver;
+
+class WidgetResolver extends ComponentResolver
+{
+ protected string $accessor = 'widget';
+
+ public function complies(AbstractBlock $block, mixed $magewire = null): bool
+ {
+ // Check for widget-specific data
+ $this->conditions()->if(
+ fn () => $block->getData('widget_instance_id') !== null,
+ 'is-widget'
+ );
+
+ return parent::complies($block, $magewire);
+ }
+
+ public function construct(AbstractBlock $block): AbstractBlock
+ {
+ // Instantiate component and bind to block
+ $component = Factory::create(MyWidgetComponent::class);
+ $block->setData('magewire', $component);
+ return $block;
+ }
+
+ public function reconstruct(ComponentRequestContext $request): AbstractBlock
+ {
+ // Rebuild from snapshot — get widget ID from memo, recreate block
+ $widgetId = $request->snapshot->memo['widget_id'] ?? null;
+ $block = $this->widgetRepository->getBlockById($widgetId);
+ return $this->construct($block);
+ }
+
+ public function arguments(): MagewireArguments
+ {
+ return $this->widgetBlockArgumentsFactory->create();
+ }
+}
+```
+
+### ComponentArguments: Structured Block Arguments
+
+Magewire introduces a structured argument system for passing data to components through layout XML. Arguments are extracted from block data during the assembly phase.
+
+**Public arguments** — prefixed with `magewire.`, become component properties:
+
+```xml
+
+
+ Vendor\Module\Magewire\MyComponent
+ 42
+ price
+
+
+```
+
+The `magewire.` prefix is stripped and kebab-case is converted to camelCase:
+- `magewire.product-id` → `productId`
+- `magewire.sort-order` → `sortOrder`
+
+**Group arguments** — prefixed with `magewire:{group}:{key}`, organized into named groups:
+
+```xml
+
+
+ Vendor\Module\Magewire\MyComponent
+ 10
+ 20
+ 3600
+
+
+```
+
+Group arguments are accessed via the `MagewireArguments` API:
+
+```php
+// In your component's mount() — receives the 'mount' group
+public function mount(int $categoryId = 0, int $pageSize = 10): void
+{
+ // $categoryId = 10, $pageSize = 20 (from magewire:mount:*)
+}
+
+// In a resolver or feature — access any group
+$arguments->forMount(); // ['categoryId' => 10, 'pageSize' => 20]
+$arguments->forGroup('config'); // ['cacheTtl' => 3600]
+$arguments->toParams(); // All public arguments as array
+```
+
+**Resolver-specific arguments** — the `magewire:resolver` key can force a specific resolver:
+
+```xml
+widget
+```
+
+**Alias** — the `magewire:alias` key sets a component alias for lookup:
+
+```xml
+shipping-form
+```
+
+### Argument Class Hierarchy
+
+Each resolver provides its own `MagewireArguments` subclass:
+
+```
+MagewireArguments (abstract, extends DataObject)
+ └── BlockMagewireArguments
+ └── LayoutBlockArguments (used by LayoutResolver, marked shared="false" in DI)
+```
+
+Custom resolvers create their own subclass for specialized argument handling (e.g., `WidgetBlockArguments` could extract widget configuration parameters).
+
+### The Layout Resolver
+
+The built-in `LayoutResolver` (accessor: `layout`) handles the most common case — components declared in layout XML. It supports three binding formats:
+
+```xml
+
+Vendor\Module\Magewire\MyComponent
+
+
+
+ - Vendor\Module\Magewire\MyComponent
+
+
+
+
+ - true
+
+```
+
+On dehydrate, the `LayoutResolver` stores the active layout handles in the snapshot memo. On reconstruction (AJAX), it regenerates the layout from those handles to recover the block.
+
+### Writing a Custom Resolver
+
+To add a new component source (e.g., Magento widgets):
+
+1. Create a resolver class extending `ComponentResolver`
+2. Create a `MagewireArguments` subclass for your argument format
+3. Register in area-scoped DI with a unique accessor name and sort order
+
+```xml
+
+
+
+ -
+ Vendor\Module\Mechanisms\ResolveComponents\ComponentResolver\WidgetResolver
+
+
+
+
+```
+
+Lower sort orders are evaluated first. The `LayoutResolver` intentionally uses a high sort order (99900) so it acts as a fallback — custom resolvers should use lower numbers to take priority when their `complies()` check matches.
+
+---
+
+## Snapshot & State Flow
+
+The `Snapshot` is the serialized state of a component, round-tripped between frontend and backend as JSON embedded in the HTML.
+
+```
+Snapshot = {
+ data: { /* public properties */ },
+ memo: { /* metadata: resolver class, children, bindings, etc. */ },
+ checksum: /* HMAC of data+memo */
+}
+```
+
+The checksum prevents tampering. On each AJAX request, Magewire validates it before processing.
+
+`Memo` carries non-property metadata. The `ResolveComponents` mechanism writes the component resolver class name into memo so the component can be reconstructed on subsequent requests without needing layout context.
+
+`Effects` accumulates side effects during the request (redirect, events, method return values) and is sent back to the frontend alongside the new snapshot.
+
+### Synthesizers
+
+Synthesizers (in `dist/Mechanisms/HandleComponents/Synthesizers/` for ported ones, `lib/Magewire/Mechanisms/HandleComponents/Synthesizers/` for Magento-specific ones) handle serialization of non-scalar property types (e.g. `DataObject`, PHP enums, collections). To support a custom type in properties, create a synthesizer and register it in DI.
+
+---
+
+## Request Flow (AJAX Update)
+
+```
+POST magewire/update
+ → Update controller (src/Controller/Magewire/Update.php)
+ → HandleRequestFacade::update()
+ → HandleRequests mechanism
+ 1. Reconstruct component from snapshot memo (resolver class)
+ 2. boot() + initialize() hooks
+ 3. hydrate() hooks
+ 4. booted() hook
+ 5. Apply property updates → updating/updated hooks
+ 6. Execute method calls
+ 7. rendering() hook → render template → rendered() hook
+ 8. dehydrate() hooks
+ 9. Generate new snapshot + checksum
+ 10. Return { snapshot, effects, html }
+ ← Alpine.js morphs DOM, stores new snapshot
+```
+
+---
+
+## Layout & Template Integration
+
+Magewire's own layout is defined in `src/view/base/layout/default.xml`. It creates a `magewire` root block that outputs all setup scripts (Alpine.js init, utilities, addons, directives, feature scripts).
+
+Magewire can be bound to any block class — not just `Magewirephp\Magewire\Block\Magewire`. The `ResolveComponents` mechanism discovers all blocks with a `magewire` data argument during layout render, uses the appropriate `ComponentResolver` to instantiate the PHP component class, mounts it, and embeds the snapshot as a `wire:snapshot` attribute on the root element.
+
+### Layout Containers
+
+All Magewire layout output is organized in named containers. Extend via standard Magento layout XML:
+
+```xml
+
+
+
+```
+
+Full layout reference (from `src/view/base/layout/default.xml`). Elements marked **(block)** have templates; **(container)** are injection points.
+
+| Name | Type | Purpose |
+|------|------|---------|
+| `magewire` | block | Root block (`root.phtml`) |
+| `magewire.global` | block | Global JS setup (`js/magewire/global.phtml`) |
+| `magewire.global.before` | container | Before global — holds Alpine load, Alpine code, Alpine components |
+| `magewire.alpinejs.load` | container | Alpine JS loading |
+| `magewire.alpinejs` | container | Custom Alpine JS code (before Magewire Alpine) |
+| `magewire.alpinejs.components` | container | Alpine `Alpine.data()` registrations |
+| `magewire.utilities` | block | Utility parent (`js/magewire/utilities.phtml`) — renders child utilities |
+| `magewire.utilities.after` | container | Inject custom utilities after core ones |
+| `magewire.addons` | block | Addon parent (`js/magewire/addons.phtml`) — renders child addons |
+| `magewire.addons.after` | container | Inject custom addons after core ones |
+| `magewire.global.after` | container | Custom extensions after the global block |
+| `magewire.before` | container | Holds Alpine directives, UI components, Alpine after |
+| `magewire.alpinejs.directives` | container | Custom Alpine directives |
+| `magewire.ui-components` | container | Alpine UI components (notifier lives here) |
+| `magewire.alpinejs.after` | container | Custom Alpine code after Magewire Alpine code |
+| `magewire.before.internal` | container | Debug state blocks |
+| `magewire.internal` | block | Non-overridable core block — do not inject here |
+| `magewire.internal.backwards-compatibility` | container | BC injection point inside internal block |
+| `magewire.directives` | block | Directive parent (`js/magewire/directives.phtml`) — renders child directives |
+| `magewire.features` | block | Feature parent (`js/magewire/features.phtml`) — renders child feature scripts |
+| `magewire.after.internal` | container | Inject content after the internal block |
+| `magewire.disabled` | container | Renders only when Magewire is inactive |
+| `magewire.after` | container | Everything else (debug tools, HTML blocks) |
+| `magewire.legacy` | container | V1 backwards compatibility — do not use for new code |
+
+Note: `magewire.utilities`, `magewire.addons`, `magewire.directives`, and `magewire.features` are **blocks** (not containers). They have parent templates that call `$block->getChildHtml()` to render their children. Add custom children as blocks inside them.
+
+---
+
+## JavaScript Architecture
+
+**All JavaScript must be CSP-compliant.** Never use raw `
+end() ?>
+```
+
+## Facades for Public APIs
+
+When a Feature or Mechanism needs a public API that other modules consume, register a Facade via DI.
+
+```xml
+-
+
- Vendor\Module\Features\SupportMyFeature
+ - Vendor\Module\Features\SupportMyFeature\MyFeatureFacade
+ - 5050
+
+```
+
+Access via the service provider:
+
+```php
+$facade = $magewireServiceProvider->getMyFeatureFacade();
+```
+
+Facades live in their feature's subdirectory. This pattern is experimental — check the `magewire-architecture` skill for current status.
diff --git a/.claude/skills/magewire-best-practices/references/javascript.md b/.claude/skills/magewire-best-practices/references/javascript.md
new file mode 100644
index 00000000..b122a3f3
--- /dev/null
+++ b/.claude/skills/magewire-best-practices/references/javascript.md
@@ -0,0 +1,101 @@
+# JavaScript Integration
+
+## CSP-Compatible Scripts Only
+
+All inline JavaScript must use the fragment utility (`$magewireFragment->make()->script()->start()/end()`). Never write raw `
+end() ?>
+```
+
+- `$escaper` is always imported even when not used directly in this file, so subblocks can rely on it.
+- The `@internal` docblock signals that this file is not intended for theme override.
+- Never omit `$script->end()`. The fragment mechanism is only complete when `end()` is called.
+
+**PHP values in JS strings** — always escape through `$escaper->escapeJs()`:
+
+```php
+'= $escaper->escapeJs(__('Too many requests! Please wait.')) ?>'
+```
+
+**PHP comments inside `
+end() ?>
+```
+
+**Layout block** in `src/view/base/layout/default.xml`, inside `magewire.utilities`:
+
+```xml
+
+```
+
+---
+
+## Addon
+
+An addon is a stateful, framework-agnostic API. It is Alpine-reactive when the third argument to
+`register()` is `true`, allowing its state to drive the DOM directly.
+
+**`src/view/base/templates/js/magewire/addons/poll.phtml`**
+
+```php
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
+```
+
+**Layout block** in `src/view/base/layout/default.xml`, inside `magewire.addons`:
+
+```xml
+
+```
+
+---
+
+## Alpine component
+
+An Alpine component is a thin wrapper that exposes addon state and a limited set of methods to HTML templates.
+All logic lives in the addon. The component only provides what the template needs.
+
+Magewire ships `magewire.csp.min.js`, an unmodified copy of Livewire's JavaScript bundle, which includes
+the Alpine CSP build. HTML attribute expressions can only access properties and methods defined on the
+component's returned data object. Closure variables like `const poll = window.MagewireAddons.poll` are
+not in scope from HTML — they must be wrapped in component methods. That is the sole reason for the
+`START/END` comment markers.
+
+**`src/view/base/templates/js/alpinejs/components/magewire-poll.phtml`**
+
+```php
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
+```
+
+**Usage in a theme template:**
+
+```html
+
+
+ Toggle
+ Reset
+
+```
+
+**Layout block** in `src/view/base/layout/default.xml`, inside `magewire.alpinejs.components`:
+
+```xml
+
+```
+
+---
+
+## Directive
+
+A directive adds a declarative HTML attribute. It is self-contained and exports nothing.
+
+**`src/view/base/templates/js/magewire/directives/copy.phtml`**
+
+```php
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
+```
+
+**Usage:**
+
+```html
+
+Copy me
+
+
+npm install magewire
+Copy command
+```
+
+**Layout block** in `src/view/base/layout/default.xml`, inside `magewire.directives`:
+
+```xml
+
+```
+
+---
+
+## Feature
+
+A feature bridges a Magewire PHP Feature's effects to an addon or utility. It reads effects pushed by PHP
+and calls the appropriate addon methods.
+
+The PHP side pushes a custom effect in `dehydrate()`:
+
+```php
+public function dehydrate(ComponentContext $context): void
+{
+ $context->pushEffect('poll', ['reset' => true]);
+}
+```
+
+**`src/view/base/templates/magewire-features/support-magewire-poll/support-magewire-poll.phtml`**
+
+```php
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
+```
+
+**Layout block** in `src/view/base/layout/default.xml`, inside `magewire.features`:
+
+```xml
+
+```
+
+---
+
+## Frontend-only component
+
+When a component uses frontend-specific APIs (e.g., Hyva's `hyva.getFormKey()`), it goes in `frontend/` instead of `base/`.
+The structure under `js/` is identical — only the view area changes.
+
+**`src/view/frontend/templates/js/alpinejs/components/magewire-form.phtml`**
+
+```php
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
+```
+
+**Layout block** in `src/view/frontend/layout/default.xml`:
+
+```xml
+
+```
diff --git a/.claude/skills/magewire-javascript/references/reference.md b/.claude/skills/magewire-javascript/references/reference.md
new file mode 100644
index 00000000..add97b6f
--- /dev/null
+++ b/.claude/skills/magewire-javascript/references/reference.md
@@ -0,0 +1,372 @@
+# Reference
+
+---
+
+## Naming conventions
+
+| Type | Function name | Example |
+|---|---|---|
+| Utility | `magewire{Name}Utility` | `magewireClipboardUtility` |
+| Addon | `magewire{Name}Addon` | `magewirePollAddon` |
+| Alpine component | `magewire{Name}` | `magewirePoll` |
+| Alpine bindings | `magewire{Name}Bindings` | `magewirePollBindings` |
+| Directive | — (IIFE, no export) | — |
+| Feature | — (event listener, no export) | — |
+
+Registration keys are the camelCase name without prefix or suffix: `'clipboard'`, `'poll'`, `'notifier'`.
+
+---
+
+## Registration calls
+
+```javascript
+// Utility — waits for alpine:init
+document.addEventListener('alpine:init', () => window.MagewireUtilities.register('name', factoryFn), { once: true });
+
+// Addon — immediate; MagewireResource queues reactive items if Alpine is not yet ready
+window.MagewireAddons.register('name', factoryFn, true); // true = Alpine.reactive
+window.MagewireAddons.register('name', factoryFn, false); // false = plain object
+
+// Alpine component
+document.addEventListener('alpine:init', () => Alpine.data('magewire{Name}', factoryFn), { once: true });
+
+// Alpine bindings (optional companion)
+document.addEventListener('alpine:init', () => Alpine.bind('magewire{Name}Bindings', bindingsFn), { once: true });
+
+// Alpine store
+document.addEventListener('alpine:init', () => Alpine.store('name', { ... }));
+
+// Directive
+document.addEventListener('magewire:initialized', event => Magewire.directive('mage:name', handler));
+```
+
+---
+
+## Event timing
+
+| Event | Fires when | Register here |
+|---|---|---|
+| `alpine:init` | Alpine starts, before DOM walk | `Alpine.data`, `Alpine.store`, `Alpine.bind`, utilities, addons (if not immediate) |
+| `magewire:init` | Magewire runtime ready | `Magewire.hook('commit')`, `Magewire.hook('request')`, feature init |
+| `magewire:initialized` | Full init complete | `Magewire.directive()` |
+
+Always add `{ once: true }` to registration listeners.
+
+---
+
+## PHP boilerplate
+
+**Minimal (no PHP values in JS):**
+
+```php
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
+```
+
+**With PHP enum values:**
+
+```php
+use Magewirephp\Magewire\Model\Magewire\Notifier\NotificationStateEnum;
+
+$state = NotificationStateEnum::IDLE->getState(); // used inside escapeJs()
+```
+
+```javascript
+const state = '= $escaper->escapeJs(NotificationStateEnum::IDLE->getState()) ?>';
+const levels = JSON.parse('= json_encode(NotificationStateEnum::cases()) ?>');
+```
+
+**PHP comment inside `
+
+ = $block->getChildHtml() ?>
+
+```
+
+See `themes/Hyva/view/frontend/templates/script.phtml` for the production version.
+
+## 5. Registering a Feature-scoped bridge script
+
+Use when your theme Feature needs a companion JS file rendered into `magewire.features`.
+
+**`themes/MyTheme/view/frontend/layout/default_mytheme.xml`**
+```xml
+
+
+
+```
+
+**`themes/MyTheme/view/frontend/templates/magewire-features/support-mytheme/bridge.phtml`**
+```php
+getData('magewire_utility');
+$magewireFragment = $magewireUtility->utils()->fragment();
+$script = $magewireFragment->make()->script()->start();
+?>
+
+end(); ?>
+```
+
+The fragment-based script pattern is non-negotiable: it rewrites inline scripts into CSP-compliant form. Raw `
+ HTML;
+ }
+ public static function scriptConfig($options = [])
+ {
+ app(static::class)->hasRenderedScripts = true;
+ $nonce = static::nonce($options);
+ $progressBar = config('livewire.navigate.show_progress_bar', true) ? '' : 'data-no-progress-bar';
+ $attributes = json_encode(['csrf' => app()->has('session.store') ? csrf_token() : '', 'uri' => app('livewire')->getUpdateUri(), 'progressBar' => $progressBar, 'nonce' => isset($options['nonce']) ? $options['nonce'] : '']);
+ return <<window.livewireScriptConfig = {$attributes};
+ HTML;
+ }
+ protected static function usePublishedAssetsIfAvailable($url, $manifest, $nonce)
+ {
+ $assetWarning = null;
+ // Check to see if static assets have been published...
+ if (!file_exists(public_path('vendor/livewire/manifest.json'))) {
+ return [$url, $assetWarning];
+ }
+ $publishedManifest = json_decode(file_get_contents(public_path('vendor/livewire/manifest.json')), true);
+ $versionedFileName = $publishedManifest['/livewire.js'];
+ $fileName = config('app.debug') ? '/livewire.js' : '/livewire.min.js';
+ $versionedFileName = "{$fileName}?id={$versionedFileName}";
+ $assertUrl = config('livewire.asset_url') ?? (app('livewire')->isRunningServerless() ? rtrim(config('app.asset_url'), '/') . "/vendor/livewire{$versionedFileName}" : url("vendor/livewire{$versionedFileName}"));
+ $url = $assertUrl;
+ if ($manifest !== $publishedManifest) {
+ $assetWarning = <<
+ console.warn('Livewire: The published Livewire assets are out of date\\n See: https://livewire.laravel.com/docs/installation#publishing-livewires-frontend-assets')
+
+
+ HTML;
+ }
+ return [$url, $assetWarning];
+ }
+ protected static function minify($subject)
+ {
+ return preg_replace('~(\v|\t|\s{2,})~m', '', $subject);
+ }
+ protected static function nonce($options = [])
+ {
+ $nonce = $options['nonce'] ?? Vite::cspNonce();
+ return $nonce ? "nonce=\"{$nonce}\"" : '';
+ }
+ function __construct(private readonly AssetsRepository $assetsRepository, private readonly RequestInterface $request)
+ {
+ //
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleComponents/Checksum.php b/dist/Mechanisms/HandleComponents/Checksum.php
new file mode 100644
index 00000000..b5dbc069
--- /dev/null
+++ b/dist/Mechanisms/HandleComponents/Checksum.php
@@ -0,0 +1,50 @@
+generate($snapshot)) {
+ trigger('checksum.fail', $checksum, $comparator, $snapshot);
+ throw new CorruptComponentPayloadException();
+ }
+ }
+ /**
+ * @throws FileSystemException
+ * @throws RuntimeException
+ */
+ function generate(array $snapshot): string
+ {
+ $hashKey = $this->deploymentConfig->get('crypt/key');
+ $checksum = hash_hmac('sha256', $this->serializer->serialize($snapshot), $hashKey);
+ trigger('checksum.generate', $checksum, $snapshot);
+ return $checksum;
+ }
+ function __construct(private readonly DeploymentConfig $deploymentConfig, private readonly SerializerInterface $serializer)
+ {
+ //
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleComponents/ComponentContext.php b/dist/Mechanisms/HandleComponents/ComponentContext.php
new file mode 100644
index 00000000..6ddcb744
--- /dev/null
+++ b/dist/Mechanisms/HandleComponents/ComponentContext.php
@@ -0,0 +1,118 @@
+effects = $effects instanceof Effects ? $effects : null ?? ObjectManager::getInstance()->create(Effects::class);
+ $this->memo = $memo instanceof Memo ? $memo : null ?? ObjectManager::getInstance()->create(Memo::class);
+ }
+ public function isMounting()
+ {
+ return $this->mounting;
+ }
+ public function addEffect($key, $value)
+ {
+ $this->getEffects()->setData($key, $value);
+ }
+ public function pushEffect($key, $value, $iKey = null)
+ {
+ $effects = $this->getEffects()->getData();
+ if (!is_array($effects)) {
+ $effects = [];
+ }
+ if ($iKey) {
+ $effects[$key][$iKey] = $value;
+ } else {
+ $effects[$key][] = $value;
+ }
+ $this->getEffects()->setData($effects);
+ return $this;
+ }
+ public function addMemo($key, $value)
+ {
+ $this->getMemo()->setData($key, $value);
+ }
+ public function pushMemo($key, $value, $iKey = null)
+ {
+ $memo = $this->getMemo()->getData();
+ if (!is_array($memo)) {
+ $memo = [];
+ }
+ if ($iKey) {
+ $memo[$key][$iKey] = $value;
+ } else {
+ $memo[$key][] = $value;
+ }
+ $this->getMemo()->setData($memo);
+ return $this;
+ }
+ public function setEffects(Effects $effects)
+ {
+ $this->effects = $effects;
+ return $this;
+ }
+ public function setMemo(Memo $memo)
+ {
+ $this->memo = $memo;
+ return $this;
+ }
+ public function getEffects(): Effects
+ {
+ return $this->effects;
+ }
+ public function getMemo(): Memo
+ {
+ return $this->memo;
+ }
+ public function getComponent(): Component
+ {
+ return $this->component;
+ }
+ public function getBlock(): AbstractBlock
+ {
+ return $this->block;
+ }
+ public function hasEffect($key, $iKey = null): bool
+ {
+ $has = $this->getEffects()->hasData($key);
+ if ($has && $iKey) {
+ $data = $this->getEffects()->getData($key);
+ if (is_array($data)) {
+ return isset($data[$iKey]);
+ }
+ return false;
+ }
+ return $has;
+ }
+ public function hasMemo($key, $iKey = null)
+ {
+ $has = $this->getMemo()->hasData($key);
+ if ($has && $iKey) {
+ $data = $this->getMemo()->getData($key);
+ if (is_array($data)) {
+ return isset($data[$iKey]);
+ }
+ return false;
+ }
+ return $has;
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleComponents/HandleComponents.php b/dist/Mechanisms/HandleComponents/HandleComponents.php
new file mode 100644
index 00000000..0c0bdd62
--- /dev/null
+++ b/dist/Mechanisms/HandleComponents/HandleComponents.php
@@ -0,0 +1,507 @@
+propertySynthesizers, $class);
+ }
+ }
+ public function mount($name, $params = [], $key = null, AbstractBlock|null $block = null, Component|null $component = null)
+ {
+ $parent = last(self::$componentStack);
+ if ($html = $this->shortCircuitMount($name, $params, $key, $parent)) {
+ $shortCircuitHtml = $html;
+ return function (AbstractBlock $block, string $html) use ($shortCircuitHtml) {
+ return [$block, $shortCircuitHtml];
+ };
+ }
+ if ($block === null) {
+ throw new InvalidArgumentException('Argument $block can not be null.');
+ }
+ $context = $this->componentContextFactory->create(['block' => $block, 'component' => $component, 'mounting' => true]);
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ $finish = trigger('mount', $component, $params, $key, $parent);
+ if (config('app.debug')) {
+ trigger('profile', 'mount', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+ $this->pushOntoComponentStack($component);
+ $start = config('app.debug') ? microtime(true) : null;
+ /*
+ * This function is divided based on the original design due to the presence of both pre and post-render
+ * mechanisms within Magento. It serves as a form of middleware where the Magewire mechanism is embedded
+ * around it, instead of being directly responsible for rendering the component. The standard Block render
+ * mechanism within Magewire remains responsible for the actual rendering process and does return HTML.
+ */
+ return function (AbstractBlock $block, string $html) use ($component, $context, $start, $finish) {
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ $html = $this->render($component, '
', $html);
+ if (config('app.debug')) {
+ trigger('profile', 'render', $component->getId(), [$start, microtime(true)]);
+ }
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ trigger('dehydrate', $component, $context);
+ $snapshot = $this->snapshot($component, $context);
+ if (config('app.debug')) {
+ trigger('profile', 'dehydrate', $component->getId(), [$start, microtime(true)]);
+ }
+ trigger('destroy', $component, $context);
+ $html = Utils::insertAttributesIntoHtmlRoot($html, ['wire:snapshot' => $snapshot, 'wire:effects' => $context->getEffects()->toArray()]);
+ $this->popOffComponentStack();
+ return [$block, $finish($html, $snapshot)];
+ };
+ }
+ protected function shortCircuitMount($name, $params, $key, $parent)
+ {
+ $newHtml = null;
+ trigger('pre-mount', $name, $params, $key, $parent, function ($html) use (&$newHtml) {
+ $newHtml = $html;
+ });
+ return $newHtml;
+ }
+ /**
+ * @throws FileSystemException
+ * @throws ComponentNotFoundException
+ * @throws MethodNotFoundException
+ * @throws RuntimeException
+ */
+ public function update($snapshot, $updates, $calls, AbstractBlock|null $block = null)
+ {
+ if ($block === null) {
+ throw new InvalidArgumentException('Argument $block can not be of type null.');
+ }
+ $data = $snapshot['data'];
+ $memo = $snapshot['memo'];
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ [$component, $context] = $this->fromSnapshot($snapshot, $block);
+ $this->pushOntoComponentStack($component);
+ trigger('hydrate', $component, $memo, $context);
+ $this->updateProperties($component, $updates, $data, $context);
+ if (config('app.debug')) {
+ trigger('profile', 'hydrate', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+ $this->callMethods($component, $calls, $context);
+ /*
+ * This function is divided based on the original design due to the presence of both pre and post-render
+ * mechanisms within Magento. It serves as a form of middleware where the Magewire mechanism is embedded
+ * around it, instead of being directly responsible for rendering the component. The standard Block render
+ * mechanism within Magewire remains responsible for the actual rendering process.
+ */
+ return function (AbstractBlock $block, string $html) use ($snapshot, $context, $component) {
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ if ($html = $this->render($component, '', $html)) {
+ $context->addEffect('html', $html);
+ if (config('app.debug')) {
+ trigger('profile', 'render', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+ }
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ trigger('dehydrate', $component, $context);
+ $snapshot = $this->snapshot($component, $context);
+ if (config('app.debug')) {
+ trigger('profile', 'dehydrate', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+ trigger('destroy', $component, $context);
+ $this->popOffComponentStack();
+ return [$snapshot, $context->effects];
+ };
+ }
+ /**
+ * @throws ComponentNotFoundException
+ * @throws FileSystemException
+ * @throws RuntimeException
+ * @throws Exception
+ */
+ public function fromSnapshot($snapshot, AbstractBlock|null $block = null)
+ {
+ if (!$block instanceof AbstractBlock) {
+ throw new Exception(sprintf('Invalid block type: %s', is_object($block) ? get_class($block) : 'Unknown'));
+ } elseif (!$block->getData('magewire') instanceof Component) {
+ throw new ComponentNotFoundException(sprintf('Unable to find component: [%s]', $block->getNameInLayout()));
+ }
+ /** @var Component $component */
+ $component = $block->getData('magewire');
+ $context = $this->componentContextFactory->create(['component' => $component, 'block' => $block]);
+ $this->hydrateProperties($context->getComponent(), $snapshot['data'], $context);
+ return [$component, $context];
+ }
+ public function snapshot($component, $context = null)
+ {
+ $context ??= $this->componentContextFactory->create(['component' => $component]);
+ $data = $this->dehydrateProperties($component, $context);
+ $snapshot = ['data' => $data, 'memo' => ['id' => $component->getId(), 'name' => $component->getName(), ...$context->getMemo()->toArray()]];
+ $snapshot['checksum'] = $this->checksum->generate($snapshot);
+ return $snapshot;
+ }
+ protected function dehydrateProperties($component, $context)
+ {
+ $data = Utils::getPublicPropertiesDefinedOnSubclass($component);
+ foreach ($data as $key => $value) {
+ $data[$key] = $this->dehydrate($value, $context, $key);
+ }
+ return $data;
+ }
+ protected function dehydrate($target, $context, $path)
+ {
+ if (Utils::isAPrimitive($target)) {
+ return $target;
+ }
+ $synth = $this->propertySynth($target, $context, $path);
+ [$data, $meta] = $synth->dehydrate($target, function ($name, $child) use ($context, $path) {
+ return $this->dehydrate($child, $context, "{$path}.{$name}");
+ });
+ $meta['s'] = $synth::getKey();
+ return [$data, $meta];
+ }
+ protected function hydrateProperties($component, $data, $context)
+ {
+ foreach ($data as $key => $value) {
+ if (!property_exists($component, $key)) {
+ continue;
+ }
+ $child = $this->hydrate($value, $context, $key);
+ // Typed properties shouldn't be set back to "null". It will throw an error...
+ if ((new \ReflectionProperty($component, $key))->getType() && is_null($child)) {
+ continue;
+ }
+ $component->{$key} = $child;
+ }
+ }
+ protected function hydrate($valueOrTuple, $context, $path)
+ {
+ if (!Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) {
+ return $value;
+ }
+ [$value, $meta] = $tuple;
+ if ($this->isRemoval($value) && str($path)->contains('.')) {
+ return $value;
+ }
+ $synth = $this->propertySynth($meta['s'], $context, $path);
+ return $synth->hydrate($value, $meta, function ($name, $child) use ($context, $path) {
+ return $this->hydrate($child, $context, "{$path}.{$name}");
+ });
+ }
+ protected function hydratePropertyUpdate($valueOrTuple, $context, $path)
+ {
+ if (!Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) {
+ return $value;
+ }
+ [$value, $meta] = $tuple;
+ // Nested properties get set as `__rm__` when they are removed. We don't want to hydrate these.
+ if ($this->isRemoval($value) && str($path)->contains('.')) {
+ return $value;
+ }
+ $synth = $this->propertySynth($meta['s'], $context, $path);
+ return $synth->hydrate($value, $meta, function ($name, $child) {
+ return $child;
+ });
+ }
+ protected function render($component, $default = null, string|null $html = null)
+ {
+ $replace = store($component)->get('skipRender', false);
+ if ($replace) {
+ $replace = value(is_string($html) ? $html : $default);
+ if (!$replace) {
+ return '';
+ }
+ return Utils::insertAttributesIntoHtmlRoot($html, ['wire:id' => $component->getId()]);
+ }
+ [$block, $properties] = $this->getView($component);
+ return $this->trackInRenderStack($component, function () use ($component, $block, $properties, $html) {
+ $finish = trigger('render', $component, $block, $properties);
+ $html = Utils::insertAttributesIntoHtmlRoot($html, ['wire:id' => $component->getId()]);
+ return $finish($html, function ($newHtml) use (&$html) {
+ $html = $newHtml;
+ }, $component->block());
+ });
+ }
+ protected function getView($component)
+ {
+ // WIP
+ // if (method_exists($component, 'render')) {
+ // $block = wrap($component)->render();
+ //
+ // if ($block instanceof AbstractBlock) {
+ // $block->setData($component->getBlock()->getData());
+ // $block->setNameInLayout($component->getBlock()->getNameInLayout());
+ //
+ // $component->getResolver()->construct($block);
+ // }
+ // }
+ return [$component->block(), Utils::getPublicPropertiesDefinedOnSubclass($component)];
+ }
+ protected function trackInRenderStack($component, $callback)
+ {
+ static::$renderStack[] = $component;
+ return tap($callback(), function () {
+ array_pop(static::$renderStack);
+ });
+ }
+ protected function updateProperties($component, $updates, $data, $context)
+ {
+ $finishes = [];
+ foreach ($updates as $path => $value) {
+ $value = $this->hydrateForUpdate($data, $path, $value, $context);
+ // We only want to run "updated" hooks after all properties have
+ // been updated so that each individual hook has the ability
+ // to overwrite the updated states of other properties...
+ $finishes[] = $this->updateProperty($component, $path, $value, $context);
+ }
+ foreach ($finishes as $finish) {
+ $finish();
+ }
+ }
+ public function updateProperty($component, $path, $value, $context)
+ {
+ $segments = explode('.', $path);
+ $property = array_shift($segments);
+ $finish = trigger('update', $component, $path, $value);
+ // Ensure that it's a public property, not on the base class first...
+ if (!in_array($property, array_keys(Utils::getPublicPropertiesDefinedOnSubclass($component)))) {
+ throw new PublicPropertyNotFoundException($property, $component->getName());
+ }
+ // If this isn't a "deep" set, set it directly, otherwise we have to
+ // recursively get up and set down the value through the synths...
+ if (empty($segments)) {
+ $this->setComponentPropertyAwareOfTypes($component, $property, $value);
+ } else {
+ $propertyValue = $component->{$property};
+ $this->setComponentPropertyAwareOfTypes($component, $property, $this->recursivelySetValue($property, $propertyValue, $value, $segments, 0, $context));
+ }
+ return $finish;
+ }
+ protected function hydrateForUpdate($raw, $path, $value, $context)
+ {
+ $meta = $this->getMetaForPath($raw, $path);
+ // If we have meta data already for this property, let's use that to get a synth...
+ if ($meta) {
+ return $this->hydratePropertyUpdate([$value, $meta], $context, $path, $raw);
+ }
+ // If we don't, let's check to see if it's a typed property and fetch the synth that way...
+ $parent = str($path)->contains('.') ? data_get($context->component, str($path)->beforeLast('.')->toString()) : $context->component;
+ $childKey = str($path)->afterLast('.');
+ if ($parent && is_object($parent) && property_exists($parent, $childKey) && Utils::propertyIsTyped($parent, $childKey)) {
+ $type = Utils::getProperty($parent, $childKey)->getType();
+ $types = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type];
+ foreach ($types as $type) {
+ $synth = $this->getSynthesizerByType($type->getName(), $context, $path);
+ if ($synth) {
+ return $synth->hydrateFromType($type->getName(), $value);
+ }
+ }
+ }
+ return $value;
+ }
+ protected function getMetaForPath($raw, $path)
+ {
+ $segments = explode('.', $path);
+ $first = array_shift($segments);
+ [$data, $meta] = Utils::isSyntheticTuple($raw) ? $raw : [$raw, null];
+ if ($path !== '') {
+ $value = $data[$first] ?? null;
+ return $this->getMetaForPath($value, implode('.', $segments));
+ }
+ return $meta;
+ }
+ protected function recursivelySetValue($baseProperty, $target, $leafValue, $segments, $index = 0, $context = null)
+ {
+ $isLastSegment = count($segments) === $index + 1;
+ $property = $segments[$index];
+ $path = implode('.', array_slice($segments, 0, $index + 1));
+ $synth = $this->propertySynth($target, $context, $path);
+ if ($isLastSegment) {
+ $toSet = $leafValue;
+ } else {
+ $propertyTarget = $synth->get($target, $property);
+ // "$path" is a dot-notated key. This means we may need to drill
+ // down and set a value on a deeply nested object. That object
+ // may not exist, so let's find the first one that does...
+ // Here's we've determined we're trying to set a deeply nested
+ // value on an object/array that doesn't exist, so we need
+ // to build up that non-existant nesting structure first.
+ if ($propertyTarget === null) {
+ $propertyTarget = [];
+ }
+ $toSet = $this->recursivelySetValue($baseProperty, $propertyTarget, $leafValue, $segments, $index + 1, $context);
+ }
+ $method = $this->isRemoval($leafValue) && $isLastSegment ? 'unset' : 'set';
+ $pathThusFar = collect([$baseProperty, ...$segments])->slice(0, $index + 1)->join('.');
+ $fullPath = collect([$baseProperty, ...$segments])->join('.');
+ $synth->{$method}($target, $property, $toSet, $pathThusFar, $fullPath);
+ return $target;
+ }
+ protected function setComponentPropertyAwareOfTypes($component, $property, $value)
+ {
+ try {
+ $component->{$property} = $value;
+ } catch (\TypeError $e) {
+ // If an "int" is being set to empty string, unset the property (making it null).
+ // This is common in the case of `wire:model`ing an int to a text field...
+ // If a value is being set to "null", do the same...
+ if ($value === '' || $value === null) {
+ unset($component->{$property});
+ } else {
+ throw $e;
+ }
+ }
+ }
+ /**
+ * @throws MethodNotFoundException
+ */
+ protected function callMethods($root, $calls, $context)
+ {
+ $returns = [];
+ foreach ($calls as $idx => $call) {
+ $method = $call['method'];
+ $params = $call['params'];
+ $earlyReturnCalled = false;
+ $earlyReturn = null;
+ $returnEarly = function ($return = null) use (&$earlyReturnCalled, &$earlyReturn) {
+ $earlyReturnCalled = true;
+ $earlyReturn = $return;
+ };
+ $finish = trigger('call', $root, $method, $params, $context, $returnEarly);
+ if ($earlyReturnCalled) {
+ $returns[] = $finish($earlyReturn);
+ continue;
+ }
+ $methods = Utils::getPublicMethodsDefinedBySubClass($root);
+ // Also remove "render" from the list...
+ $methods = array_values(array_diff($methods, ['render']));
+ // @todo: put this in a better place:
+ $methods[] = '__dispatch';
+ if (!in_array($method, $methods)) {
+ throw new MethodNotFoundException($method);
+ }
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ $return = wrap($root)->{$method}(...$params);
+ if (config('app.debug')) {
+ trigger('profile', 'call' . $idx, $root->getId(), [$start, microtime(true)]);
+ }
+ $returns[] = $finish($return);
+ }
+ $context->addEffect('returns', $returns);
+ }
+ public function findSynth($keyOrTarget, $component): ?Synth
+ {
+ $context = new ComponentContext($component);
+ try {
+ return $this->propertySynth($keyOrTarget, $context, null);
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+ public function propertySynth($keyOrTarget, $context, $path): Synth
+ {
+ return is_string($keyOrTarget) ? $this->getSynthesizerByKey($keyOrTarget, $context, $path) : $this->getSynthesizerByTarget($keyOrTarget, $context, $path);
+ }
+ /**
+ * @throws Exception
+ */
+ protected function getSynthesizerByKey($key, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::getKey() === $key) {
+ return ObjectManager::getInstance()->create($synth, ['context' => $context, 'path' => $path]);
+ }
+ }
+ throw new Exception('No synthesizer found for key: "' . $key . '"');
+ }
+ /**
+ * @throws Exception
+ */
+ protected function getSynthesizerByTarget($target, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::match($target)) {
+ return ObjectManager::getInstance()->create($synth, ['context' => $context, 'path' => $path]);
+ }
+ }
+ throw new Exception('Property type not supported in Magewire for property: [' . json_encode($target) . ']');
+ }
+ protected function getSynthesizerByType($type, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::matchByType($type)) {
+ return new $synth($context, $path);
+ }
+ }
+ return null;
+ }
+ protected function pushOntoComponentStack($component)
+ {
+ array_push($this::$componentStack, $component);
+ }
+ protected function popOffComponentStack()
+ {
+ array_pop($this::$componentStack);
+ }
+ protected function isRemoval($value)
+ {
+ return $value === '__rm__';
+ }
+ public function __construct(private readonly ComponentContextFactory $componentContextFactory, private readonly Checksum $checksum, protected array $synthesizers = [])
+ {
+ // Improve modularity by injecting property synthesizers via dependency injection.
+ $this->propertySynthesizers = $synthesizers;
+ }
+ public static function provide()
+ {
+ on('pre-mount', function ($target, $view) {
+ return function ($html) use ($view, $target) {
+ if (method_exists($target, 'render')) {
+ return $target->render();
+ }
+ return $html;
+ };
+ });
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php b/dist/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php
new file mode 100644
index 00000000..3f963151
--- /dev/null
+++ b/dist/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php
@@ -0,0 +1,48 @@
+ $child) {
+ $target[$key] = $dehydrateChild($key, $child);
+ }
+ return [$target, []];
+ }
+ function hydrate($value, $meta, $hydrateChild)
+ {
+ // If we are "hydrating" a value about to be used in an update,
+ // Let's make sure it's actually an array before try to set it.
+ // This is most common in the case of "__rm__" values, but also
+ // applies to any non-array value...
+ if (!is_array($value)) {
+ return $value;
+ }
+ foreach ($value as $key => $child) {
+ $value[$key] = $hydrateChild($key, $child);
+ }
+ return $value;
+ }
+ function set(&$target, $key, $value)
+ {
+ $target[$key] = $value;
+ }
+ function unset(&$target, $key)
+ {
+ unset($target[$key]);
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php b/dist/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php
new file mode 100644
index 00000000..c67dbfed
--- /dev/null
+++ b/dist/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php
@@ -0,0 +1,42 @@
+value, ['class' => get_class($target)]];
+ }
+ function hydrate($value, $meta)
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+ $class = $meta['class'];
+ return $class::from($value);
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleComponents/Synthesizers/FloatSynth.php b/dist/Mechanisms/HandleComponents/Synthesizers/FloatSynth.php
new file mode 100644
index 00000000..cc2bf729
--- /dev/null
+++ b/dist/Mechanisms/HandleComponents/Synthesizers/FloatSynth.php
@@ -0,0 +1,34 @@
+ $child) {
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+ return [$data, []];
+ }
+ function hydrate($value, $meta, $hydrateChild)
+ {
+ $obj = new stdClass();
+ foreach ($value as $key => $child) {
+ $obj->{$key} = $hydrateChild($key, $child);
+ }
+ return $obj;
+ }
+ function set(&$target, $key, $value)
+ {
+ $target->{$key} = $value;
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleComponents/Synthesizers/Synth.php b/dist/Mechanisms/HandleComponents/Synthesizers/Synth.php
new file mode 100644
index 00000000..36e1b5aa
--- /dev/null
+++ b/dist/Mechanisms/HandleComponents/Synthesizers/Synth.php
@@ -0,0 +1,59 @@
+{$key};
+ }
+ function __call($method, $params)
+ {
+ if ($method === 'dehydrate') {
+ throw new \Exception('You must define a "dehydrate" method');
+ }
+ if ($method === 'hydrate') {
+ throw new \Exception('You must define a "hydrate" method');
+ }
+ if ($method === 'hydrateFromType') {
+ throw new \Exception('You must define a "hydrateFromType" method');
+ }
+ if ($method === 'get') {
+ throw new \Exception('This synth doesn\'t support getting properties: ' . get_class($this));
+ }
+ if ($method === 'set') {
+ throw new \Exception('This synth doesn\'t support setting properties: ' . get_class($this));
+ }
+ if ($method === 'unset') {
+ throw new \Exception('This synth doesn\'t support unsetting properties: ' . get_class($this));
+ }
+ if ($method === 'call') {
+ throw new \Exception('This synth doesn\'t support calling methods: ' . get_class($this));
+ }
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/HandleRequests/HandleRequests.php b/dist/Mechanisms/HandleRequests/HandleRequests.php
new file mode 100644
index 00000000..94b3d04e
--- /dev/null
+++ b/dist/Mechanisms/HandleRequests/HandleRequests.php
@@ -0,0 +1,150 @@
+findUpdateRoute() !== null;
+ }
+ function getUpdateUri()
+ {
+ // When routes are cached, $this->updateRoute may be null because
+ // setUpdateRoute() was never called (the route already existed).
+ // In this case, find the route from the router.
+ $route = $this->updateRoute ?? $this->findUpdateRoute();
+ return (string) str(route($route->getName(), [], false))->start('/');
+ }
+ protected function findUpdateRoute()
+ {
+ // Find the route with name ending in 'livewire.update'.
+ // Custom routes can have prefixes (e.g., 'tenant.livewire.update')
+ // so we check for routes ending with 'livewire.update', not just exact matches.
+ // Prioritise custom routes over the default route.
+ $defaultRoute = null;
+ foreach (Route::getRoutes()->getRoutes() as $route) {
+ if (str($route->getName())->endsWith('livewire.update')) {
+ // If it's the default route, save it but keep looking for a custom one
+ if ($route->getName() === 'default.livewire.update') {
+ $defaultRoute = $route;
+ continue;
+ }
+ // Found a custom route, return it immediately
+ return $route;
+ }
+ }
+ return $defaultRoute;
+ }
+ function skipRequestPayloadTamperingMiddleware()
+ {
+ \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::skipWhen(function () {
+ return $this->isLivewireRequest();
+ });
+ \Illuminate\Foundation\Http\Middleware\TrimStrings::skipWhen(function () {
+ return $this->isLivewireRequest();
+ });
+ }
+ function setUpdateRoute($callback)
+ {
+ $route = $callback([self::class, 'handleUpdate']);
+ // Append `livewire.update` to the existing name, if any.
+ if (!str($route->getName())->endsWith('livewire.update')) {
+ $route->name('livewire.update');
+ }
+ $this->updateRoute = $route;
+ }
+ public function isLivewireRequest()
+ {
+ return $this->isMagewireRequest();
+ }
+ function isLivewireRoute()
+ {
+ // @todo: Rename this back to `isLivewireRequest` once the need for it in tests has been fixed.
+ $route = request()->route();
+ if (!$route) {
+ return false;
+ }
+ /*
+ * Check to see if route name ends with `livewire.update`, as if
+ * a custom update route is used and they add a name, then when
+ * we call `->name('livewire.update')` on the route it will
+ * suffix the existing name with `livewire.update`.
+ */
+ return $route->named('*livewire.update');
+ }
+ /**
+ * @return MagewireUpdateResult|mixed|null
+ * @throws ComponentNotFoundException
+ * @throws NoSuchEntityException
+ */
+ public function handleUpdate()
+ {
+ /** @var ComponentRequestContext[] $updates */
+ $requestPayload = $this->request->getParam('components');
+ $finish = trigger('request', $requestPayload);
+ $requestPayload = $finish($requestPayload);
+ $componentResponses = [];
+ foreach ($requestPayload as $componentPayload) {
+ $reconstruct = trigger('magewire:component:reconstruct', $componentPayload);
+ $block = $reconstruct();
+ $component = $block->getData('magewire');
+ if (!$component instanceof Component) {
+ throw new ComponentNotFoundException('Something went wrong during block reconstruction');
+ }
+ /*
+ * Marks the component to indicate that it is being updated, distinguishing it from a preceding page load
+ * or refresh. This notification is crucial for informing other systems about the context of the operation.
+ */
+ store($component)->set('magewire:update', $componentPayload);
+ /*
+ * When the 'toHtml' method is invoked on any block with the 'magewire' argument, it initiates the
+ * rendering lifecycle. During initial (in other words: preceding) page renders, this process is
+ * automatically managed by the framework. However, on subsequent requests, it becomes necessary to
+ * manually trigger this lifecycle for the targeted block.
+ */
+ [$snapshot, $effects] = $this->magewireManager->render($block, $block->toHtml());
+ $componentResponses[] = ['effects' => $effects->toArray(), 'snapshot' => $this->serializer->serialize($snapshot)];
+ }
+ $responsePayload = ['components' => $componentResponses ?? [], 'assets' => []];
+ $finish = trigger('response', $responsePayload);
+ return $finish($responsePayload);
+ }
+ public function __construct(private readonly Http $request, private readonly MagewireManager $magewireManager, private readonly SerializerInterface $serializer, private readonly MagewireServiceProvider $magewireServiceProvider)
+ {
+ //
+ }
+ public function isMagewireRequest()
+ {
+ return $this->magewireServiceProvider->runtime()->mode()->isSubsequent();
+ }
+ public function request(): Http
+ {
+ return $this->request;
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/Mechanism.php b/dist/Mechanisms/Mechanism.php
new file mode 100644
index 00000000..a63fe663
--- /dev/null
+++ b/dist/Mechanisms/Mechanism.php
@@ -0,0 +1,22 @@
+instance(static::class, $this);
+ }
+ function boot()
+ {
+ //
+ }
+}
\ No newline at end of file
diff --git a/dist/Mechanisms/PersistentMiddleware/PersistentMiddleware.php b/dist/Mechanisms/PersistentMiddleware/PersistentMiddleware.php
new file mode 100644
index 00000000..5bd48298
--- /dev/null
+++ b/dist/Mechanisms/PersistentMiddleware/PersistentMiddleware.php
@@ -0,0 +1,146 @@
+extractPathAndMethodFromRequest();
+ $context->addMemo('path', $path);
+ // Although it's a POST request (stored in $method), Livewire still requires a GET value.
+ $context->addMemo('method', 'GET');
+ });
+ on('flush-state', function () {
+ // Only flush these at the end of a full request, so that child components have access to this data.
+ $this->path = null;
+ $this->method = null;
+ });
+ }
+ function addPersistentMiddleware($middleware)
+ {
+ static::$persistentMiddleware = Router::uniqueMiddleware(array_merge(static::$persistentMiddleware, (array) $middleware));
+ }
+ function setPersistentMiddleware($middleware)
+ {
+ static::$persistentMiddleware = Router::uniqueMiddleware((array) $middleware);
+ }
+ function getPersistentMiddleware()
+ {
+ return static::$persistentMiddleware;
+ }
+ protected function extractPathAndMethodFromRequest()
+ {
+ return [$this->request->getBasePath(), $this->request->getMethod()];
+ }
+ protected function extractPathAndMethodFromSnapshot($snapshot)
+ {
+ if (!isset($snapshot['memo']['path']) || !isset($snapshot['memo']['method'])) {
+ return;
+ }
+ // Store these locally, so dynamically added child components can use this data.
+ $this->path = $snapshot['memo']['path'];
+ $this->method = $snapshot['memo']['method'];
+ }
+ protected function applyPersistentMiddleware()
+ {
+ $request = $this->makeFakeRequest();
+ $middleware = $this->getApplicablePersistentMiddleware($request);
+ // Only send through pipeline if there are middleware found
+ if (is_null($middleware)) {
+ return;
+ }
+ Utils::applyMiddleware($request, $middleware);
+ }
+ protected function makeFakeRequest()
+ {
+ $originalPath = $this->formatPath($this->path);
+ $originalMethod = $this->method;
+ $currentPath = $this->formatPath(request()->path());
+ // Clone server bag to ensure changes below don't overwrite the original.
+ $serverBag = clone request()->server;
+ // Replace the Livewire endpoint path with the path from the original request.
+ $serverBag->set('REQUEST_URI', str_replace($currentPath, $originalPath, $serverBag->get('REQUEST_URI')));
+ $serverBag->set('REQUEST_METHOD', $originalMethod);
+ /**
+ * Make the fake request from the current request with path and method changed so
+ * all other request data, such as headers, are available in the fake request,
+ * but merge in the new server bag with the updated `REQUEST_URI`.
+ */
+ $request = request()->duplicate(server: $serverBag->all());
+ return $request;
+ }
+ protected function formatPath($path)
+ {
+ return '/' . ltrim($path, '/');
+ }
+ protected function getApplicablePersistentMiddleware($request)
+ {
+ $route = $this->getRouteFromRequest($request);
+ if (!$route) {
+ return [];
+ }
+ $middleware = app('router')->gatherRouteMiddleware($route);
+ return $this->filterMiddlewareByPersistentMiddleware($middleware);
+ }
+ protected function getRouteFromRequest($request)
+ {
+ try {
+ $route = app('router')->getRoutes()->match($request);
+ $request->setRouteResolver(fn() => $route);
+ } catch (NotFoundHttpException $e) {
+ return null;
+ }
+ return $route;
+ }
+ protected function filterMiddlewareByPersistentMiddleware($middleware)
+ {
+ $middleware = collect($middleware);
+ $persistentMiddleware = collect(app(PersistentMiddleware::class)->getPersistentMiddleware());
+ return $middleware->filter(function ($value, $key) use ($persistentMiddleware) {
+ return $persistentMiddleware->contains(function ($iValue, $iKey) use ($value) {
+ // Some middlewares can be closures.
+ if (!is_string($value)) {
+ return false;
+ }
+ // Ensure any middleware arguments aren't included in the comparison
+ return Str::before($value, ':') == $iValue;
+ });
+ })->values()->all();
+ }
+ function __construct(private readonly Request $request)
+ {
+ //
+ }
+}
\ No newline at end of file
diff --git a/dist/Pipe.php b/dist/Pipe.php
new file mode 100644
index 00000000..f8d46c48
--- /dev/null
+++ b/dist/Pipe.php
@@ -0,0 +1,40 @@
+target = $target;
+ }
+ function __invoke(...$params)
+ {
+ if (empty($params)) {
+ return $this->target;
+ }
+ [$before, $through, $after] = [[], null, []];
+ foreach ($params as $key => $param) {
+ if (!$through) {
+ if (is_callable($param)) {
+ $through = $param;
+ } else {
+ $before[$key] = $param;
+ }
+ } else {
+ $after[$key] = $param;
+ }
+ }
+ $params = [...$before, $this->target, ...$after];
+ $this->target = $through(...$params);
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/dist/Transparency.php b/dist/Transparency.php
new file mode 100644
index 00000000..77be5485
--- /dev/null
+++ b/dist/Transparency.php
@@ -0,0 +1,44 @@
+target;
+ }
+ function offsetExists(mixed $offset): bool
+ {
+ return isset($this->target[$offset]);
+ }
+ function offsetGet(mixed $offset): mixed
+ {
+ return $this->target[$offset];
+ }
+ function offsetSet(mixed $offset, mixed $value): void
+ {
+ $this->target[$offset] = $value;
+ }
+ function offsetUnset(mixed $offset): void
+ {
+ unset($this->target[$offset]);
+ }
+ function getIterator(): Traversable
+ {
+ return (function () {
+ foreach ($this->target as $key => $value) {
+ yield $key => $value;
+ }
+ })();
+ }
+}
\ No newline at end of file
diff --git a/dist/Wrapped.php b/dist/Wrapped.php
new file mode 100644
index 00000000..319c6e26
--- /dev/null
+++ b/dist/Wrapped.php
@@ -0,0 +1,47 @@
+fallback = $fallback;
+ return $this;
+ }
+ function __call($method, $params)
+ {
+ if (!method_exists($this->target, $method)) {
+ return value($this->fallback);
+ }
+ try {
+ $arguments = $this->methodsMap->getMethodParams($this->target::class, $method);
+ if (count($params) !== 0 && isset($params[0])) {
+ $params = array_combine(array_column($arguments, 'name'), array_intersect_key($params, $arguments));
+ }
+ // Remove parameters that are not required by the method.
+ $params = array_intersect_key($params, array_flip(array_column($arguments, 'name')));
+ return $this->target->{$method}(...$params);
+ } catch (\Throwable $e) {
+ $shouldPropagate = true;
+ $stopPropagation = function () use (&$shouldPropagate) {
+ $shouldPropagate = false;
+ };
+ trigger('exception', $this->target, $e, $stopPropagation);
+ $shouldPropagate && throw $e;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/Compatibility.md b/docs/Compatibility.md
deleted file mode 100644
index 11325565..00000000
--- a/docs/Compatibility.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Magewire - Compatibility
-> Please keep in mind that Magewire is currently in a Beta-phase. Therefore not all architectural choices are set in
-> concrete. So make sure you are aware of the risks of building on top of the platform in it's current state.
-
-Magewire is **not** compatible with any specific Laravel Livewire backend version. The Livewire JS package does have a
-matching version as this is a identical copy of it's origin. The goal is to make as much practible front- & backend
-features compatible.
-
-## Magento - 2.3.x
-As it's core, Magewire is build primarily for Magento 2.4.x to get rid of the deprated App\Action controller extend.
-I've gave it a headspin to still be able to use Magewire in Magento 2.3.x. I call it the "vintage" concept of handling
-HTTP subsequent requests. Therefore 2.3.x version will make Magewire HTTP requests to the ```Vintage``` instead of the
-default ```Post``` controller. Just install the extension, no extra configuration required.
-
-```
-composer require magewirephp/magewire
-```
-
-## Magento - RequireJS
-Magewire by default is meant as a Hyva Themes first extension. This means it will only work on Hyva based themes out of
-the box and **will not** work on e.g. Luma or Blank based Magento 2 themes (RequireJS dependend themes).
-
-Because most developers want to work with a more modern and fun tech-stack doesn't mean we forgot all those die-hards
-who still work with the original Magento frontend. Magewire is made compatible via a so-called compatibility
-extension.
-
-Simply install the original Magewire extension and require the compatibility extension alongside. You should be good to
-go after you're done with your default workflow when installed a new extension.
-
-```
-composer require magewirephp/magewire-requirejs
-```
-
-## Magento - Custom XHR requests
-You can walk into a situations where you load HTML via a custom XHR request where the layout gets loaded, the block HTML
-gets rendered and returned with a child block _(A)_ inside of it. When this child _(B)_ block is wired, it will use the
-'magewire_post_livewire' handle for it's subsequent requests. Therefore it's required to define the child (B) block
-inside the 'hyva_default.xml' (Hyvä Themes theme) or 'default.xml' (Luma/Blank theme) layout.
diff --git a/docs/Component.md b/docs/Component.md
deleted file mode 100644
index e369dbdf..00000000
--- a/docs/Component.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# Magewire - Component
-> Please keep in mind that Magewire is currently in a Beta-phase. Therefore not all architectural choices are set in
-> concrete. So make sure you are aware of the risks of building on top of the platform in it's current state.
-
-## Making components
-
-**My/Module/Magewire/Explanation.php**
-```php
-getMagewire();
-
-?>
-
- hasFoo()): ?>
- = $magewire->getFoo() ?>
-
-
-```
-You will have $magewire at your disposal as long as you're inside a Magewire component. You're able to validate if a public property is set with the ```has``` prefix. Use a ```get``` prefix to get a public property value.
-
-## Layout
-The idea behind Magewire is that, like Hyva, it uses the strengths of the Magento layout structure. A block can be converted to a Magewire component in an instant just by assigning a Magewire component class. It's the exact same concept as done with ViewModels.
-
-### Register components
-You're not obliged to set a template if the block has a Magewire component. When it's parent block doesn't have a template, Magewire will set the templates based on the component class.
-
-```xml
-
-
-
-
-
-
- \My\Module\Magewire\Explanation
-
-
-
-
-
-```
-
-### Data
-Public properties can be set on page load by an optional ```mount()``` method or via the layout XML. When you want to set it via the layout, you have to use a specific structure.
-
-```xml
-
-
-
-
-
-
-
- - \My\Module\Magewire\Explanation
- - bar
-
-
-
-
-
-
-```
-**Note**: The public ```$foo``` property will be set if it exists inside you component.
diff --git a/docs/Component/Form.md b/docs/Component/Form.md
deleted file mode 100644
index 2c3fa25f..00000000
--- a/docs/Component/Form.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# Magewire - Form Component
-> Please keep in mind that Magewire is currently in a Beta-phase. Therefore not all architectural choices are set in
-> concrete. So make sure you are aware of the risks of building on top of the platform in it's current state.
-
-## Concept
-Soon more...
diff --git a/docs/Component/Pagination.md b/docs/Component/Pagination.md
deleted file mode 100644
index b52abf8b..00000000
--- a/docs/Component/Pagination.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Magewire - Pagination Component
-> Please keep in mind that Magewire is currently in a Beta-phase. Therefore not all architectural choices are set in
-> concrete. So make sure you are aware of the risks of building on top of the platform in it's current state.
-
-## Concept
-Pagination in its current state is designed to give developers complete freedom on how data is retrieved and served.
-A number of obligations are established through the Pagination interface. Its possible there will be an extension of
-this concept in the future that is the roughest of the implementation, taking into account e.g. the use of repositories.
diff --git a/docs/Features.md b/docs/Features.md
deleted file mode 100644
index 37f102fa..00000000
--- a/docs/Features.md
+++ /dev/null
@@ -1,801 +0,0 @@
-# Magewire - Features
-> Please keep in mind that Magewire is currently in a Beta-phase. Therefore not all architectural choices are set in
-> concrete. So make sure you are aware of the risks of building on top of the platform in it's current state.
-
-## Best Practices
-1. Use the Magewire naming conventions and structures.
-2. Use Hydrators to manipulate data before or after a method gets called.
-3. Contribute or create an issue when you found bugs or sucurity issues.
-4. Keep components small and clean
-5. Use the ```My/Module/Magewire/Component``` folder when you introduce a new component type.
-
-## Folder Structure
-| Folder | Description |
-|--------|----------------------------------------------------------------------------------------------------|
-| /src | Root folder inside your module |
-| /src/Magewire | All Magewire components live here (subfolder allowed) |
-| /src/view/frontend/templates/magewire | All Magewire component template files live here (subfolder allowed) |
-
-## JavaScript
-The ```Magewire``` (alias for ```Livewire```) object is globally available on the page after DomContentLoaded. More
-information can be found inside the [Livewire docs](https://laravel-livewire.com/docs/2.x/reference#global-livewire-js).
-
-### Document Events
-- magewire:load
-- magewire:update
-- magewire:available
-- magewire:loader:start ```(event) => {}```
-- magewire:loader:stop ```(event) => {}```
-
-### Lifecycle Hooks
-[Read all about hooks](https://laravel-livewire.com/docs/2.x/reference#js-hooks)
-
-## Block Structure
-```xml
-
-
- \My\Module\Magewire\Explanation
-
-
-
-
-
-
-
-
- - \My\Module\Magewire\Explanation
-
-
-
-
-
-
-
-
- \My\Module\Magewire\Explanation
-
-
-```
-
-## Templates
-Options within your block template.
-```php
-getMagewire();
-
-// Check if property exists
-$magewire->hasFoo();
-// Get property value
-$magewire->getFoo();
-// Call a custom method inside your component
-$magewire->myCustomMethod();
-// Overwrite a property after the (optional) mount method was executed
-$magewire->assign('foo', 'barbar');
-?>
-```
-
-### Switch Template
-Switch to another template during a subsequent request.
-```html
-
- Login
-
-```
-
-```php
-public function login()
-{
- $this->switchTemplate('My_Module::customer/account/dashboard.phtml');
-}
-```
-> **Tip**: Use the power of the layout xml to assign a "switch" template path as a data param assigned to the component.
-> This way your component becomes more dynamic and extensible for other developers.
-
-### Skip Rendering
-Set some data but prevent a frontend DOM replacement while on a subsequent request.
-```php
-public function setSomeProperties(string $value = 'bar')
-{
- // Will set the data but will return null as effects html value.
- $this->assign('foo', $value)->skipRender();
-}
-```
-
-## Component Types
-The base idea behind de default component is to keep things as simple and clean as possible without any constructor
-dependencies. Therefore I've decided to create multiple component to inherit from, who give you the option to use stuff
-like for instance property validation.
-
-```php
-class Explanation extends Magewirephp\Magewire\Component {}
-// OR
-class Explanation extends \Magewirephp\Magewire\Component\Form {}
-// OR
-class Explanation extends \Magewirephp\Magewire\Component\Pagination {}
-```
-
-## Magic Actions & Properties
-Toggle, set or emit without writing any PHP.
-```html
-
-Toggle Foo
-
-
-Set Foo
-Set Foo with $bar property value
-Set Foo with $bar property value
-
-
-
-assign('publicProperty', $value);
-}
-
-/**
- * Mass assign() the given property/value array.
- */
-public function setDataBatch()
-{
- $this->fill([
- 'publicPropertyOne' => 'valueOne',
- 'publicPropertyTwo' => 'valueTwo',
- ]);
-}
-
-/**
- * OPTIONAL METHOD: Gets executed right before the property gets assigned.
- */
-public function updating($value, string $name)
-{
- // Bad practice on listening for a name specific property.
- if ($name === 'publicProperty') { return ucfirst($value); }
- // Best practice
- return ucfirst($value);
-}
-
-/**
- * OPTIONAL METHOD: Gets executed immediately after the property has been assigned.
- */
-public function updated($value, string $name)
-{
- // Bad practice on listening for a name specific property.
- if ($name === 'publicProperty') { return ucfirst($value); }
- // Best practice
- return ucfirst($value);
-}
-```
-> **Note**: Trap your public property value into a variable when you use for instance array functions, who accept
-> variables as a reference, to avoid a lifecycle interruption.
-
-### Name specific
-Listen for updates on targeted properties.
-```php
-public $foo;
-
-/**
- * OPTIONAL METHOD: Before the property gets updated.
- * Gets executed before the updating() lifecycle hook.
- */
-public function updatingFoo(string $value): string
-{
- // $value: foo
- return ucfirst('updating-' . $value);
-}
-
-/**
- * OPTIONAL METHOD: Gets executed when the property gets assigned.
- */
-public function defineFoo(string $value): string
-{
- // $value: Updating-foo
- return strtolower('define-' . $value);
-}
-
-/**
- * OPTIONAL METHOD: After the property has been updated.
- * Gets executed after the updated() lifecycle hook.
- */
-public function updatedFoo(string $value): string
-{
- // $value: set-updating-foo
- return strtoupper('updated-' . $value);
-}
-```
-> **Note**: Final result of this property lifecycle would be "**UPDATED-DEFINE-UPDATING-FOO**"
-
-### Nesting
-Nested array properties can be targeted specifically.
-```php
-class Explanation extends \Magewirephp\Magewire\Component
-{
- public $nested = ['foo' => ['bar' => 'Hello world']];
-
- // These nested methods will also work for the 'updating' & 'define' lifecycle methods
- public function updatedNestedFooBar(array $value): array
- {
- // A updated $nested array will be given as value of $value
- $value['foo']['bar'] = strtoupper($value['foo']['bar']);
-
- // Will result in a uppercased value for $nested['foo']['bar']
- return $value;
- }
-}
-```
-
-```html
-
-
-
-
-
-
-Set
-```
-## Flash Messages
-Show a flash message on the page without a reload.
-```php
-public function myCustomMessageMethod(string $message)
-{
- $this->dispatchErrorMessage($message);
- $this->dispatchWarningMessage($message);
- $this->dispatchNoticeMessage($message);
- $this->dispatchSuccessMessage($message);
-}
-```
-> **Translations**: Messages will automatically be transformed into a translatable phrase.
-
-## Redirects
-Redirect your customer with or without additional parameters.
-```php
-public function myCustomRedirectMethod()
-{
- $this->redirect('/some/custom/path');
-}
-```
-
-```html
-Redirect
-
-
-
-
- Thanks for your purchase! You will be redirected after 5 seconds.
-
-```
-
-## Listeners & Emits
-Emit functionality in targeted or non-targeted components based on event listeners.
-```php
-/**
- * Component A.
- * @block my.custom.block.name
- */
-class A extends \Magewirephp\Magewire\Component
-{
- protected $listeners = ['youCanCallMe'];
- public function youCanCallMe($value) {}
-
- // OR
-
- protected $listeners = ['youCanCallMe' => 'toDoSomething'];
- public function toDoSomething($value) {}
-}
-
-/**
- * Component B.
- */
-class B extends \Magewirephp\Magewire\Component
-{
- public function letsCallSomeone()
- {
- // Emit to every component who listens to 'youCanCallMe'.
- $this->emit('youCanCallMe', ['value' => 'hi there']);
- // Emit only to the 'my.custom.block.name' component.
- $this->emitTo('my.custom.block.name', 'youCanCallMe', ['value' => 'hi there']);
- }
-}
-```
-
-### JavaScript
-```js
-// Emit to every component who listens to 'youCanCallMe'.
-Magewire.emit('youCanCallMe', {value: 'hi there'})
-// Emit only to the 'my.custom.block.name' component.
-Magewire.emitTo('my.custom.block.name', 'youCanCallMe', {value: 'hi there'})
-```
-
-### Magic Actions Compatibility
-You are able to use magic methods within your emits if this is required. Thanks to this feature you are able to for example refresh a component or set data without having to write this functionality inside your targeted component.
-```php
-/**
- * Component A.
- * @block my.custom.block.name
- */
-class A extends \Magewirephp\Magewire\Component
-{
- public $stringProperty;
- public $boolProperty = false;
-
- protected $listeners = ['$refresh']; // OR ['myEventName' => '$refresh']
- // OR
- protected $listeners = ['$set']; // OR ['myEventName' => '$set']
- // OR
- protected $listeners = ['$toggle']; // OR ['myEventName' => '$toggle']
-}
-
-/**
- * Component B.
- */
-class B extends \Magewirephp\Magewire\Component
-{
- public function setSomeProperties()
- {
- // Force refresh for a separate component who is listening.
- $this->emit('$refresh', []);
- // Set a public property value for a separate component who is listening.
- $this->emitTo('layout.block.name', '$set', ['stringProperty', 'hello world']);
- // Toggle a property value for a separate component who is listening.
- $this->emitTo('layout.block.name', '$toggle', ['boolProperty']);
- }
-}
-```
-> **Note**: Emits only work during subsequent requests. They won't be dispatched on page load when you emit them
-> in for example the ```mount()``` method. Use ```wire:init``` to dispatch a method on page load where an emit could
-> take place.
-
-### Global Refresh Listener
-Each Magewire component has by default no ```$listeners``` attached to itself. Still, you're able to refresh a
-component from within another component thanks to a global ```refresh``` listener who get's injected during a preceding
-request.
-
-Thanks to this global listener, Magewire introduces the emitToRefresh method. This gives the option to refresh any
-component on the page from within your own component.
-```php
-public function refreshSomeOtherComponent()
-{
- $this->emitToRefresh('layout.block.name');
-}
-```
-OR
-```html
- **Note**: Be aware of the fact that a Magewire component state will get cached when for example FPC is enabled. This
-> means the ```mount()``` method will only run once during an initial page load.
-
-## Browser Events
-You're able to trigger browser events from within your component.
-```php
-public function openSubscribeModal()
-{
- // With data
- $this->dispatchBrowserEvent('open-subscribe-modal', ['user' => $user->getFullName()]);
- // Without data
- $this->dispatchBrowserEvent('open-subscribe-modal');
-}
-```
-
-```html
-
- Thanks for your purchase! You will be redirected after 5 seconds.
-
-
-
-
-
- Thanks for your purchase! You will be redirected after 5 seconds.
-
-
-
-```
-
-## Prefetching
-Prefetch a component and show differences on click.
-```php
-public function prefetchMyContent()
-{
- $this->assign('myContent', 'Hello world');
-}
-```
-
-```html
-Prefetch
-hasMyContent()): echo $magewire->getMyContent(); endif; ?>
-```
-
-## Lazy Updating
-Prevent sending out requests for every press of a button.
-```php
-class Explanation extends \Magewirephp\Magewire\Component
-{
- public $lazyProperty;
-}
-```
-
-```html
-
-```
-
-## Keydown Modifiers
-Perform actions on keydown
-```php
-public function keyUp()
-{
- $this->assign('random', random_int(100, 999));
-}
-
-public function keyDown()
-{
- $this->assign('random', random_int(10, 99));
-}
-```
-
-```html
-
-```
-> You can also use vanilla JS instead of a PHP class method.
->
-> **Quick List**
-> - backspace
-> - escape
-> - shift
-> - tab
-> - arrow- right / left / up / down
-
-## Restricted Public Methods
-Public methods can be restricted from subsequent request executions. Prevent method executions who are meant for
-inside the phtml template only. It's not a best practice and you should use a ViewModel in most cases.
-
-### Global
-```xml
-
-
-
- - myCustomSetMethod
-
-
-
-```
-**File**: etc/frontend/di.xml
-
-### Component
-```php
-class Explanation extends \Magewirephp\Magewire\Component
-{
- private $uncallables = ['myCustomSetMethod'];
-}
-```
-
-## Pagination
-Render a pagination pager inside your Component's view.
-```php
-class Explanation extends \Magewirephp\Magewire\Component\Pagination
-{
- public $page = 1;
- public $pageSize = 20;
-}
-```
-
-```html
-
- = $magewire->renderPagination() ?>
-
- = $magewire->renderPagination('My_Module::html/pagination/custom_pager') ?>
-
-```
-
-# Error Handling
-An error will automatically bind a Phrase with it property name as key. Every single property can have one error per
-round-trip. The build in validation also uses the Component's errors bag on failure.
-```php
-class MySearchForm extends \Magewirephp\Magewire\Component\Form
-{
- public $searchText;
-
- protected $rules = [
- 'searchText' => 'required|min:6'
- ];
-
- public function search()
- {
- // Won't continue on failure.
- $this->validate();
-
- $this->someSearchProvider->search($this->getSearchText());
-
- // OR
-
- if (strlen($this->getSearchText()) < 6) {
- $this->error('searchText', 'Minimal length is 6');
- }
- }
-}
-```
-Render all errors on top as a list.
-```html
-
- hasErrors()): ?>
-
- getErrors() as $error): ?>
- = $error ?>
-
-
-
-
-
-
-```
-
-Render each error underneath a specific input field.
-```html
-
-```
-
-## Query String
-> **Note**: The query string feature is currently incomplete compared to the original Laravel Livewire implementation.
-> At the moment you can only set public properties values via the URL when loading a page.
-
-Define public properties via URL params on a page load
-```php
-class MySearchForm extends \Magewirephp\Magewire\Component\Form
-{
- public $searchText;
-
- /**
- * @url https://your.domain/customer/account/dashboard?searchText=foo
- */
- protected $queryString = [
- 'searchText'
- ];
-
- // OR
-
- /**
- * @url https://your.domain/customer/account/dashboard?q=foo
- */
- protected $queryString = [
- 'searchText' => [
- 'alias' => 'q' // map 'searchText' as 'q'
- ]
- ];
-}
-```
-
-## Set A Predictable wire:id
-Magewire generates a SHA1 hash wire:id attribute value by default. This is based on the component's layout block name.
-```php
-class Explanation extends \Magewirephp\Magewire\Component
-{
- public $id = 'my-predictable-wire-id';
-
- public $bar;
-
- public function foo(string $bar)
- {
- $this->assign('bar', $bar);
- }
-}
-```
-
-Will output as:
-```html
-
-```
-
-> **Note**: SHA1 hashing the wire:id value is an idea which can change in the future. I'm still tumbling around the
-> acceptance of just using the block name which has to be unique which is the most important part. I need to look into
-> the security aspect when switching to an un-hashed version of the wire:id attribute.
->
-> On of the benefits would be that Magewire components are more predictable when it comes to trying to find them with
-> for example Livewire.find().
-
-Find the component and trigger the ```foo``` method:
-```js
-document.addEventListener('livewire:load', function () {
- Magewire.find(['my-predictable-wire-id']).foo('Some Value')
-});
-```
-
-## Display Loading State
-Display a loading state only when performing a (targeted) subsequent method call.
-```php
-class Explanation extends \Magewirephp\Magewire\Component
-{
- `// Show a loading state for all methods
- protected $loader = true;
- // Show a loading state for specific methods
- protected $loader = ['foo'];`
-
- public function foo() {
-
- // Loading state will stay active until the listener has run
- // Loading state will disapear when there are no active listeners
- $this->emit('some_event')
-
- }
-
- public function bar() {
- //
- }
-}
-```
-> **Note:** Keep in mind that the ```$loader``` mapping only understands subsequent executable methods.
-
-```html
-
-Execute "foo"
-```
-
-### Indicator Customization
-```xml
-
-
-
-```
-**File**: view/frontend/layout/default_hyva.xml
-```html
-
-```
-**File**: html/magewire/loader.phtml
-
-### Indicator Removal
-In some cases you want to implement your own loader because you have a global one in place or your just don't need to
-notify your customer. Whatever the case, I centered all frontend logic into two phtml files to let you do whatever's
-needed for the project.
-```xml
-
-
-
-
-
-
-```
-**File**: view/frontend/layout/default_hyva.xml
-
-### Custom Example
-Ofcourse you're able to build it more custom for your wired component only.
-```html
-
-
- Start
-
-
-```
-
-```php
-public function start(int $seconds)
-{
- // Let's take a nap.
- sleep($seconds);
- // Unlock the disabled state.
- $this->dispatchBrowserEvent('switch-state');
-}
-```
-> **Note**: This is just an example. For a disabled state you should or could use the ```wire:loading``` directive.
-
-## Plugins
-> **Important**: This is still a proof of concept. It's possible this won't make it into the first official release.
-
-It's a best practice to add your custom additions to Magewire inside the designated ```magewire.plugin``` container.
-This can come in handy when you need to check if a plugin gives any trouble after installation to just temporary remove
-it.
-
-```xml
-
-```
-
-### Intersect Directive Plugin
-> **Under construction**: This features is still under construction. Don't use this feature in any project until a
-> first and final release.
-
-Magewire has an (unique) ```intersect``` directive. This is a custom plugin can be compared with the ```init``` directive but
-only when the Magewire block is inside the viewport. This can really speed up the page when for instance it's
-dealing with a large dataset on the bottom of the page.
-
-```html
-
-
-
-
-
-
-
-
-
-```
-
-```php
-class Explanation extends \Magewirephp\Magewire\Component
-{
- public $fooPropertyValue;
-
- public function foo()
- {
- $this->assign('fooPropertyValue', 'bar');
- }
-
- public function bar(string $textOne, string $textTwo)
- {
- $this->assign('barPropertyValue', $textOne . ' & ' . $textTwo);
- }
-}
-```
diff --git a/docs/Hydrators.md b/docs/Hydrators.md
deleted file mode 100644
index 4399259e..00000000
--- a/docs/Hydrators.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Magewire - Hydrators
-> Please keep in mind that Magewire is currently in a Beta-phase. Therefore not all architectural choices are set in
-> concrete. So make sure you are aware of the risks of building on top of the platform in it's current state.
-
-By default Magewire come's with a stack of core hydrators who can not be overwritten by default. These take precedence
-over the rest to ensure the bootstrap lifecycle.
-
-| Order | Hydration | Dehydration |
-| ----- | --------------| --------------|
-| 1 | Security | Redirect |
-| 2 | Browser Event | Emit |
-| 3 | Flash Message | Loader |
-| 4 | Error | Listener |
-| 5 | Hash | Property |
-| 6 | Component | QueryString |
-| 7 | QueryString | Component |
-| 8 | Property | Hash |
-| 9 | Listener | Error |
-| 10 | Loader | Flash Message |
-| 11 | Emit | Browser Event |
-| 10 | Redirect | Security |
-
-## Make Your Own
-As a developer you can manipulate the Request and/or Response going back and forth. The core concept op hydration is
-that it acts as a shell where the core hydrators will encapsulate all extended. This is done to ensure the core always
-has precedent on its attendants.
-
-All core hydrators are pluggable which means in theory you should be able to write Plugins on top of all core hydrators.
-This is not best practice and we always encourage you to write your own to avoid problems on future updates.
-
-### Example
-**My/Module/etc/frontend/di.xml**
-```xml
-
-
-
-
-
- -
-
-
- My\Module\Model\Magewire\Hydrator\MyAwesomeHydrator
-
- - 50
-
-
-
-
-
-```
-
-**My/Module/Model/Magewire/Hydrator/MyAwesomeHydrator.php**
-```php
-isSubsequent()) {}
-
- // Initial page load hydration request
- if ($request->isPreceding()) {}
-}
-
-public function dehydrate(Component $component, ResponseInterface $response): void
-{
- // Subsequent hydration request
- if ($response->getRequest()->isSubsequent()) {}
-
- // Initial page load hydration request
- if ($response->getRequest()->isPreceding()) {}
-}
-```
diff --git a/docs/Notes.md b/docs/Notes.md
deleted file mode 100644
index cf0b9faa..00000000
--- a/docs/Notes.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Magewire - Notes
-> Please keep in mind that Magewire is currently in a Beta-phase. Therefore not all architectural choices are set in
-> concrete. So make sure you are aware of the risks of building on top of the platform in it's current state.
-
-## Assign Method Usage
-- **Date**: 07/10/2021
-- **Category**: Property Binding
-
-So this assign method is here because we need a system where we can trigger methods like ```updating``` & ```updated```.
-Thanks to the ```assign``` method, this will trigger immediately after the public property get a new value assigned.
-
-Now, what if this was replaced with a Hydrator? It would make it a lot more simple because developers don't have to
-write extra code to assign a value onto a property. In that case you would be able to just use ```$this->foo = 'bar'```.
diff --git a/lib/Magento/App/Cache/MagewireCacheSection.php b/lib/Magento/App/Cache/MagewireCacheSection.php
new file mode 100644
index 00000000..d9269df6
--- /dev/null
+++ b/lib/Magento/App/Cache/MagewireCacheSection.php
@@ -0,0 +1,45 @@
+tags)) {
+ $this->tags[] = $this->identifier;
+ }
+
+ return $this->magewireCacheType->save($this->serializer->serialize($data), $this->identifier, $this->tags, $ttl ?? $this->ttl);
+ }
+
+ public function fetch(): array
+ {
+ $data = $this->magewireCacheType->load($this->identifier);
+
+ return ! $data ? [] : $data;
+ }
+}
diff --git a/lib/Magento/App/Cache/Type/Magewire.php b/lib/Magento/App/Cache/Type/Magewire.php
new file mode 100644
index 00000000..0f38875b
--- /dev/null
+++ b/lib/Magento/App/Cache/Type/Magewire.php
@@ -0,0 +1,36 @@
+get(self::TYPE_IDENTIFIER), self::CACHE_TAG);
+ }
+
+ public function load($identifier)
+ {
+ $data = parent::load($identifier);
+
+ return is_string($data) ? $this->serializer->unserialize($data) : $data;
+ }
+}
diff --git a/lib/Magento/App/Router/MagewireRouteValidator.php b/lib/Magento/App/Router/MagewireRouteValidator.php
new file mode 100644
index 00000000..357fffec
--- /dev/null
+++ b/lib/Magento/App/Router/MagewireRouteValidator.php
@@ -0,0 +1,58 @@
+isAdminRequest($request)) {
+ $path = '/' . $this->frontNameResolver->getFrontName() . $path;
+ }
+
+ return $this->isMagewireUri($path, $request);
+ }
+
+ private function isAdminRequest(RequestInterface $request): bool
+ {
+ if (preg_match('#/([^/]+)/magewire/update#', $request->getPathInfo(), $matches)) {
+ return $matches[1] === $this->frontNameResolver->getFrontName();
+ }
+
+ return false;
+ }
+
+ private function isMagewireUri(string $path, RequestInterface $request): bool
+ {
+ return str_contains($request->getPathInfo(), $path);
+ }
+}
diff --git a/lib/Magento/Controller/MagewireUpdateResult.php b/lib/Magento/Controller/MagewireUpdateResult.php
new file mode 100644
index 00000000..d59c506a
--- /dev/null
+++ b/lib/Magento/Controller/MagewireUpdateResult.php
@@ -0,0 +1,73 @@
+rendered) {
+ return $this;
+ }
+
+ $this->renderer = $renderer;
+
+ return $this;
+ }
+
+ public function getComponents(): array
+ {
+ return $this->components;
+ }
+
+ public function getAssets(): array
+ {
+ return $this->assets;
+ }
+
+ public function render(HttpResponseInterface $response): self
+ {
+ $this->renderer ??= function (HttpResponseInterface $response, MagewireUpdateResult $result): HttpResponseInterface {
+ return $response->setBody($this->jsonSerializer->serialize([
+ 'components' => $result->getComponents(),
+ 'assets' => $result->getAssets()
+ ]));
+ };
+
+ call_user_func($this->renderer, $response, $this);
+ $this->rendered = true;
+
+ trigger('magewire:response.render', $response);
+
+ $response->setHeader('Content-Type', 'application/json', true);
+ $response->setHeader('X-Built-With', 'Magewire', true);
+
+ return $this;
+ }
+}
diff --git a/lib/Magento/Framework/Reflection/TypeProcessor.php b/lib/Magento/Framework/Reflection/TypeProcessor.php
new file mode 100644
index 00000000..2280aa2a
--- /dev/null
+++ b/lib/Magento/Framework/Reflection/TypeProcessor.php
@@ -0,0 +1,22 @@
+layout->getUpdate()->getHandles();
+
+ sort($handles);
+ $hash = hash('xxh3', json_encode($handles));
+
+ // Early return when a version with the identical handles already exists.
+ if ($force === false && array_key_exists($hash, $this->builds)) {
+ return $this->builds[$hash];
+ }
+
+ // Register build so it only run once to avoid running infinitely.
+ $this->builds[$hash] = $this->layout;
+
+ $this->layout->getUpdate()->load($handles);
+ $this->layout->generateXml();
+ $this->layout->generateElements();
+
+ return $this->builds[$hash];
+ }
+
+ public function reset(): static
+ {
+ $this->builds = [];
+
+ return $this;
+ }
+
+ /**
+ * @throws LocalizedException
+ */
+ public function rebuild(): LayoutInterface
+ {
+ return $this->build(true);
+ }
+}
diff --git a/lib/Magento/Framework/View/DynamicLayoutDecorator.php b/lib/Magento/Framework/View/DynamicLayoutDecorator.php
new file mode 100644
index 00000000..2e675cf4
--- /dev/null
+++ b/lib/Magento/Framework/View/DynamicLayoutDecorator.php
@@ -0,0 +1,34 @@
+dynamicLayoutBuilderFactory->create(['layout' => $layout]);
+
+ $layout->setGeneratorPool($this->magewireGeneratorPool);
+ $layout->setBuilder($builder);
+
+ return $layout;
+ }
+}
diff --git a/lib/Magento/Framework/View/Layout/GeneratorPool.php b/lib/Magento/Framework/View/Layout/GeneratorPool.php
new file mode 100644
index 00000000..5f72a883
--- /dev/null
+++ b/lib/Magento/Framework/View/Layout/GeneratorPool.php
@@ -0,0 +1,46 @@
+helper = $magewireLayoutScheduledStructureHelper;
+ }
+
+ protected function addGenerators(array $generators)
+ {
+ // Limit the generators to just blocks and containers.
+ parent::addGenerators(array_filter($generators, static fn ($generator) => in_array($generator, ['block', 'container']), ARRAY_FILTER_USE_KEY));
+ }
+}
diff --git a/lib/Magento/Framework/View/Layout/ScheduledStructure/Helper.php b/lib/Magento/Framework/View/Layout/ScheduledStructure/Helper.php
new file mode 100644
index 00000000..0dca25fe
--- /dev/null
+++ b/lib/Magento/Framework/View/Layout/ScheduledStructure/Helper.php
@@ -0,0 +1,71 @@
+getStructureElement($key);
+ $data = $scheduledStructure->getStructureElementData($key);
+
+ if (! isset($row[self::SCHEDULED_STRUCTURE_INDEX_TYPE])) {
+ $this->logger->critical('Broken reference: missing declaration of the element "{$key}".');
+
+ $scheduledStructure->unsetPathElement($key);
+ $scheduledStructure->unsetStructureElement($key);
+
+ return;
+ }
+
+ list($type, $alias, $parentName, $siblingName, $isAfter) = $row;
+
+ $name = $this->_createStructuralElement($structure, $key, $type, $parentName . $alias);
+
+ if ($parentName) {
+ if ($scheduledStructure->hasStructureElement($parentName)) {
+ $this->scheduleElement($scheduledStructure, $structure, $parentName);
+ }
+
+ if (! $structure->hasElement($parentName)) {
+ /*
+ * Without a fully loaded page, there won't be a wrapping element acting as the root content container.
+ * This leads to a problem where the starting parent element isn't available, causing an error.
+ * Emulation becomes necessary to attach their respective children when this is the case.
+ */
+ $structure->createElement($parentName, [
+ 'attributes' => [
+ 'label' => ucfirst($parentName)
+ ],
+ 'type' => \Magento\Framework\View\Layout\Element::TYPE_CONTAINER
+ ]);
+ }
+
+ $structure->setAsChild($name, $parentName, $alias);
+ }
+
+ // Transforming a scheduledStructure into a scheduledElement.
+ $scheduledStructure->unsetStructureElement($key);
+ $scheduledStructure->setElement($name, [$type, $data]);
+
+ /*
+ * Some elements provide info "after" or "before" which sibling they are supposed to go
+ * Add an element into a list of sorting
+ */
+ if ($siblingName) {
+ $scheduledStructure->setElementToSortList($parentName, $name, $siblingName, $isAfter);
+ }
+ }
+}
diff --git a/lib/Magento/Framework/View/TemplateEngine/Php/TemplateRenderDataTransferObject.php b/lib/Magento/Framework/View/TemplateEngine/Php/TemplateRenderDataTransferObject.php
new file mode 100644
index 00000000..82679533
--- /dev/null
+++ b/lib/Magento/Framework/View/TemplateEngine/Php/TemplateRenderDataTransferObject.php
@@ -0,0 +1,56 @@
+block;
+ }
+
+ public function filename(string|null $filename = null): string
+ {
+ if ($filename !== null) {
+ $this->filename = $filename;
+ }
+
+ return $this->filename;
+ }
+
+ public function dictionary(array|null $dictionary = null): array
+ {
+ if ($dictionary !== null) {
+ // Deliberately not using an array merge for performance reasons.
+ foreach ($dictionary as $key => $value) {
+ $this->dictionary[$key] = $value;
+ }
+ }
+
+ return $this->dictionary;
+ }
+
+ public function existsInDictionary(string $key): bool
+ {
+ return isset($this->dictionary[$key]);
+ }
+}
diff --git a/lib/Magewire/Concerns/WithTagging.php b/lib/Magewire/Concerns/WithTagging.php
new file mode 100644
index 00000000..1ef9cb96
--- /dev/null
+++ b/lib/Magewire/Concerns/WithTagging.php
@@ -0,0 +1,81 @@
+ */
+ protected array $withTags = [];
+
+ /**
+ * Tag a fragment with a recognizable name.
+ */
+ public function withTag(string $tag): static
+ {
+ $sanitized = preg_replace('/[^a-z0-9]/', '', strtolower($tag));
+
+ if ($sanitized !== '') {
+ $this->withTags[] = $sanitized;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Clears all tags either completely or by a provided filter callback.
+ */
+ public function clearTags(callable|null $filter = null): static
+ {
+ $this->withTags = $filter ? array_filter($this->withTags, $filter) : [];
+
+ return $this;
+ }
+
+ /**
+ * Define multiple tags.
+ */
+ public function withTags(array $tags): static
+ {
+ foreach ($tags as $tag) {
+ if ($this->hasTags([$tag])) {
+ continue;
+ }
+
+ $this->withTag($tag);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Retrieve the fragment alias (if set).
+ */
+ public function getWithTags(): array
+ {
+ return $this->withTags;
+ }
+
+ /**
+ * Retrieve if the fragment possesses an alias.
+ */
+ public function hasTags(array $tags = [], bool $strict = false): bool
+ {
+ if (empty($tags)) {
+ return false;
+ }
+ if ($strict) {
+ return empty(array_diff($tags, $this->withTags));
+ }
+
+ return ! empty(array_intersect($this->withTags, $tags));
+ }
+}
diff --git a/lib/Magewire/Config.php b/lib/Magewire/Config.php
new file mode 100644
index 00000000..5eef4c87
--- /dev/null
+++ b/lib/Magewire/Config.php
@@ -0,0 +1,46 @@
+paths)) {
+ $path = $this->paths[$path];
+ }
+
+ return $this->systemConfig->getValue($path, $scope, $scopeCode) ?? ( $this->environmentConfig->isAvailable() ? $this->environmentConfig->get($path) : null );
+ }
+}
diff --git a/lib/Magewire/Containers.php b/lib/Magewire/Containers.php
new file mode 100644
index 00000000..54dcdc91
--- /dev/null
+++ b/lib/Magewire/Containers.php
@@ -0,0 +1,27 @@
+ true;
+ }
+
+ protected function getBootModeFallback(): ServiceTypeItemBootMode
+ {
+ return ServiceTypeItemBootMode::ALWAYS;
+ }
+}
diff --git a/lib/Magewire/Containers/Livewire.php b/lib/Magewire/Containers/Livewire.php
new file mode 100644
index 00000000..46b2462a
--- /dev/null
+++ b/lib/Magewire/Containers/Livewire.php
@@ -0,0 +1,25 @@
+component = $component;
+
+ return $this;
+ }
+
+ public function response($to)
+ {
+ }
+}
diff --git a/lib/Magewire/Enums/RequestMode.php b/lib/Magewire/Enums/RequestMode.php
new file mode 100644
index 00000000..992e83c9
--- /dev/null
+++ b/lib/Magewire/Enums/RequestMode.php
@@ -0,0 +1,34 @@
+value > $state->value;
+ }
+
+ /**
+ * Check if this state is below the given state.
+ */
+ public function isBelow(RuntimeState $state): bool
+ {
+ return $this->value < $state->value;
+ }
+
+ public function isMinimally(RuntimeState $state): bool
+ {
+ return $this->value >= $state->value;
+ }
+
+ public function isMaximally(RuntimeState $state): bool
+ {
+ return $this->value <= $state->value;
+ }
+}
diff --git a/lib/Magewire/Enums/ServiceTypeItemBootMode.php b/lib/Magewire/Enums/ServiceTypeItemBootMode.php
new file mode 100644
index 00000000..2e0349be
--- /dev/null
+++ b/lib/Magewire/Enums/ServiceTypeItemBootMode.php
@@ -0,0 +1,120 @@
+is(self::PERSISTENT);
+ }
+
+ /**
+ * Returns true when the boot mode is lazy.
+ */
+ public function isLazy(): bool
+ {
+ return $this->is(self::LAZY);
+ }
+
+ public function isAlways(): bool
+ {
+ return $this->is(self::ALWAYS);
+ }
+
+ /**
+ * Returns true when the current case matches the given case.
+ */
+ public function is(self $case): bool
+ {
+ return $this === $case;
+ }
+
+ public function isHigherThan(self $case): bool
+ {
+ return $this->value > $case->value;
+ }
+
+ public function isHigherThanOrEqual(self $case): bool
+ {
+ return $this->value >= $case->value;
+ }
+
+ public function isLowerThan(self $case): bool
+ {
+ return $this->value < $case->value;
+ }
+
+ public function isLowerThanOrEqual(self $case): bool
+ {
+ return $this->value <= $case->value;
+ }
+
+ /**
+ * Returns the lowercase name of the current case.
+ */
+ public function name(): string
+ {
+ return strtolower($this->name);
+ }
+
+ /**
+ * Returns the default boot mode for any service type item.
+ */
+ public static function default(): self
+ {
+ return self::ALWAYS;
+ }
+
+ /**
+ * Returns the given value if it exists or returns the default when it doesn't.
+ */
+ public static function try(mixed $value = null, self|null $fallback = null): self
+ {
+ if (is_numeric($value)) {
+ $value = (int) $value;
+ }
+
+ return ( self::exists($value) ? self::tryFrom($value) : null ) ?? $fallback ?? self::default();
+ }
+
+ public static function exists(mixed $value): bool
+ {
+ return is_int($value) && self::tryFrom($value) !== null;
+ }
+
+ /**
+ * Returns the case with the highest integer value.
+ */
+ public static function highest(): self
+ {
+ return self::from(max(array_column(self::cases(), 'value')));
+ }
+
+ /**
+ * Returns the case with the lowest integer value.
+ */
+ public static function lowest(): self
+ {
+ return self::from(min(array_column(self::cases(), 'value')));
+ }
+}
diff --git a/lib/Magewire/Exceptions/ComponentNotFoundException.php b/lib/Magewire/Exceptions/ComponentNotFoundException.php
new file mode 100644
index 00000000..f6f6ff54
--- /dev/null
+++ b/lib/Magewire/Exceptions/ComponentNotFoundException.php
@@ -0,0 +1,20 @@
+componentHookRegistry::boot();
+ }
+
+ return $booted;
+ }
+
+ protected function callback(): callable
+ {
+ return function (object $type) {
+ $this->componentHookRegistry::register($type);
+ };
+ }
+
+ protected function getBootModeFallback(): ServiceTypeItemBootMode
+ {
+ return ServiceTypeItemBootMode::LAZY;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagentoFlashMessages/FlashMessage.php b/lib/Magewire/Features/SupportMagentoFlashMessages/FlashMessage.php
new file mode 100644
index 00000000..fca14495
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagentoFlashMessages/FlashMessage.php
@@ -0,0 +1,88 @@
+message = $message;
+ return $this;
+ }
+
+ public function asSuccess(): static
+ {
+ return $this->as(FlashMessageType::Success);
+ }
+
+ public function asError(): static
+ {
+ return $this->as(FlashMessageType::Error);
+ }
+
+ public function asWarning(): static
+ {
+ return $this->as(FlashMessageType::Warning);
+ }
+
+ public function asNotice(): static
+ {
+ return $this->as(FlashMessageType::Notice);
+ }
+
+ public function as(FlashMessageType $type): static
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function message(): Phrase
+ {
+ return $this->message;
+ }
+
+ public function type(): FlashMessageType
+ {
+ return $this->type;
+ }
+
+ /**
+ * @deprecated Use the message() method instead.
+ * @see static::message()
+ */
+ function getMessage(): Phrase
+ {
+ return $this->message;
+ }
+
+ /**
+ * @deprecated Use the type() method instead.
+ * @see static::type()
+ */
+ function getType(): FlashMessageType
+ {
+ return $this->type;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagentoFlashMessages/FlashMessageType.php b/lib/Magewire/Features/SupportMagentoFlashMessages/FlashMessageType.php
new file mode 100644
index 00000000..97bcf7f4
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagentoFlashMessages/FlashMessageType.php
@@ -0,0 +1,20 @@
+magewireFlashMessages ??= Factory::create(MagewireFlashMessages::class);
+ }
+
+ /**
+ * @deprecated Flash Messages have been moved into their own object. Chain the magewireFlashMessages method instead.
+ * @see static::magewireFlashMessages()
+ */
+ public function dispatchErrorMessage($message): FlashMessageElement
+ {
+ return $this->magewireFlashMessages()->make($message, FlashMessageType::Error);
+ }
+
+ /**
+ * @deprecated Flash Messages have been moved into their own object. Chain the magewireFlashMessages method instead.
+ * @see static::magewireFlashMessages()
+ */
+ public function dispatchWarningMessage($message): FlashMessageElement
+ {
+ return $this->magewireFlashMessages()->make($message, FlashMessageType::Warning);
+ }
+
+ /**
+ * @deprecated Flash Messages have been moved into their own object. Chain the magewireFlashMessages method instead.
+ * @see static::magewireFlashMessages()
+ */
+ public function dispatchNoticeMessage($message): FlashMessageElement
+ {
+ return $this->magewireFlashMessages()->make($message, FlashMessageType::Notice);
+ }
+
+ /**
+ * @deprecated Flash Messages have been moved into their own object. Chain the magewireFlashMessages method instead.
+ * @see static::magewireFlashMessages()
+ */
+ public function dispatchSuccessMessage($message): FlashMessageElement
+ {
+ return $this->magewireFlashMessages()->make($message, FlashMessageType::Success);
+ }
+
+ /**
+ * @deprecated Flash Messages have been moved into their own object. Chain the magewireFlashMessages method instead.
+ * @see static::magewireFlashMessages()
+ */
+ public function dispatchMessage(string $type, $message): FlashMessageElement
+ {
+ $type = match ($type) {
+ FlashMessageType::Error->value => FlashMessageType::Error,
+ FlashMessageType::Warning->value => FlashMessageType::Warning,
+ FlashMessageType::Success->value => FlashMessageType::Success,
+
+ default => FlashMessageType::Notice
+ };
+
+ return $this->magewireFlashMessages()->make($message, $type);
+ }
+
+ /**
+ * @deprecated Flash Messages have been moved into their own object. Chain the magewireFlashMessages method instead.
+ * @see static::magewireFlashMessages()
+ */
+ public function hasFlashMessages(): bool
+ {
+ return $this->magewireFlashMessages()->count() > 0;
+ }
+
+ /**
+ * @deprecated Flash Messages have been moved into their own object. Chain the magewireFlashMessages method instead.
+ * @see static::magewireFlashMessages()
+ */
+ public function getFlashMessages(): array
+ {
+ return $this->magewireFlashMessages()->fetch();
+ }
+
+ /**
+ * @deprecated Clearing all messages at once is not advisable. Use the unset method instead.
+ * @see MagewireFlashMessages::unset()
+ */
+ public function clearFlashMessages(): void
+ {
+ $this->magewireFlashMessages()->clear();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagentoFlashMessages/MagewireFlashMessages.php b/lib/Magewire/Features/SupportMagentoFlashMessages/MagewireFlashMessages.php
new file mode 100644
index 00000000..d5354b0c
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagentoFlashMessages/MagewireFlashMessages.php
@@ -0,0 +1,63 @@
+messages[$name ?? Random::string()] ??= Factory::create(FlashMessage::class, [
+ 'message' => is_string($message) ? __($message) : $message,
+ 'type' => $type
+ ]);
+ }
+
+ public function unset(string $name): static
+ {
+ if (isset($this->messages[$name])) {
+ unset($this->messages[$name]);
+ }
+
+ return $this;
+ }
+
+ public function fetch(): array
+ {
+ return $this->messages;
+ }
+
+ public function count(): int
+ {
+ return count($this->messages);
+ }
+
+ /**
+ * @deprecated clearing all flash messages shouldn't be something you use. Instead, use unset to remove
+ * a single flash message by its name.
+ * @see static::unset()
+ */
+ public function clear(): static
+ {
+ $this->messages = [];
+ return $this;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagentoFlashMessages/SupportMagentoFlashMessages.php b/lib/Magewire/Features/SupportMagentoFlashMessages/SupportMagentoFlashMessages.php
new file mode 100644
index 00000000..17bf3b6a
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagentoFlashMessages/SupportMagentoFlashMessages.php
@@ -0,0 +1,40 @@
+component();
+
+ if ($component && $component->magewireFlashMessages()->count() > 0) {
+ $context->pushEffect('dispatches', [
+ 'name' => 'magewire:flash-messages:dispatch',
+ 'params' => $this->mapFlashMessages($component->magewireFlashMessages())
+ ]);
+ }
+ }
+
+ private function mapFlashMessages(MagewireFlashMessages $messages): array
+ {
+ return array_map(static function (FlashMessage $message) {
+ return [
+ 'text' => $message->message()->render(),
+ 'type' => $message->type()
+ ];
+ }, $messages->fetch());
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagentoLayouts/HandlesMagentoLayout.php b/lib/Magewire/Features/SupportMagentoLayouts/HandlesMagentoLayout.php
new file mode 100644
index 00000000..c73a3205
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagentoLayouts/HandlesMagentoLayout.php
@@ -0,0 +1,89 @@
+magewireResolver = $resolver;
+ }
+
+ return $this->magewireResolver;
+ }
+
+ public function magewireBlock(AbstractBlock|null $block = null): AbstractBlock|null
+ {
+ if ($block) {
+ $this->magewireBlock = $block;
+ }
+
+ return $this->magewireBlock;
+ }
+
+ public function magewireLayoutLifecycle(LayoutLifecycle|null $lifecycle = null): LayoutLifecycle|null
+ {
+ if ($lifecycle) {
+ $this->magewireLayoutLifecycle = $lifecycle;
+ }
+
+ return $this->magewireLayoutLifecycle;
+ }
+
+ /**
+ * @deprecated has been replaced with block()
+ * @see static::magewireBlock()
+ */
+ public function setParent(Template|null $parent): static
+ {
+ $this->magewireBlock($parent);
+
+ return $this;
+ }
+
+ /**
+ * @deprecated has been replaced with block()
+ * @see static::magewireBlock()
+ */
+ public function getParent(): AbstractBlock|null
+ {
+ return $this->magewireBlock();
+ }
+
+ /**
+ * @deprecated has been replaced with magewireBlock()
+ * @see static::magewireBlock()
+ */
+ public function block(AbstractBlock|null $block = null): AbstractBlock|null
+ {
+ return $this->magewireBlock($block);
+ }
+
+ /**
+ * @deprecated has been replaced with magewireResolver()
+ * @see static::magewireResolver()
+ */
+ public function resolver(AbstractBlock|null $block = null): AbstractBlock|null
+ {
+ return $this->magewireBlock($block);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagentoLayouts/SupportMagentoLayouts.php b/lib/Magewire/Features/SupportMagentoLayouts/SupportMagentoLayouts.php
new file mode 100644
index 00000000..30986540
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagentoLayouts/SupportMagentoLayouts.php
@@ -0,0 +1,18 @@
+ */
+ private array $listeners = [];
+
+ public function with(callable $callback): static
+ {
+ $this->listeners[] = $callback;
+
+ return $this;
+ }
+
+ public function listeners(): array
+ {
+ return $this->listeners;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagentoObserverEvents/SupportMagentoObserverEvents.php b/lib/Magewire/Features/SupportMagentoObserverEvents/SupportMagentoObserverEvents.php
new file mode 100644
index 00000000..913f5160
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagentoObserverEvents/SupportMagentoObserverEvents.php
@@ -0,0 +1,133 @@
+ $this->dispatch('magewire_on_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $event), ...$args));
+ }
+ }
+
+ private function dispatch(string $event, ...$arguments): callable
+ {
+ $listener = $this->listenerDataTransferObjectFactory->create();
+ $this->eventManager->dispatch($event, ['listener' => $listener]);
+
+ $afters = [];
+
+ foreach ($listener->listeners() as $listener) {
+ if (! is_callable($listener)) {
+ continue;
+ }
+
+ $result = $listener(...$arguments);
+
+ if (is_callable($result)) {
+ $afters[] = $result;
+ }
+ }
+
+ return function (...$args) use ($afters, $event) {
+ $pipeline = $this->pipelineFactory->create();
+
+ if ($afters) {
+ foreach ($afters as $after) {
+ $pipeline->pipe(static fn (array $args, callable $next) => $next($after(...$args)));
+ }
+
+ return $pipeline->run($args);
+ }
+
+ // Can only have a single return item, just like a regular class method.
+ return $args[0] ?? null;
+ };
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/Exception/DirectiveExpressionParseException.php b/lib/Magewire/Features/SupportMagewireCompiling/Exception/DirectiveExpressionParseException.php
new file mode 100644
index 00000000..7ad9c8aa
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/Exception/DirectiveExpressionParseException.php
@@ -0,0 +1,18 @@
+magewireCompiler = $compiler;
+ }
+
+ return $this->magewireCompiler;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/MagewireUnderscoreViewModel.php b/lib/Magewire/Features/SupportMagewireCompiling/MagewireUnderscoreViewModel.php
new file mode 100644
index 00000000..531e7611
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/MagewireUnderscoreViewModel.php
@@ -0,0 +1,49 @@
+actionManager->load($class);
+ }
+
+ public function arguments(): DataScope
+ {
+ return $this->arguments;
+ }
+
+ public function utils(): Utils
+ {
+ return $this->utils;
+ }
+
+ public function factory(): ViewFactory
+ {
+ return $this->viewFactory;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/SupportMagewireCompiling.php b/lib/Magewire/Features/SupportMagewireCompiling/SupportMagewireCompiling.php
new file mode 100644
index 00000000..79b74d78
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/SupportMagewireCompiling.php
@@ -0,0 +1,157 @@
+block() instanceof DataObject) {
+ return;
+ }
+
+ $component = $dto->block()->getData('magewire');
+
+ if (! $component instanceof Component) {
+ return;
+ }
+
+ $compiler = $component->magewireCompiler() ?? $component->magewireCompiler(
+ $this->compilerManager->factory()->newCompilerInstance()
+ );
+
+ $dto->dictionary(['magewire' => $component]);
+
+ if ($component->magewireCompiler()->canCompile()) {
+ $compiledPath = $compiler->management()->file()->generateFilePath($dto->filename());
+
+ if ($compiler->requiresRecompile($dto->filename())) {
+ trigger('magewire:view:compile', $compiler, $component, $dto->block());
+ $compiler->compile($dto->filename(), $compiledPath);
+ }
+
+ $dto->filename($compiledPath);
+ }
+
+ // Concept: Include the Magewire underscore object optionally required by compiled views.
+ $dto->dictionary(['__magewire' => $dto->dictionary()['__magewire'] ?? $this->underscoreViewModelFactory->create()]);
+
+ // Currently only for dev-purposes, will change over time and shouldn't be used.
+ if ($this->slotsRegistry->hasAreas()) {
+ $dto->dictionary([
+ '__slot' => $dto->dictionary()['__slot'] ?? $this->slotsRegistry->snapshot(),
+ '__el' => $this->slotsRegistry->element(),
+ ]);
+ }
+ });
+
+ before('magewire:view:compile', static function (Compiler $compiler) {
+ $runs['html'] = 0;
+
+ $compiler
+ ->pipelines()
+ ->html()
+ ->middleware()
+ ->group('first-line', 2)
+ ->pipe(static function (string $throughput, callable $next) use (&$runs, $compiler) {
+ $runs['html']++;
+
+ if ($runs['html'] === 1) {
+ return '@template()' . PHP_EOL . $next($throughput);
+ }
+
+ return $next($throughput);
+ });
+
+ $compiler
+ ->pipelines()
+ ->template()
+ ->middleware()
+ ->group('last')
+ ->pipe(static function (string $throughput, callable $next): string {
+ return $next($throughput) . '@endtemplate';
+ });
+
+ $compiler
+ ->pipelines()
+ ->template()
+ ->middleware()
+ ->group('last')
+ ->pipe(static function (string $throughput, callable $next): string {
+ $result = $next($throughput);
+ $date = new DateTime();
+
+ return $result . sprintf('' . PHP_EOL, $date->format('Y-m-d H:i:s.u'));
+ })
+ ->pipe(static function (string $throughput, callable $next) use ($compiler): string {
+ $result = $next($throughput);
+
+ return $result . sprintf('' . PHP_EOL, $compiler->basePath());
+ })
+ ->pipe(static function (string $throughput, callable $next) use ($compiler): string {
+ $start = $compiler->compileStartTime();
+ $result = $next($throughput);
+
+ $durationMs = round(( microtime(true) - $start ) * 1000, 2);
+ $durationSec = round($durationMs / 1000, 4);
+
+ return $result . sprintf('' . PHP_EOL, $durationMs, $durationSec);
+ });
+ });
+ }
+
+ /**
+ * WIP
+ *
+ * Generate a readable file structure for generated files.
+ *
+ * Instead of unreadable filenames, create organized directories that map to source files,
+ * making it easier for developers to locate and debug generated output.
+ *
+ * TODO: Consider enabling this only in development mode.
+ */
+ private function transformToViewPath(Template $block): string
+ {
+ $template = $block->getTemplate();
+ $parts = explode('::', $template);
+
+ if (count($parts) === 2) {
+ $parts[0] = str_replace('_', '/', $parts[0]);
+
+ return implode('/', $parts);
+ }
+
+ return $block->getTemplateFile();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Action/Magento/Auth.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Action/Magento/Auth.php
new file mode 100644
index 00000000..1891b30d
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Action/Magento/Auth.php
@@ -0,0 +1,34 @@
+httpContext->getValue(Context::CONTEXT_AUTH);
+ }
+
+ public function isGuest(): bool
+ {
+ return ! $this->isCustomer();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Action/Magewire.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Action/Magewire.php
new file mode 100644
index 00000000..54179c70
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Action/Magewire.php
@@ -0,0 +1,18 @@
+basePath = $basePath;
+ $this->targetPath = $targetPath;
+ $this->compileStartTime = microtime(true);
+
+ $filesystem = $this->management()->file()->system();
+ $content = $this->compiler()->run($filesystem->read($basePath));
+
+ $filesystem->ensureDirectoryExists($targetPath);
+ $filesystem->write($content, $targetPath);
+
+ return $filesystem->exists($targetPath);
+ }
+
+ public function basePath(): string
+ {
+ return $this->basePath;
+ }
+
+ public function targetPath(): string
+ {
+ return $this->targetPath;
+ }
+
+ public function compileStartTime(): float
+ {
+ return $this->compileStartTime;
+ }
+
+ /**
+ * Compiler management entry point.
+ */
+ public function management(): CompilerManager
+ {
+ return $this->manager;
+ }
+
+ /**
+ * @return CompilerPipelines
+ */
+ public function pipelines(): CompilerPipelines
+ {
+ return $this->compilerPipelines ??= $this->newCompilerPipelineDistributorInstance();
+ }
+
+ /**
+ * Gets or sets the compile flag.
+ *
+ * When called without arguments, this method returns the current compile state.
+ * When a boolean value is provided, it updates the compile flag and returns
+ * the current instance for method chaining.
+ */
+ public function canCompile(bool|null $choice = null): bool|static
+ {
+ if ($choice) {
+ $this->compile = $choice;
+ return $this;
+ }
+
+ return $this->compile;
+ }
+
+ /**
+ * Determine if the view at the given path is expired.
+ *
+ * @throws FileSystemException
+ */
+ public function requiresRecompile(string $path): bool
+ {
+ $filesystem = $this->management()->file()->system();
+ $compiled = $this->management()->file()->generateFilePath($path);
+
+ if ($filesystem->exists($compiled)) {
+ return $filesystem->lastModified($path) >= $filesystem->lastModified($compiled);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the template pipeline.
+ */
+ protected function compiler(): Pipeline
+ {
+ return $this->pipelines()->template();
+ }
+
+ /**
+ * Compiles a PHP template string by iterating through its tokens.
+ * @throws Throwable
+ */
+ protected function compileTokens(string $input): string
+ {
+ $output = '';
+
+ foreach (token_get_all($input) as $token) {
+ $output .= is_array($token) ? $this->parseToken($token) : $token;
+ }
+
+ return $output;
+ }
+
+ /**
+ * Parses and compiles a single token from the tokenized template.
+ * @throws Throwable
+ */
+ protected function parseToken(#[\SensitiveParameter] array $token): string
+ {
+ [$id, $content] = $token;
+
+ if ($id == T_INLINE_HTML) {
+ return $this->pipelines()->html()->run($content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Compile directives starting with "@".
+ */
+ protected function compileDirectives(string $template): string
+ {
+ preg_match_all('/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( [\S\s]*? ) \))?/x', $template, $matches);
+
+ $offset = 0;
+
+ for ($i = 0; isset($matches[0][$i]); $i++) {
+ $match = [
+ $matches[0][$i],
+ $matches[1][$i],
+ $matches[2][$i],
+ $matches[3][$i] ?: null,
+ $matches[4][$i] ?: null
+ ];
+
+ while (isset($match[4]) && str_ends_with($match[0], ')') && ! $this->management()->utils()->hasEvenNumberOfParentheses($match[0])) {
+ $after = strstr($template, $match[0]);
+
+ if ($after === false) {
+ break;
+ }
+
+ $after = substr($after, strlen($match[0]));
+ $pos = strpos($after, ')');
+
+ if ($pos === false) {
+ break;
+ }
+
+ $rest = substr($after, 0, $pos);
+
+ if (isset($matches[0][$i + 1]) && str_contains($rest . ')', $matches[0][$i + 1])) {
+ unset($matches[0][$i + 1]);
+ $i++;
+ }
+
+ $match[0] .= $rest . ')';
+ $match[3] .= $rest . ')';
+ $match[4] .= $rest;
+ }
+
+ [$template, $offset] = $this->replaceFirstStatement($match[0], $this->compileDirective($match), $template, $offset);
+ }
+
+ return $template;
+ }
+
+ /**
+ * Compile a single "@" directive.
+ */
+ protected function compileDirective(array $match): string
+ {
+ [$area, $directive] = $this->management()->directives()->tryToLocateArea($match[1]);
+
+ if (str_contains($match[1], '@')) {
+ $match[0] = isset($match[3]) ? $match[1] . $match[3] : $match[1];
+ } elseif ($area instanceof DirectiveArea && is_string($directive)) {
+ if ($area->responsibilities()->has($directive)) {
+ $match[0] = $area->responsibilities()->pop($directive)->compile($match[4] ?? '', $directive);
+ } elseif ($area->has($directive)) {
+ $match[0] = $area->get($directive)->compile($match[4] ?? '', $directive);
+ }
+ } elseif ($directive = $this->management()->directives()->area()->get($directive)) {
+ $match[0] = $directive->compile($match[4] ?? '', $match[1]);
+ } else {
+ return $match[0];
+ }
+
+ return isset($match[3]) ? $match[0] : $match[0] . $match[2];
+ }
+
+ /**
+ * Replace the first match for a statement compilation operation.
+ */
+ protected function replaceFirstStatement(string $search, string $replace, string $subject, int $offset): array|string
+ {
+ if ($search === '') {
+ return $subject;
+ }
+
+ $position = strpos($subject, $search, $offset);
+
+ if ($position !== false) {
+ return [
+ substr_replace($subject, $replace, $position, strlen($search)),
+ $position + strlen($replace)
+ ];
+ }
+
+ return [$subject, 0];
+ }
+
+ protected function newCompilerPipelineDistributorInstance(): CompilerPipelines
+ {
+ $distributor = $this->compilerPipelinesFactory->create(['type' => Pipeline::class]);
+
+ $distributor
+ ->template()
+ ->pipe(function (string $throughput, callable $next) {
+ return $next($this->compileTokens($throughput));
+ });
+
+ // Reserves the 'security' groups at the very earliest position
+ // so security-related pipes always run before everything else.
+ $distributor->template()->middleware()->group('security', 0);
+ $distributor->template()->middleware()->group('last', 900);
+ $distributor->html()->middleware()->group('security', 0);
+
+ $distributor
+ ->html()
+ ->pipe(function (string $throughput, callable $next) {
+ return $next($this->compileDirectives($throughput));
+ });
+
+ return $distributor;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Compiler/MagentoTemplateCompiler.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Compiler/MagentoTemplateCompiler.php
new file mode 100644
index 00000000..3602ff8a
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Compiler/MagentoTemplateCompiler.php
@@ -0,0 +1,45 @@
+template()
+ ->middleware()
+ ->group('blade', 25)
+ ->pipe(function (string $throughput, callable $next) {
+ return $next($this->bladeMiddleware->compile($throughput));
+ });
+
+ return $pipeline;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Compiler/Middleware/Blade.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Compiler/Middleware/Blade.php
new file mode 100644
index 00000000..2cfaf133
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Compiler/Middleware/Blade.php
@@ -0,0 +1,304 @@
+compileSelfClosingTags($value);
+ $value = $this->compileSlots($value);
+ $value = $this->compileOpeningTags($value);
+ return $this->compileClosingTags($value);
+ }
+
+ /**
+ * Compile the opening tags within the given string.
+ */
+ protected function compileOpeningTags(string $value): string
+ {
+ $pattern = "/
+ <\s*
+ magewire-
+ (?[\w\-:]+)
+ :
+ (?[\w\-]+)
+ (?
+ (?:
+ \s+
+ (?:
+ @(?:class)\( (?: (?>[^()]+) | (?-1) )* \)
+ |
+ @(?:style)\( (?: (?>[^()]+) | (?-1) )* \)
+ |
+ \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ |
+ (:\\\$)(\w+)
+ |
+ [\w\-:.@%]+
+ (
+ =
+ (?:
+ \\\"[^\\\"]*\\\"
+ |
+ \'[^\']*\'
+ |
+ [^\'\\\"=<>]+
+ )
+ )?
+ )
+ )*
+ \s*
+ )
+ (?/x";
+
+ return preg_replace_callback(
+ $pattern,
+ function (array $matches) {
+ $attributes = $this->parseParams($matches['attributes']);
+
+ return $this->componentString($matches['component'], $attributes, $matches['variant'] ?? null);
+ },
+ $value
+ );
+ }
+
+ /**
+ * Compile the self-closing tags within the given string.
+ */
+ protected function compileSelfClosingTags(string $value): string
+ {
+ $pattern = "/
+ <\s*
+ magewire-
+ (?[\w\-:]+)
+ :
+ (?[\w\-]+)
+ (?
+ (?:
+ \s+
+ (?:
+ @(?:class)\( (?: (?>[^()]+) | (?-1) )* \)
+ |
+ @(?:style)\( (?: (?>[^()]+) | (?-1) )* \)
+ |
+ \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ |
+ (:\\\$)(\w+)
+ |
+ [\w\-:.@%]+
+ (
+ =
+ (?:
+ \\\"[^\\\"]*\\\"
+ |
+ \'[^\']*\'
+ |
+ [^\'\\\"=<>]+
+ )
+ )?
+ )
+ )*
+ \s*
+ )
+ (?/x";
+
+ return preg_replace_callback(
+ $pattern,
+ function (array $matches) {
+ $attributes = $this->parseParams($matches['attributes']);
+
+ return $this->componentString($matches['component'], $attributes, $matches['variant']) . "\n@endComponent";
+ },
+ $value
+ );
+ }
+
+ protected function componentString(string $component, string $attributes = '[]', string|null $variant = 'default'): string
+ {
+ $variant ??= 'default';
+ $var = 'component' . ucfirst(strtolower($variant)) . ucfirst(Random::alphabetical(5, true));
+
+ return "@magewireComponent(type: '{$component}', id: '{$var}', variant: '{$variant}')
+
+ dictionary()->fill(get_defined_vars()) ?>
+ data()->distribute({$attributes}) ?>
+ start() ?>
+ ";
+ }
+
+ /**
+ * Compile the closing tags within the given string.
+ */
+ protected function compileClosingTags(string $value): string
+ {
+ return preg_replace("/<\/\s*magewire-[\:]?[\w\-\:\.]*\s*>/", ' @magewireEndComponent', $value);
+ }
+
+ /**
+ * Compile the slot tags within the given string.
+ */
+ public function compileSlots(string $value): string
+ {
+ $pattern = "/
+ <
+ \s*
+ magewire-slot
+ (?:\:(?\w+(?:-\w+)*))?
+ (?:\s+name=(?(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+)))?
+ (?:\s+\:name=(?(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+)))?
+ (?
+ (?:
+ \s+
+ (?:
+ (?:
+ @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
+ )
+ |
+ (?:
+ @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \))
+ )
+ |
+ (?:
+ \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ )
+ |
+ (?:
+ [\w\-:.@]+
+ (
+ =
+ (?:
+ \\\"[^\\\"]*\\\"
+ |
+ \'[^\']*\'
+ |
+ [^\'\\\"=<>]+
+ )
+ )?
+ )
+ )
+ )*
+ \s*
+ )
+ (?
+ /x";
+
+ $value = preg_replace_callback(
+ $pattern,
+ function ($matches) {
+ $name = $this->stripQuotes($matches['inlineName'] ?: $matches['name'] ?: $matches['boundName']) ?: "'slot'";
+
+ $attributes = $this->parseParams($matches['attributes']);
+ $var = 'slot' . ucfirst(strtolower($name)) . ucfirst(Random::alphabetical(5, true));
+
+ return "@magewireSlot(target: '{$name}', id: '{$var}')
+
+ dictionary()->fill(get_defined_vars()) ?>
+ data()->distribute({$attributes}) ?>
+ start() ?>
+ ";
+ },
+ $value
+ );
+
+ return preg_replace('/<\/\s*magewire-slot[^>]*>/', ' @magewireEndSlot', $value);
+ }
+
+ /**
+ * Strip any quotes from the given string.
+ */
+ public function stripQuotes(string $value): string
+ {
+ return str_starts_with($value, '"') || str_starts_with($value, "'") ? substr($value, 1, -1) : $value;
+ }
+
+ /**
+ * @tddo Minimal implementation, needs a lot more work to make edge cases work
+ * For example, Dynamic classes based on a template variable.
+ */
+ protected function parseParams($params): string
+ {
+ preg_match_all('/([a-zA-Z0-9:-]*?)\s*?=\s*?(.+?)(\s|$)/ms', $params, $matches);
+ $params = [];
+
+ foreach ($matches[1] as $i => $key) {
+ $value = str_replace('"', '', $matches[2][$i]);
+
+ $value = match ($value) {
+ 'false' => false,
+ 'true' => true,
+ default => $value
+ };
+
+ if (str_starts_with($key, ':magewire:')) {
+ $params['magewire'][] = '"' . substr($key, 1) . '" => ' . $value;
+ continue;
+ }
+ if (str_starts_with($key, 'bind:magewire:')) {
+ $params['magewire'][] = '"' . substr($key, 5) . '" => ' . $value;
+ continue;
+ }
+ if (str_starts_with($key, 'magewire:')) {
+ $params['magewire'][] = '"' . $key . '" => "' . $value . '"';
+ continue;
+ }
+ if (str_starts_with($key, '::')) {
+ $params['attributes'][] = '"' . substr($key, 2) . '" => "' . $value . '"';
+ continue;
+ }
+ if (str_starts_with($key, ':')) {
+ $params['properties'][] = '"' . substr($key, 1) . '" => ' . $value;
+ continue;
+ }
+ if (str_starts_with($key, 'bind:')) {
+ $params['properties'][] = '"' . substr($key, 5) . '" => ' . $value;
+ continue;
+ }
+
+ $filling = is_bool($value) ? ( $value ? 'true' : 'false' ) : '"' . $value . '"';
+ $params['properties'][] = '"' . $key . '"' . ' => ' . $filling;
+ }
+
+ $result = '[';
+
+ foreach ($params as $key => $value) {
+ $result .= '"' . $key . '" => [' . implode(', ', $value) . '], ';
+ }
+
+ $result = rtrim($result, ', ');
+ $result .= ']';
+
+ return $result;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/CompilerFactory.php b/lib/Magewire/Features/SupportMagewireCompiling/View/CompilerFactory.php
new file mode 100644
index 00000000..00183216
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/CompilerFactory.php
@@ -0,0 +1,35 @@
+newInstance($arguments);
+ }
+
+ private function newInstanceType(): string
+ {
+ return MagentoTemplateCompiler::class;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/CompilerPipelines.php b/lib/Magewire/Features/SupportMagewireCompiling/View/CompilerPipelines.php
new file mode 100644
index 00000000..a8b04611
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/CompilerPipelines.php
@@ -0,0 +1,27 @@
+getExpressionParserFor($directive) )) {
+ $parser = $this->parser($type)->parse($expression);
+
+ $allArgs = $parser->arguments()->all();
+ $method = new ReflectionMethod($this, $directive);
+ $args = [];
+
+ foreach ($method->getParameters() as $param) {
+ $paramName = $param->getName();
+
+ if (array_key_exists($paramName, $allArgs)) {
+ $args[$paramName] = $allArgs[$paramName];
+ }
+ }
+
+ return $this->{$directive}(...$args);
+ }
+
+ return $this->{$directive}();
+ }
+
+ protected function parser(ExpressionParserType $parser, array $arguments = []): ExpressionParser
+ {
+ return $parser->create($arguments);
+ }
+
+ protected function getExpressionParserFor(string $directive): ExpressionParserType|null
+ {
+ $directive = implode('::', [static::class, $directive]);
+
+ if (! ( $this->expressionParsers[$directive] ?? null )) {
+ $reflection = new ReflectionClass($this);
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $attributes = $method->getAttributes(ScopeDirectiveParser::class);
+ $attribute = $attributes[0] ?? null ? $attributes[0]->newInstance() : null;
+
+ if ($attribute) {
+ $this->expressionParsers[implode('::', [static::class, $method->getName()])] = $attribute->expressionParserType;
+ }
+ }
+ }
+
+ return $this->expressionParsers[$directive] ?? null;
+ }
+
+ protected function var(string $name, bool $pop = false): string
+ {
+ $var = $this->variables[$name] ??= Random::alphabetical(10, true);
+
+ if ($pop) {
+ unset($this->variables[$name]);
+ }
+
+ return $var;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Base.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Base.php
new file mode 100644
index 00000000..9a34fd16
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Base.php
@@ -0,0 +1,35 @@
+escapeHtml(__('{$value}')) ?>";
+ }
+
+ return "= __('{$value}') ?>";
+ }
+
+ #[ScopeDirectiveParser(ExpressionParserType::FUNCTION_ARGUMENTS)]
+ public function child(string $alias): string
+ {
+ return "= \$block->getChildHtml('{$alias}') ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Fragment.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Fragment.php
new file mode 100644
index 00000000..fdd74926
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Fragment.php
@@ -0,0 +1,32 @@
+var('fragment')} = \$__magewire->utils()->fragment()->make({$type})->start() ?>";
+ }
+
+ public function endfragment(): string
+ {
+ return "var('fragment')}->end() ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Json.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Json.php
new file mode 100644
index 00000000..ebb323a0
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Json.php
@@ -0,0 +1,33 @@
+parser(Directive\Parser\ExpressionParserType::FUNCTION_ARGUMENTS)->parse($expression)->arguments();
+
+ $value = $arguments->get('value', $arguments->get('default', []));
+ $flags = $arguments->get('flags', $this->encodingOptions);
+ $depth = $arguments->get('depth', 512);
+
+ return "";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Auth.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Auth.php
new file mode 100644
index 00000000..504cb495
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Auth.php
@@ -0,0 +1,40 @@
+action('magento.auth')->execute('is_customer')): ?>";
+ }
+
+ public function endauth(): string
+ {
+ return parent::endif();
+ }
+
+ #[ScopeDirectiveChain(methods: ['endguest'])]
+ public function guest(): string
+ {
+ return "action('magento.auth')->execute('is_guest')): ?>";
+ }
+
+ public function endguest(): string
+ {
+ return parent::endif();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Block.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Block.php
new file mode 100644
index 00000000..ffd4224f
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Block.php
@@ -0,0 +1,25 @@
+getChildBlock('{$alias}') ? \$block->getChildBlock('{$alias}')->toHtml() : '' ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Escape.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Escape.php
new file mode 100644
index 00000000..487107a9
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magento/Escape.php
@@ -0,0 +1,25 @@
+escapeUrl({$url}) ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magewire/Component.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magewire/Component.php
new file mode 100644
index 00000000..247cfe2c
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magewire/Component.php
@@ -0,0 +1,37 @@
+variableScopeStart($id);
+ $variant ??= 'default';
+
+ return "factory()->elements()->element('{$type}', \$block, '{$variant}')->track() ?>";
+ }
+
+ public function endComponent(): string
+ {
+ $var = $this->variableScopeEnd();
+
+ return "end()->untrack() ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magewire/Slot.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magewire/Slot.php
new file mode 100644
index 00000000..75026709
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Magewire/Slot.php
@@ -0,0 +1,36 @@
+variableScopeStart($id);
+
+ return "factory()->elements()->slot('{$target}', \$block) ?>";
+ }
+
+ public function endSlot(): string
+ {
+ $var = $this->variableScopeEnd();
+
+ return "end() ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/Arguments.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/Arguments.php
new file mode 100644
index 00000000..608715c5
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/Arguments.php
@@ -0,0 +1,49 @@
+values()
+ );
+
+ return implode(', ', array_map(static fn ($k, $v) => str_replace(['$key', '$value'], [$k, var_export($v, true)], $format), $this->keys(), $values));
+ }
+
+ public function renderAsNamed(): string
+ {
+ return $this->render('$key: $value');
+ }
+
+ public function renderAsCondition(): string
+ {
+ return ''; // wip
+ }
+
+ public function renderAsIterationClause(): string
+ {
+ return ''; // wip
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ConditionExpressionParser.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ConditionExpressionParser.php
new file mode 100644
index 00000000..e3771a87
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ConditionExpressionParser.php
@@ -0,0 +1,20 @@
+ $expression];
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ExpressionParser.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ExpressionParser.php
new file mode 100644
index 00000000..fb745a22
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ExpressionParser.php
@@ -0,0 +1,39 @@
+parseArguments($content);
+
+ $this->arguments()->merge($content)->snapshot();
+ return $this;
+ }
+
+ abstract protected function parseArguments(string $expression): array;
+
+ public function arguments(): Arguments
+ {
+ return $this->arguments ??= $this->argumentsFactory->create();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ExpressionParserType.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ExpressionParserType.php
new file mode 100644
index 00000000..900d3abc
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/ExpressionParserType.php
@@ -0,0 +1,50 @@
+ ObjectManager::getInstance()->create($this->getTypeClass(), $arguments)
+ };
+ }
+
+ public function getTypeClass(): string
+ {
+ return match ($this) {
+ self::CONDITION => ConditionExpressionParser::class,
+ self::FUNCTION_ARGUMENTS => FunctionExpressionParser::class,
+ self::ITERATION_CLAUSE => IterationClauseExpressionParser::class
+ };
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/FunctionExpressionParser.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/FunctionExpressionParser.php
new file mode 100644
index 00000000..d50d1e88
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/FunctionExpressionParser.php
@@ -0,0 +1,413 @@
+ 'b', "x-data" => "init()"]
+ */
+ public function parseArguments(string $expression): array
+ {
+ $expression = trim($expression);
+
+ if ($expression === '') {
+ return [];
+ }
+
+ // Fast path: entire expression is a valid JSON object.
+ if ($this->isJsonString($expression)) {
+ return $this->parseJsonArguments($expression);
+ }
+
+ // Main path: parse named arguments with complex values (including PHP arrays).
+ return $this->parseNamedArguments($expression);
+ }
+
+ /**
+ * Parse named arguments: key: value, key2: value2, ...
+ */
+ private function parseNamedArguments(string $expression): array
+ {
+ $arguments = [];
+ $pos = 0;
+ $length = strlen($expression);
+
+ while ($pos < $length) {
+ $pos = $this->skipWhitespace($expression, $pos);
+
+ if ($pos >= $length) {
+ break;
+ }
+
+ // Extract key (unquoted identifier: name, attributes, etc.).
+ [$key, $pos] = $this->extractKey($expression, $pos);
+
+ if ($key === null) {
+ throw new InvalidArgumentException("Invalid or missing key near position {$pos}");
+ }
+
+ $pos = $this->skipWhitespace($expression, $pos);
+
+ if ($pos >= $length || $expression[$pos] !== ':') {
+ throw new InvalidArgumentException("Expected ':' after key '{$key}' near position {$pos}");
+ }
+
+ $pos++; // skip ':'.
+ $pos = $this->skipWhitespace($expression, $pos);
+
+ if ($pos >= $length) {
+ throw new InvalidArgumentException("Missing value for key '{$key}'");
+ }
+
+ // Extract value — supports strings, JSON, and full PHP arrays.
+ [$value, $pos] = $this->extractComplexValue($expression, $pos);
+
+ $arguments[$key] = $value;
+
+ $pos = $this->skipWhitespace($expression, $pos);
+
+ // Optional comma separator.
+ if ($pos < $length && $expression[$pos] === ',') {
+ $pos++;
+ }
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Extract an identifier key (e.g. name, attributes)
+ */
+ private function extractKey(string $expression, int $pos): array
+ {
+ $start = $pos;
+ while ($pos < strlen($expression) && preg_match('/[a-zA-Z0-9_]/', $expression[$pos])) {
+ $pos++;
+ }
+
+ if ($pos === $start) {
+ return [null, $pos];
+ }
+
+ $key = substr($expression, $start, $pos - $start);
+
+ // Keep top-level keys strict (valid PHP identifiers).
+ if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $key)) {
+ return [null, $pos];
+ }
+
+ return [$key, $pos];
+ }
+
+ /**
+ * Extract complex value: quoted string, JSON {}, JSON [], or PHP array []
+ */
+ private function extractComplexValue(string $expression, int $pos): array
+ {
+ if ($pos >= strlen($expression)) {
+ throw new InvalidArgumentException('Unexpected end of expression');
+ }
+
+ $char = $expression[$pos];
+
+ if ($char === '"' || $char === "'") {
+ return [$this->extractQuotedString($expression, $pos), $pos];
+ }
+
+ if ($char === '{' || $char === '[') {
+ // Could be JSON or PHP array — try PHP array first if starts with [.
+ if ($char === '[') {
+ // Peek ahead to see if it looks like PHP array (contains =>).
+ $peekPos = $pos + 1;
+ $peekPos = $this->skipWhitespace($expression, $peekPos);
+ if ($peekPos < strlen($expression) && ( $expression[$peekPos] === '"' || $expression[$peekPos] === "'" || $expression[$peekPos] === '$' )) {
+ // Likely PHP array — try to parse it.
+ try {
+ $tempPos = $pos;
+ $array = $this->extractPhpArray($expression, $tempPos);
+ return [$array, $tempPos];
+ } catch (Exception) {
+ // Fall through to JSON array.
+ }
+ }
+ }
+
+ // JSON object/array.
+ return [$this->extractJsonStructure($expression, $pos), $pos];
+ }
+
+ // Unquoted value (true, false, null, number, variable, unquoted string).
+ return [$this->extractUnquotedValue($expression, $pos), $pos];
+ }
+
+ /**
+ * Extract and evaluate a full PHP array literal: ['class' => 'btn', "x-data" => "init()"]
+ */
+ private function extractPhpArray(string $expression, int &$pos): array
+ {
+ $start = $pos;
+ $depth = 0;
+ $inString = false;
+ $escape = false;
+ $quoteChar = null;
+
+ $pos++; // skip opening [.
+
+ while ($pos < strlen($expression)) {
+ $char = $expression[$pos];
+
+ if ($escape) {
+ $escape = false;
+ $pos++;
+ continue;
+ }
+
+ if ($char === '\\') {
+ $escape = true;
+ $pos++;
+ continue;
+ }
+
+ if ($char === '"' || $char === "'") {
+ if (! $inString) {
+ $inString = true;
+ $quoteChar = $char;
+ } elseif ($quoteChar === $char) {
+ $inString = false;
+ $quoteChar = null;
+ }
+ $pos++;
+ continue;
+ }
+
+ if (! $inString) {
+ if ($char === '[') {
+ $depth++;
+ } elseif ($char === ']') {
+ if ($depth === 0) {
+ $pos++; // include closing ].
+ $arrayStr = substr($expression, $start, $pos - $start);
+ return $this->evaluatePhpArray($arrayStr);
+ }
+ $depth--;
+ }
+ }
+
+ $pos++;
+ }
+
+ throw new InvalidArgumentException("Unclosed PHP array starting at position {$start}");
+ }
+
+ /**
+ * Safely evaluate a PHP array literal using eval in a controlled way
+ */
+ private function evaluatePhpArray(string $arrayStr): array
+ {
+ $code = "return {$arrayStr};";
+
+ $result = @eval($code);
+
+ if ($result === false && $arrayStr !== '[]') {
+ throw new InvalidArgumentException("Failed to parse PHP array: syntax error in {$arrayStr}");
+ }
+
+ if (! is_array($result)) {
+ throw new InvalidArgumentException("Expression did not evaluate to an array: {$arrayStr}");
+ }
+
+ return $result;
+ }
+
+ /**
+ * Extract JSON object { ... } or array [ ... ]
+ */
+ private function extractJsonStructure(string $expression, int &$pos): array
+ {
+ $start = $pos;
+ $opening = $expression[$pos];
+ $closing = $opening === '{' ? '}' : ']';
+ $depth = 0;
+ $inString = false;
+ $escape = false;
+
+ $pos++;
+
+ while ($pos < strlen($expression)) {
+ $char = $expression[$pos];
+
+ if ($escape) {
+ $escape = false;
+ } elseif ($char === '\\') {
+ $escape = true;
+ } elseif ($char === '"') {
+ $inString = ! $inString;
+ } elseif (! $inString) {
+ if ($char === $opening) {
+ $depth++;
+ } elseif ($char === $closing) {
+ if ($depth === 0) {
+ $pos++;
+ $json = substr($expression, $start, $pos - $start);
+ $decoded = json_decode($json, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new InvalidArgumentException('Invalid JSON: ' . json_last_error_msg());
+ }
+ return $decoded;
+ }
+ $depth--;
+ }
+ }
+ $pos++;
+ }
+
+ throw new InvalidArgumentException("Unclosed JSON structure starting at position {$start}");
+ }
+
+ /**
+ * Extract quoted string
+ */
+ private function extractQuotedString(string $expression, int &$pos): string
+ {
+ $quote = $expression[$pos];
+ $pos++;
+ $start = $pos;
+ $escape = false;
+
+ while ($pos < strlen($expression)) {
+ $char = $expression[$pos];
+
+ if ($escape) {
+ $escape = false;
+ $pos++;
+ continue;
+ }
+
+ if ($char === '\\') {
+ $escape = true;
+ $pos++;
+ continue;
+ }
+
+ if ($char === $quote) {
+ $value = substr($expression, $start, $pos - $start);
+ $pos++;
+ return str_replace(['\\' . $quote, '\\\\'], [$quote, '\\'], $value);
+ }
+
+ $pos++;
+ }
+
+ throw new InvalidArgumentException('Unclosed quoted string');
+ }
+
+ /**
+ * Extract unquoted value (true, false, null, number, variable, simple string)
+ */
+ private function extractUnquotedValue(string $expression, int &$pos): mixed
+ {
+ $start = $pos;
+
+ while ($pos < strlen($expression) && ! in_array($expression[$pos], [',', '}', ']', ')', ' '], true)) {
+ $pos++;
+ }
+
+ $value = trim(substr($expression, $start, $pos - $start));
+
+ if ($value === '') {
+ throw new InvalidArgumentException('Empty unquoted value');
+ }
+
+ return match (strtolower($value)) {
+ 'null' => null,
+ 'true' => true,
+ 'false' => false,
+ default => $this->parseNumberOrVariableOrString($value)
+ };
+ }
+
+ private function parseNumberOrVariableOrString(string $value): mixed
+ {
+ if ($this->isNumber($value)) {
+ return $this->parseNumber($value);
+ }
+
+ if ($this->isVariable($value)) {
+ return $this->parseVariable($value);
+ }
+
+ return $value; // unquoted string.
+ }
+
+ private function skipWhitespace(string $expression, int $pos): int
+ {
+ while ($pos < strlen($expression) && ctype_space($expression[$pos])) {
+ $pos++;
+ }
+ return $pos;
+ }
+
+ private function isJsonString(string $expression): bool
+ {
+ $trimmed = trim($expression);
+ if (! str_starts_with($trimmed, '{') || ! str_ends_with($trimmed, '}')) {
+ return false;
+ }
+
+ json_decode($trimmed);
+ return json_last_error() === JSON_ERROR_NONE;
+ }
+
+ private function parseJsonArguments(string $jsonString): array
+ {
+ $decoded = json_decode(trim($jsonString), true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new InvalidArgumentException('Invalid JSON: ' . json_last_error_msg());
+ }
+
+ if (! is_array($decoded)) {
+ throw new InvalidArgumentException('JSON must decode to an array');
+ }
+
+ // No more strict key validation — allows 'x-data', 'aria-*', etc.
+ return $decoded;
+ }
+
+ private function isNumber(string $value): bool
+ {
+ return preg_match('/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/', $value) === 1;
+ }
+
+ private function parseNumber(string $value): float|int
+ {
+ return str_contains($value, '.') || str_contains(strtolower($value), 'e') ? (float) $value : (int) $value;
+ }
+
+ private function isVariable(string $value): bool
+ {
+ return preg_match('/^\$[a-zA-Z_][a-zA-Z0-9_]*$/', $value) === 1;
+ }
+
+ private function parseVariable(string $value): array
+ {
+ return [
+ 'type' => 'variable',
+ 'name' => substr($value, 1)
+ ];
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/IterationClauseExpressionParser.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/IterationClauseExpressionParser.php
new file mode 100644
index 00000000..0cd16293
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Parser/IterationClauseExpressionParser.php
@@ -0,0 +1,20 @@
+ $expression];
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Scope.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Scope.php
new file mode 100644
index 00000000..1b39f4f8
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Scope.php
@@ -0,0 +1,55 @@
+";
+ }
+
+ #[ScopeDirectiveParser(ExpressionParserType::CONDITION)]
+ public function elseif(string $condition): string
+ {
+ return "";
+ }
+
+ public function else(): string
+ {
+ return '';
+ }
+
+ public function endif(): string
+ {
+ return '';
+ }
+
+ #[ScopeDirectiveChain(methods: ['endforeach'])]
+ #[ScopeDirectiveParser(ExpressionParserType::ITERATION_CLAUSE)]
+ public function foreach(string $iterationClause): string
+ {
+ return "";
+ }
+
+ public function endforeach(): string
+ {
+ return '';
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Script.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Script.php
new file mode 100644
index 00000000..a6015249
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Script.php
@@ -0,0 +1,29 @@
+var('fragment')} = \$__magewire->utils()->fragment()->make()->script()->start() ?>";
+ }
+
+ public function endscript(): string
+ {
+ return "var('fragment')}->end() ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Template.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Template.php
new file mode 100644
index 00000000..29352213
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Directive/Template.php
@@ -0,0 +1,36 @@
+variableScopeStart();
+
+ return "utils()->fragment()->make()->component(\$block) ?>";
+ }
+
+ public function endtemplate(): string
+ {
+ $var = $this->variableScopeEnd();
+
+ return "end() ?>";
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/DirectiveArea.php b/lib/Magewire/Features/SupportMagewireCompiling/View/DirectiveArea.php
new file mode 100644
index 00000000..c0738f94
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/DirectiveArea.php
@@ -0,0 +1,91 @@
+ $directives
+ */
+ public function __construct(
+ private array $directives = []
+ ) {
+ }
+
+ public function set(string $name, Directive $directive, bool $force = false): Directive
+ {
+ if ($this->has($name) && $force === false) {
+ return $this->get($name);
+ }
+
+ return $this->directives[$name] = $directive;
+ }
+
+ public function unset(string $name): static
+ {
+ if ($this->has($name)) {
+ unset($this->directives[$name]);
+ }
+
+ return $this;
+ }
+
+ public function has(string $directive): bool
+ {
+ return array_key_exists($directive, $this->directives);
+ }
+
+ public function responsibilities(): DirectiveResponsibilities
+ {
+ return $this->responsibilities ??= Factory::create(DirectiveResponsibilities::class);
+ }
+
+ public function get(string $directive): Directive|null
+ {
+ $type = $this->directives[$directive] ?? null;
+ $standalone = is_string($type);
+
+ if ($standalone) {
+ $type = Factory::create($type);
+ }
+
+ /*
+ * On a scoped directive, we need to make sure it closing or chaining responsibilities are
+ * being memorized for when a directive
+ */
+ if ($type instanceof ScopeDirective) {
+ $type = $standalone ? $type : Factory::create($type::class);
+
+ foreach ($type->getResponsibilitiesFor($directive) as $responsibility) {
+ if ($responsibility === $directive) {
+ continue;
+ }
+
+ $this->responsibilities()->push($responsibility, $type);
+ }
+ }
+
+ return $type;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/DirectiveResponsibilities.php b/lib/Magewire/Features/SupportMagewireCompiling/View/DirectiveResponsibilities.php
new file mode 100644
index 00000000..5adf93b4
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/DirectiveResponsibilities.php
@@ -0,0 +1,38 @@
+items[$directive][] = $predecessor;
+
+ return $this;
+ }
+
+ public function pop(string $directive): Directive|null
+ {
+ if (is_array($this->items[$directive] ?? null)) {
+ return array_pop($this->items[$directive]);
+ }
+
+ return null;
+ }
+
+ public function has(string $directive): bool
+ {
+ return count($this->items[$directive] ?? []) > 0;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/DomElementData.php b/lib/Magewire/Features/SupportMagewireCompiling/View/DomElementData.php
new file mode 100644
index 00000000..2e202e09
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/DomElementData.php
@@ -0,0 +1,86 @@
+ Attributes::class,
+ 'properties' => Properties::class
+ ], $mapping);
+
+ parent::__construct($type ?? DataArray::class, $mapping);
+ }
+
+ public function distribution(): DataArray
+ {
+ return $this->data();
+ }
+
+ /**
+ * Distribute keyed arrays into individual data types.
+ *
+ * Iterates through the provided data and creates a separate slot for each string key
+ * that contains an array value. Each slot is then populated with its corresponding array content.
+ *
+ * Non-string keys and non-array values are ignored during distribution.
+ *
+ * @example
+ * $slots->distribute([
+ * 'header' => ['title' => 'Welcome', 'subtitle' => 'Hello World'],
+ * 'sidebar' => ['widgets' => [...]]
+ * ]);
+ */
+ public function distribute(array $data): static
+ {
+ foreach ($data as $key => $value) {
+ if (! ( is_string($key) && is_array($value) )) {
+ continue;
+ }
+
+ $this->create($key)->fill($value);
+ }
+
+ return $this;
+ }
+
+ protected function create(string|null $type = null, array $arguments = []): object
+ {
+ if ($type === 'data') {
+ return parent::create($type, $arguments);
+ }
+
+ return $this->data()->subset($type, $this->resolve($type), $arguments);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/File/StubCollector.php b/lib/Magewire/Features/SupportMagewireCompiling/View/File/StubCollector.php
new file mode 100644
index 00000000..8b65856c
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/File/StubCollector.php
@@ -0,0 +1,65 @@
+dirSearch, $this->fileFactory, 'stubs');
+ }
+
+ public function collect(): array
+ {
+ if ($this->collected) {
+ return $this->stubs;
+ }
+
+ /** @var array $stubs */
+ $stubs = $this->getFiles($this->design->getDesignTheme(), '*.stub');
+
+ foreach ($stubs as $stub) {
+ if ($stub instanceof File) {
+ $stub = $this->viewStubFactory->create($stub);
+ }
+
+ $this->stubs[$stub->getNamespace()] = $stub;
+ }
+
+ // Flag as collected to implement lazy loading and avoid redundant disk access.
+ $this->collected = true;
+
+ return $this->stubs;
+ }
+
+ public function recollect(): array
+ {
+ $this->collected = false;
+
+ return $this->collect();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/FileSystem.php b/lib/Magewire/Features/SupportMagewireCompiling/View/FileSystem.php
new file mode 100644
index 00000000..e7e440f6
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/FileSystem.php
@@ -0,0 +1,90 @@
+magentoFileSystemDriver->fileGetContents($from);
+ }
+
+ /**
+ * @throws FileSystemException
+ */
+ public function write(string $content, string $to, int|string $mode = 0): void
+ {
+ $this->magentoFileSystemDriver->filePutContents($to, $content, $mode);
+ }
+
+ public function exists(string $path): bool
+ {
+ try {
+ return $this->magentoFileSystemDriver->isExists($path);
+ } catch (FileSystemException $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+ }
+
+ return false;
+ }
+
+ /**
+ * @throws FileSystemException
+ */
+ public function stats(string $path): array
+ {
+ return $this->magentoFileSystemDriver->stat($path);
+ }
+
+ /**
+ * @throws FileSystemException
+ */
+ public function makeDirectory(string $path, int $mode = 0o755, bool $recursive = false): void
+ {
+ $this->magentoFileSystemDriver->createDirectory($path, $mode);
+ }
+
+ /**
+ * @throws FileSystemException
+ */
+ public function lastModified(string $path): int
+ {
+ $stat = $this->stats($path);
+
+ return $stat['mtime'] ?? time();
+ }
+
+ /**
+ * @throws FileSystemException
+ */
+ public function ensureDirectoryExists(string $path): void
+ {
+ if ($this->exists(dirname($path))) {
+ return;
+ }
+
+ $this->makeDirectory(dirname($path), 0o777, true);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/FunctionDirective.php b/lib/Magewire/Features/SupportMagewireCompiling/View/FunctionDirective.php
new file mode 100644
index 00000000..9b99a942
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/FunctionDirective.php
@@ -0,0 +1,27 @@
+parser(ExpressionParserType::FUNCTION_ARGUMENTS)->parse($expression);
+
+ // TODO: should handle exceptions, logging them and return an empty string when so.
+ return method_exists($this, $directive) ? $this->$directive(...$parsed->arguments()->all()) : '';
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Management/ActionManager.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/ActionManager.php
new file mode 100644
index 00000000..e34905c7
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/ActionManager.php
@@ -0,0 +1,70 @@
+ $namespaces
+ */
+ public function __construct(
+ private readonly ViewAction $action,
+ private readonly ActionManagerFactory $actionManagerFactory,
+ private readonly array $namespaces = []
+ ) {
+ }
+
+ public function execute(string $method, ...$arguments): mixed
+ {
+ $method = lcfirst(str_replace('_', '', ucwords($method, '_')));
+
+ return $this->action->$method(...$arguments);
+ }
+
+ /**
+ * Load and create an ActionManager instance based on the provided namespace.
+ *
+ * This method attempts to resolve the namespace in the following order:
+ * 1. If the namespace is an existing class, creates an ActionManager with that class
+ * 2. If the namespace exists in the registered namespaces array:
+ * - Use the object directly if it's already instantiated
+ * - Creates the object via ObjectManager if it's a class name
+ * 3. Returns the current instance if no resolution is possible
+ *
+ * @param string $namespace The namespace/class name to load, or a key from registered namespaces
+ * @return ActionManager Returns a new ActionManager instance or the current instance as fallback
+ */
+ public function load(string $namespace): ActionManager
+ {
+ if (class_exists($namespace)) {
+ return $this->actionManagerFactory->create([
+ 'action' => $namespace
+ ]);
+ } elseif (array_key_exists($namespace, $this->namespaces)) {
+ if (is_object($this->namespaces[$namespace])) {
+ return $this->actionManagerFactory->create([
+ 'action' => $this->namespaces[$namespace]
+ ]);
+ }
+
+ return $this->actionManagerFactory->create([
+ 'action' => Factory::create($this->namespaces[$namespace])
+ ]);
+ }
+
+ return $this;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Management/CompilerManager.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/CompilerManager.php
new file mode 100644
index 00000000..a77b5f33
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/CompilerManager.php
@@ -0,0 +1,46 @@
+directiveManager;
+ }
+
+ public function file(): FileManager
+ {
+ return $this->fileManager;
+ }
+
+ public function factory(): CompilerFactory
+ {
+ return $this->compilerFactory;
+ }
+
+ public function utils(): CompilerUtils
+ {
+ return $this->compilerUtils;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Management/DirectiveManager.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/DirectiveManager.php
new file mode 100644
index 00000000..4090710a
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/DirectiveManager.php
@@ -0,0 +1,56 @@
+ $areas
+ */
+ public function __construct(
+ private readonly DirectiveArea $directives,
+ private array $areas = []
+ ) {
+ }
+
+ public function area(string|null $name = null, DirectiveArea|null $area = null): DirectiveArea|null
+ {
+ if (is_string($name) && strlen($name) !== 0) {
+ if ($area) {
+ return $this->areas[$name] = $area;
+ }
+
+ return $this->areas[$name] ?? null;
+ }
+
+ return $this->directives;
+ }
+
+ public function tryToLocateArea(string $subject): array
+ {
+ foreach ($this->areas as $area => $object) {
+ if (str_starts_with($subject, $area)) {
+ return [$object, lcfirst(substr($subject, strlen($area)))];
+ }
+ }
+
+ $object = $this->directives->responsibilities()->has($subject);
+
+ if ($object) {
+ return [$this->directives, $subject];
+ }
+
+ return [null, $subject];
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Management/FileManager.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/FileManager.php
new file mode 100644
index 00000000..fcfef6af
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/FileManager.php
@@ -0,0 +1,55 @@
+filesystem;
+ }
+
+ /**
+ * Get the path to the compiled version of a view.
+ */
+ public function generateFilePath(string $path, bool $includeResourceDir = true): string
+ {
+ $resource = $this->getResourcePath();
+ $path = sha1($path) . '.phtml';
+
+ return $includeResourceDir ? $resource . DIRECTORY_SEPARATOR . $path : $path;
+ }
+
+ protected function getResourcePath(): string
+ {
+ return (
+ $this->directoryList->getPath(\Magento\Framework\App\Filesystem\DirectoryList::GENERATED)
+ . DIRECTORY_SEPARATOR
+ . 'code'
+ . DIRECTORY_SEPARATOR
+ . 'Magewirephp'
+ . DIRECTORY_SEPARATOR
+ . 'Magewire'
+ . DIRECTORY_SEPARATOR
+ . 'views'
+ );
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Management/SlotsManager.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/SlotsManager.php
new file mode 100644
index 00000000..b0e7b85c
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/SlotsManager.php
@@ -0,0 +1,39 @@
+getNameInLayout() . '_' . $name;
+
+ // if (isset($this->items[$id])) {
+ // throw new AlreadyExistsException();
+ // }
+
+ $this->items[$id] = $content;
+
+ return $id;
+ }
+
+ public function render(string $name, AbstractBlock $block): string
+ {
+ $id = $block->getNameInLayout() . '_' . $name;
+
+ return $this->items[$id] ?? 'NONO';
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/Management/StubsManager.php b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/StubsManager.php
new file mode 100644
index 00000000..f19282ff
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/Management/StubsManager.php
@@ -0,0 +1,29 @@
+collector->collect();
+
+ return $stubs[$namespace] ?? null;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/ScopeDirective.php b/lib/Magewire/Features/SupportMagewireCompiling/View/ScopeDirective.php
new file mode 100644
index 00000000..8a3a4c1e
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/ScopeDirective.php
@@ -0,0 +1,75 @@
+scopeResponsibilities[$directive] ?? null )) {
+ $reflection = new ReflectionClass($this);
+
+ foreach ($reflection->getMethods() as $method) {
+ $attributes = $method->getAttributes(ScopeDirectiveChain::class);
+ $attribute = $attributes[0] ?? null ? $attributes[0]->newInstance() : null;
+
+ if ($attribute) {
+ $this->scopeResponsibilities[$method->getName()] = $attribute->methods;
+ }
+ }
+ }
+
+ return $this->scopeResponsibilities[$directive] ?? [];
+ }
+
+ /**
+ * Start a new scoped block and return the generated variable name.
+ */
+ protected function variableScopeStart(string|null $var = null): string
+ {
+ $var ??= Random::alphabetical(10);
+
+ $this->scopeVariables[] = $var;
+
+ return $var;
+ }
+
+ /**
+ * End the most recent scope and return its variable name.
+ */
+ protected function variableScopeEnd(): string
+ {
+ if (empty($this->scopeVariables)) {
+ throw new LogicException('Trying to end a scope without a matching start.');
+ }
+
+ return array_pop($this->scopeVariables);
+ }
+
+ /**
+ * Get the current (most recent) scoped variable name without ending it.
+ */
+ protected function variableScope(): string|null
+ {
+ return $this->scopeVariables[count($this->scopeVariables) - 1] ?? null;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/ScopeDirectiveChain.php b/lib/Magewire/Features/SupportMagewireCompiling/View/ScopeDirectiveChain.php
new file mode 100644
index 00000000..1e527066
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/ScopeDirectiveChain.php
@@ -0,0 +1,24 @@
+fragmentFactory;
+ }
+
+ public function elements(): FragmentElementFactory
+ {
+ return $this->fragments()->elements();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireCompiling/View/ViewStub.php b/lib/Magewire/Features/SupportMagewireCompiling/View/ViewStub.php
new file mode 100644
index 00000000..d27a7523
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireCompiling/View/ViewStub.php
@@ -0,0 +1,34 @@
+stub->getName(), '.stub'));
+ }
+
+ public function getContent(): string
+ {
+ return $this->content ??= file_get_contents($this->stub->getFilename());
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireExceptionHandling/SupportMagewireExceptionHandling.php b/lib/Magewire/Features/SupportMagewireExceptionHandling/SupportMagewireExceptionHandling.php
new file mode 100644
index 00000000..0b7a138b
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireExceptionHandling/SupportMagewireExceptionHandling.php
@@ -0,0 +1,28 @@
+type, $arguments);
+ }
+
+ /**
+ * Create and returns a new instance of a named Flake block bound with
+ * a Magewire component and its resolver.
+ */
+ public function createByName(string $name, array $data = []): AbstractBlock|false
+ {
+ $layout = $this->layoutManager->decorator()->decorateForPagelessBlockFetching($this->layoutManager->factory()->create());
+
+ $layout->getUpdate()->addHandle('magewire_flakes');
+ $block = $layout->getBlock($name);
+
+ if ($block instanceof AbstractBlock) {
+ $data['magewire'] ??= $this->create();
+ $data['magewire:resolver'] ??= 'flake';
+ $data['magewire:alias'] ??= $name;
+
+ $block->addData($data);
+ }
+
+ return $block;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireFlakes/Mechanisms/ResolveComponent/ComponentResolver/FlakeResolver.php b/lib/Magewire/Features/SupportMagewireFlakes/Mechanisms/ResolveComponent/ComponentResolver/FlakeResolver.php
new file mode 100644
index 00000000..47a7b546
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireFlakes/Mechanisms/ResolveComponent/ComponentResolver/FlakeResolver.php
@@ -0,0 +1,70 @@
+conditions, $this->layoutBlockArgumentsFactory, $this->layoutManager);
+ }
+
+ public function complies(mixed $block, mixed $magewire = null): bool
+ {
+ $this->conditions()->if(static fn () => $block instanceof AbstractBlock);
+ /** @var AbstractBlock $block */
+ $this->conditions()->if(static fn () => in_array(static::FLAKES_HANDLE, $block->getLayout()->getUpdate()->getHandles()));
+
+ return $this->conditions()->evaluate($block, $magewire);
+ }
+
+ public function construct(AbstractBlock $block): AbstractBlock
+ {
+ /*
+ * For the situations where a Flake exists, but is by layout not bound with a Magewire arguments,
+ * missing the actual object. Flakes do not always have to be a Magewire component, but when not,
+ * it will become an empty Magewire Flake component to at least give it all it's powers.
+ */
+ if ($block->hasData('magewire:alias') && ! $block->hasData('magewire')) {
+ $block->setData('magewire', $this->flakeFactory->create());
+ }
+
+ return parent::construct($block);
+ }
+
+ protected function canMemorizeLayoutHandles(): bool
+ {
+ return false;
+ }
+
+ protected function recoverLayoutHandles(Snapshot $snapshot): array
+ {
+ return ['magewire_flakes'];
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireFlakes/SupportMagewireFlakes.php b/lib/Magewire/Features/SupportMagewireFlakes/SupportMagewireFlakes.php
new file mode 100644
index 00000000..88854ca9
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireFlakes/SupportMagewireFlakes.php
@@ -0,0 +1,43 @@
+magewireBlock();
+
+ if (is_array($memo['flake'] ?? null)) {
+ $block->setData('magewire:flake', $memo['flake']);
+ }
+ });
+
+ on('dehydrate', static function (Component $component, ComponentContext $context) {
+ $metadata = $component->magewireBlock()->getData('magewire:flake');
+
+ if (is_array($metadata) && is_array($metadata['element'] ?? null)) {
+ $context->pushMemo('flake', $metadata['element'], 'element');
+ }
+ });
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireFlakes/View/Action/Magewire/FlakeViewAction.php b/lib/Magewire/Features/SupportMagewireFlakes/View/Action/Magewire/FlakeViewAction.php
new file mode 100644
index 00000000..80235cc8
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireFlakes/View/Action/Magewire/FlakeViewAction.php
@@ -0,0 +1,61 @@
+attributesFactory->create()->fill($data);
+
+ $data->each(static function (DataArray $array, $value, $key) use ($variables) {
+ if (str_starts_with($value, '$')) {
+ $value = trim($value, '$');
+
+ if (array_key_exists($value, $variables)) {
+ $array->put($key, $variables[$value]);
+ }
+ }
+ });
+
+ $data = $data->all();
+ $block = $this->flakeFactory->createByName($flake, $data);
+
+ if (! $block) {
+ throw new RuntimeException('Flake block can not be found.'); // @todo
+ }
+
+ $block->setData('magewire:alias', $flake);
+
+ $block->setNameInLayout($data['magewire:name']);
+ return $block;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireFlakes/View/Compiler/FlakeCompilerBu.php b/lib/Magewire/Features/SupportMagewireFlakes/View/Compiler/FlakeCompilerBu.php
new file mode 100644
index 00000000..84a45d66
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireFlakes/View/Compiler/FlakeCompilerBu.php
@@ -0,0 +1,195 @@
+parseTags($value, 0, 50)[0];
+ }
+
+ protected function parseTags(string $value, int $iterations, int $maxIterations): array
+ {
+ if ($iterations >= $maxIterations) {
+ return [$value, false];
+ }
+
+ $result = '';
+ $pos = 0;
+ $length = strlen($value);
+
+ while ($pos < $length) {
+ // Try to match self-closing tag: .
+ if (preg_match('/\/]|\/(?!>))*)\s*\/\s*>/', $value, $match, 0, $pos)) {
+ $tagStart = strpos($value, $match[0], $pos);
+
+ // Check if there's an opening tag before this position.
+ $nextOpeningMatch = preg_match('/\/]|\/(?!>))*)\s*>/', $value, $openingMatch, 0, $pos);
+ $nextOpeningPos = $nextOpeningMatch ? strpos($value, $openingMatch[0], $pos) : PHP_INT_MAX;
+
+ // If self-closing tag comes first, process it.
+ if ($tagStart <= $nextOpeningPos) {
+ $component = $match[1];
+ $attributes = $match[2];
+
+ // Append content before the tag
+ $result .= substr($value, $pos, $tagStart - $pos);
+
+ // Compile the self-closing tag.
+ $compiled = $this->compileFlake($component, $attributes, false);
+
+ $result .= $compiled;
+ $pos = $tagStart + strlen($match[0]);
+ continue;
+ }
+ }
+
+ // Try to match opening tag: .
+ if (preg_match('/\/]|\/(?!>))*)\s*>/', $value, $match, 0, $pos)) {
+ $tagStart = strpos($value, $match[0], $pos);
+ $component = $match[1];
+ $attributes = $match[2];
+ $openingTagEnd = $tagStart + strlen($match[0]);
+
+ // Find the corresponding closing tag.
+ $closingTagPattern = '/<\/flake:' . preg_quote($component) . '\s*>/';
+
+ if (preg_match($closingTagPattern, $value, $closingMatch, 0, $openingTagEnd)) {
+ $closingTagStart = strpos($value, $closingMatch[0], $openingTagEnd);
+ $content = substr($value, $openingTagEnd, $closingTagStart - $openingTagEnd);
+ $closingTagEnd = $closingTagStart + strlen($closingMatch[0]);
+
+ // Append content before the tag.
+ $result .= substr($value, $pos, $tagStart - $pos);
+ // Recursively parse nested magewire tags in content.
+ [$content, $changes] = $this->parseTags($content, $iterations + 1, $maxIterations);
+
+ $compiled = $this->compileFlake($component, $attributes, empty($content) ? false : $content);
+
+ $result .= $compiled;
+ $pos = $closingTagEnd;
+ continue;
+ }
+ }
+
+ // No more magewire tags found, append the rest
+ $result .= substr($value, $pos);
+ break;
+ }
+
+ return [$result, $result !== $value];
+ }
+
+ protected function compileFlake(string $component, string $attributes, string|false $content): string
+ {
+ $attributes = trim($attributes);
+
+ $parse = $this->domElementParser
+ ->newInstance()
+ ->parse($attributes)
+ ->attributes()
+ // Set a random unique component id when none is provided.
+ ->default('magewire:id', Random::string(10))
+ // Map the mw:id as the mw:name when it doesn't exist.
+ ->default('magewire:name', ':magewire:id')
+ // Use the component name as the name alias.
+ ->default('magewire:alias', $component)
+ // 1. Take care of all data groups (e.g., mount, prop)
+ ->each(function (DataArray $array, $value, $key) {
+ if ($to = $this->renameToMagewireAttributeMeta($key)) {
+ $array->rename($key, $to);
+ }
+ })
+ ->each(static function (DataArray $array, $value, $key) {
+ $subject = 'data';
+
+ if ($key === $subject) {
+ $map = [];
+ foreach ($array->get($subject) as $name => $value) {
+ $map[$subject][substr($name, strlen(':'))] = $value;
+ }
+
+ // Try and replace the subject value when already exists.
+ $array->put($subject, $map[$subject] ?? []);
+ }
+ });
+
+ // Fetch and transform DOM attributes.
+ $attributes = $parse->fetch(static fn ($value, $key) => str_starts_with($key, 'attr:'));
+
+ $arguments = [
+ 'flake' => $component,
+ // Accept everything as data, except those who start with attr:.
+ 'data' => $parse->fetch(static function ($value, $key) {
+ return ! str_starts_with($key, 'attr:');
+ }),
+ 'metadata' => [
+ 'attributes' => array_combine(array_map(static fn ($key) => substr($key, 5), array_keys($attributes)), $attributes)
+ ]
+ ];
+
+ if ($content) {
+ return '@flake(arguments: ' . json_encode($arguments) . ')' . $content . '@endFlake';
+ }
+
+ return '@flakeSingleLiner(arguments: ' . json_encode($arguments) . ')';
+ }
+
+ private function renameToMagewireAttributeMeta(string $attribute): string|null
+ {
+ $parts = explode(':', $attribute);
+
+ // @todo Hardcoded for the time being until more specifics gets added.
+ $map = [
+ 'name' => [
+ 'prefix' => 'magewire:',
+ 'expects' => 1
+ ],
+ 'prop' => [
+ 'prefix' => 'magewire.',
+ 'expects' => 2
+ ],
+ 'mount' => [
+ 'prefix' => 'magewire:mount:',
+ 'expects' => 2
+ ]
+ ];
+
+ // Magewire group indicator.
+ $group = $parts[0];
+ // The number of parts it expects per attribute.
+ $expects = $map[$group]['expects'] ?? null;
+
+ if (is_int($expects) && is_array($parts) && count($parts) === $expects) {
+ // What the new value should be prefixed with when found.
+ $prefix = $map[$group]['prefix'];
+
+ if (in_array($group, array_keys($map), true)) {
+ return $prefix . $parts[$expects - 1];
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireFlakes/View/Fragment/Element/Flake.php b/lib/Magewire/Features/SupportMagewireFlakes/View/Fragment/Element/Flake.php
new file mode 100644
index 00000000..10e874ae
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireFlakes/View/Fragment/Element/Flake.php
@@ -0,0 +1,73 @@
+slotsRegistry->update('default', $this->output);
+
+ try {
+ $flake = $this->flakeFactory->createByName($this->variant, [
+ 'magewire:id' => Random::alphabetical(4),
+ 'magewire:name' => Random::alphabetical(4)
+ ]);
+
+ if ($flake === false) {
+ throw new ComponentNotFoundException(sprintf('Magewire: Flake "%s" could not be found or doesnt exist.', $this->variant));
+ }
+
+ // Render the final Flake component.
+ echo $flake->toHtml();
+ } catch (Throwable $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+
+ if ($this->applicationState->getMode() !== ApplicationState::MODE_PRODUCTION) {
+ echo '';
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireLoaders/HandlesMagewireLoaders.php b/lib/Magewire/Features/SupportMagewireLoaders/HandlesMagewireLoaders.php
new file mode 100644
index 00000000..594bea8f
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireLoaders/HandlesMagewireLoaders.php
@@ -0,0 +1,26 @@
+loader;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireLoaders/SupportMagewireLoaders.php b/lib/Magewire/Features/SupportMagewireLoaders/SupportMagewireLoaders.php
new file mode 100644
index 00000000..fa4074f1
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireLoaders/SupportMagewireLoaders.php
@@ -0,0 +1,44 @@
+component->getLoader();
+
+ if ($loader) {
+ if (is_array($loader)) {
+ $loader = map_with_keys(static function ($value, $key) {
+ if (is_string($value)) {
+ $value = [$value];
+ }
+ if (is_array($value)) {
+ $value = array_map('__', array_filter($value, 'is_string'));
+ }
+
+ return [$key => $value];
+ }, $loader);
+ } elseif (is_string($loader)) {
+ $loader = __($loader);
+ }
+
+ $context->pushEffect('loader', $loader);
+ }
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireNestingComponents/SupportMagewireNestingComponents.php b/lib/Magewire/Features/SupportMagewireNestingComponents/SupportMagewireNestingComponents.php
new file mode 100644
index 00000000..3426f691
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireNestingComponents/SupportMagewireNestingComponents.php
@@ -0,0 +1,74 @@
+dictionary();
+
+ if (isset($dictionary['magewire'])) {
+ return;
+ }
+
+ $closest = $this->renderLifecycleManager->target('magewire')->closestComponent($dto->block());
+
+ if ($closest) {
+ $dto->dictionary(['magewire' => $closest]);
+ }
+
+ return static function ($html) {
+ return $html;
+ };
+ });
+
+ on('magewire:component:construct', function () {
+ // Returns a callable that will execute after the component is constructed.
+ return function (AbstractBlock $block) {
+ $component = $block->getData('magewire');
+
+ if ($component instanceof Component) {
+ $this->renderLifecycleManager->target('magewire')->bind($component);
+ }
+
+ return $block;
+ };
+ });
+
+ on('magewire:component:reconstruct', function () {
+ // Returns a callable that will execute after the component is reconstructed.
+ return function (AbstractBlock $block) {
+ $component = $block->getData('magewire');
+
+ if ($component instanceof Component) {
+ $this->renderLifecycleManager->target('magewire')->bind($component);
+ }
+
+ return $block;
+ };
+ });
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireNotifications/HandlesMagewireNotifications.php b/lib/Magewire/Features/SupportMagewireNotifications/HandlesMagewireNotifications.php
new file mode 100644
index 00000000..ddbc74e6
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireNotifications/HandlesMagewireNotifications.php
@@ -0,0 +1,24 @@
+magewireNotifications ??= Factory::create(MagewireNotifications::class);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireNotifications/MagewireNotifications.php b/lib/Magewire/Features/SupportMagewireNotifications/MagewireNotifications.php
new file mode 100644
index 00000000..e84b3266
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireNotifications/MagewireNotifications.php
@@ -0,0 +1,48 @@
+notifications[$name ?? Random::string()] ??= Factory::create(Notification::class, [
+ 'notification' => $notification
+ ]);
+ }
+
+ public function unset(string $name): static
+ {
+ if (isset($this->notifications[$name])) {
+ unset($this->notifications[$name]);
+ }
+
+ return $this;
+ }
+
+ public function fetch(): array
+ {
+ return $this->notifications;
+ }
+
+ public function count(): int
+ {
+ return count($this->notifications);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireNotifications/Notification.php b/lib/Magewire/Features/SupportMagewireNotifications/Notification.php
new file mode 100644
index 00000000..c04a1212
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireNotifications/Notification.php
@@ -0,0 +1,99 @@
+withMessage($notification);
+ }
+
+ public function asSuccess(): static
+ {
+ return $this->as(NotificationType::Success);
+ }
+
+ public function asError(): static
+ {
+ return $this->as(NotificationType::Error);
+ }
+
+ public function asWarning(): static
+ {
+ return $this->as(NotificationType::Warning);
+ }
+
+ public function asNotice(): static
+ {
+ return $this->as(NotificationType::Notice);
+ }
+
+ public function as(NotificationType $type): static
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function withMessage(string|Phrase $message): static
+ {
+ $this->message = is_string($message) ? __($message) : $message;
+ return $this;
+ }
+
+ public function withTitle(string|Phrase $title): static
+ {
+ $this->title = is_string($title) ? __($title) : $title;
+ return $this;
+ }
+
+ public function withoutTitle(): static
+ {
+ $this->title = null;
+ return $this;
+ }
+
+ public function withDuration(int $milliseconds): static
+ {
+ $this->duration = $milliseconds;
+ return $this;
+ }
+
+ public function notification(): Phrase
+ {
+ return $this->message;
+ }
+
+ public function type(): NotificationType
+ {
+ return $this->type;
+ }
+
+ public function title(): Phrase|null
+ {
+ return $this->title;
+ }
+
+ public function duration(): int
+ {
+ return $this->duration;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireNotifications/NotificationType.php b/lib/Magewire/Features/SupportMagewireNotifications/NotificationType.php
new file mode 100644
index 00000000..5f292c82
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireNotifications/NotificationType.php
@@ -0,0 +1,20 @@
+component();
+
+ if ($component && $component->magewireNotifications()->count() > 0) {
+ $context->pushEffect('dispatches', [
+ 'name' => 'magewire:notifications',
+ 'params' => $this->mapNotifications($component->magewireNotifications())
+ ]);
+ }
+ }
+
+ private function mapNotifications(MagewireNotifications $notifications): array
+ {
+ return array_map(static function (Notification $message) {
+ return [
+ 'text' => $message->notification()->render(),
+ 'type' => $message->type()
+ ];
+ }, $notifications->fetch());
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/Config/Source/RateLimitingVariant.php b/lib/Magewire/Features/SupportMagewireRateLimiting/Config/Source/RateLimitingVariant.php
new file mode 100644
index 00000000..619286e6
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/Config/Source/RateLimitingVariant.php
@@ -0,0 +1,30 @@
+ self::COMPONENTS_ONLY, 'label' => 'Components only'],
+ ['value' => self::REQUESTS_ONLY, 'label' => 'Requests only'],
+ ['value' => self::NONE, 'label' => 'None']
+ ];
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/Config/Source/RequestsScope.php b/lib/Magewire/Features/SupportMagewireRateLimiting/Config/Source/RequestsScope.php
new file mode 100644
index 00000000..7cdd6721
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/Config/Source/RequestsScope.php
@@ -0,0 +1,28 @@
+ self::ISOLATED, 'label' => 'Isolated'],
+ ['value' => self::SHARED, 'label' => 'Shared']
+ ];
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/Exceptions/TooManyRequestsException.php b/lib/Magewire/Features/SupportMagewireRateLimiting/Exceptions/TooManyRequestsException.php
new file mode 100644
index 00000000..da6c1fe0
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/Exceptions/TooManyRequestsException.php
@@ -0,0 +1,18 @@
+dateTime->gmtTimestamp();
+
+ $hits = $this->getCleanHits($key, $decaySeconds, $currentTime);
+ $hits[] = $currentTime;
+
+ $this->storage->set($key, $hits, $decaySeconds);
+
+ return count($hits);
+ }
+
+ /**
+ * Validate if key is within the rate limit.
+ */
+ public function validate(string $key, int $maxAttempts, int $decaySeconds = 60): bool
+ {
+ $hits = $this->getCleanHits($key, $decaySeconds, $this->dateTime->gmtTimestamp());
+
+ return count($hits) < $maxAttempts;
+ }
+
+ /**
+ * Reset rate limit for given key
+ */
+ public function reset(string $key): bool
+ {
+ return $this->storage->unset($key);
+ }
+
+ /**
+ * Get current attempts for a key.
+ */
+ public function getAttempts(string $key, int $decaySeconds = 60): int
+ {
+ return count($this->getCleanHits($key, $decaySeconds, $this->dateTime->gmtTimestamp()));
+ }
+
+ /**
+ * Get remaining attempts for a key.
+ */
+ public function getRemainingAttempts(string $key, int $maxAttempts, int $decaySeconds = 60): int
+ {
+ $current = $this->getAttempts($key, $decaySeconds);
+
+ return max(0, $maxAttempts - $current);
+ }
+
+ /**
+ * Get clean hits (remove expired ones).
+ */
+ private function getCleanHits(string $key, int $decaySeconds, int $currentTime): array
+ {
+ return array_filter($this->storage->get($key), static fn ($timestamp) => ( $currentTime - $timestamp ) < $decaySeconds);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterCache.php b/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterCache.php
new file mode 100644
index 00000000..034cba8d
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterCache.php
@@ -0,0 +1,19 @@
+getRateLimitingVariant() === RateLimitingVariant::REQUESTS_ONLY;
+ }
+
+ public function canRateLimitComponents(): bool
+ {
+ return $this->getRateLimitingVariant() === RateLimitingVariant::COMPONENTS_ONLY;
+ }
+
+ public function isSharedScope(): bool
+ {
+ return $this->getRateLimitingScope() === RequestsScope::SHARED;
+ }
+
+ public function isIsolatedScope(): bool
+ {
+ return $this->getRateLimitingScope() === RequestsScope::ISOLATED;
+ }
+
+ public function getRequestsMaxAttempts(): int
+ {
+ return (int) ($this->config()->getFeaturesGroupValue('rate_limiting/requests/max_attempts') ?? 4);
+ }
+
+ public function getRequestsDecaySeconds(): int
+ {
+ return (int) ($this->config()->getFeaturesGroupValue('rate_limiting/requests/decay_seconds') ?? 5);
+ }
+
+ protected function getRateLimitingVariant(): string
+ {
+ return $this->config()->getFeaturesGroupValue('rate_limiting/variant') ?? RateLimitingVariant::NONE;
+ }
+
+ protected function getRateLimitingScope(): string
+ {
+ return $this->config()->getFeaturesGroupValue('rate_limiting/requests/scope') ?? RequestsScope::SHARED;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterExceptionHandler.php b/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterExceptionHandler.php
new file mode 100644
index 00000000..5463a8f9
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterExceptionHandler.php
@@ -0,0 +1,34 @@
+setBody('Too Many Requests.');
+ $response->setHttpResponseCode(429);
+
+ return $response;
+ };
+ }
+
+ return $exception;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterStorageInterface.php b/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterStorageInterface.php
new file mode 100644
index 00000000..d97eb137
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/RateLimiterStorageInterface.php
@@ -0,0 +1,30 @@
+cache->fetch();
+
+ return $storage[$key] ?? [];
+ }
+
+ public function set(string $key, array $data, int|null $ttl = null): bool
+ {
+ $storage = $this->cache->fetch();
+ $storage[$key] = $data;
+
+ return $this->cache->save($storage, $ttl);
+ }
+
+ public function unset(string $key): bool
+ {
+ $storage = $this->cache->fetch();
+ unset($storage[$key]);
+
+ return $this->cache->save($storage);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/SupportMagewireRateLimiting.php b/lib/Magewire/Features/SupportMagewireRateLimiting/SupportMagewireRateLimiting.php
new file mode 100644
index 00000000..c42097b9
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/SupportMagewireRateLimiting.php
@@ -0,0 +1,53 @@
+rateLimiterConfig->canRateLimitRequests()) {
+ on('request', function (array $payload) {
+ $context = $payload[0] ?? false;
+
+ // Global scope rate limiting validation.
+ if ($context && ! $this->rateLimiter->validateWithComponentRequestContext($context)) {
+ throw new TooManyRequestsException();
+ }
+ });
+ } elseif ($this->rateLimiterConfig->canRateLimitComponents()) {
+ on('magewire:component:reconstruct', function () {
+ return function (Template $block) {
+ $component = $block->getData('magewire');
+
+ // Component scope rate limiting validation.
+ if ($component instanceof Component && ! $this->rateLimiter->validateWithComponent($component)) {
+ throw new TooManyRequestsException();
+ }
+ };
+ });
+ }
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireRateLimiting/UpdateRequestRateLimiter.php b/lib/Magewire/Features/SupportMagewireRateLimiting/UpdateRequestRateLimiter.php
new file mode 100644
index 00000000..d4974491
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireRateLimiting/UpdateRequestRateLimiter.php
@@ -0,0 +1,67 @@
+storage, $this->datetime);
+ }
+
+ public function validateWithComponentRequestContext(ComponentRequestContext $componentRequestContext): bool
+ {
+ $key = $this->generateKeyByRequestContext($componentRequestContext);
+ $attempts = $this->rateLimiterConfig->getRequestsMaxAttempts();
+ $decay = $this->rateLimiterConfig->getRequestsDecaySeconds();
+
+ if ($result = $this->validate($key, $attempts, $decay)) {
+ $this->hit($key);
+ }
+
+ return $result;
+ }
+
+ public function validateWithComponent(Component $component): bool
+ {
+ $key = $this->generateKeyByComponent($component);
+
+ if ($result = $this->validate($key, 4, 5)) {
+ $this->hit($key);
+ }
+
+ return $result;
+ }
+
+ private function generateKeyByRequestContext(ComponentRequestContext $componentRequestContext): string
+ {
+ $key = '123@RL';
+
+ if ($this->rateLimiterConfig->isIsolatedScope()) {
+ $key .= $componentRequestContext->getSnapshot()->getMemoValue('id');
+ }
+
+ return $key;
+ }
+
+ private function generateKeyByComponent(Component $component): string
+ {
+ return '123@RLC' . $component->id();
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireViewModel/HandlesMagewireViewModel.php b/lib/Magewire/Features/SupportMagewireViewModel/HandlesMagewireViewModel.php
new file mode 100644
index 00000000..2dd4dc5f
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireViewModel/HandlesMagewireViewModel.php
@@ -0,0 +1,34 @@
+magewireViewModel ??= Factory::get(MagewireViewModel::class);
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireViewModel/MagewireViewModel.php b/lib/Magewire/Features/SupportMagewireViewModel/MagewireViewModel.php
new file mode 100644
index 00000000..83434185
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireViewModel/MagewireViewModel.php
@@ -0,0 +1,27 @@
+utils->$name($arguments) : $this->utils;
+ }
+}
diff --git a/lib/Magewire/Features/SupportMagewireViewModel/MagewireViewModelInterface.php b/lib/Magewire/Features/SupportMagewireViewModel/MagewireViewModelInterface.php
new file mode 100644
index 00000000..5c8a4431
--- /dev/null
+++ b/lib/Magewire/Features/SupportMagewireViewModel/MagewireViewModelInterface.php
@@ -0,0 +1,19 @@
+isRootMagewireBlock($block)) {
+ $this->includeMagewireViewModel = true;
+ }
+
+ /*
+ * Ensures the Magewire view model is automatically bound to the block as a "view_model" data argument,
+ * if it hasn't been set or is not an instance of the expected Magewire view model class.
+ *
+ * This reduces the need for manual binding, which is often repetitive since many sibling blocks depend on it.
+ *
+ * Why bind a view model instead of exposing a global template variable like $magewireViewModel?
+ * Because when a block is moved outside its parent "magewire" wrapper, it still needs to maintain compatibility.
+ * Using a shared view model ensures this compatibility without requiring changes to the template.
+ *
+ * Relying on global dictionary variables would force template modifications in such cases—something this method avoids.
+ */
+ if ($this->includeMagewireViewModel) {
+ $model = $block->getData('view_model');
+
+ if ($model && ! $model instanceof MagewireViewModelInterface) {
+ throw new InvalidArgumentException('View model must be an instance of MagewireViewModelInterface');
+ } elseif ($model === null) {
+ $block->setData('view_model', $this->magewireViewModelFactory->create());
+ }
+ }
+ });
+ }
+
+ private function isRootMagewireBlock(AbstractBlock $block): bool
+ {
+ return $block->getNameInLayout() === 'magewire';
+ }
+}
diff --git a/lib/Magewire/Mechanisms.php b/lib/Magewire/Mechanisms.php
new file mode 100644
index 00000000..ccf5d14c
--- /dev/null
+++ b/lib/Magewire/Mechanisms.php
@@ -0,0 +1,31 @@
+boot();
+ }
+ };
+ }
+
+ protected function getBootModeFallback(): ServiceTypeItemBootMode
+ {
+ return ServiceTypeItemBootMode::LAZY;
+ }
+}
diff --git a/lib/Magewire/Mechanisms/FrontendAssets/FrontendAssetsFacade.php b/lib/Magewire/Mechanisms/FrontendAssets/FrontendAssetsFacade.php
new file mode 100644
index 00000000..b2832aec
--- /dev/null
+++ b/lib/Magewire/Mechanisms/FrontendAssets/FrontendAssetsFacade.php
@@ -0,0 +1,32 @@
+mechanism->returnJavaScriptAsFile();
+ }
+
+ public function getMagewireScriptAttributes(): array
+ {
+ return $this->mechanism->getDataByPath('script.html_attributes', []);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/FrontendAssets/FrontendAssetsViewModel.php b/lib/Magewire/Mechanisms/FrontendAssets/FrontendAssetsViewModel.php
new file mode 100644
index 00000000..f0ac62e3
--- /dev/null
+++ b/lib/Magewire/Mechanisms/FrontendAssets/FrontendAssetsViewModel.php
@@ -0,0 +1,43 @@
+frontendAssetsMechanism->returnJavaScriptAsFile();
+ }
+
+ public function getScriptAttributes(): string
+ {
+ $attributes = $this->frontendAssetsMechanism->getDataByPath('script.html_attributes', []);
+
+ return implode(' ', array_map(
+ function ($attribute, string|null $expression) {
+ return $expression === null ? $attribute : sprintf('%s="%s"', $attribute, $this->escaper->escapeHtmlAttr($expression));
+ },
+ array_keys($attributes),
+ array_values($attributes)
+ ));
+ }
+}
diff --git a/lib/Magewire/Mechanisms/HandleComponents/ComponentContext/Effects.php b/lib/Magewire/Mechanisms/HandleComponents/ComponentContext/Effects.php
new file mode 100644
index 00000000..0791969f
--- /dev/null
+++ b/lib/Magewire/Mechanisms/HandleComponents/ComponentContext/Effects.php
@@ -0,0 +1,39 @@
+setData($key, $value);
+ }
+
+ function exclude($key): static
+ {
+ return $this->unsetData($key);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/HandleComponents/ComponentContext/Memo.php b/lib/Magewire/Mechanisms/HandleComponents/ComponentContext/Memo.php
new file mode 100644
index 00000000..34e52376
--- /dev/null
+++ b/lib/Magewire/Mechanisms/HandleComponents/ComponentContext/Memo.php
@@ -0,0 +1,28 @@
+mechanism->update($snapshot->toArray(), $updates, $calls, $block);
+ }
+
+ /**
+ * @throws RuntimeException
+ */
+ public function mount(string $name, array $params, AbstractBlock $block, Component $component): Closure
+ {
+ return $this->mechanism->mount($name, $params, $block->getCacheKey(), $block, $component);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/HandleComponents/Snapshot.php b/lib/Magewire/Mechanisms/HandleComponents/Snapshot.php
new file mode 100644
index 00000000..f0617946
--- /dev/null
+++ b/lib/Magewire/Mechanisms/HandleComponents/Snapshot.php
@@ -0,0 +1,159 @@
+data = $data;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ function getData(): array
+ {
+ return $this->data;
+ }
+
+ /**
+ * @param array $memo
+ * @return $this
+ */
+ function setMemo(array $memo): self
+ {
+ $this->memo = $memo;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ function getMemo(): array
+ {
+ return $this->memo;
+ }
+
+ /**
+ * @throws FileSystemException
+ * @throws RuntimeException
+ */
+ function generateChecksum(): self
+ {
+ $this->checksum = $this->checksumHandler->generate($this->toArray(['checksum']));
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ function getChecksum(): string
+ {
+ return $this->checksum;
+ }
+
+ /**
+ * @param string $property
+ * @param $value
+ * @return $this
+ */
+ function setDataValue(string $property, $value): self
+ {
+ $this->data[$property] = $value;
+ return $this;
+ }
+
+ /**
+ * @param string $property
+ * @return mixed|null
+ */
+ function getDataValue(string $property): mixed
+ {
+ return $this->data[$property] ?? null;
+ }
+
+ /**
+ * @param string $key
+ * @param mixed $value
+ * @return $this
+ */
+ function setMemoValue(string $key, mixed $value): Snapshot
+ {
+ $this->memo[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $key
+ * @param mixed|null $default
+ * @return mixed|null
+ */
+ function getMemoValue(string $key, mixed $default = null): mixed
+ {
+ return $this->memo[$key] ?? $default;
+ }
+
+ /**
+ * @param array $exclude
+ * @return array
+ */
+ function toArray(array $exclude = []): array
+ {
+ return array_diff_key([
+ 'data' => $this->data,
+ 'memo' => $this->memo,
+ 'checksum' => $this->checksum
+ ], array_flip($exclude));
+ }
+
+ /**
+ * @param array $exclude
+ * @return string
+ */
+ function toString(array $exclude = []): string
+ {
+ return $this->serializer->serialize($this->toArray($exclude));
+ }
+}
diff --git a/lib/Magewire/Mechanisms/HandleComponents/Synthesizers/DataObjectSynth.php b/lib/Magewire/Mechanisms/HandleComponents/Synthesizers/DataObjectSynth.php
new file mode 100644
index 00000000..0a543806
--- /dev/null
+++ b/lib/Magewire/Mechanisms/HandleComponents/Synthesizers/DataObjectSynth.php
@@ -0,0 +1,60 @@
+ $child) {
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [$data, []];
+ }
+
+ public function hydrate($value, $meta, $hydrateChild)
+ {
+ $obj = $this->dataObjectFactory->create();
+
+ foreach ($value as $key => $child) {
+ $obj->setData($key, $hydrateChild($key, $child));
+ }
+
+ return $obj;
+ }
+
+ public function set(DataObject $target, $key, $value)
+ {
+ return $target->setData($key, $value);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/HandleRequests/ComponentContext/EffectsInterface.php b/lib/Magewire/Mechanisms/HandleRequests/ComponentContext/EffectsInterface.php
new file mode 100644
index 00000000..28110452
--- /dev/null
+++ b/lib/Magewire/Mechanisms/HandleRequests/ComponentContext/EffectsInterface.php
@@ -0,0 +1,37 @@
+calls = $calls;
+ return $this;
+ }
+
+ function getCalls(): array
+ {
+ return $this->calls;
+ }
+
+ function setSnapshot(Snapshot $snapshot): self
+ {
+ $this->snapshot = $snapshot;
+ return $this;
+ }
+
+ function getSnapshot(): Snapshot
+ {
+ return $this->snapshot;
+ }
+
+ public function setUpdates(array $updates = []): self
+ {
+ $this->updates = $updates;
+ return $this;
+ }
+
+ public function getUpdates(): array
+ {
+ return $this->updates;
+ }
+}
diff --git a/lib/Magewire/Mechanisms/HandleRequests/HandleRequestFacade.php b/lib/Magewire/Mechanisms/HandleRequests/HandleRequestFacade.php
new file mode 100644
index 00000000..c4e25596
--- /dev/null
+++ b/lib/Magewire/Mechanisms/HandleRequests/HandleRequestFacade.php
@@ -0,0 +1,43 @@
+mechanism;
+ }
+
+ /**
+ * @throws NoSuchEntityException
+ * @throws ComponentNotFoundException
+ */
+ public function update()
+ {
+ return $this->mechanism->handleUpdate();
+ }
+
+ public function getUpdateUri(): string
+ {
+ return $this->mechanism->getUpdateUri();
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/ComponentArguments/BlockMagewireArguments.php b/lib/Magewire/Mechanisms/ResolveComponents/ComponentArguments/BlockMagewireArguments.php
new file mode 100644
index 00000000..22237485
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/ComponentArguments/BlockMagewireArguments.php
@@ -0,0 +1,16 @@
+assembled) {
+ return $this;
+ }
+
+ // Assemble public arguments.
+ $this->addData($this->assemblePublicArguments($block));
+ // Assemble private group arguments.
+ $this->groups = $this->assembleGroupArguments($block);
+
+ $this->assembled = true;
+
+ return $this;
+ }
+
+ public function reassemble(AbstractBlock $block): static
+ {
+ return $this->assemble($block, true);
+ }
+
+ public function __call($method, $args)
+ {
+ switch (substr((string) $method, 0, 3)) {
+ case 'get':
+ $key = $this->_underscore(substr($method, 3));
+ $index = $args[0] ?? null;
+
+ return $this->getData($key, $index);
+ case 'set':
+ $key = $this->_underscore(substr($method, 3));
+ $value = $args[0] ?? null;
+
+ return $this->setData($key, $value);
+ case 'has':
+ $key = $this->_underscore(substr($method, 3));
+
+ return isset($this->_data[$key]);
+ }
+
+ throw new LocalizedException(new Phrase('Invalid method %1::%2', [get_class($this), $method]));
+ }
+
+ /**
+ * Returns mount arguments.
+ */
+ public function forMount(): array
+ {
+ return $this->forGroup('mount');
+ }
+
+ /**
+ * Returns arguments for the given group.
+ */
+ public function forGroup(string $name, array $default = []): array
+ {
+ return $this->groups[$name] ?? $default;
+ }
+
+ /**
+ * Besides the "toArray" method, this method ensures that we have the flexibility to implement
+ * different logic for the parameters in the future, compared to the "toArray" method.
+ * For now, this function primarily serves as a precautionary measure.
+ */
+ public function toParams(): array
+ {
+ return $this->toArray();
+ }
+
+ public function isLazy(): bool
+ {
+ return (bool) $this->getData('lazy');
+ }
+
+ protected function assemblePublicArguments(AbstractBlock $block): array
+ {
+ $arguments = array_filter(
+ $block->getData(),
+ static function ($key) {
+ return str_starts_with($key, 'magewire.');
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+
+ // Remove the "magewire." prefix and convert a kebab-case to camelCase
+ return array_combine(array_map(static function ($key) {
+ return Str::camel(substr($key, 9));
+ }, array_keys($arguments)), array_values($arguments));
+ }
+
+ protected function assembleGroupArguments(AbstractBlock $block): array
+ {
+ $arguments = array_filter(
+ $block->getData(),
+ static function ($key) {
+ return str_starts_with($key, 'magewire:');
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+
+ foreach ($arguments as $key => $value) {
+ if (! preg_match('/^magewire:([^:]+):([^:]+)/', $key, $matches)) {
+ continue;
+ }
+
+ $groups[$matches[1]][Str::camel($matches[2])] = $value;
+ }
+
+ return $groups ?? [];
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolver/ComponentResolver.php b/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolver/ComponentResolver.php
new file mode 100644
index 00000000..75063675
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolver/ComponentResolver.php
@@ -0,0 +1,158 @@
+> $complyChecks */
+ protected array $complyChecks = [];
+
+ public function __construct(
+ private readonly Conditions $conditions
+ ) {
+ }
+
+ /**
+ * The complies method is intended to be a lightweight check that verifies whether the given block
+ * meets the requirements to be constructed as a Magewire component. This can involve various conditions,
+ * such as a specific data key being defined or a block belonging to a specific class.
+ *
+ * Aim to refine the requirements to their utmost specificity, minimizing the risk of conflicts
+ * with potential other component resolvers. The overarching goal is to ensure seamless
+ * integration and coexistence with various resolver modules.
+ */
+ public function complies(AbstractBlock $block, mixed $magewire = null): bool
+ {
+ if ($magewire) {
+ $this->conditions()->if(static fn () => $magewire instanceof Component, 'instanceof-component');
+ }
+
+ // Accept this as the resolver if the blocks data key is equal to the resolver accessor.
+ $this->conditions()->or(fn () => $block->getData(self::RESOLVER) === $this->accessor, 'block-resolver-data-key');
+
+ return $this->conditions()->evaluate($block, $magewire);
+ }
+
+ abstract public function arguments(): MagewireArguments;
+
+ /**
+ * After a block meets specific requirements, as verified by the compile method,
+ * the block can be constructed.
+ *
+ * Ultimately, the block _must_ meet two specific criteria:
+ * 1. An instance of \Magewirephp\Magewire\Component bound to the "magewire" block data key
+ * 2. The component has bound "name" and "id"
+ *
+ * @throws \Magewirephp\Magewire\Exceptions\ComponentNotFoundException
+ */
+ abstract public function construct(AbstractBlock $block): AbstractBlock;
+
+ /**
+ * After the given snapshot has been verified, an attempt can be made to reconstruct the block,
+ * including the component. At this point, only the snapshot data is available.
+ *
+ * Ultimately, the block _must_ meet two specific criteria:
+ * 1. An instance of \Magewirephp\Magewire\Component bound to the "magewire" block data key
+ * 2. The component has bound "name" and "id"
+ *
+ * It is recommended to have the reconstruction process invoke the original construct method.
+ * This best practice helps prevent code duplication and maintains consistency between
+ * preceding and subsequent components, fostering a more streamlined end result.
+ */
+ abstract public function reconstruct(ComponentRequestContext $request): AbstractBlock;
+
+ /**
+ * The accessor represents a unique public name used to retrieve the appropriate resolver
+ * during the reconstruction process, essential for constructing both the block and the component.
+ *
+ * The accessor must match the DI-mapping Resolver item's name exactly.
+ */
+ public function getAccessor(): string
+ {
+ return $this->accessor;
+ }
+
+ /**
+ * Assembles all the necessary components after a block has been constructed or reconstructed.
+ * It is considered best practice to perform block assembly as the final step to ensure all
+ * required Magewire elements are properly set.
+ *
+ * Responsible for the following tasks:
+ * 1. Setting the block onto the component
+ * 2. Setting the resolver onto the component
+ * 3. Assembling data arguments
+ *
+ * @throws RuntimeException
+ */
+ public function assemble(AbstractBlock $block, Component $component): AbstractBlock
+ {
+ if ($this->key === null) {
+ $this->key = $block->getCacheKey();
+ }
+
+ return $block;
+ }
+
+ /**
+ * Flags whether the resolver should be reused (cached) or always re-evaluated.
+ *
+ * When the resolver is reused (cached), it improves performance by avoiding redundant
+ * calculations, making it easier to determine which resolver needs to be used
+ * for either the construction or reconstruction of the component.
+ *
+ * On the other hand, making the resolver dynamic (fluent) requires more processing,
+ * but ensures that the component is resolved based on different terms or conditions
+ * at runtime.
+ */
+ public function remember(): bool
+ {
+ return true;
+ }
+
+ protected function conditions(): Conditions
+ {
+ return $this->conditions;
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolver/ComponentResolverFactory.php b/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolver/ComponentResolverFactory.php
new file mode 100644
index 00000000..aabc4e48
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolver/ComponentResolverFactory.php
@@ -0,0 +1,22 @@
+conditions);
+ }
+
+ /**
+ * To create a Magewire component using regular Layout XML, you have three options for binding:
+ *
+ * 1. Bind the component directly to a child argument named "magewire" with a xsi:type of "object".
+ * This allows you to associate the component with the Magewire framework.
+ *
+ * 2. Bind the component as a Magewire argument with an xsi:type of "array".
+ * This array should contain a child item called "type" with an xsi:type of "object".
+ * This method also associates the component with the Magewire framework.
+ *
+ * 3. Bind the component using Option 2 as a template, but instead of assigning it an xsi:type of "object",
+ * set it as a "bool" with a value of true.
+ *
+ * This approach enables you to create the component dynamically, where you won't have a physical object
+ * to control interaction, but you can still utilize it for tasks like polling data at regular intervals.
+ *
+ * By setting the value to true, you establish a connection with the Magewire framework without the need
+ * for a specific object instance.
+ */
+ public function complies(mixed $block, mixed $magewire = null): bool
+ {
+ $this->conditions()->validate(fn () => $this->isBlock($block), 'is_block');
+
+ return parent::complies($block, $magewire);
+ }
+
+ /**
+ * @throws ComponentNotFoundException
+ */
+ public function construct(AbstractBlock $block): AbstractBlock
+ {
+ $magewire = $block->getData('magewire') ?? null;
+
+ if (! $magewire) {
+ throw new ComponentNotFoundException(sprintf('No component object found for "%s"', $block->getNameInLayout()));
+ }
+
+ // Magewire block can be directly passed as an argument or nested within a type argument.
+ $component = is_array($magewire) ? $magewire['type'] : $magewire;
+
+ if (! $component instanceof Component) {
+ throw new ComponentNotFoundException(sprintf('Component "%s" could not be constructed', $block->getNameInLayout()));
+ }
+
+ $this->handleBackwardsCompatibilityForComponent($magewire);
+ // Fulfill the main promise to establish or reset a Component instance within the block.
+ $block->setData('magewire', $component);
+
+ // Register a dehydrate listener to attach necessary layout handles to the server memo.
+ on('dehydrate', function (Component $component, ComponentContext $context) {
+ $resolver = $component->magewireResolver();
+
+ if ($resolver->getAccessor() === $this->getAccessor()) {
+ if ($this->canMemorizeLayoutHandles()) {
+ $context->addMemo('handles', array_values($this->determineLayoutHandles($component, $context)));
+ }
+
+ if ($alias = $component->magewireBlock()->getData('magewire:alias')) {
+ $context->addMemo('alias', $alias);
+ }
+ }
+ });
+
+ return $block;
+ }
+
+ /**
+ * Layout reconstruction involves loading layout XML-generated blocks
+ * based on the handles passed into the server memo during component construction.
+ *
+ * By this point, we can be certain that the layout handles accompany the
+ * XHR request's server memo, stored in the snapshot object.
+ *
+ * After locating the block, standard construction is performed to complete
+ * the process, creating the Magewire component as it would be during a regular page load.
+ *
+ * @throws ComponentNotFoundException|LocalizedException
+ */
+ public function reconstruct(ComponentRequestContext $request): AbstractBlock
+ {
+ $snapshot = $request->getSnapshot();
+
+ $alias = $snapshot->getMemoValue('alias');
+ $name = $snapshot->getMemoValue('name');
+
+ // Retrieve the layout handles that were stored on the context snapshot.
+ $handles = $this->recoverLayoutHandles($snapshot);
+ // Build the complete layout structure by processing the recovered handles into renderable blocks.
+ $blocks = $this->generateBlocks($handles);
+
+ /** @var Template|false $block */
+ $block = $blocks[$alias ?? $name] ?? false;
+
+ if ($block === false) {
+ throw new HttpException(404, sprintf('Magewire component "%s" could not be found', $alias ?? $name));
+ }
+
+ if ($alias) {
+ $block->setData('magewire:alias', $alias);
+ $block->setNameInLayout($name);
+ }
+
+ // Now everything is prepared, we can simply re-call the construct method like during preceding requests.
+ return $this->construct($block);
+ }
+
+ public function arguments(): MagewireArguments
+ {
+ return $this->arguments ??= $this->layoutBlockArgumentsFactory->create();
+ }
+
+ /**
+ * @throws RuntimeException
+ */
+ public function assemble(AbstractBlock $block, Component $component): AbstractBlock
+ {
+ /*
+ * The block assembly occurs after the component is constructed, finalizing it with
+ * necessary attributes such as name and ID. For the layout, the block's name and ID
+ * are derived from the block itself, using the nameInLayout method provided by Layout XML.
+ */
+ $component->setName($block->getNameInLayout());
+ $component->setId($block->getNameInLayout());
+ $component->setAlias($component->getAlias() ?? $block->getData('magewire:alias'));
+
+ $this->determineTemplate($block, $component);
+
+ return parent::assemble($block, $component);
+ }
+
+ protected function logger(): LoggerInterface
+ {
+ return $this->logger ??= Factory::get(LoggerInterface::class);
+ }
+
+ /**
+ * Determines the template by a default template path
+ * when the path is not defined within the layout.
+ *
+ * Convention: {Vendor_Module::magewire/dashed-class-name.phtml}
+ */
+ protected function determineTemplate(AbstractBlock $block, Component $component): void
+ {
+ if ($block->getTemplate() !== null) {
+ return;
+ }
+
+ $classParts = array_values(
+ array_filter(
+ explode('\\', get_class($component)),
+ static fn (string $part) => $part !== 'Interceptor'
+ )
+ );
+
+ $prefix = $classParts[0] . '_' . $classParts[1];
+ $suffix = strtolower(preg_replace('/(?setTemplate($prefix . '::magewire/' . $suffix . '.phtml');
+ }
+
+ protected function generateBlocks(array $handles): array
+ {
+ $layout = $this->layoutManager->singleton();
+
+ /**
+ * Preferably, a total new instance of the layout should be used. But since this singleton is
+ * used all over the place, it's very hard to manage a custom layout and have difference between
+ * the two of them.
+ *
+ * This causes issues all over the place where we've tried to fix it using a Layout Manager
+ * to provide developers with the correct layout object defaulting to the global singleton.
+ *
+ * But testing this for a while, didn't give a strong and solid foundation for future usage.
+ * Therefore, we've decided to go with the global singleton for now.
+ */
+ if ($layout instanceof Layout) {
+ $layout->getUpdate()->addHandle($handles);
+ return $layout->getAllBlocks();
+ }
+
+ throw new \RuntimeException('Unable to generate layout');
+ }
+
+ protected function isBlock(mixed $block): bool
+ {
+ return $block instanceof AbstractBlock;
+ }
+
+ protected function determineLayoutHandles(Component $component, ComponentContext $context): array
+ {
+ return array_diff(
+ $context->getBlock()->getLayout()->getUpdate()->getHandles(),
+ ['default']
+ );
+ }
+
+ protected function recoverLayoutHandles(Snapshot $snapshot): array
+ {
+ if ($this->canMemorizeLayoutHandles()) {
+ return $snapshot->getMemoValue('handles') ?? [];
+ }
+
+ return [];
+ }
+
+ protected function canMemorizeLayoutHandles(): bool
+ {
+ return true;
+ }
+
+ protected function handleBackwardsCompatibilityForComponent(Component $component): void
+ {
+ $bc = store($component)->get('magewire:bc');
+
+ try {
+ $within = AttributesReader::for($component)->first(HandleBackwardsCompatibility::class);
+
+ if ($within instanceof HandleBackwardsCompatibility) {
+ $bc = $within->isBackwardsCompatible();
+ }
+ } catch (Exception $exception) {
+ $this->logger()->critical($exception->getMessage(), ['exception' => $exception]);
+ }
+
+ store($component)->set('magewire:bc', $bc ?? false);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolverNotFoundException.php b/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolverNotFoundException.php
new file mode 100644
index 00000000..65113ee4
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/ComponentResolverNotFoundException.php
@@ -0,0 +1,18 @@
+ All blocks seen during render: route → block. */
+ private array $blocks = [];
+
+ /** @var array Bound components: route → component. */
+ private array $components = [];
+
+ /** @var array Anonymous block counters per parent route. */
+ private array $anonymousCounters = [];
+
+ /**
+ * Push a block onto the render stack.
+ *
+ * Builds a route for the block based on its nameInLayout and the current
+ * stack depth. Anonymous blocks or duplicate-named siblings are assigned
+ * a numeric index from a per-parent counter to guarantee route uniqueness.
+ */
+ public function push(AbstractBlock $block): static
+ {
+ $parent = end($this->stack) ?: '';
+ $name = $block->getNameInLayout();
+
+ if ($name === null || $name === '') {
+ $counterKey = $parent !== '' ? $parent : '__root';
+ $this->anonymousCounters[$counterKey] ??= 0;
+ $segment = (string) $this->anonymousCounters[$counterKey]++;
+ } else {
+ $segment = $name;
+ }
+
+ $route = $parent !== '' ? $parent . DIRECTORY_SEPARATOR . $segment : $segment;
+
+ if (isset($this->blocks[$route])) {
+ $counterKey = $parent !== '' ? $parent : '__root';
+ $this->anonymousCounters[$counterKey] ??= 0;
+ $route = $parent !== '' ? $parent . DIRECTORY_SEPARATOR . $this->anonymousCounters[$counterKey]++ : (string) $this->anonymousCounters[$counterKey]++;
+ }
+
+ $this->stack[] = $route;
+ $this->blocks[$route] = $block;
+
+ $component = $block->getData('magewire');
+
+ if ($component instanceof Component) {
+ $this->components[$route] = $component;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Pop the current block off the render stack.
+ *
+ * Reverts the active route to the parent, effectively closing the current
+ * block's rendering scope. The stack is strictly LIFO — no identification
+ * of the block is needed.
+ */
+ public function pop(): static
+ {
+ if ($this->stack !== []) {
+ $route = array_pop($this->stack);
+
+ if (! isset($this->components[$route])) {
+ unset($this->blocks[$route]);
+ }
+
+ unset($this->anonymousCounters[$route]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Bind a Magewire component to the block currently on top of the stack.
+ *
+ * Called during component construction/reconstruction to associate a
+ * Component instance with its block's route. This link enables all
+ * subsequent relationship queries (parentComponent, closestComponent, etc.).
+ */
+ public function bind(Component $component): static
+ {
+ $route = end($this->stack);
+
+ if ($route !== false) {
+ $this->components[$route] = $component;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the component bound to a specific block, or null if the block has no component.
+ */
+ public function componentFor(AbstractBlock $block): ?Component
+ {
+ $route = $this->routeForBlock($block);
+
+ return $route !== null ? ($this->components[$route] ?? null) : null;
+ }
+
+ /**
+ * Get the block that a component is bound to, or null if the component is not registered.
+ */
+ public function blockFor(Component $component): ?AbstractBlock
+ {
+ $route = $this->routeForComponent($component);
+
+ return $route !== null ? ($this->blocks[$route] ?? null) : null;
+ }
+
+ /**
+ * Find the nearest ancestor component above a block in the render tree.
+ *
+ * Walks upward from the block's parent route (skipping the block itself)
+ * and returns the first component found. Used by SupportMagewireNestingComponents
+ * to inject the owning component into child template dictionaries.
+ */
+ public function closestComponent(AbstractBlock $block): ?Component
+ {
+ $route = $this->routeForBlock($block);
+
+ if ($route === null) {
+ return null;
+ }
+
+ $route = $this->parentRoute($route);
+
+ while ($route !== '') {
+ if (isset($this->components[$route])) {
+ return $this->components[$route];
+ }
+
+ $route = $this->parentRoute($route);
+ }
+
+ return null;
+ }
+
+ /**
+ * Find the nearest ancestor component of a given component.
+ *
+ * Walks upward from the component's route through the block tree and
+ * returns the first other component encountered. Returns null if the
+ * component is at the root or has no component ancestors.
+ */
+ public function parentComponent(Component $component): ?Component
+ {
+ $route = $this->routeForComponent($component);
+
+ if ($route === null) {
+ return null;
+ }
+
+ $route = $this->parentRoute($route);
+
+ while ($route !== '') {
+ if (isset($this->components[$route])) {
+ return $this->components[$route];
+ }
+
+ $route = $this->parentRoute($route);
+ }
+
+ return null;
+ }
+
+ /**
+ * Collect all ancestor components above a given component, ordered nearest-first.
+ *
+ * Walks the full route upward, collecting every component found along the
+ * way. An optional filter callback can narrow the result set.
+ */
+ public function componentAncestors(Component $component, ?callable $filter = null): array
+ {
+ $route = $this->routeForComponent($component);
+
+ if ($route === null) {
+ return [];
+ }
+
+ $ancestors = [];
+ $route = $this->parentRoute($route);
+
+ while ($route !== '') {
+ if (isset($this->components[$route])) {
+ $ancestors[] = $this->components[$route];
+ }
+
+ $route = $this->parentRoute($route);
+ }
+
+ return $filter ? array_filter($ancestors, $filter) : $ancestors;
+ }
+
+ /**
+ * Collect all descendant components below a given component.
+ *
+ * Finds every component whose route starts with this component's route
+ * prefix, meaning they are nested somewhere within its block subtree.
+ * Includes all depths (children, grandchildren, etc.). An optional
+ * filter callback can narrow the result set.
+ */
+ public function componentChildren(Component $component, ?callable $filter = null): array
+ {
+ $route = $this->routeForComponent($component);
+
+ if ($route === null) {
+ return [];
+ }
+
+ $prefix = $route . DIRECTORY_SEPARATOR;
+
+ $children = array_values(array_filter(
+ $this->components,
+ static fn (Component $c, string $r) => str_starts_with($r, $prefix),
+ ARRAY_FILTER_USE_BOTH
+ ));
+
+ return $filter ? array_filter($children, $filter) : $children;
+ }
+
+ /**
+ * Check whether the current rendering context is inside a block with the given name.
+ *
+ * Inspects the segments of the active route to determine if a block with
+ * this nameInLayout is an ancestor of (or is) the block currently being rendered.
+ */
+ public function within(string $name): bool
+ {
+ $current = end($this->stack);
+
+ if ($current === false) {
+ return false;
+ }
+
+ return in_array($name, explode(DIRECTORY_SEPARATOR, $current), true);
+ }
+
+ /**
+ * Whether any Magewire components have been bound during this render cycle.
+ */
+ public function hasComponents(): bool
+ {
+ return $this->components !== [];
+ }
+
+ /**
+ * Get the full route of the block currently being rendered.
+ *
+ * Returns an empty string when the stack is empty (no block is rendering).
+ */
+ public function route(): string
+ {
+ return end($this->stack) ?: '';
+ }
+
+ private function routeForBlock(AbstractBlock $block): ?string
+ {
+ $route = array_search($block, $this->blocks, true);
+
+ return $route !== false ? $route : null;
+ }
+
+ private function routeForComponent(Component $component): ?string
+ {
+ $route = array_search($component, $this->components, true);
+
+ return $route !== false ? $route : null;
+ }
+
+ private function parentRoute(string $route): string
+ {
+ $pos = strrpos($route, DIRECTORY_SEPARATOR);
+
+ return $pos !== false ? substr($route, 0, $pos) : '';
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/Layout/MagewireLayoutDecorator.php b/lib/Magewire/Mechanisms/ResolveComponents/Layout/MagewireLayoutDecorator.php
new file mode 100644
index 00000000..d1d2a53c
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/Layout/MagewireLayoutDecorator.php
@@ -0,0 +1,38 @@
+dynamicLayoutBuilder->newInstance(['layout' => $layout]);
+
+ $layout->setGeneratorPool($this->generatorPool);
+ $layout->setBuilder($builder);
+ }
+
+ return $layout;
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/Management/ComponentResolverManager.php b/lib/Magewire/Mechanisms/ResolveComponents/Management/ComponentResolverManager.php
new file mode 100644
index 00000000..f9cc7aa6
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/Management/ComponentResolverManager.php
@@ -0,0 +1,204 @@
+ $resolvers
+ */
+ public function __construct(
+ private LoggerInterface $logger,
+ private ResolversCache $resolversCache,
+ private ComponentResolverFactory $componentResolverFactory,
+ private array $resolvers = []
+ ) {
+ $this->resolvers = array_filter($this->resolvers, static fn ($resolver) => is_object($resolver) || is_string($resolver));
+ }
+
+ /**
+ * Locate a matching resolver which complies to the given block.
+ *
+ * @throws ComponentResolverNotFoundException
+ * @throws RuntimeException
+ */
+ public function resolve(AbstractBlock $block): ComponentResolver
+ {
+ $cache = $this->resolversCache->fetch();
+
+ // BACKWARDS COMPATIBILITY START: Handling cases where resolver cache values were stored as strings and
+ // the magewire cache not yet has been flushed.
+ $resolver = $cache[$block->getCacheKey()] ?? $cache[$block->getCacheKey()]['accessor'] ?? null;
+ // BACKWARDS COMPATIBILITY END.
+
+ /*
+ * The resolver is determined through one of the following options:
+ *
+ * 1. The resolver is manually assigned via a Magewire resolver argument,
+ * specified as a xsi-type string representing a DI-mapped resolver key or full class path.
+ * 2. The resolver is automatically filtered and executed by all available
+ * injected resolvers, each calling the `complies()` method.
+ */
+ $resolver ??= $block->getData('magewire:resolver') ?? null;
+
+ if ($resolver instanceof ComponentResolver) {
+ return $resolver;
+ }
+
+ // Check the Magewire blocks cache to find the cached resolver accessor.
+ $resolver ??= $cache['blocks'][$this->getBlockCacheKey($block)] ?? null;
+
+ if (is_string($resolver)) {
+ if ($this->hasResolverClassInMapping($resolver)) {
+ try {
+ return $this->createResolverByAccessor($resolver);
+ } catch (NotFoundException $exception) {
+ $this->logger->info($exception->getMessage(), ['exception' => $exception]);
+ }
+ }
+
+ // If the resolver string wasn't found in the class mapping or instantiation failed,
+ // attempt to resolve the class dynamically and instantiate it if successful.
+ try {
+ return $this->createResolverByType($this->getResolverClass($resolver));
+ } catch (NotFoundException $exception) {
+ $this->logger->info(sprintf('Magewire resolver data value found on block, but "%s" can not be resolved.', $resolver), ['exception' => $exception]);
+ }
+ }
+
+ try {
+ // Last resort: find a resolver that matches the given block.
+ // @todo Consider prioritizing resolvers instead of always using the first match.
+ $resolver = array_key_first(array_filter($this->resolvers, static fn (ComponentResolver $resolver) => $resolver->complies($block, $block->getData('magewire'))));
+
+ $resolver = $this->createResolverByAccessor($resolver);
+ } catch (Exception $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+
+ throw new ComponentResolverNotFoundException(sprintf('No component resolver complies for block %s.', $block->getNameInLayout()));
+ }
+
+ if ($resolver->remember()) {
+ $this->cache($block, $resolver);
+ }
+
+ return $resolver;
+ }
+
+ public function resolverFactory(): ComponentResolverFactory
+ {
+ return $this->componentResolverFactory;
+ }
+
+ /**
+ * Create a resolver instance by its accessor.
+ *
+ * @throws ComponentResolverNotFoundException
+ */
+ public function createResolverByAccessor(string $resolver, array $arguments = []): ComponentResolver
+ {
+ return $this->createResolverByType($this->getResolverClass($resolver), $arguments);
+ }
+
+ /**
+ * @throws ComponentResolverNotFoundException
+ */
+ public function createResolverByType(string $type, array $arguments = []): ComponentResolver
+ {
+ $instance = $this->resolverFactory()->create($type, $arguments);
+
+ if ($instance instanceof ComponentResolver) {
+ return $instance;
+ }
+
+ throw new ComponentResolverNotFoundException('Component resolver cannot be found.');
+ }
+
+ /**
+ * Returns if the given resolver can be found by its accessor.
+ */
+ public function hasResolverClassInMapping(string $resolver): bool
+ {
+ try {
+ return is_string($this->getResolverClass($resolver));
+ } catch (ComponentResolverNotFoundException $exception) {
+ }
+
+ return false;
+ }
+
+ /**
+ * @template T of ComponentResolver
+ * @return class-string
+ * @throws ComponentResolverNotFoundException
+ */
+ private function getResolverClass(string $resolver): string
+ {
+ $cache = $this->resolversCache->fetch();
+ $data = $cache['resolvers'][$resolver] ?? null;
+
+ if ($data) {
+ return $data['class'];
+ }
+ if (class_exists($resolver)) {
+ return $resolver;
+ }
+
+ if (isset($this->resolvers[$resolver])) {
+ if (is_object($this->resolvers[$resolver])) {
+ return $this->resolvers[$resolver]::class;
+ }
+
+ return $this->resolvers[$resolver];
+ }
+
+ throw new ComponentResolverNotFoundException(sprintf('No block resolver found for accessor "%s"', $resolver));
+ }
+
+ /**
+ * @throws RuntimeException
+ */
+ private function getBlockCacheKey(AbstractBlock $block, bool $unique = true): string
+ {
+ return $unique ? sprintf('%s_%s', $block->getCacheKey(), $block->getNameInLayout()) : $block->getCacheKey();
+ }
+
+ /**
+ * @throws RuntimeException
+ */
+ private function cache(AbstractBlock $block, ComponentResolver $resolver): void
+ {
+ $cache = $this->resolversCache->fetch();
+ $cache['blocks'][$this->getBlockCacheKey($block)] = $resolver->getAccessor();
+
+ $resolvers = $cache['resolvers'] ?? [$resolver->getAccessor() => []];
+
+ $resolvers[$resolver->getAccessor()]['blocks'][] = $this->getBlockCacheKey($block);
+ $resolvers[$resolver->getAccessor()]['class'] ??= $resolver::class;
+ $resolvers[$resolver->getAccessor()]['name'] ??= $resolver->getAccessor();
+
+ $cache['resolvers'] = $resolvers;
+
+ // Attempt to cache the resolvers, regardless of whether caching is enabled or disabled.
+ $this->resolversCache->save($cache);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/Management/LayoutLifecycleManager.php b/lib/Magewire/Mechanisms/ResolveComponents/Management/LayoutLifecycleManager.php
new file mode 100644
index 00000000..62e6e713
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/Management/LayoutLifecycleManager.php
@@ -0,0 +1,34 @@
+lifecycles()->get($area, Factory::create(LayoutLifecycle::class), true);
+ }
+
+ private function lifecycles(): DataCollection
+ {
+ return $this->lifecycles ??= Factory::create(DataCollection::class);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/Management/LayoutManager.php b/lib/Magewire/Mechanisms/ResolveComponents/Management/LayoutManager.php
new file mode 100644
index 00000000..d2126091
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/Management/LayoutManager.php
@@ -0,0 +1,54 @@
+layout;
+ }
+
+ /**
+ * Returns a new Layout instance.
+ */
+ public function factory(): LayoutFactory
+ {
+ return $this->factory;
+ }
+
+ public function decorator(): LayoutDecorator
+ {
+ return $this->decorator;
+ }
+
+ public function lifecycle(string $name = 'magewire'): LayoutLifecycle
+ {
+ return $this->lifecycleManager->target($name);
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/ResolveComponents.php b/lib/Magewire/Mechanisms/ResolveComponents/ResolveComponents.php
new file mode 100644
index 00000000..9cc44189
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/ResolveComponents.php
@@ -0,0 +1,108 @@
+magewireServiceProvider->runtime()->mode()->isSubsequent()) {
+ $this->layoutManager->decorator()->decorateForPagelessBlockFetching($this->layoutManager->singleton());
+ }
+
+ on('magewire:component:construct', function (AbstractBlock $block) {
+ return $this->build(function () use ($block): array {
+ $resolver = $this->componentResolverManagement->resolve($block);
+ $block = $resolver->construct($block);
+
+ return [$resolver, $block];
+ });
+ });
+
+ on('magewire:component:reconstruct', function (ComponentRequestContext $request) {
+ if (! $this->componentResolverManagement->hasResolverClassInMapping($request->getSnapshot()->getMemoValue('resolver'))) {
+ throw new HttpException(400);
+ }
+
+ return $this->build(function () use ($request): array {
+ $resolver = $this->componentResolverManagement->createResolverByAccessor($request->getSnapshot()->getMemoValue('resolver'));
+ $block = $resolver->reconstruct($request);
+
+ return [$resolver, $block];
+ });
+ });
+
+ // Register a dehydrate listener to attach the used resolver accessor for easy reconstruction.
+ on('dehydrate', static function (Component $component, ComponentContext $context) {
+ $context->addMemo('resolver', $component->magewireResolver()->getAccessor());
+ });
+ }
+
+ /**
+ * @throws ComponentNotFoundException
+ */
+ protected function build(callable $builder): callable
+ {
+ $lifecycle = $this->layoutManager->lifecycle();
+ [$resolver, $block] = $builder();
+
+ if (! $block->getData('magewire') instanceof Component) {
+ throw new ComponentNotFoundException(sprintf('Resolver "%s" failed to construct a Magewire component.', $resolver->getAccessor()));
+ }
+
+ return static function () use ($resolver, $block, $lifecycle) {
+ $resolver->arguments()->assemble($block, true);
+
+ /** @var Component $component */
+ $component = $block->getData('magewire');
+
+ $component->magewireBlock($block);
+ $component->magewireResolver($resolver);
+ $component->magewireLayoutLifecycle($lifecycle);
+
+ $assembly = $resolver->assemble($block, $component);
+ trigger('magewire:component:build', $assembly, $component, $resolver);
+
+ return $assembly;
+ };
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/ResolveComponentsViewModel.php b/lib/Magewire/Mechanisms/ResolveComponents/ResolveComponentsViewModel.php
new file mode 100644
index 00000000..0a05f164
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/ResolveComponentsViewModel.php
@@ -0,0 +1,28 @@
+renderLifecycleManager->target('magewire')->hasComponents();
+ }
+}
diff --git a/lib/Magewire/Mechanisms/ResolveComponents/ResolversCache.php b/lib/Magewire/Mechanisms/ResolveComponents/ResolversCache.php
new file mode 100644
index 00000000..d1c08aa3
--- /dev/null
+++ b/lib/Magewire/Mechanisms/ResolveComponents/ResolversCache.php
@@ -0,0 +1,19 @@
+state = $state;
+ }
+
+ return $this->state;
+ }
+
+ /**
+ * @throws RuntimeException
+ */
+ public function mode(RequestMode|null $mode = null): RequestMode
+ {
+ if ($mode === null || $mode === $this->mode) {
+ return $this->mode;
+ }
+ if ($mode === RequestMode::UNDEFINED) {
+ throw new RuntimeException('Magewire request mode cannot be unset.');
+ }
+
+ return $this->mode = $mode;
+ }
+}
diff --git a/lib/Magewire/ServiceType.php b/lib/Magewire/ServiceType.php
new file mode 100644
index 00000000..dc4de7c7
--- /dev/null
+++ b/lib/Magewire/ServiceType.php
@@ -0,0 +1,214 @@
+ $items
+ */
+ public function __construct(
+ private array $items = []
+ ) {
+ }
+
+ /**
+ * Returns true when fully booted, false when only partially booted.
+ */
+ public function boot(ServiceTypeItemBootMode|null $mode = null): bool
+ {
+ // Short circuit when the service is fully booted.
+ if ($this->items()->booted()) {
+ return true;
+ }
+
+ return $this->items()->boot($mode, $this->callback())->booted();
+ }
+
+ public function booter(): ServiceTypeBooter
+ {
+ return $this->booter ??= Factory::create(ServiceTypeBooter::class);
+ }
+
+ /**
+ * Returns an operation type facade which simplifies the interface of a complex type by
+ * exposing only the necessary methods, thus defining a clear and concise
+ * API for interacting with the type.
+ *
+ * @throws NotFoundException
+ */
+ public function facade(string $for)
+ {
+ $for = preg_replace('/(?items[$for]['facade'];
+
+ if (is_string($facade)) {
+ $this->items[$for]['facade'] = Factory::get($facade);
+ } elseif (! is_object($facade)) {
+ throw new NotFoundException(__('Operation type facade "%1" could not be found.', $for));
+ }
+
+ return $this->items[$for]['facade'];
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ public function item(string $name): object
+ {
+ $name = preg_replace('/(?items[$name])) {
+ return $this->items[$name]['type'];
+ }
+
+ throw new NotFoundException(__('Operation type item "%1" could not be found.', $name));
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ public function viewModel(string $name): ArgumentInterface
+ {
+ $name = preg_replace('/(?items[$name]['view_model'])) {
+ return $this->items[$name]['view_model'];
+ }
+
+ throw new NotFoundException(__('Operation view model "%1" could not be found.', $name));
+ }
+
+ private function assemble(): static
+ {
+ if ($this->assembled) {
+ return $this;
+ }
+
+ $assembled = [];
+
+ foreach (array_filter($this->items, static fn ($value) => is_string($value) || is_array($value)) as $key => $item) {
+ if (is_string($item)) {
+ $item = ['type' => $item];
+ }
+ if (is_object($item['type'])) {
+ $assembled[$key] = $item;
+ continue;
+ }
+
+ $item['type'] = Factory::get($item['type']);
+
+ // Injects data if a `setData()` method is present in the operation type item class.
+ if (method_exists($item['type'], 'setData')) {
+ foreach ($item['data'] ?? [] as $dataKey => $value) {
+ $item['type']->setData($dataKey, $value);
+ }
+ }
+
+ if ($item['sort_order'] ?? false) {
+ $this->sortOrder = (int) $item['sort_order'];
+ } else {
+ $this->sortOrder++;
+ $item['sort_order'] = $this->sortOrder;
+ }
+
+ // Ensure the facade key exists, defaulting to null if not set.
+ $item['facade'] ??= null;
+ // Keep only active sequences (where the value is true).
+ $item['sequence'] = array_filter($item['sequence'] ?? [], static fn ($item) => $item === true);
+ // Ensure the view model key exists, defaulting to null if not set.
+ $item['view_model'] ??= null;
+ // Ensure the config key exists, defaulting to null if not set.
+ $item['config'] ??= null;
+ // Use the array key as the item name if not explicitly defined.
+ $item['name'] ??= $key;
+
+ // Ensure the boot mode exists, or set the default if not set.
+ $item['boot_mode'] = ServiceTypeItemBootMode::try($item['boot_mode'] ?? null, $this->getBootModeFallback());
+
+ $assembled[$key] = $item;
+ }
+
+ $this->items = $assembled;
+
+ $this->assembled = true;
+ return $this;
+ }
+
+ private function sort(): array
+ {
+ if ($this->sorted) {
+ return $this->items;
+ }
+
+ uasort($this->items, static function ($a, $b) {
+ if ($a['sort_order'] == $b['sort_order']) {
+ if (isset($a['sequence'])) {
+ foreach ($a['sequence'] as $dependency) {
+ if ($dependency == $b['type']) {
+ return 1; // $a should come after $b
+ }
+ }
+ }
+
+ if (isset($b['sequence'])) {
+ foreach ($b['sequence'] as $dependency) {
+ if ($dependency == $a['type']) {
+ return -1; // $a should come before $b
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ return ( $a['sort_order'] ?? 0 ) < ( $b['sort_order'] ?? 0 ) ? -1 : 1;
+ });
+
+ $this->sorted = true;
+ return $this->items;
+ }
+
+ protected function items(): ServiceTypeBooter
+ {
+ return $this->booter()->setup($this->assemble()->sort());
+ }
+
+ protected function getBootModeFallback(): ServiceTypeItemBootMode
+ {
+ return ServiceTypeItemBootMode::lowest();
+ }
+
+ /**
+ * Callback ran during a booting process of a service type item.
+ */
+ abstract protected function callback(): callable;
+}
diff --git a/lib/Magewire/ServiceTypeBooter.php b/lib/Magewire/ServiceTypeBooter.php
new file mode 100644
index 00000000..7700bf9b
--- /dev/null
+++ b/lib/Magewire/ServiceTypeBooter.php
@@ -0,0 +1,117 @@
+ */
+ private array $processed = [];
+
+ private DataCollection|null $items = null;
+
+ public function __construct(
+ private readonly DataCollectionFactory $dataArrayFactory
+ ) {
+ }
+
+ public function setup(array $items): static
+ {
+ if ($this->setup) {
+ return $this;
+ }
+
+ foreach ($items as $item) {
+ if (! ($item['boot_mode'] instanceof ServiceTypeItemBootMode)) {
+ $item['boot_mode'] = ServiceTypeItemBootMode::try($item['boot_mode'] ?? null);
+ }
+
+ $key = $item['name'] ?? Random::string();
+ $this->items()->set($key, $item);
+ $this->processed[$key] = false;
+ }
+
+ $this->setup = true;
+ return $this;
+ }
+
+ /**
+ * Returns true when all items at or above the given mode have been booted.
+ * When no mode is given, checks all items and caches the global booted state.
+ */
+ public function booted(ServiceTypeItemBootMode|null $mode = null): bool
+ {
+ if ($this->booted) {
+ return true;
+ }
+
+ foreach ($this->items()->all() as $key => $item) {
+ if ($mode !== null && $item['boot_mode']->isLowerThan($mode)) {
+ continue;
+ }
+
+ if (($this->processed[$key] ?? false) === false) {
+ return false;
+ }
+ }
+
+ if ($mode === null) {
+ $this->booted = true;
+ }
+
+ return true;
+ }
+
+ public function boot(ServiceTypeItemBootMode|null $mode, callable $callback): static
+ {
+ if ($this->booted) {
+ return $this;
+ }
+
+ foreach ($this->items()->all() as $key => $item) {
+ // Skip items that were already booted.
+ if ($this->processed[$key] ?? false) {
+ continue;
+ }
+
+ // Skip items with a lower priority than the requested mode.
+ if ($mode !== null && $item['boot_mode']->isLowerThan($mode)) {
+ continue;
+ }
+
+ try {
+ $callback($item['type'], $item);
+ $this->processed[$key] = true;
+ } catch (Exception $exception) {
+ // TBD
+ }
+ }
+
+ // Cache the global booted flag if all items are now done.
+ $this->booted();
+
+ return $this;
+ }
+
+ private function items(): DataCollection
+ {
+ return $this->items ??= $this->dataArrayFactory->create();
+ }
+}
diff --git a/lib/Magewire/Support/AttributesReader.php b/lib/Magewire/Support/AttributesReader.php
new file mode 100644
index 00000000..379f68d6
--- /dev/null
+++ b/lib/Magewire/Support/AttributesReader.php
@@ -0,0 +1,111 @@
+reflection = $subject instanceof ReflectionClass ? $subject : new ReflectionClass($subject);
+
+ $this->target = $this->reflection;
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ public static function for(object|string $subject): static
+ {
+ return new static($subject);
+ }
+
+ /**
+ * Scope subsequent calls to a specific property.
+ *
+ * @throws ReflectionException
+ */
+ public function property(string $name): static
+ {
+ $clone = clone $this;
+ $clone->target = $this->reflection->getProperty($name);
+
+ return $clone;
+ }
+
+ /**
+ * Returns the first instantiated attribute of $attributeClass, or null.
+ *
+ * @template T
+ * @param class-string $attributeClass
+ * @return T|null
+ */
+ public function first(string $attributeClass): object|null
+ {
+ $attrs = $this->target->getAttributes($attributeClass);
+
+ return ! empty($attrs) ? $attrs[0]->newInstance() : null;
+ }
+
+ /**
+ * Returns all instantiated attributes of $attributeClass.
+ *
+ * @template T
+ * @param class-string $attributeClass
+ * @return T[]
+ */
+ public function all(string $attributeClass): array
+ {
+ return array_map(static fn (ReflectionAttribute $attr) => $attr->newInstance(), $this->target->getAttributes($attributeClass));
+ }
+
+ public function has(string $attributeClass): bool
+ {
+ return ! empty($this->target->getAttributes($attributeClass));
+ }
+
+ /**
+ * Returns a map of property name => first attribute instance for all properties bearing $attributeClass.
+ *
+ * @template T
+ * @param class-string $attributeClass
+ * @return array
+ */
+ public function properties(string $attributeClass): array
+ {
+ $result = [];
+
+ foreach ($this->reflection->getProperties() as $property) {
+ $attrs = $property->getAttributes($attributeClass);
+
+ if (! empty($attrs)) {
+ $result[$property->getName()] = $attrs[0]->newInstance();
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/Magewire/Support/Concerns/AsDataObject.php b/lib/Magewire/Support/Concerns/AsDataObject.php
new file mode 100644
index 00000000..7ce59681
--- /dev/null
+++ b/lib/Magewire/Support/Concerns/AsDataObject.php
@@ -0,0 +1,93 @@
+data[$key] ?? $default;
+ }
+
+ return $default;
+ }
+
+ function getDataByPath(string $path, $default = null, string $separator = '.')
+ {
+ $keys = explode($separator, $path);
+ $data = $this->data;
+
+ foreach ($keys as $key) {
+ if (is_array($data) && array_key_exists($key, $data)) {
+ $data = $data[$key];
+ } else {
+ return $default;
+ }
+ }
+
+ if ($this->data === $data) {
+ return $default;
+ }
+
+ return $data ?? $default;
+ }
+
+ function setData(string $key, mixed $value): self
+ {
+ if (property_exists(self::class, 'data')) {
+ $this->data[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ function hasData($key): bool
+ {
+ if (property_exists(self::class, 'data')) {
+ if (empty($key) || ! is_string($key)) {
+ return ! empty($this->data);
+ }
+
+ return array_key_exists($key, $this->data);
+ }
+
+ return false;
+ }
+
+ /**
+ * @throws LocalizedException
+ */
+ function __call($method, $args)
+ {
+ switch (substr((string) $method, 0, 3)) {
+ case 'get':
+ return $this->getData(substr($method, 3));
+ case 'set':
+ return $this->setData(substr($method, 3), $args[0] ?? null);
+ case 'has':
+ return isset($this->data[substr($method, 3)]);
+ }
+
+ throw new LocalizedException(new Phrase('Invalid method %1::%2', [get_class($this), $method]));
+ }
+
+ function populate(array $data): void
+ {
+ $this->data = $data;
+ }
+}
diff --git a/lib/Magewire/Support/Concerns/AsFactory.php b/lib/Magewire/Support/Concerns/AsFactory.php
new file mode 100644
index 00000000..2f7420e3
--- /dev/null
+++ b/lib/Magewire/Support/Concerns/AsFactory.php
@@ -0,0 +1,24 @@
+create($this->newInstanceType());
+ }
+}
diff --git a/lib/Magewire/Support/Concerns/WithDiagnostics.php b/lib/Magewire/Support/Concerns/WithDiagnostics.php
new file mode 100644
index 00000000..8ff8abc6
--- /dev/null
+++ b/lib/Magewire/Support/Concerns/WithDiagnostics.php
@@ -0,0 +1,27 @@
+withDiagnostics ??= $this->newTypeInstance(Diagnostics::class);
+ }
+}
diff --git a/lib/Magewire/Support/Concerns/WithFactory.php b/lib/Magewire/Support/Concerns/WithFactory.php
new file mode 100644
index 00000000..fb0542fc
--- /dev/null
+++ b/lib/Magewire/Support/Concerns/WithFactory.php
@@ -0,0 +1,58 @@
+|null $type
+ * @return T
+ */
+ public function newInstance(array $arguments = [], string|null $type = null): static
+ {
+ $type ??= static::class;
+
+ if ($type !== static::class && ! is_subclass_of($type, static::class)) {
+ throw new InvalidArgumentException(sprintf('Type %s must be equal to or a subclass of %s', $type, static::class));
+ }
+
+ return $this->newTypeInstance($type, $arguments);
+ }
+
+ /**
+ * Creates a new instance of the current or given class using its factory.
+ * When a type is provided, it must be equal to or a subclass of the current class.
+ *
+ * @template T
+ * @param class-string $type
+ * @return T
+ */
+ public function newTypeInstance(string $type, array $arguments = []): mixed
+ {
+ if (str_ends_with($type, 'Factory')) {
+ $type = substr($type, 0, -strlen('Factory'));
+ }
+ if (! class_exists($type)) {
+ throw new InvalidArgumentException(sprintf('Class %s does not exist', $type));
+ }
+
+ $factory = Factory::get(trim($type) . 'Factory');
+ return $factory->create($arguments);
+ }
+}
diff --git a/lib/Magewire/Support/Conditions.php b/lib/Magewire/Support/Conditions.php
new file mode 100644
index 00000000..a288cfe1
--- /dev/null
+++ b/lib/Magewire/Support/Conditions.php
@@ -0,0 +1,125 @@
+> */
+ private array $conditions = [];
+
+ public function validate(callable $condition, string|null $name = null): static
+ {
+ return $this->if($condition, $name);
+ }
+
+ public function if(callable $condition, string|null $name = null): static
+ {
+ return $this->set($condition, ConditionEnum::AND, $name);
+ }
+
+ /**
+ * @alias if
+ */
+ private function set(callable $condition, ConditionEnum $type, string|null $name = null): static
+ {
+ $name ?? count($this->get($type));
+ $this->conditions[$type->value][$name] = $condition;
+
+ return $this;
+ }
+
+ public function and(callable $condition, string|null $name = null): static
+ {
+ return $this->set($condition, ConditionEnum::AND, $name);
+ }
+
+ public function or(callable|array $alternative, string|null $name = null): static
+ {
+ if (is_array($alternative)) {
+ $alternative = array_filter($alternative, static fn ($item) => is_callable($item));
+ }
+
+ return $this->set($alternative, ConditionEnum::OR, $name);
+ }
+
+ public function swap(string $name, ConditionEnum $from, ConditionEnum $to): static
+ {
+ $target = $this->get($from, $name);
+
+ if (is_callable($target) && ! is_callable($to)) {
+ $this->set($target, $to, $name);
+ }
+
+ return $this;
+ }
+
+ public function isset(string $name, ConditionEnum $type = ConditionEnum::AND): bool
+ {
+ return isset($this->conditions[$type->value][$name]);
+ }
+
+ public function unset(string $name, ConditionEnum|null $type = null): static
+ {
+ if ($type && isset($this->conditions[$type->value])) {
+ unset($this->conditions[$type->value][$name]);
+
+ return $this;
+ }
+
+ if (count($this->get(ConditionEnum::AND))) {
+ $this->unset($name, ConditionEnum::AND);
+ }
+ if (count($this->get(ConditionEnum::OR))) {
+ $this->unset($name, ConditionEnum::OR);
+ }
+
+ return $this;
+ }
+
+ public function evaluate(...$args): bool
+ {
+ foreach ($this->get(ConditionEnum::AND) as $condition) {
+ if ($condition(...$args)) {
+ continue;
+ }
+
+ foreach ($this->get(ConditionEnum::OR) as $alternative) {
+ if (is_array($alternative) && $this->evaluateGroup($alternative, ...$args)) {
+ return true;
+ } else {
+ return $alternative(...$args);
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @return callable|array
+ */
+ private function get(ConditionEnum $type, string|null $name = null): callable|array
+ {
+ $type = $this->conditions[$type->value] ?? [];
+
+ return $name ? $type[$name] : $type;
+ }
+
+ private function evaluateGroup(array $group, ...$args): bool
+ {
+ return array_reduce($group, static fn ($carry, $item) => $carry && $item(...$args), true);
+ }
+}
diff --git a/lib/Magewire/Support/Conditions/ConditionEnum.php b/lib/Magewire/Support/Conditions/ConditionEnum.php
new file mode 100644
index 00000000..279c80e9
--- /dev/null
+++ b/lib/Magewire/Support/Conditions/ConditionEnum.php
@@ -0,0 +1,18 @@
+isset($offset);
+ }
+
+ public function offsetGet($offset): mixed
+ {
+ return $this->get($offset);
+ }
+
+ public function offsetSet($offset, $value): void
+ {
+ is_string($offset) || is_int($offset) ? $this->set($offset, $value) : $this->push($value);
+ }
+
+ public function offsetUnset($offset): void
+ {
+ $this->unset($offset);
+ }
+}
diff --git a/lib/Magewire/Support/DataCollection.php b/lib/Magewire/Support/DataCollection.php
new file mode 100644
index 00000000..dbff7126
--- /dev/null
+++ b/lib/Magewire/Support/DataCollection.php
@@ -0,0 +1,467 @@
+> */
+ private array $hooks = [];
+
+ /**
+ * @param array $items
+ */
+ public function __construct(
+ private readonly Filter $filter,
+ private array $items = [],
+ private readonly int $level = 0,
+ private readonly string|int $name = 'root',
+ private readonly DataCollection|null $parent = null
+ ) {
+ }
+
+ public function map(array $map): static
+ {
+ foreach ($map as $from => $to) {
+ if (! is_string($to)) {
+ continue;
+ }
+
+ $this->rename($from, $to);
+ }
+
+ return $this;
+ }
+
+ public function rename(string|int $from, string|int $to): static
+ {
+ return $this->copy($from, $to, true);
+ }
+
+ public function each(callable $callback, callable|TypeFilter $filter = TypeFilter::ALL): static
+ {
+ $items = $this->filter()
+ ->with($filter)
+ ->return()
+ ->all();
+
+ foreach ($items as $key => $value) {
+ $callback($this, $value, $key);
+ }
+
+ return $this;
+ }
+
+ public function copy(string|int $from, string|int $to, bool $unset = false): static
+ {
+ if ($this->isset($from)) {
+ $this->set($to, $this->items[$from]);
+
+ if ($unset) {
+ $this->unset($from);
+ }
+ }
+
+ return $this;
+ }
+
+ public function unset(string|int ...$keys): static
+ {
+ foreach ($keys as $key) {
+ if (! $this->isset($key)) {
+ continue;
+ }
+
+ unset($this->items[$key]);
+
+ $this->dispatch(Hook::UNSET, $key);
+ }
+
+ return $this;
+ }
+
+ public function set(string|int $name, $value): static
+ {
+ if ($this->isset($name)) {
+ return $this;
+ }
+
+ $this->items[$name] = $value;
+
+ $this->dispatch(Hook::SET, $name, $value);
+ return $this;
+ }
+
+ public function push($value): static
+ {
+ $this->items[] = $value;
+
+ $this->dispatch(Hook::PUSH, $value);
+ return $this;
+ }
+
+ public function subset(string|int|null $name = null, string|null $type = null, array $arguments = []): DataCollection
+ {
+ $level = $name !== null ? $this->level + 1 : 0;
+ $name = is_string($name) ? Str::snake($name) : $name;
+
+ if ($name !== null && $this->subsets()->has($name)) {
+ return $this->subsets()->get($name);
+ }
+
+ $arguments = array_merge($arguments, [
+ 'parent' => $this,
+ 'level' => $level,
+ 'name' => $name ?? Random::string(),
+ ]);
+
+ if ($name === null) {
+ return $this->newTypeInstance($type ?? static::class, $arguments);
+ }
+
+ $instance = $this->newTypeInstance($type ?? static::class, $arguments);
+ $this->subsets()->set($name, $instance);
+
+ return $instance;
+ }
+
+ /**
+ * Data collection subsets entry point.
+ */
+ public function subsets(): static
+ {
+ return $this->subsets ??= Factory::create(static::class, [
+ 'parent' => $this,
+ 'name' => 'subsets'
+ ]);
+ }
+
+ /**
+ * @deprecated Has been replaced using $this->subsets()->count()
+ * @param array $names
+ */
+ public function hasSubsets(array $names, bool $strict = true): bool
+ {
+ $expected = count($names);
+ $found = 0;
+
+ foreach ($names as $name) {
+ if (! ($this->subsets()->has($name))) {
+ continue;
+ }
+
+ $found++;
+ }
+
+ return $strict ? ($found === $expected) : ($found > 0);
+ }
+
+ /**
+ * Update (or set) an (existing) value by name.
+ */
+ public function put(string|int $name, $value, bool $force = false): static
+ {
+ if ($this->isset($name) || $force) {
+ $this->items[$name] = $value;
+
+ $this->dispatch(Hook::PUT, $name, $value);
+ }
+
+ return $this;
+ }
+
+ public function merge(array $items): static
+ {
+ foreach ($items as $key => $value) {
+ $this->isset($key) ? $this->put($key, $value) : $this->set($key, $value);
+ }
+
+ return $this;
+ }
+
+ public function fill(array $items, bool $arrayAsSubset = false): static
+ {
+ foreach ($items as $key => $value) {
+ $target = $this;
+
+ if (is_array($value) && $arrayAsSubset) {
+ $target = $this->subset($key);
+ }
+
+ $target->isset($key) ? $target->put($key, $value) : $target->set($key, $value);
+ }
+
+ return $this;
+ }
+
+ public function isset(string|int $name): bool
+ {
+ return isset($this->items[$name]);
+ }
+
+ public function has(string|int $name): bool
+ {
+ return $this->isset($name);
+ }
+
+ /**
+ * Check if all given props are set within the collection.
+ */
+ public function contains(array $names, bool $strict = true): bool
+ {
+ return ! in_array(false, array_map($this->has(...), $names), $strict);
+ }
+
+ public function all(): array
+ {
+ $result = [];
+
+ foreach ($this->items as $key => $value) {
+ if ($value instanceof DataCollection) {
+ $value = $value->all();
+ }
+
+ $result[$key] = $value;
+ }
+
+ return $result;
+ }
+
+ public function raw(): array
+ {
+ return $this->items;
+ }
+
+ /**
+ * Encodes the collection to a JSON string.
+ * When a filter is provided, only items passing both the JSON_ENCODABLE
+ * filter and the supplied filter are included in the output.
+ *
+ * @todo Implement lazy JSON caching when PHP 8.4+ property hooks are available.
+ * Use a cached $json property with a set hook that invalidates the cache whenever
+ * $items is modified. This would allow returning pre-encoded JSON if items haven't
+ * changed, eliminating redundant encoding operations. The cache would only contain
+ * JSON-encodable items to ensure validity.
+ */
+ public function json(callable|TypeFilter|null $filter = null): string|false
+ {
+ $items = $this;
+
+ if ($filter) {
+ $items = $this->subset()->fill(
+ $this->filter()->with(TypeFilter::JSON_ENCODABLE)->return()->all()
+ )->filter()->with($filter)->return();
+ }
+
+ return json_encode($items->all());
+ }
+
+ public function filter(): Filter
+ {
+ return $this->filter->using($this);
+ }
+
+ public function pluck(array $keys, array $defaults = []): array
+ {
+ $result = [];
+
+ foreach ($keys as $key) {
+ if ($this->isset($key)) {
+ $result[$key] = $this->get($key);
+ } elseif (array_key_exists($key, $defaults)) {
+ $result[$key] = $defaults[$key];
+ }
+ }
+
+ return $result;
+ }
+
+ public function values(): array
+ {
+ return array_values($this->items);
+ }
+
+ public function keys(): array
+ {
+ return array_keys($this->items);
+ }
+
+ public function get(string|int $name, $default = null, bool $set = false): mixed
+ {
+ $value = $this->items[$name] ?? ($set ? $this->default($name, $default)->get($name) : $default);
+
+ $this->dispatch(Hook::GET, $name, $value);
+ return $value;
+ }
+
+ /**
+ * Harvest the collection by passing all items into a callback to compute the final output.
+ */
+ public function harvest(callable $callback, array $carry = []): array
+ {
+ return (array) $callback($this->items, $carry);
+ }
+
+ public function reset(): static
+ {
+ $this->items = [];
+
+ $this->dispatch(Hook::RESET);
+ return $this;
+ }
+
+ public function destroy(): static
+ {
+ foreach ($this->subsets !== null ? $this->subsets->raw() : [] as $subset) {
+ if (!($subset instanceof DataCollection)) {
+ continue;
+ }
+
+ $subset->destroy();
+ }
+
+ $this->subsets = null;
+ $this->reset();
+
+ return $this;
+ }
+
+ public function default(string|int $key, $value): static
+ {
+ if ($this->isset($key)) {
+ return $this;
+ }
+
+ return $this->set($key, $value);
+ }
+
+ public function defaults(array $defaults): static
+ {
+ foreach ($defaults as $key => $value) {
+ if (! ( is_string($key) || is_int($key) )) {
+ continue;
+ }
+
+ $this->default($key, $value);
+ }
+
+ return $this;
+ }
+
+ public function clear(callable|TypeFilter $filter = TypeFilter::NONE): static
+ {
+ $this->items = $this->filter()
+ ->with($filter)
+ ->return()
+ ->all();
+
+ $this->dispatch(Hook::CLEAR);
+ return $this;
+ }
+
+ public function count(): int
+ {
+ return count($this->items);
+ }
+
+ public function snapshot(): static
+ {
+ return $this->newInstance(['items' => $this->all()]);
+ }
+
+ public function walk(callable $callback): static
+ {
+ $result = $callback($this);
+ $subsets = $this->subsets();
+
+ // Callback returned nothing or this instance.
+ if ($result === $this || $result instanceof static || $result === null) {
+ foreach ($subsets as $subitem) {
+ $subitem->walk($callback);
+ }
+ }
+
+ return $this;
+ }
+
+ public function collect(callable $callback): array
+ {
+ $result = $callback($this);
+ $subsets = $this->subsets();
+
+ // Gather all results into a flat array.
+ $collection = [$this->name() => $result];
+
+ foreach ($subsets as $subitem) {
+ $collection = array_merge($collection, $subitem->collect($callback));
+ }
+
+ return $collection;
+ }
+
+ public function level(): int
+ {
+ return $this->level;
+ }
+
+ public function name(): string|int
+ {
+ return $this->name;
+ }
+
+ public function parent(): static|null
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Registers a callable to be triggered when the specified hook event occurs.
+ */
+ public function hook(Hook|string $onto, callable $then): static
+ {
+ $hook = is_string($onto) ? $onto : $onto->value;
+ $this->hooks[$hook][] = $then;
+
+ return $this;
+ }
+
+ public function getIterator(): Traversable
+ {
+ return $this->newTypeInstance(ArrayIterator::class, ['array' => $this->all()]);
+ }
+
+ /**
+ * Dispatches a hook by invoking all registered actions for the given hook,
+ * passing the current instance followed by any additional arguments.
+ */
+ protected function dispatch(Hook|string $hook, mixed ...$args): static
+ {
+ $hook = is_string($hook) ? $hook : $hook->value;
+
+ foreach ($this->hooks[$hook] ?? [] as $action) {
+ $action($this, ...$args);
+ }
+
+ return $this;
+ }
+}
diff --git a/lib/Magewire/Support/DataCollection/Filter.php b/lib/Magewire/Support/DataCollection/Filter.php
new file mode 100644
index 00000000..781f196c
--- /dev/null
+++ b/lib/Magewire/Support/DataCollection/Filter.php
@@ -0,0 +1,183 @@
+ $presets */
+ private array $presets = [];
+
+ public function with(callable|TypeFilter|string $filter): static
+ {
+ if (is_string($filter)) {
+ $filter = $this->presets[$filter] ?? throw new InvalidArgumentException(sprintf('Data array filter preset "%s" does not exist.', $filter));
+ }
+ if (is_callable($filter)) {
+ return $this->byClosure($filter);
+ }
+
+ return $this->byType($filter);
+ }
+
+ public function byType(TypeFilter $filter): static
+ {
+ return $this->byClosure($filter->get());
+ }
+
+ public function byAnyType(TypeFilter ...$filters): static
+ {
+ return $this->byClosure(TypeFilter::any(...$filters));
+ }
+
+ public function byOnlyType(TypeFilter ...$filters): static
+ {
+ return $this->byClosure(TypeFilter::only(...$filters));
+ }
+
+ public function byClosure(callable $callable): static
+ {
+ return $this->chain(array_filter($this->items(), $callable, ARRAY_FILTER_USE_BOTH));
+ }
+
+ public function byOffset(int $offset, int $start = 0): static
+ {
+ return $this->chain(array_slice($this->items(), $start, $offset, true));
+ }
+
+ public function byPage(int $page, int $limit = 10): static
+ {
+ return $this->byOffset($limit, ( $page - 1 ) * $limit);
+ }
+
+ public function byKeys(array $keys): static
+ {
+ return $this->chain(array_intersect_key($this->items(), array_flip($keys)));
+ }
+
+ public function byValues(array $values): static
+ {
+ return $this->chain(array_intersect($this->items(), $values));
+ }
+
+ public function byKeyPattern(string $pattern): static
+ {
+ return $this->chain(array_filter(
+ $this->items(),
+ static function ($value, $key) use ($pattern) {
+ return preg_match($pattern, (string) $key);
+ },
+ ARRAY_FILTER_USE_BOTH
+ ));
+ }
+
+ public function byValuePattern(string $pattern): static
+ {
+ return $this->chain(array_filter($this->items(), static function ($value) use ($pattern) {
+ return preg_match($pattern, (string) $value);
+ }));
+ }
+
+ public function byRange($min, $max): static
+ {
+ return $this->chain(array_filter($this->items(), static function ($value) use ($min, $max) {
+ return $value >= $min && $value <= $max;
+ }));
+ }
+
+ public function byInstance(string $type): static
+ {
+ return $this->chain(array_filter($this->items(), static function ($value) use ($type) {
+ return $value instanceof $type;
+ }));
+ }
+
+ public function byUnique(): static
+ {
+ return $this->chain(array_unique($this->items(), SORT_REGULAR));
+ }
+
+ public function byEmpty(): static
+ {
+ return $this->chain(array_filter($this->items(), static fn ($value) => empty($value)));
+ }
+
+ public function byNotEmpty(): static
+ {
+ return $this->chain(array_filter($this->items(), static fn ($value) => ! empty($value)));
+ }
+
+ public function byJsonEncodable(): static
+ {
+ return $this->byType(TypeFilter::JSON_ENCODABLE);
+ }
+
+ public function using(DataCollection $collection): static
+ {
+ $this->collection = $collection;
+ return $this;
+ }
+
+ public function preset(string $name, callable $filter): static
+ {
+ $this->presets[$name] = $filter;
+ return $this;
+ }
+
+ public function shorten(callable $callable): array
+ {
+ return $this->byClosure($callable)->return()->all();
+ }
+
+ /**
+ * Chain method to return to the collection output.
+ *
+ * @alias return
+ */
+ public function and(): DataCollection
+ {
+ return $this->collection();
+ }
+
+ public function return(): DataCollection
+ {
+ return $this->and();
+ }
+
+ protected function items(): array
+ {
+ return $this->collection()->raw();
+ }
+
+ protected function collection(): DataCollection
+ {
+ if ($this->collection) {
+ return $this->collection;
+ }
+
+ throw new RuntimeException('No data collection found.');
+ }
+
+ protected function chain(array $result): static
+ {
+ return $this->collection()
+ ->newInstance([
+ 'items' => $result,
+ 'filter' => $this
+ ])
+ ->filter();
+ }
+}
diff --git a/lib/Magewire/Support/DataCollection/Filter/DataCollectionFilter.php b/lib/Magewire/Support/DataCollection/Filter/DataCollectionFilter.php
new file mode 100644
index 00000000..4daf4056
--- /dev/null
+++ b/lib/Magewire/Support/DataCollection/Filter/DataCollectionFilter.php
@@ -0,0 +1,18 @@
+ static fn ($value) => true,
+ self::NONE => static fn ($value) => false,
+ self::STRINGS => static fn ($value) => is_string($value),
+ self::INTEGERS => static fn ($value) => is_int($value),
+ self::FLOATS => static fn ($value) => is_float($value),
+ self::BOOLEANS => static fn ($value) => is_bool($value),
+ self::ARRAYS => static fn ($value) => is_array($value),
+ self::OBJECTS => static fn ($value) => is_object($value),
+ self::NULLS => static fn ($value) => is_null($value),
+ self::NUMERIC => static fn ($value) => is_numeric($value),
+ self::SCALAR => static fn ($value) => is_scalar($value),
+ self::CALLABLE => static fn ($value) => is_callable($value),
+ self::ITERABLE => static fn ($value) => is_iterable($value),
+ self::COUNTABLE => static fn ($value) => $value instanceof Countable,
+ self::SERIALIZABLE => fn ($value) => $this->isSerializable($value),
+ self::JSON_ENCODABLE => fn ($value) => $this->isJsonEncodable($value)
+ };
+ }
+
+ /**
+ * Combine multiple filters with OR logic.
+ */
+ public static function any(self ...$filters): callable
+ {
+ return static function ($value) use ($filters) {
+ if ($value instanceof DataArray) {
+ $value = $value->all();
+ }
+
+ foreach ($filters as $filter) {
+ $filter = $filter->get();
+
+ if ($filter($value)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+
+ /**
+ * Combine multiple filters with AND logic.
+ */
+ public static function only(self ...$filters): callable
+ {
+ return static function ($value) use ($filters) {
+ if ($value instanceof DataArray) {
+ $value = $value->all();
+ }
+
+ foreach ($filters as $filter) {
+ $filter = $filter->get();
+
+ if ($filter($value)) {
+ return false;
+ }
+ }
+ return true;
+ };
+ }
+
+ private function isSerializable($value): bool
+ {
+ if (is_scalar($value) || is_null($value)) {
+ return true;
+ }
+ if (is_array($value)) {
+ return array_reduce($value, fn ($carry, $item) => $carry && $this->isSerializable($item), true);
+ }
+
+ return false;
+ }
+
+ private function isJsonEncodable($value): bool
+ {
+ try {
+ json_encode($value);
+ } catch (Exception) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/lib/Magewire/Support/DataScope.php b/lib/Magewire/Support/DataScope.php
new file mode 100644
index 00000000..53c46bc4
--- /dev/null
+++ b/lib/Magewire/Support/DataScope.php
@@ -0,0 +1,252 @@
+ $latest */
+ private array $latest = [];
+
+ public function __construct(
+ private readonly DataScope|null $root = null,
+ protected array $data = []
+ ) {
+ }
+
+ /**
+ * Sets a value at the given path, supporting aliasing and array stacking for paths ending in 's'.
+ * Automatically tracks the latest modified path, including numeric keys or aliases.
+ * Ensures the target structure exists before assigning the value.
+ */
+ public function set(string $path, mixed $value, string|null $alias = null): static
+ {
+ $keys = explode('.', $path);
+ $target = array_pop($keys);
+
+ $current = &$this->data($keys, true);
+
+ if (! is_array($current)) {
+ $current = [];
+ }
+
+ if (str_ends_with($path, 's')) {
+ if (! isset($current[$target]) || ! is_array($current[$target])) {
+ $current[$target] = [];
+ }
+
+ if ($alias) {
+ $current[$target][$alias] = $value;
+ $this->latest[] = $path . '.' . $alias;
+ } else {
+ $current[$target][] = $value;
+ $this->latest[] = $path . '.' . array_key_last($current[$target]);
+ }
+
+ return $this;
+ }
+
+ $current[$target] = $value;
+ $this->latest[] = $path;
+
+ return $this;
+ }
+
+ /**
+ * Pushes a value onto the specified section, appending it to an array if the section ends with 's'.
+ * Supports aliasing for array elements and ensures the target structure exists before adding the value.
+ * Automatically tracks the latest modified path.
+ */
+ public function push(string $section, mixed $value, string|null $alias = null, string|null $path = null): static
+ {
+ $section = rtrim($section, 's') . 's';
+
+ return $this->set($path ? $path . '.' . $section : $section, $value, $alias);
+ }
+
+ /**
+ * Groups a value under the specified path by appending it to an array within the given group.
+ * Ensures the target structure exists, initializing it as an array if necessary, before adding the value.
+ * Automatically tracks the latest modified path.
+ */
+ public function group(string $group, string $path, $value): static
+ {
+ $path = $path . '.' . $group;
+
+ $keys = explode('.', $path);
+ $current = &$this->data($keys, true);
+
+ if (! is_array($current)) {
+ $current = [];
+ }
+
+ $current[] = $value;
+ $this->latest[] = $path . '.' . array_key_last($current);
+
+ return $this;
+ }
+
+ /**
+ * Retrieves a value from the given path, with optional alias support and a default fallback.
+ * Uses the data method to navigate nested structures while handling array keys gracefully.
+ * Returns the value if found, or the default if the path or alias does not exist.
+ */
+ public function get(string $path, string|null $alias = null, mixed $default = null): mixed
+ {
+ $keys = explode('.', $path);
+ $target = array_pop($keys);
+
+ $current = $this->data($keys);
+ $target = $alias ?? $target;
+
+ if (is_array($current) && array_key_exists($target, $current)) {
+ return $current[$target];
+ }
+
+ return $default;
+ }
+
+ /**
+ * Checks if a value exists at the given path, optionally using an alias.
+ * Returns true if the value exists, false otherwise.
+ */
+ public function isset(string $path, string|null $alias = null): bool
+ {
+ return $this->get($path, $alias) !== null;
+ }
+
+ /**
+ * Removes the value at the given path, supporting aliases.
+ * Uses the data method to navigate and unset the key if it exists.
+ */
+ public function unset(string $path, string|null $alias = null): static
+ {
+ $keys = explode('.', $path);
+ $target = array_pop($keys);
+
+ $current = &$this->data($keys);
+ $target = $alias ?? $target;
+
+ if (is_array($current) && array_key_exists($target, $current)) {
+ unset($current[$target]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Replaces the value at the given path with the specified value, supporting aliasing.
+ * Uses the data method to navigate nested structures and modifies the value if the key exists.
+ * Does nothing if the path or alias does not exist.
+ */
+ public function replace(string $path, mixed $value, string|null $alias = null): static
+ {
+ $keys = explode('.', $path);
+ $target = array_pop($keys);
+
+ $current = &$this->data($keys);
+ $target = $alias ?? $target;
+
+ if (is_array($current) && array_key_exists($target, $current)) {
+ $current[$target] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns all data, optionally filtered using a callback.
+ * Applies the provided filter to the data if given; otherwise, returns the full dataset.
+ */
+ public function fetch(callable|null $filter = null): array
+ {
+ return $filter ? array_filter($this->data, $filter) : $this->data;
+ }
+
+ public function merge(array $data): static
+ {
+ return $this;
+ }
+
+ /**
+ * Retrieves the most recently modified or added item, or the full dataset if none are tracked.
+ */
+ public function latest(): mixed
+ {
+ $latest = array_pop($this->latest);
+
+ return $latest ? $this->get($latest) : $this->data;
+ }
+
+ /**
+ * Creates a new data scope branching from the root.
+ *
+ * Initializes a new data scope at the specified path with a given alias.
+ * The newly created scope retains access to the root data, allowing for hierarchical data management.
+ * If a scope already exists at the given path and alias, it is returned directly.
+ */
+ public function scope(string $path, string $alias, bool $isolate = false, string $scope = DataScope::class): DataScope
+ {
+ $branch = $this->get($path, $alias);
+
+ if ($branch) {
+ return $branch;
+ }
+
+ $branch = ObjectManager::getInstance()->create($scope, [
+ 'root' => $isolate ? $this : $this->root()
+ ]);
+
+ return $this->set($path, $branch, $alias)->latest();
+ }
+
+ /**
+ * Returns the root data scope instance.
+ * If no root is set, returns the current instance as the root.
+ */
+ public function root(): DataScope
+ {
+ return $this->root ?? $this;
+ }
+
+ /**
+ * Traverses the data structure by following the given keys, optionally creating missing keys.
+ * Returns a reference to the target value if found, or null if the path is invalid and no construction is allowed.
+ * Supports array key navigation with an option to auto-construct missing arrays.
+ */
+ private function &data(array $keys, bool $construct = false): mixed
+ {
+ $current = &$this->data;
+
+ foreach ($keys as $key) {
+ if (! isset($current[$key])) {
+ if ($construct) {
+ $current[$key] = [];
+ }
+ }
+
+ if (! is_array($current[$key])) {
+ if ($construct) {
+ $current[$key] = [];
+ }
+ }
+
+ $current = &$current[$key];
+ }
+
+ return $current;
+ }
+}
diff --git a/lib/Magewire/Support/DataScope/Compiler.php b/lib/Magewire/Support/DataScope/Compiler.php
new file mode 100644
index 00000000..0789fd63
--- /dev/null
+++ b/lib/Magewire/Support/DataScope/Compiler.php
@@ -0,0 +1,38 @@
+ $uses */
+ protected array $uses = [];
+
+ abstract public function compile(DataScope $data): array|string;
+
+ /**
+ * Assign an additional dependency to be used by callable values requiring
+ * access to global variables that are otherwise inaccessible during injection.
+ */
+ public function use(string $name, object $object): static
+ {
+ $this->uses[$name] = $object;
+
+ return $this;
+ }
+
+ protected function uses(): array
+ {
+ return $this->uses;
+ }
+}
diff --git a/lib/Magewire/Support/DataScope/Compiler/RecursiveArray.php b/lib/Magewire/Support/DataScope/Compiler/RecursiveArray.php
new file mode 100644
index 00000000..c285c2f2
--- /dev/null
+++ b/lib/Magewire/Support/DataScope/Compiler/RecursiveArray.php
@@ -0,0 +1,47 @@
+fetch() as $key => $value) {
+ if (is_array($value)) {
+ $result[$key] = array_map(function ($item) {
+ if ($item instanceof DataScope) {
+ return $this->compile($item);
+ }
+
+ return $item;
+ }, $value);
+ } elseif ($value instanceof DataScope) {
+ $result[$key] = $this->compile($value);
+ } elseif (is_callable($value)) {
+ try {
+ $result[$key] = $value(array_reverse(array_values($this->uses())));
+ } catch (Exception $exception) {
+ // WIP: logging needs to be implemented in a way...
+ }
+ } else {
+ $result[$key] = $value;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/Magewire/Support/DataScope/InvalidDataScopeException.php b/lib/Magewire/Support/DataScope/InvalidDataScopeException.php
new file mode 100644
index 00000000..3470c4c6
--- /dev/null
+++ b/lib/Magewire/Support/DataScope/InvalidDataScopeException.php
@@ -0,0 +1,18 @@
+getMessage() : $message;
+ $this->metadata()->push('logs', $message);
+
+ return $this;
+ }
+
+ /**
+ * Metadata entry.
+ */
+ public function metadata(): Metadata
+ {
+ return $this->data()->get(self::METADATA);
+ }
+
+ public function report(): array
+ {
+ return array_merge($this->data()->all(), ['__metadata' => $this->metadata()]);
+ }
+
+ protected function data(): DataArray
+ {
+ return $this->data ??= $this->newTypeInstance(DataArray::class)->set(self::METADATA, $this->newTypeInstance(Metadata::class)
+ ->data()
+ ->defaults([
+ 'logs' => []
+ ]));
+ }
+}
diff --git a/lib/Magewire/Support/Distributor.php b/lib/Magewire/Support/Distributor.php
new file mode 100644
index 00000000..9bfdb555
--- /dev/null
+++ b/lib/Magewire/Support/Distributor.php
@@ -0,0 +1,74 @@
+ */
+ protected array $instances = [];
+
+ /**
+ * @param class-string $type
+ * @param array> $mapping
+ */
+ public function __construct(
+ protected string $type,
+ protected array $mapping = []
+ ) {
+ }
+
+ /**
+ * @return object
+ */
+ public function __call(string $name, array $arguments = [])
+ {
+ return $this->instances[$name] ??= $this->create($name, $arguments);
+ }
+
+ /**
+ * Resolve the appropriate type for a given name.
+ *
+ * @param string $name The instance identifier
+ * @return class-string The resolved class name
+ */
+ protected function resolve(string $name): string
+ {
+ $map = $this->mapping[$name] ?? null;
+
+ if ($map !== null && is_a($map, $this->type, true)) {
+ return $map;
+ }
+
+ return $this->type;
+ }
+
+ protected function create(string|null $type = null, array $arguments = []): object
+ {
+ if ($type === null) {
+ return Factory::create($this->type, $arguments);
+ }
+
+ return Factory::create($this->resolve($type), $arguments);
+ }
+}
diff --git a/lib/Magewire/Support/Factory.php b/lib/Magewire/Support/Factory.php
new file mode 100644
index 00000000..1818c5d8
--- /dev/null
+++ b/lib/Magewire/Support/Factory.php
@@ -0,0 +1,42 @@
+ $type
+ * @return T
+ */
+ public static function create(string $type, array $arguments = [])
+ {
+ // POC: Factories can be singletons because of their single responsibility.
+ if (str_ends_with($type, 'Factory')) {
+ return static::get($type);
+ }
+
+ return ObjectManager::getInstance()->create($type, $arguments); // phpcs:ignore
+ }
+
+ /**
+ * @template T
+ * @param class-string $type
+ * @return T
+ */
+ public static function get(string $type)
+ {
+ return ObjectManager::getInstance()->get($type); // phpcs:ignore
+ }
+}
diff --git a/lib/Magewire/Support/Metadata.php b/lib/Magewire/Support/Metadata.php
new file mode 100644
index 00000000..5d12ce23
--- /dev/null
+++ b/lib/Magewire/Support/Metadata.php
@@ -0,0 +1,74 @@
+data()->get($prop, 0);
+
+ if (is_int($subject)) {
+ $this->data()->set($prop, $subject + $by);
+ }
+
+ return $this;
+ }
+
+ public function decrement(string $prop, int $by = 1): static
+ {
+ $subject = $this->data()->get($prop, 0);
+
+ if (is_int($subject) && $subject > 0) {
+ $this->data()->set($prop, $subject - $by);
+ }
+
+ return $this;
+ }
+
+ public function push(string $prop, mixed $value, string|null $key = null): static
+ {
+ $key ??= Random::integer();
+
+ if (is_array($this->get($prop))) {
+ $this->data()->set($prop, array_merge($this->get($prop), [$key => $value]));
+ }
+
+ return $this;
+ }
+
+ public function get(string $prop): mixed
+ {
+ return $this->data()->get($prop);
+ }
+
+ public function fetch(callable $filter): array
+ {
+ return $this->data()
+ ->filter()
+ ->byClosure($filter)
+ ->all();
+ }
+
+ protected function data(): DataArray
+ {
+ return $this->data ??= $this->dataArrayFactory->create();
+ }
+}
diff --git a/lib/Magewire/Support/Middleware.php b/lib/Magewire/Support/Middleware.php
new file mode 100644
index 00000000..5e05d168
--- /dev/null
+++ b/lib/Magewire/Support/Middleware.php
@@ -0,0 +1,81 @@
+ */
+ protected array $groups = [];
+ /** @var array */
+ protected array $positions = [];
+
+ public function run(mixed $throughput, callable|null $action = null): mixed
+ {
+ $origin = $this->pipes;
+ $this->pipes[] = $action ?? static fn (mixed $t) => $t;
+
+ try {
+ return parent::run($this->processGroups($throughput));
+ } finally {
+ $this->pipes = $origin;
+ }
+ }
+
+ /**
+ * Create or retrieve a middleware group with execution position.
+ */
+ public function group(string $name, int $position = 500): Pipeline
+ {
+ $this->positions[$name] ??= $position;
+
+ if (! isset($this->groups[$name])) {
+ $this->groups[$name] = $this->newTypeInstance(Pipeline::class);
+ }
+
+ return $this->groups[$name];
+ }
+
+ /**
+ * Execute all groups in position order (ascending = lower first).
+ *
+ * @throws Throwable
+ */
+ protected function processGroups(mixed $throughput): mixed
+ {
+ if (empty($this->groups)) {
+ return $throughput;
+ }
+
+ $groups = $this->groups;
+
+ uasort($groups, function ($a, $b) {
+ $posA = $this->positions[array_search($a, $this->groups, true)] ?? 500;
+ $posB = $this->positions[array_search($b, $this->groups, true)] ?? 500;
+
+ return $posA <=> $posB;
+ });
+
+ foreach ($groups as $group) {
+ $throughput = $group->run($throughput);
+ }
+
+ return $throughput;
+ }
+}
diff --git a/lib/Magewire/Support/Parser.php b/lib/Magewire/Support/Parser.php
new file mode 100644
index 00000000..c4417d22
--- /dev/null
+++ b/lib/Magewire/Support/Parser.php
@@ -0,0 +1,24 @@
+attributes()->set($match[1], $match[3]);
+ }
+
+ return $this;
+ }
+
+ public function attributes(): DataArray
+ {
+ return $this->attributes ??= $this->attributesFactory->create();
+ }
+}
diff --git a/lib/Magewire/Support/Pipeline.php b/lib/Magewire/Support/Pipeline.php
new file mode 100644
index 00000000..40b804fe
--- /dev/null
+++ b/lib/Magewire/Support/Pipeline.php
@@ -0,0 +1,206 @@
+ $pipes */
+ protected array $pipes = [];
+ /** @var Middleware|null $middleware */
+ protected Middleware|null $middleware = null;
+ /** @var array $handlers */
+ private array $handlers = [];
+
+ public function __construct(
+ protected LoggerInterface $logger
+ ) {
+ }
+
+ /**
+ * @throws Throwable
+ */
+ public function run(mixed $throughput): mixed
+ {
+ return $this->processPipes($throughput);
+ }
+
+ /**
+ * Register a pipe in the pipeline.
+ *
+ * Pipes are callbacks that receive the current throughput and a $next callable.
+ * They must call $next($throughput) to continue the pipeline chain.
+ *
+ * @example $pipeline->pipe(fn ($throughput, callable $next) => $next($throughput));
+ */
+ public function pipe(callable $callback): static
+ {
+ $this->pipes[] = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Pipeline middleware entrypoint.
+ */
+ public function middleware(): Middleware
+ {
+ return $this->middleware ??= $this->newInstance([], Middleware::class);
+ }
+
+ /**
+ * Register a handler to execute when an exception occurs during pipeline execution.
+ *
+ * Catch handlers receive the exception and logger as arguments.
+ * Multiple catch handlers can be registered and will all be executed.
+ *
+ * @example $pipeline->onCatch(fn($e, $logger) => $logger->error($e->getMessage()));
+ */
+ public function onCatch(callable $handler, string|null $alias = null): static
+ {
+ return $this->registerHandler('catch', $handler, $alias);
+ }
+
+ /**
+ * Register a handler to execute after pipeline completion (success or failure).
+ *
+ * Finally handlers receive the throughput as an argument and are always executed,
+ * similar to a try-finally block.
+ *
+ * @example $pipeline->onFinally(fn($data) => cleanup($data));
+ */
+ public function onFinally(callable $handler, string|null $alias = null): static
+ {
+ return $this->registerHandler('finally', $handler, $alias);
+ }
+
+ /**
+ * Register a handler to execute after pipeline completion (success only).
+ *
+ * Success handlers receive the throughput as an argument and are always executed
+ *
+ * @example $pipeline->onFinally(fn($data) => cleanup($data));
+ */
+ public function onSuccess(callable $handler, string|null $alias = null): static
+ {
+ return $this->registerHandler('success', $handler, $alias);
+ }
+
+ /**
+ * Get the number of currently registered pipes.
+ */
+ public function count(): int
+ {
+ return count($this->pipes);
+ }
+
+ /**
+ * Process all pipes in the pipeline with the given throughput.
+ *
+ * Couples all pipes into a single callable chain, applies middleware if configured,
+ * handles exceptions with catch handlers, and ensures finally handlers are executed.
+ * Performs cleanup after successful execution to remove one-time pipes.
+ */
+ protected function processPipes(mixed $throughput): mixed
+ {
+ try {
+ $pipeline = $this->couple($this->pipes);
+
+ $throughput = $this->middleware ? $this->middleware()->run($throughput, $pipeline) : $pipeline($throughput);
+
+ $this->processHandler('success', $throughput);
+ } catch (Throwable $exception) {
+ $this->processHandler('catch', $exception, $this->logger);
+ } finally {
+ $this->processHandler('finally', $throughput);
+ }
+
+ return $throughput;
+ }
+
+ /**
+ * Execute all registered handlers for a specific event type.
+ *
+ * Automatically logs critical exceptions when processing 'catch' handlers.
+ * If a handler itself throws an exception, it's logged but doesn't interrupt other handlers.
+ */
+ protected function processHandler(string $handler, mixed ...$args): static
+ {
+ $handlers = $this->handlers[$handler] ?? [];
+
+ // Re-throw exception if no catch handlers are registered to process it.
+ if (empty($handlers) && $handler === 'catch' && ( $args[0] ?? null ) instanceof Throwable) {
+ throw $args[0];
+ }
+
+ $class = basename(str_replace('\\', '/', __CLASS__));
+
+ foreach ($handlers as $callback) {
+ try {
+ $callback(...$args);
+ } catch (Throwable $exception) {
+ $this->logger->error($class . ' exception: ' . $exception->getMessage(), [
+ 'handler' => $handler,
+ 'exception' => $exception
+ ]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Register a handler for a specific event type.
+ *
+ * Prevents duplicate registration of handlers with the same alias unless
+ * the force flag is set to true.
+ */
+ protected function registerHandler(
+ string $event,
+ callable $handler,
+ string|null $alias = null
+ ): static {
+ $this->handlers[$event][$alias ?? Random::alphabetical()] = $handler;
+
+ return $this;
+ }
+
+ /**
+ * Couple multiple pipes into a single callable chain.
+ *
+ * Uses array_reduce to build a nested chain of callables where each pipe
+ * wraps the next, creating a middleware-style execution pattern.
+ */
+ protected function couple(array $pipes): callable
+ {
+ $core = static fn (mixed $throughput): mixed => $throughput;
+
+ foreach (array_reverse($pipes) as $pipe) {
+ $core = static function (mixed $throughput) use ($pipe, $core): mixed {
+ return $pipe($throughput, $core);
+ };
+ }
+
+ return $core;
+ }
+}
diff --git a/lib/Magewire/Support/Random.php b/lib/Magewire/Support/Random.php
new file mode 100644
index 00000000..4265a3ed
--- /dev/null
+++ b/lib/Magewire/Support/Random.php
@@ -0,0 +1,34 @@
+actions[] = static function () use ($action, $scheduler) {
+ // TBD.
+ };
+
+ return $scheduler;
+ }
+}
diff --git a/lib/Magewire/Support/Scheduler.php b/lib/Magewire/Support/Scheduler.php
new file mode 100644
index 00000000..eb0630e9
--- /dev/null
+++ b/lib/Magewire/Support/Scheduler.php
@@ -0,0 +1,63 @@
+newTypeInstance(Interval::class, ['interval' => $interval]);
+ }
+
+ public function daily(): static
+ {
+ $this->every(1)->days();
+ return $this;
+ }
+
+ public function monthly(): Interval
+ {
+ return $this->every(1)->months();
+ }
+
+ public function yearly($interval): Interval
+ {
+ return $this->every(1)->years();
+ }
+
+ public function when(callable $condition): Conditions
+ {
+ return $this->conditions()->if($condition);
+ }
+
+ public function unless(callable $condition): Conditions
+ {
+ return $this->when(static fn (...$args) => ! $condition(...$args));
+ }
+
+ protected function conditions(): Conditions
+ {
+ return $this->conditions ??= $this->newTypeInstance(Conditions::class);
+ }
+}
diff --git a/lib/Magewire/Support/Scheduler/Interval.php b/lib/Magewire/Support/Scheduler/Interval.php
new file mode 100644
index 00000000..cdba7779
--- /dev/null
+++ b/lib/Magewire/Support/Scheduler/Interval.php
@@ -0,0 +1,55 @@
+unit = 'minutes';
+ return $this;
+ }
+
+ public function hours(): static
+ {
+ $this->unit = 'hours';
+ return $this;
+ }
+
+ public function days(): static
+ {
+ $this->unit = 'days';
+ return $this;
+ }
+
+ public function months(): static
+ {
+ $this->unit = 'months';
+ return $this;
+ }
+
+ public function years(): static
+ {
+ $this->unit = 'years';
+ return $this;
+ }
+}
diff --git a/lib/Magewire/Support/Str.php b/lib/Magewire/Support/Str.php
new file mode 100644
index 00000000..4e425650
--- /dev/null
+++ b/lib/Magewire/Support/Str.php
@@ -0,0 +1,16 @@
+enabled;
+ }
+}
diff --git a/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/HandlesComponentBackwardsCompatibility.php b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/HandlesComponentBackwardsCompatibility.php
new file mode 100644
index 00000000..50395016
--- /dev/null
+++ b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/HandlesComponentBackwardsCompatibility.php
@@ -0,0 +1,66 @@
+magewireResolvePublicProperties();
+ }
+
+ if ($refresh || $this->__publicProperties === null) {
+ $this->__publicProperties = $this->magewireResolvePublicProperties();
+ }
+
+ return $this->__publicProperties;
+ }
+
+ private function magewireResolvePublicProperties(): array
+ {
+ return array_diff_key(Utils::getPublicProperties($this), array_flip(self::RESERVED_PROPERTIES));
+ }
+}
diff --git a/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/HandlesFormComponentBackwardsCompatibility.php b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/HandlesFormComponentBackwardsCompatibility.php
new file mode 100644
index 00000000..b9cf7c52
--- /dev/null
+++ b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/HandlesFormComponentBackwardsCompatibility.php
@@ -0,0 +1,20 @@
+component();
+
+ // Continue if components isn't available or backwards compatible.
+ if (! is_string($name) || ! $component || ! store($component)->get('magewire:bc') ?? false) {
+ return [$name, $params];
+ }
+
+ foreach ($this->resolvers as $resolver) {
+ if (! $resolver->supports($component, $name)) {
+ continue;
+ }
+
+ [$name, $params] = $resolver->resolve($component, $name, $params);
+ break;
+ }
+
+ return [$name, $params];
+ }
+}
diff --git a/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/Resolver/ArrayPropertyReconstructionResolver.php b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/Resolver/ArrayPropertyReconstructionResolver.php
new file mode 100644
index 00000000..8f3bc698
--- /dev/null
+++ b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/Resolver/ArrayPropertyReconstructionResolver.php
@@ -0,0 +1,105 @@
+mapping as $class => $methods) {
+ if (is_a($component, $class) && ($methods[$method] ?? null) !== null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function resolve(Component $component, string $method, array $params): array
+ {
+ if (count($params) < 2) {
+ return [$method, $params];
+ }
+
+ $property = $this->extractPropertyName($method);
+
+ if ($property === null || ! property_exists($component, $property)) {
+ return [$method, $params];
+ }
+
+ [$value, $path] = $params;
+
+ $data = $component->{$property};
+
+ if (is_array($data) && is_string($path)) {
+ $this->fill($data, $path, $value);
+ }
+
+ return [$method, [$property => $data]];
+ }
+
+ /**
+ * Extract the property name from a hook method like 'updatingData' or 'updatedAddress'.
+ */
+ private function extractPropertyName(string $method): string|null
+ {
+ if (preg_match('/^(?:updating|updated)(.+)$/', $method, $matches)) {
+ return lcfirst($matches[1]);
+ }
+
+ return null;
+ }
+
+ /**
+ * Set a value in a nested array using dot notation path.
+ */
+ private function fill(array &$array, string $path, mixed $value): void
+ {
+ $parts = explode('.', $path);
+ $current = &$array;
+
+ foreach ($parts as $index => $part) {
+ if ($index === array_key_last($parts)) {
+ $current[$part] = $value;
+ continue;
+ }
+
+ if (! is_array($current[$part] ?? null)) {
+ $current[$part] = [];
+ }
+
+ $current = &$current[$part];
+ }
+ }
+}
diff --git a/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/Resolver/UpdatingUpdatedArgumentSwapResolver.php b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/Resolver/UpdatingUpdatedArgumentSwapResolver.php
new file mode 100644
index 00000000..ff941c45
--- /dev/null
+++ b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/Resolver/UpdatingUpdatedArgumentSwapResolver.php
@@ -0,0 +1,36 @@
+$method($component, $method, $params);
+ }
+
+ /**
+ * Swap parameters when backwards compatible.
+ */
+ private function updated(Component $component, string $method, array $params): array
+ {
+ return [$method, [$params[1], $params[0]]];
+ }
+}
diff --git a/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/SupportMagewireBackwardsCompatibility.php b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/SupportMagewireBackwardsCompatibility.php
new file mode 100644
index 00000000..01ea28e7
--- /dev/null
+++ b/lib/MagewireBc/Features/SupportMagewireBackwardsCompatibility/SupportMagewireBackwardsCompatibility.php
@@ -0,0 +1,52 @@
+component
+ ->getRequest()
+ ->setServerMemo([
+ 'data' => $this->getProperties()
+ ]);
+ }
+
+ public function dehydrate(ComponentContext $context): void
+ {
+ $bc = [
+ 'map' => $this->resolvePathsMap(),
+ 'preferences' => $this->resolvePreferences()
+ ];
+
+ foreach ($bc as $ikey => $value) {
+ $context->pushEffect('bc', $value, $ikey);
+ }
+ }
+
+ private function resolvePathsMap(): array
+ {
+ return [
+ 'data' => 'path:$wire',
+ '__livewire' => 'path:queuedUpdates'
+ ];
+ }
+
+ private function resolvePreferences(): array
+ {
+ return [];
+ }
+}
diff --git a/lib/MagewireBc/Model/Component/Resolver/Layout.php b/lib/MagewireBc/Model/Component/Resolver/Layout.php
new file mode 100644
index 00000000..4c3c3fd2
--- /dev/null
+++ b/lib/MagewireBc/Model/Component/Resolver/Layout.php
@@ -0,0 +1,134 @@
+resultPageFactory = $resultPageFactory;
+ $this->eventManager = $eventManager;
+ $this->componentFactory = $componentFactory;
+ }
+
+ public function getName(): string
+ {
+ return 'layout';
+ }
+
+ public function complies(AbstractBlock $block): bool
+ {
+ return true;
+ }
+
+ /**
+ * @throws MissingComponentException
+ */
+ public function construct(AbstractBlock $block): Component
+ {
+ $magewire = $block->getData('magewire');
+
+ if ($magewire) {
+ $component = is_array($magewire)
+ ? $magewire['type'] : (is_object($magewire)
+ ? $magewire : $this->componentFactory->create());
+
+ if ($component instanceof Component) {
+// if ($this->init) {
+// $component = $this->componentFactory->create($component);
+// }
+
+ $component->name = $block->getNameInLayout();
+ $component->id ??= $component->name;
+
+ return $component->setParent($this->determineTemplate($block, $component));
+ }
+ }
+
+ throw new MissingComponentException(__('Magewire component not found'));
+ }
+
+ /**
+ * @throws MissingComponentException
+ */
+ public function reconstruct(RequestInterface $request): Component
+ {
+ $page = $this->resultPageFactory->create();
+ $page->addHandle(strtolower($request->getFingerprint('handle')))->initLayout();
+
+ /**
+ * @deprecated this code is no longer supported and may cause issues if used.
+ * Please do not use it in the future.
+ */
+ $this->eventManager->dispatch('locate_wire_component_before', [
+ 'post' => $request->toArray(),
+ 'page' => $page
+ ]);
+
+ /** @var Template|false $block */
+ $block = $page->getLayout()->getBlock($request->getFingerprint('name'));
+
+ if ($block === false) {
+ throw new HttpException(
+ 404,
+ sprintf('Magewire component "%1s" could not be found', $request->getFingerprint('name'))
+ );
+ }
+
+ return $this->construct($block);
+ }
+
+ /**
+ * Determines the template by a default template path
+ * when the path is not defined within the layout.
+ *
+ * Results in: {Module_Name::magewire/dashed-class-name.phtml}
+ */
+ protected function determineTemplate(BlockInterface $block, MagewireComponent $component): Template
+ {
+ /** @var Template $block */
+
+ if ($block->getTemplate() !== null) {
+ return $block;
+ }
+
+ $module = array_filter(
+ explode('\\', get_class($component)),
+ static fn (string $part) => $part !== 'Interceptor'
+ );
+
+ $prefix = $module[0] . '_' . $module[1];
+ $affix = strtolower(preg_replace('/(?setTemplate($prefix . '::magewire/' . $affix . '.phtml');
+ }
+}
diff --git a/lib/MagewireBc/Model/Component/ResolverInterface.php b/lib/MagewireBc/Model/Component/ResolverInterface.php
new file mode 100644
index 00000000..dec89240
--- /dev/null
+++ b/lib/MagewireBc/Model/Component/ResolverInterface.php
@@ -0,0 +1,52 @@
+objectManager = $objectManager;
+ $this->layout = $layout;
+ }
+
+ public function create(?Component $component = null, array $data = []): Component
+ {
+ $class = $component ? get_class($component) : Component::class;
+
+ if (isset($this->instances[$class])) {
+ return $this->objectManager->create($class, [$data]);
+ }
+
+ $this->instances[$class] = $component;
+ return $component;
+ }
+}
diff --git a/lib/MagewireBc/Model/ComponentResolver.php b/lib/MagewireBc/Model/ComponentResolver.php
new file mode 100644
index 00000000..2364d8ff
--- /dev/null
+++ b/lib/MagewireBc/Model/ComponentResolver.php
@@ -0,0 +1,21 @@
+dispatchQueue;
+ }
+
+ /**
+ * @deprecated Has been replaced with a universal "dispatch" method.
+ * @see dispatch()
+ */
+ public function dispatchBrowserEvent($event, $data = null): void
+ {
+ $this->dispatch($event, ...$data);
+ }
+}
diff --git a/lib/MagewireBc/Model/Concern/Emit.php b/lib/MagewireBc/Model/Concern/Emit.php
new file mode 100644
index 00000000..34f8672c
--- /dev/null
+++ b/lib/MagewireBc/Model/Concern/Emit.php
@@ -0,0 +1,75 @@
+ $params
+ */
+ public function emit(string $event, $params = []): Event
+ {
+ return $this->dispatch($event, $params);
+ }
+
+ /**
+ * @param array $params
+ */
+ public function emitUp(string $event, $params = []): Event
+ {
+ }
+
+ /**
+ * Only emit an event on the component that fired the event.
+ *
+ * @param array $params
+ */
+ public function emitSelf(string $event, $params = []): Event
+ {
+ }
+
+ /**
+ * Only emit an event to other components of the same type.
+ *
+ * @param array $params
+ */
+ public function emitTo(string $name, string $event, $params = []): Event
+ {
+ return $this->emit($event, $params)->to($name);
+ }
+
+ /**
+ * Only emit a "refresh" event to other components of the same type.
+ *
+ * @param array $params
+ */
+ public function emitToRefresh(string $name, $params = []): Event
+ {
+ }
+
+ /**
+ * Refresh all parents.
+ *
+ * @param array $params
+ */
+ public function emitToRefreshUp($params = []): Event
+ {
+ }
+}
diff --git a/src/Model/Concern/Error.php b/lib/MagewireBc/Model/Concern/Error.php
similarity index 67%
rename from src/Model/Concern/Error.php
rename to lib/MagewireBc/Model/Concern/Error.php
index 6fbcd10c..abb182c6 100644
--- a/src/Model/Concern/Error.php
+++ b/lib/MagewireBc/Model/Concern/Error.php
@@ -1,4 +1,5 @@
-errors;
}
- foreach ($targets as $key => $value)
- {
+ foreach ($targets as $key => $value) {
if (isset($this->errors[$value])) {
$targets[$value] = $this->errors[$value];
}
@@ -52,30 +49,24 @@ public function getErrors(array $targets = []): array
return $targets;
}
- /**
- * @param array $targets
- * @return bool
- */
public function hasErrors(array $targets = []): bool
{
- return !empty($this->getErrors($targets));
+ return ! empty($this->getErrors($targets));
}
- /**
- * @param string $property
- * @return bool
- */
public function hasError(string $property): bool
{
return isset($this->errors[$property]);
}
- /**
- * @param string $property
- * @return Phrase
- */
public function getError(string $property): Phrase
{
return $this->hasError($property) ? $this->errors[$property] : __('No %1 error found', [$property]);
}
+
+ public function clearErrors(): self
+ {
+ $this->errors = [];
+ return $this;
+ }
}
diff --git a/lib/MagewireBc/Model/Concern/Request.php b/lib/MagewireBc/Model/Concern/Request.php
new file mode 100644
index 00000000..a539b579
--- /dev/null
+++ b/lib/MagewireBc/Model/Concern/Request.php
@@ -0,0 +1,36 @@
+__magewireRequest = $request;
+ }
+
+ return $this->__magewireRequest ??= Factory::create(RequestInterface::class);
+ }
+}
diff --git a/lib/MagewireBc/Model/Concern/View.php b/lib/MagewireBc/Model/Concern/View.php
new file mode 100644
index 00000000..7ecbfba0
--- /dev/null
+++ b/lib/MagewireBc/Model/Concern/View.php
@@ -0,0 +1,58 @@
+skipRender;
+ }
+
+ return store($this)->get('skipRender', false);
+ }
+
+ /**
+ * Switch template of the parent block.
+ *
+ * @deprecated Has been replaced with calling the 'magewireBlock' manually and is no longer a shortcut method.
+ * @throws BackwardsIncompatibilityException
+ */
+ public function switchTemplate(string $template): void
+ {
+ if (method_exists($this, 'magewireBlock')) {
+ $block = $this->magewireBlock();
+
+ if ($block instanceof Template) {
+ $block->setTemplate($template);
+ return;
+ }
+ }
+
+ throw new BackwardsIncompatibilityException('Something went wrong while switching template.');
+ }
+}
diff --git a/src/Model/HydratorInterface.php b/lib/MagewireBc/Model/HydratorInterface.php
similarity index 89%
rename from src/Model/HydratorInterface.php
rename to lib/MagewireBc/Model/HydratorInterface.php
index 5a61d15a..87ca770d 100644
--- a/src/Model/HydratorInterface.php
+++ b/lib/MagewireBc/Model/HydratorInterface.php
@@ -1,4 +1,5 @@
+ */
+ private array $history = [];
+
+ /**
+ * @var array
+ */
+ private array $views = [];
+
+ private string|null $start = null;
+
+ /**
+ * Marks view as 'start rendering'.
+ */
+ public function start(string $name): LayoutRenderLifecycle
+ {
+ if ($this->start === null) {
+ $this->start = $name;
+ }
+
+ $this->views[$name] = null;
+ return $this;
+ }
+
+ /**
+ * Marks view as 'stop rendering'.
+ */
+ public function stop(string $parent): LayoutRenderLifecycle
+ {
+ $views = $this->getViews();
+ $position = array_search($parent, array_keys($views), true);
+
+ if ($position === false) {
+ return $this;
+ }
+
+ $children = array_slice($views, $position + 1, count($views), true);
+
+ // Special use case where a single component on the page doesn't have a child.
+ if (isset($this->views[$parent]) && $parent === $this->start) {
+ $children[$parent] = $this->views[$parent];
+ }
+
+ foreach ($children as $key => $value) {
+ if ($parent === $this->start) {
+ $this->history[$key] = $value;
+ } else {
+ $this->history[$parent][$key] = $value;
+ }
+
+ unset($this->views[$key]);
+ }
+
+ return $this;
+ }
+
+ public function exists(string $name): bool
+ {
+ return array_key_exists($name, $this->views);
+ }
+
+ public function canStop(string $name): bool
+ {
+ return $name !== array_search($name, array_reverse($this->views), true) || $name === $this->start;
+ }
+
+ public function getViews(): array
+ {
+ return $this->views;
+ }
+
+ public function hasHistory(): bool
+ {
+ return count($this->getHistory()) !== 0;
+ }
+
+ public function getHistory(): array
+ {
+ return $this->history;
+ }
+
+ public function setStartTag(string $tag, string $for): LayoutRenderLifecycle
+ {
+ $this->views[$for] = $tag;
+ return $this;
+ }
+
+ /**
+ * Validate if it's the overall highest laying component without a parent.
+ */
+ public function isParent(string $name): bool
+ {
+ $views = $this->getViews();
+ $parent = array_search($name, array_keys($views), true);
+
+ return $parent === 0;
+ }
+
+ /**
+ * Validate if it's a nested component (no matter the depth).
+ */
+ public function isChild(string $name): bool
+ {
+ return ! $this->isParent($name);
+ }
+
+ /**
+ * Validate if family member of the given parent name.
+ */
+ public function isDescendant(string $from): bool
+ {
+ return array_key_exists($from, $this->getViews()) && $this->views[$from] === null;
+ }
+}
diff --git a/lib/MagewireBc/Model/Request.php b/lib/MagewireBc/Model/Request.php
new file mode 100644
index 00000000..bd9f3fb6
--- /dev/null
+++ b/lib/MagewireBc/Model/Request.php
@@ -0,0 +1,107 @@
+memo)) {
+ return $this->memo[$index] ?? null;
+ }
+
+ return $this->memo;
+ }
+
+ public function setServerMemo($memo): \Magewirephp\Magewire\Model\RequestInterface
+ {
+ $this->memo = $memo;
+ return $this;
+ }
+
+ public function getUpdates(string $index)
+ {
+ // TODO: Implement getUpdates() method.
+ }
+
+ public function setUpdates($updates): \Magewirephp\Magewire\Model\RequestInterface
+ {
+ return $this;
+ }
+
+ public function getSectionByName(string $section): array|null
+ {
+ return null;
+ }
+
+ public function isSubsequent(bool|null $flag = null, bool $force = false)
+ {
+ return $this->magewireServiceProvider
+ ->runtime()
+ ->mode()
+ ->isSubsequent();
+ }
+
+ public function isPreceding(): bool
+ {
+ return ! $this->isSubsequent();
+ }
+
+ public function isRefreshing(bool|null $flag = null)
+ {
+ return false;
+ }
+
+ public function toArray(): array
+ {
+ return [];
+ }
+}
diff --git a/src/Model/RequestInterface.php b/lib/MagewireBc/Model/RequestInterface.php
similarity index 66%
rename from src/Model/RequestInterface.php
rename to lib/MagewireBc/Model/RequestInterface.php
index 0d3b6bd7..eb182eac 100644
--- a/src/Model/RequestInterface.php
+++ b/lib/MagewireBc/Model/RequestInterface.php
@@ -1,4 +1,5 @@
obj = $obj;
+ $this->reflected = new ReflectionClass($obj);
+ }
+
+ public function &__get($name)
+ {
+ $getProperty = function &() use ($name) {
+ return $this->{$name};
+ };
+
+ $getProperty = $getProperty->bindTo($this->obj, get_class($this->obj));
+
+ return $getProperty();
+ }
+
+ public function __set($name, $value)
+ {
+ $setProperty = function () use ($name, &$value) {
+ $this->{$name} = $value;
+ };
+
+ $setProperty = $setProperty->bindTo($this->obj, get_class($this->obj));
+
+ $setProperty();
+ }
+
+ public function __call($name, $params)
+ {
+ $method = $this->reflected->getMethod($name);
+
+ $method->setAccessible(true);
+
+ return $method->invoke($this->obj, ...$params);
+ }
+ };
+}
+
+function once($fn)
+{
+ $hasRun = false;
+
+ return function (...$params) use ($fn, &$hasRun) {
+ if ($hasRun) {
+ return;
+ }
+
+ $hasRun = true;
+
+ return $fn(...$params);
+ };
+}
+
+function of(...$params)
+{
+ return $params;
+}
+
+function revert(&$variable)
+{
+ $cache = $variable;
+
+ return function () use (&$variable, $cache) {
+ $variable = $cache;
+ };
+}
+
+function wrap($subject)
+{
+ return ObjectManager::getInstance()->create(Wrapped::class, [
+ 'target' => $subject
+ ]);
+}
+
+function pipe($subject)
+{
+ return ObjectManager::getInstance()->create(Pipe::class, [
+ 'target' => $subject
+ ]);
+}
+
+function trigger($name, ...$params)
+{
+ return ObjectManager::getInstance()->get(EventBus::class)->trigger($name, ...$params);
+}
+
+function on($name, $callback)
+{
+ return ObjectManager::getInstance()->get(EventBus::class)->on($name, $callback);
+}
+
+function after($name, $callback)
+{
+ return ObjectManager::getInstance()->get(EventBus::class)->after($name, $callback);
+}
+
+function before($name, $callback)
+{
+ return ObjectManager::getInstance()->get(EventBus::class)->before($name, $callback);
+}
+
+function off($name, $callback)
+{
+ return ObjectManager::getInstance()->get(EventBus::class)->off($name, $callback);
+}
+
+function memoize($target)
+{
+ static $memo = new \WeakMap();
+
+ return new class($target, $memo) {
+ public function __construct(
+ protected $target,
+ protected &$memo
+ ) {
+ }
+
+ public function __call($method, $params)
+ {
+ $this->memo[$this->target] ??= [];
+
+ $signature = $method . crc32(json_encode($params));
+
+ return $this->memo[$this->target][$signature] ??= $this->target->$method(...$params);
+ }
+ };
+}
+
+function store($instance = null)
+{
+ if (! $instance) {
+ $instance = ObjectManager::getInstance()->get(\Magewirephp\Magewire\Mechanisms\DataStore::class);
+ }
+
+ return new class($instance) {
+ public function __construct(
+ protected $instance
+ ) {
+ }
+
+ public function get($key, $default = null)
+ {
+ return ObjectManager::getInstance()->get(\Magewirephp\Magewire\Mechanisms\DataStore::class)->get($this->instance, $key, $default);
+ }
+
+ public function set($key, $value): void
+ {
+ ObjectManager::getInstance()->get(\Magewirephp\Magewire\Mechanisms\DataStore::class)->set($this->instance, $key, $value);
+ }
+
+ public function push($key, $value, $iKey = null): void
+ {
+ ObjectManager::getInstance()->get(\Magewirephp\Magewire\Mechanisms\DataStore::class)->push($this->instance, $key, $value, $iKey);
+ }
+
+ public function find($key, $iKey = null, $default = null)
+ {
+ return ObjectManager::getInstance()->get(\Magewirephp\Magewire\Mechanisms\DataStore::class)->find($this->instance, $key, $iKey, $default);
+ }
+
+ public function has($key, $iKey = null)
+ {
+ return ObjectManager::getInstance()->get(\Magewirephp\Magewire\Mechanisms\DataStore::class)->has($this->instance, $key, $iKey);
+ }
+
+ public function unset($key, $iKey = null)
+ {
+ return ObjectManager::getInstance()->get(\Magewirephp\Magewire\Mechanisms\DataStore::class)->unset($this->instance, $key, $iKey);
+ }
+ };
+}
+
+/**
+ * Get the specified configuration value.
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed|array
+ */
+function config(string $key, mixed $default = null): mixed
+{
+ $magewireConfig = ObjectManager::getInstance()->get(MagewireConfig::class);
+
+ try {
+ return $magewireConfig->getValue($key) ?? $default;
+ } catch (FileSystemException|RuntimeException $exception) {
+ $logger = ObjectManager::getInstance()->get(LoggerInterface::class);
+ $logger->critical($exception->getMessage(), ['exception' => $exception]);
+ }
+
+ return null;
+}
+
+/**
+ * Get the available container instance.
+ *
+ * @param null $abstract
+ * @param array $arguments
+ *
+ * @return mixed
+ * @throws NotFoundException
+ */
+function app($abstract = null, array $arguments = []): mixed
+{
+ if (is_string($abstract) && class_exists($abstract)) {
+ return ObjectManager::getInstance()->get($abstract);
+ }
+ if (is_null($abstract)) {
+ return ObjectManager::getInstance();
+ }
+ if (is_object($abstract)) {
+ return ObjectManager::getInstance()->get($abstract);
+ }
+
+ $containers = ObjectManager::getInstance()->get(Containers::class);
+
+ return $containers->item($abstract);
+}
+
+/**
+ * Get an item from an array or object using "dot" notation.
+ *
+ * @param mixed $target
+ * @param string|array|int|null $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+function data_get($target, $key, $default = null)
+{
+ if (is_null($key)) {
+ return $target;
+ }
+
+ $key = is_array($key) ? $key : explode('.', $key);
+
+ foreach ($key as $i => $segment) {
+ unset($key[$i]);
+
+ if (is_null($segment)) {
+ return $target;
+ }
+
+ if ($segment === '*') {
+ if (! is_iterable($target)) {
+ return value($default);
+ }
+
+ $result = [];
+
+ foreach ($target as $item) {
+ $result[] = data_get($item, $key);
+ }
+
+ return in_array('*', $key) ? Arr::collapse($result) : $result;
+ }
+
+ $segment = match ($segment) {
+ '\*' => '*',
+ '\{first}' => '{first}',
+ '{first}' => array_key_first(is_array($target) ? $target : collect($target)->all()),
+ '\{last}' => '{last}',
+ '{last}' => array_key_last(is_array($target) ? $target : collect($target)->all()),
+ default => $segment
+ };
+
+ if (Arr::accessible($target) && Arr::exists($target, $segment)) {
+ $target = $target[$segment];
+ } elseif (is_object($target) && isset($target->{$segment})) {
+ $target = $target->{$segment};
+ } else {
+ return value($default);
+ }
+ }
+
+ return $target;
+}
+
+function response($content = '', $status = 200, array $headers = [])
+{
+ $factory = app(ResponseFactory::class);
+
+ if (func_num_args() === 0) {
+ return $factory;
+ }
+
+ return $factory->make($content, $status, $headers);
+}
+
+function map(callable $callback, array $data): array
+{
+ $keys = array_keys($data);
+ $items = array_map($callback, $data, $keys);
+
+ return array_combine($keys, $items);
+}
+
+function map_with_keys(callable $callback, array $data): array
+{
+ $result = [];
+
+ foreach ($data as $key => $value) {
+ $assoc = $callback($value, $key);
+
+ foreach ($assoc as $mapKey => $mapValue) {
+ $result[$mapKey] = $mapValue;
+ }
+ }
+
+ return $result;
+}
diff --git a/lib/registration.php b/lib/registration.php
new file mode 100644
index 00000000..a5a9ce39
--- /dev/null
+++ b/lib/registration.php
@@ -0,0 +1,17 @@
+ [
+ 'source' => [
+ 'portman/lib/Livewire' => [
+ 'composer' => [
+ 'name' => 'livewire/livewire',
+ 'version' => '~3.7.11',
+ 'version-lock' => '3.7.11',
+ 'base-path' => 'src'
+ ],
+ 'glob' => '**/*.php',
+ 'ignore' => [
+ '{Wi,Attr,hel,Imp,Form}*',
+ 'Livewire.php',
+ 'Attributes/**/*',
+ '!Attributes/{Locked,On}.php',
+ 'Drawer/{ImplicitRouteBinding,Regexes}*',
+ 'Exceptions/{Event,Livewire,Root}*',
+ 'Features/**/*',
+ '!Features/Support{Attributes,Events,LifecycleHooks,Locales,NestingComponents,Redirects,FormObjects,Validation,LockedProperties,Streaming}/**/*',
+ 'Features/SupportEvents/TestsEvents.php',
+ 'Features/SupportRedirects/TestsRedirects.php',
+ 'Mechanisms/CompileLivewireTags/**/*',
+ 'Mechanisms/ExtendBlade/**/*',
+ 'Mechanisms/HandleComponents/Synthesizers/{Carbon,Collection,Stringable}*',
+ 'Mechanisms/HandleComponents/{BaseRenderless,CorruptComponent,ViewContext}*',
+ 'Mechanisms/RenderComponent.php',
+ 'Component.php',
+ 'LivewireServiceProvider.php',
+ 'Features/SupportRedirects/Redirector.php'
+ ]
+ ]
+ ],
+ 'augmentation' => [
+ 'portman/Livewire'
+ ],
+ 'output' => 'dist'
+ ],
+ 'transformations' => [
+ 'Livewire\\' => [
+ 'rename' => 'Magewirephp\\Magewire\\',
+ 'children' => [
+ 'LivewireManager' => [
+ 'rename' => 'MagewireManager',
+ 'remove-methods' => [
+ 'setProvider',
+ 'provide',
+ 'component',
+ 'componentHook',
+ 'propertySynthesizer',
+ 'directive',
+ 'precompiler',
+ 'new',
+ 'isDiscoverable',
+ 'resolveMissingComponent',
+ 'snapshot',
+ 'fromSnapshot',
+ 'listen',
+ 'current',
+ 'findSynth',
+ 'updateProperty',
+ 'isLivewireRequest',
+ 'componentHasBeenRendered',
+ 'forceAssetInjection',
+ 'setUpdateRoute',
+ 'getUpdateUri',
+ 'setScriptRoute',
+ 'useScriptTagAttributes',
+ 'withUrlParams',
+ 'withQueryParams',
+ 'withCookie',
+ 'withCookies',
+ 'withHeaders',
+ 'withoutLazyLoading',
+ 'test',
+ 'visit',
+ 'actingAs',
+ 'isRunningServerless',
+ 'addPersistentMiddleware',
+ 'setPersistentMiddleware',
+ 'getPersistentMiddleware',
+ 'originalUrl',
+ 'originalPath',
+ 'originalMethod'
+ ]
+ ],
+ 'LivewireServiceProvider' => [
+ 'rename' => 'MagewireServiceProvider'
+ ],
+ 'Features\\SupportRedirects\\HandlesRedirects' => [
+ 'remove-methods' => [
+ 'redirectAction',
+ 'redirectRoute',
+ 'redirectIntended'
+ ]
+ ]
+ ]
+ ],
+ 'Magewirephp\\Magewire\\' => [
+ 'file-doc-block' => '/**
+ * Livewire copyright © Caleb Porzio (https://github.com/livewire/livewire).
+ * Magewire copyright © Willem Poortman 2024-present.
+ * All rights reserved.
+ *
+ * Please read the README and LICENSE files for more
+ * details on copyrights and license information.
+ */',
+ ]
+ ],
+ 'post-processors' => [
+ 'rector' => false,
+ 'php-cs-fixer' => false
+ ]
+];
diff --git a/portman/Livewire/ComponentHook.php b/portman/Livewire/ComponentHook.php
new file mode 100644
index 00000000..ea8ec665
--- /dev/null
+++ b/portman/Livewire/ComponentHook.php
@@ -0,0 +1,51 @@
+component;
+ }
+
+ /**
+ * @deprecated Still undecided if this should be something that needs to go into the framework.
+ */
+ public function callMagewireComponentConstruct(...$params): void
+ {
+ if (method_exists($this, 'magewireComponentConstruct')) {
+ $this->magewireComponentConstruct(...$params);
+ }
+ }
+
+ /**
+ * @deprecated Still undecided if this should be something that needs to go into the framework.
+ */
+ public function callMagewireComponentReconstruct(...$params): void
+ {
+ if (method_exists($this, 'magewireComponentReconstruct')) {
+ $this->magewireComponentReconstruct(...$params);
+ }
+ }
+
+ /**
+ * @deprecated Has been replaced with the component method eliminating the get prefix.
+ * @see self::$component()
+ */
+ public function getComponent()
+ {
+ return $this->component;
+ }
+}
diff --git a/portman/Livewire/ComponentHookRegistry.php b/portman/Livewire/ComponentHookRegistry.php
new file mode 100644
index 00000000..02c7c8c1
--- /dev/null
+++ b/portman/Livewire/ComponentHookRegistry.php
@@ -0,0 +1,111 @@
+provide();
+ }
+
+ if (in_array($hook, static::$componentHooks)) return;
+
+ static::$componentHooks[] = $hook;
+ }
+
+ static function boot()
+ {
+ static::$components = new \WeakMap();
+
+ foreach (static::$componentHooks as $hook) {
+ on('mount', function ($component, $params, $key, $parent) use ($hook) {
+ if (! $hook = static::initializeHook($hook, $component)) {
+ return;
+ }
+
+ $hook->callBoot();
+ $hook->callMount($params, $parent);
+ });
+
+ on('hydrate', function ($component, $memo) use ($hook) {
+ if (! $hook = static::initializeHook($hook, $component)) {
+ return;
+ }
+
+ $hook->callBoot();
+ $hook->callHydrate($memo);
+ });
+ }
+
+ on('update', function ($component, $fullPath, $newValue) {
+ $propertyName = \Magewirephp\Magewire\Drawer\Utils::beforeFirstDot($fullPath);
+
+ return static::proxyCallToHooks($component, 'callUpdate')($propertyName, $fullPath, $newValue);
+ });
+
+ on('call', function ($component, $method, $params, $addEffect, $earlyReturn) {
+ return static::proxyCallToHooks($component, 'callCall')($method, $params, $earlyReturn);
+ });
+
+ on('render', function ($component, $view, $data) {
+ return static::proxyCallToHooks($component, 'callRender')($view, $data);
+ });
+
+ on('rendered', function ($component, $view, $data) {
+ return static::proxyCallToHooks($component, 'callRendered')($view, $data);
+ });
+
+ on('dehydrate', function ($component, $context) {
+ static::proxyCallToHooks($component, 'callDehydrate')($context);
+ });
+
+ on('destroy', function ($component, $context) {
+ static::proxyCallToHooks($component, 'callDestroy')($context);
+ });
+
+ on('exception', function ($target, $e, $stopPropagation) {
+ if ($target instanceof \Magewirephp\Magewire\Component) {
+ static::proxyCallToHooks($target, 'callException')($e, $stopPropagation);
+ }
+ });
+ }
+
+ static public function initializeHook($hook, $target)
+ {
+ if (! isset(static::$components[$target])) {
+ static::$components[$target] = [];
+ }
+
+ $hook = ObjectManager::getInstance()->create($hook::class);
+
+ $hook->setComponent($target);
+
+ // If no `skip` method has been implemented, then boot the hook anyway
+ if (method_exists($hook, 'skip') && $hook->skip()) {
+ return;
+ }
+
+ static::$components[$target][] = $hook;
+
+ return $hook;
+ }
+}
diff --git a/portman/Livewire/Concerns/InteractsWithProperties.php b/portman/Livewire/Concerns/InteractsWithProperties.php
new file mode 100644
index 00000000..efdcafa1
--- /dev/null
+++ b/portman/Livewire/Concerns/InteractsWithProperties.php
@@ -0,0 +1,102 @@
+{Utils::beforeFirstDot($name)};
+
+ if (Utils::containsDots($name)) {
+ return data_get($value, Utils::afterFirstDot($name));
+ }
+
+ return $value;
+ }
+
+ function fill($values)
+ {
+ $publicProperties = array_keys($this->all());
+
+ if ($values instanceof DataObject) {
+ $values = $values->toArray();
+ }
+
+ foreach ($values as $key => $value) {
+ if (in_array(Utils::beforeFirstDot($key), $publicProperties)) {
+ data_set($this, $key, $value);
+ }
+ }
+ }
+
+ public function reset(...$properties)
+ {
+ $properties = count($properties) && is_array($properties[0])
+ ? $properties[0]
+ : $properties;
+
+ // Reset all
+ if (empty($properties)) {
+ $properties = array_keys($this->all());
+ }
+
+ $freshInstance = Factory::create(static::class);
+
+ foreach ($properties as $property) {
+ $property = str($property);
+
+ // Check if the property contains a dot which means it is actually on a nested object like a FormObject
+ if (str($property)->contains('.')) {
+ $propertyName = $property->afterLast('.');
+ $objectName = $property->before('.');
+
+ // form object reset
+// if (is_subclass_of($this->{$objectName}, Form::class)) {
+// $this->{$objectName}->reset($propertyName);
+// continue;
+// }
+
+ $object = data_get($freshInstance, $objectName, null);
+
+ if (is_object($object)) {
+ $isInitialized = (new \ReflectionProperty($object, (string) $propertyName))->isInitialized($object);
+ } else {
+ $isInitialized = false;
+ }
+ } else {
+ $isInitialized = (new \ReflectionProperty($freshInstance, (string) $property))->isInitialized($freshInstance);
+ }
+
+ // Handle resetting properties that are not initialized by default.
+ if (! $isInitialized) {
+ data_forget($this, (string) $property);
+ continue;
+ }
+
+ data_set($this, $property, data_get($freshInstance, $property));
+ }
+ }
+
+ function all()
+ {
+ return Utils::getPublicPropertiesDefinedOnSubclass($this);
+ }
+}
diff --git a/portman/Livewire/Drawer/BaseUtils.php b/portman/Livewire/Drawer/BaseUtils.php
new file mode 100644
index 00000000..da1321e8
--- /dev/null
+++ b/portman/Livewire/Drawer/BaseUtils.php
@@ -0,0 +1,36 @@
+getDeclaringClass()->getName() !== \Magewirephp\Magewire\Component::class;
+ });
+ }
+
+ static function getPublicMethodsDefinedBySubClass($target)
+ {
+ $methods = array_filter((new \ReflectionObject($target))->getMethods(), function ($method) {
+ $isInBaseComponentClass = $method->getDeclaringClass()->getName() === \Livewire\Component::class;
+
+ return $method->isPublic()
+ && ! $method->isStatic()
+ && ! $isInBaseComponentClass;
+ });
+
+ return array_map(function ($method) {
+ return $method->getName();
+ }, $methods);
+ }
+}
diff --git a/portman/Livewire/Drawer/Utils.php b/portman/Livewire/Drawer/Utils.php
new file mode 100644
index 00000000..8e75a947
--- /dev/null
+++ b/portman/Livewire/Drawer/Utils.php
@@ -0,0 +1,134 @@
+mapWithKeys(function ($value, $key) {
+ return [$key => static::escapeStringForHtml($value)];
+ })->map(function ($value, $key) {
+ return sprintf('%s="%s"', $key, $value);
+ })->implode(' ');
+ }
+
+ static function escapeStringForHtml($subject)
+ {
+ if (is_string($subject) || is_numeric($subject)) {
+ return htmlspecialchars($subject, ENT_QUOTES | ENT_SUBSTITUTE);
+ }
+
+ return htmlspecialchars(json_encode($subject), ENT_QUOTES | ENT_SUBSTITUTE);
+ }
+
+ static function pretendResponseIsFile($file, $mimeType = 'application/javascript')
+ {
+ $expires = strtotime('+1 year');
+ $lastModified = filemtime($file);
+ $cacheControl = 'public, max-age=31536000';
+
+ if (static::matchesCache($lastModified)) {
+ return response('', 304, [
+ 'Expires' => static::httpDate($expires),
+ 'Cache-Control' => $cacheControl,
+ ]);
+ }
+
+ $headers = [
+ 'Content-Type' => "$mimeType; charset=utf-8",
+ 'Expires' => static::httpDate($expires),
+ 'Cache-Control' => $cacheControl,
+ 'Last-Modified' => static::httpDate($lastModified),
+ ];
+
+ if (str($file)->endsWith('.br')) {
+ $headers['Content-Encoding'] = 'br';
+ }
+
+ return response()->file($file, $headers);
+ }
+
+ static function matchesCache($lastModified)
+ {
+ $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
+
+ return @strtotime($ifModifiedSince) === $lastModified;
+ }
+
+ static function httpDate($timestamp)
+ {
+ return sprintf('%s GMT', gmdate('D, d M Y H:i:s', $timestamp));
+ }
+
+ static function containsDots($subject)
+ {
+ return str_contains($subject, '.');
+ }
+
+ static function dotSegments($subject)
+ {
+ return explode('.', $subject);
+ }
+
+ static function beforeFirstDot($subject)
+ {
+ return head(explode('.', $subject));
+ }
+
+ static function afterFirstDot($subject): string
+ {
+ return str($subject)->after('.');
+ }
+
+ static function hasProperty($target, $property)
+ {
+ return property_exists($target, static::beforeFirstDot($property));
+ }
+
+ static function extractAttributeDataFromHtml($html, $attribute)
+ {
+ $data = (string)str($html)->betweenFirst($attribute . '="', '"');
+
+ return json_decode(
+ htmlspecialchars_decode($data, ENT_QUOTES | ENT_SUBSTITUTE),
+ associative: true,
+ );
+ }
+}
diff --git a/portman/Livewire/Features/SupportEvents/Event.php b/portman/Livewire/Features/SupportEvents/Event.php
new file mode 100644
index 00000000..52ac8d0a
--- /dev/null
+++ b/portman/Livewire/Features/SupportEvents/Event.php
@@ -0,0 +1,30 @@
+ $this->name, 'params' => $this->params];
+
+ if ($this->self) {
+ $output['self'] = true;
+ }
+ if ($this->component) {
+ $output['to'] = app(ComponentRegistry::class)->getName($this->component);
+ }
+
+ return $output;
+ }
+}
diff --git a/portman/Livewire/Features/SupportLocales/SupportLocales.php b/portman/Livewire/Features/SupportLocales/SupportLocales.php
new file mode 100644
index 00000000..e4a222fe
--- /dev/null
+++ b/portman/Livewire/Features/SupportLocales/SupportLocales.php
@@ -0,0 +1,35 @@
+localeResolver->getLocale();
+ $locale = strstr($code, '_', true);
+
+ $context->addMemo('locale', $locale);
+ }
+}
diff --git a/portman/Livewire/Features/SupportNestingComponents/SupportNestingComponents.php b/portman/Livewire/Features/SupportNestingComponents/SupportNestingComponents.php
new file mode 100644
index 00000000..d1bbe122
--- /dev/null
+++ b/portman/Livewire/Features/SupportNestingComponents/SupportNestingComponents.php
@@ -0,0 +1,65 @@
+{$tag}>";
+
+ static::setParentChild($parent, $key, $tag, $childId);
+
+ $hijack($finish($html));
+ }
+ });
+
+ on('mount', function ($component, $params, $key, $parent) {
+ $start = null;
+ if ($parent && config('app.debug')) $start = microtime(true);
+
+ static::setParametersToMatchingProperties($component, $params);
+
+ return function ($html) use ($component, $key, $parent, $start) {
+ if ($parent) {
+ if (config('app.debug')) trigger('profile', 'child:'.$component->getId(), $parent->getId(), [$start, microtime(true)]);
+
+ preg_match('/<([a-zA-Z0-9\-]*)/', $html, $matches, PREG_OFFSET_CAPTURE);
+ $tag = $matches[1][0];
+ static::setParentChild($parent, $key, $tag, $component->getId());
+ }
+ };
+ });
+ }
+
+ static function setParametersToMatchingProperties($component, $params)
+ {
+ $componentProperties = Utils::getPublicPropertiesDefinedOnSubclass($component);
+
+ foreach ($params as $property => $value) {
+ if (array_key_exists($property, $componentProperties)) {
+ $component->{$property} = $value; // Assign public component properties that have matching parameters.
+ }
+ }
+ }
+}
diff --git a/portman/Livewire/Features/SupportRedirects/HandlesRedirects.php b/portman/Livewire/Features/SupportRedirects/HandlesRedirects.php
new file mode 100644
index 00000000..fc102f90
--- /dev/null
+++ b/portman/Livewire/Features/SupportRedirects/HandlesRedirects.php
@@ -0,0 +1,3 @@
+component($this->component);
+ }
+
+ public function dehydrate($context)
+ {
+ $to = $this->storeGet('redirect');
+ $usingNavigate = $this->storeGet('redirectUsingNavigate');
+
+ if ($to) {
+ $context->addEffect('redirect', $to);
+ }
+
+ $usingNavigate && $context->addEffect('redirectUsingNavigate', true);
+
+ if (! $context->isMounting()) {
+ static::$atLeastOneMountedComponentHasRedirected = true;
+ }
+ }
+}
diff --git a/portman/Livewire/Features/SupportStreaming/HandlesStreaming.php b/portman/Livewire/Features/SupportStreaming/HandlesStreaming.php
new file mode 100644
index 00000000..6b057bf2
--- /dev/null
+++ b/portman/Livewire/Features/SupportStreaming/HandlesStreaming.php
@@ -0,0 +1,15 @@
+stream($to, $content, $replace);
+ }
+}
diff --git a/portman/Livewire/Features/SupportStreaming/SupportStreaming.php b/portman/Livewire/Features/SupportStreaming/SupportStreaming.php
new file mode 100644
index 00000000..6c006c22
--- /dev/null
+++ b/portman/Livewire/Features/SupportStreaming/SupportStreaming.php
@@ -0,0 +1,59 @@
+ensureStreamResponseStarted();
+ $this->streamContent(['name' => $name, 'content' => $content, 'replace' => $replace]);
+ }
+
+ public function ensureStreamResponseStarted(): void
+ {
+ if ($this->response) {
+ return;
+ }
+
+ $this->response = $this->streamedResponseFactory->create([
+ 'callback' => null,
+ 'status' => 200,
+ 'headers' => [
+ 'Cache-Control' => 'no-cache',
+ 'Content-Type' => 'text/event-stream',
+ 'X-Accel-Buffering' => 'no',
+ 'X-Livewire-Stream' => true,
+ ]
+ ]);
+
+ $this->response->sendHeaders();
+ }
+
+ public function streamContent($body): void
+ {
+ echo json_encode([
+ 'stream' => true,
+ 'body' => $body,
+ 'endStream' => true
+ ]);
+
+ if (ob_get_level() > 0) {
+ ob_flush();
+ }
+
+ flush();
+ }
+}
diff --git a/portman/Livewire/LivewireManager.php b/portman/Livewire/LivewireManager.php
new file mode 100644
index 00000000..eb478860
--- /dev/null
+++ b/portman/Livewire/LivewireManager.php
@@ -0,0 +1,70 @@
+componentRegistry->new($name, $id);
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ public function mount($name, $params = [], $key = null, AbstractBlock|null $block = null, Component|null $component = null): void
+ {
+ /** @var HandleComponentsFacade $handleComponentsMechanismFacade */
+ $handleComponentsMechanismFacade = $this->magewireServiceProvider->getHandleComponentsMechanismFacade();
+
+ $this->renderStack[$block->getNameInLayout()] = $handleComponentsMechanismFacade
+ ->mount($name, $params, $block, $component);
+ }
+
+ /**
+ * @throws FileSystemException
+ * @throws RuntimeException
+ * @throws NotFoundException
+ */
+ public function update($snapshot, $diff, $calls, AbstractBlock|null $block = null): void
+ {
+ /** @var HandleComponentsFacade $handleComponentsMechanismFacade */
+ $handleComponentsMechanismFacade = $this->magewireServiceProvider->getHandleComponentsMechanismFacade();
+
+ $this->renderStack[$block->getNameInLayout()] = $handleComponentsMechanismFacade
+ ->update($snapshot, $diff, $calls, $block);
+ }
+
+ public function render(AbstractBlock $block, string $html)
+ {
+ $renderer = $this->renderStack[$block->getNameInLayout()];
+
+ array_pop($this->renderStack);
+
+ return $renderer($block, $html);
+ }
+}
diff --git a/portman/Livewire/Mechanisms/ComponentRegistry.php b/portman/Livewire/Mechanisms/ComponentRegistry.php
new file mode 100644
index 00000000..99cb1b76
--- /dev/null
+++ b/portman/Livewire/Mechanisms/ComponentRegistry.php
@@ -0,0 +1,37 @@
+getNameAndClass($nameOrClass);
+
+ $component = new $class;
+
+ $component->setId($id ?: str()->random(20));
+
+ $component->setName($name);
+
+ // // Parameters passed in automatically set public properties by the same name...
+ // foreach ($params as $key => $value) {
+ // if (! property_exists($component, $key)) continue;
+
+ // // Typed properties shouldn't be set back to "null". It will throw an error...
+ // if ((new \ReflectionProperty($component, $key))->getType() && is_null($value)) continue;
+
+ // $component->$key = $value;
+ // }
+
+ return $component;
+ }
+}
diff --git a/portman/Livewire/Mechanisms/FrontendAssets/FrontendAssets.php b/portman/Livewire/Mechanisms/FrontendAssets/FrontendAssets.php
new file mode 100644
index 00000000..1d0f5f4a
--- /dev/null
+++ b/portman/Livewire/Mechanisms/FrontendAssets/FrontendAssets.php
@@ -0,0 +1,54 @@
+setScriptRoute(fn () => $this->returnJavaScriptAsFile());
+
+ on('flush-state', function () {
+ $this->hasRenderedScripts = false;
+ $this->hasRenderedStyles = false;
+ });
+ }
+
+ function returnJavaScriptAsFile()
+ {
+ $url = $this->assetsRepository->getUrlWithParams($this->getDataByPath('script.file_path'), [
+ '_secure' => $this->request->isSecure(),
+ ]);
+
+ $queryParams = $this->getDataByPath('script.query_params');
+
+ if (is_array($queryParams)) {
+ return $url . '?'. http_build_query($queryParams);
+ }
+
+ return $url;
+ }
+}
diff --git a/portman/Livewire/Mechanisms/HandleComponents/Checksum.php b/portman/Livewire/Mechanisms/HandleComponents/Checksum.php
new file mode 100644
index 00000000..5dac15bc
--- /dev/null
+++ b/portman/Livewire/Mechanisms/HandleComponents/Checksum.php
@@ -0,0 +1,62 @@
+generate($snapshot)) {
+ trigger('checksum.fail', $checksum, $comparator, $snapshot);
+
+ throw new CorruptComponentPayloadException;
+ }
+ }
+
+ /**
+ * @throws FileSystemException
+ * @throws RuntimeException
+ */
+ function generate(array $snapshot): string
+ {
+ $hashKey = $this->deploymentConfig->get('crypt/key');
+ $checksum = hash_hmac('sha256', $this->serializer->serialize($snapshot), $hashKey);
+
+ trigger('checksum.generate', $checksum, $snapshot);
+
+ return $checksum;
+ }
+}
diff --git a/portman/Livewire/Mechanisms/HandleComponents/ComponentContext.php b/portman/Livewire/Mechanisms/HandleComponents/ComponentContext.php
new file mode 100644
index 00000000..56ced4fe
--- /dev/null
+++ b/portman/Livewire/Mechanisms/HandleComponents/ComponentContext.php
@@ -0,0 +1,149 @@
+effects = $effects instanceof Effects ? $effects : null
+ ?? ObjectManager::getInstance()->create(Effects::class);
+ $this->memo = $memo instanceof Memo ? $memo : null
+ ?? ObjectManager::getInstance()->create(Memo::class);
+ }
+
+ public function setEffects(Effects $effects)
+ {
+ $this->effects = $effects;
+
+ return $this;
+ }
+
+ public function setMemo(Memo $memo)
+ {
+ $this->memo = $memo;
+
+ return $this;
+ }
+
+ public function getEffects(): Effects
+ {
+ return $this->effects;
+ }
+
+ public function getMemo(): Memo
+ {
+ return $this->memo;
+ }
+
+ public function getComponent(): Component
+ {
+ return $this->component;
+ }
+
+ public function getBlock(): AbstractBlock
+ {
+ return $this->block;
+ }
+
+ public function addEffect($key, $value)
+ {
+ $this->getEffects()->setData($key, $value);
+ }
+
+ public function pushEffect($key, $value, $iKey = null)
+ {
+ $effects = $this->getEffects()->getData();
+
+ if (! is_array($effects)) {
+ $effects = [];
+ }
+
+ if ($iKey) {
+ $effects[$key][$iKey] = $value;
+ } else {
+ $effects[$key][] = $value;
+ }
+
+ $this->getEffects()->setData($effects);
+
+ return $this;
+ }
+
+ public function hasEffect($key, $iKey = null): bool
+ {
+ $has = $this->getEffects()->hasData($key);
+
+ if ($has && $iKey) {
+ $data = $this->getEffects()->getData($key);
+
+ if (is_array($data)) {
+ return isset($data[$iKey]);
+ }
+
+ return false;
+ }
+
+ return $has;
+ }
+
+ public function addMemo($key, $value)
+ {
+ $this->getMemo()->setData($key, $value);
+ }
+
+ public function pushMemo($key, $value, $iKey = null)
+ {
+ $memo = $this->getMemo()->getData();
+
+ if (! is_array($memo)) {
+ $memo = [];
+ }
+
+ if ($iKey) {
+ $memo[$key][$iKey] = $value;
+ } else {
+ $memo[$key][] = $value;
+ }
+
+ $this->getMemo()->setData($memo);
+
+ return $this;
+ }
+
+ public function hasMemo($key, $iKey = null)
+ {
+ $has = $this->getMemo()->hasData($key);
+
+ if ($has && $iKey) {
+ $data = $this->getMemo()->getData($key);
+
+ if (is_array($data)) {
+ return isset($data[$iKey]);
+ }
+
+ return false;
+ }
+
+ return $has;
+ }
+}
diff --git a/portman/Livewire/Mechanisms/HandleComponents/HandleComponents.php b/portman/Livewire/Mechanisms/HandleComponents/HandleComponents.php
new file mode 100644
index 00000000..323b243e
--- /dev/null
+++ b/portman/Livewire/Mechanisms/HandleComponents/HandleComponents.php
@@ -0,0 +1,486 @@
+propertySynthesizers = $synthesizers;
+ }
+
+ public static function provide()
+ {
+ on('pre-mount', function ($target, $view) {
+ return function ($html) use ($view, $target) {
+ if (method_exists($target, 'render')) {
+ return $target->render();
+ }
+
+ return $html;
+ };
+ });
+ }
+
+ public function mount($name, $params = [], $key = null, AbstractBlock|null $block = null, Component|null $component = null)
+ {
+ $parent = last(self::$componentStack);
+
+ if ($html = $this->shortCircuitMount($name, $params, $key, $parent)) {
+ $shortCircuitHtml = $html;
+
+ return function (AbstractBlock $block, string $html) use ($shortCircuitHtml) {
+ return [$block, $shortCircuitHtml];
+ };
+ }
+
+ if ($block === null) {
+ throw new InvalidArgumentException('Argument $block can not be null.');
+ }
+
+ $context = $this->componentContextFactory->create([
+ 'block' => $block,
+ 'component' => $component,
+ 'mounting' => true,
+ ]);
+
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+
+ $finish = trigger('mount', $component, $params, $key, $parent);
+
+ if (config('app.debug')) {
+ trigger('profile', 'mount', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+
+ $this->pushOntoComponentStack($component);
+ $start = config('app.debug') ? microtime(true) : null;
+
+ /*
+ * This function is divided based on the original design due to the presence of both pre and post-render
+ * mechanisms within Magento. It serves as a form of middleware where the Magewire mechanism is embedded
+ * around it, instead of being directly responsible for rendering the component. The standard Block render
+ * mechanism within Magewire remains responsible for the actual rendering process and does return HTML.
+ */
+ return function (AbstractBlock $block, string $html) use ($component, $context, $start, $finish) {
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ $html = $this->render($component, '
', $html);
+ if (config('app.debug')) {
+ trigger('profile', 'render', $component->getId(), [$start, microtime(true)]);
+ }
+
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ trigger('dehydrate', $component, $context);
+
+ $snapshot = $this->snapshot($component, $context);
+ if (config('app.debug')) {
+ trigger('profile', 'dehydrate', $component->getId(), [$start, microtime(true)]);
+ }
+
+ trigger('destroy', $component, $context);
+
+ $html = Utils::insertAttributesIntoHtmlRoot($html, [
+ 'wire:snapshot' => $snapshot,
+ 'wire:effects' => $context->getEffects()->toArray(),
+ ]);
+
+ $this->popOffComponentStack();
+
+ return [$block, $finish($html, $snapshot)];
+ };
+ }
+
+ /**
+ * @throws FileSystemException
+ * @throws ComponentNotFoundException
+ * @throws MethodNotFoundException
+ * @throws RuntimeException
+ */
+ public function update($snapshot, $updates, $calls, AbstractBlock|null $block = null)
+ {
+ if ($block === null) {
+ throw new InvalidArgumentException('Argument $block can not be of type null.');
+ }
+
+ $data = $snapshot['data'];
+ $memo = $snapshot['memo'];
+
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+
+ [$component, $context] = $this->fromSnapshot($snapshot, $block);
+
+ $this->pushOntoComponentStack($component);
+
+ trigger('hydrate', $component, $memo, $context);
+
+ $this->updateProperties($component, $updates, $data, $context);
+
+ if (config('app.debug')) {
+ trigger('profile', 'hydrate', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+
+ $this->callMethods($component, $calls, $context);
+
+ /*
+ * This function is divided based on the original design due to the presence of both pre and post-render
+ * mechanisms within Magento. It serves as a form of middleware where the Magewire mechanism is embedded
+ * around it, instead of being directly responsible for rendering the component. The standard Block render
+ * mechanism within Magewire remains responsible for the actual rendering process.
+ */
+ return function (AbstractBlock $block, string $html) use ($snapshot, $context, $component) {
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+
+ if ($html = $this->render($component, '', $html)) {
+ $context->addEffect('html', $html);
+ if (config('app.debug')) {
+ trigger('profile', 'render', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+ }
+
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+
+ trigger('dehydrate', $component, $context);
+
+ $snapshot = $this->snapshot($component, $context);
+
+ if (config('app.debug')) {
+ trigger('profile', 'dehydrate', $component->getId(), [$start ?? 0, microtime(true)]);
+ }
+
+ trigger('destroy', $component, $context);
+
+ $this->popOffComponentStack();
+
+ return [$snapshot, $context->effects];
+ };
+ }
+
+ protected function render($component, $default = null, string|null $html = null)
+ {
+ $replace = store($component)->get('skipRender', false);
+
+ if ($replace) {
+ $replace = value(is_string($html) ? $html : $default);
+
+ if (! $replace) {
+ return '';
+ }
+
+ return Utils::insertAttributesIntoHtmlRoot($html, [
+ 'wire:id' => $component->getId(),
+ ]);
+ }
+
+ [ $block, $properties ] = $this->getView($component);
+
+ return $this->trackInRenderStack($component, function () use ($component, $block, $properties, $html) {
+ $finish = trigger('render', $component, $block, $properties);
+
+ $html = Utils::insertAttributesIntoHtmlRoot($html, [
+ 'wire:id' => $component->getId(),
+ ]);
+
+ return $finish($html, function ($newHtml) use (&$html) {
+ $html = $newHtml;
+ }, $component->block());
+ });
+ }
+
+ protected function trackInRenderStack($component, $callback)
+ {
+ static::$renderStack[] = $component;
+
+ return tap($callback(), function () {
+ array_pop(static::$renderStack);
+ });
+ }
+
+ protected function hydrateForUpdate($raw, $path, $value, $context)
+ {
+ $meta = $this->getMetaForPath($raw, $path);
+
+ // If we have meta data already for this property, let's use that to get a synth...
+ if ($meta) {
+ return $this->hydratePropertyUpdate([$value, $meta], $context, $path, $raw);
+ }
+
+ // If we don't, let's check to see if it's a typed property and fetch the synth that way...
+ $parent = str($path)->contains('.')
+ ? data_get($context->component, str($path)->beforeLast('.')->toString())
+ : $context->component;
+
+ $childKey = str($path)->afterLast('.');
+
+ if ($parent && is_object($parent) && property_exists($parent, $childKey) && Utils::propertyIsTyped($parent, $childKey)) {
+ $type = Utils::getProperty($parent, $childKey)->getType();
+
+ $types = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type];
+
+ foreach ($types as $type) {
+ $synth = $this->getSynthesizerByType($type->getName(), $context, $path);
+
+ if ($synth) return $synth->hydrateFromType($type->getName(), $value);
+ }
+ }
+
+ return $value;
+ }
+
+ protected function getMetaForPath($raw, $path)
+ {
+ $segments = explode('.', $path);
+
+ $first = array_shift($segments);
+
+ [$data, $meta] = Utils::isSyntheticTuple($raw) ? $raw : [$raw, null];
+
+ if ($path !== '') {
+ $value = $data[$first] ?? null;
+
+ return $this->getMetaForPath($value, implode('.', $segments));
+ }
+
+ return $meta;
+ }
+
+ protected function getView($component)
+ {
+// WIP
+// if (method_exists($component, 'render')) {
+// $block = wrap($component)->render();
+//
+// if ($block instanceof AbstractBlock) {
+// $block->setData($component->getBlock()->getData());
+// $block->setNameInLayout($component->getBlock()->getNameInLayout());
+//
+// $component->getResolver()->construct($block);
+// }
+// }
+
+ return [$component->block(), Utils::getPublicPropertiesDefinedOnSubclass($component)];
+ }
+
+ /**
+ * @throws ComponentNotFoundException
+ * @throws FileSystemException
+ * @throws RuntimeException
+ * @throws Exception
+ */
+ public function fromSnapshot($snapshot, AbstractBlock|null $block = null)
+ {
+ if (! $block instanceof AbstractBlock) {
+ throw new Exception(
+ sprintf('Invalid block type: %s', is_object($block) ? get_class($block) : 'Unknown')
+ );
+ } elseif (! $block->getData('magewire') instanceof Component) {
+ throw new ComponentNotFoundException(
+ sprintf('Unable to find component: [%s]', $block->getNameInLayout())
+ );
+ }
+
+ /** @var Component $component */
+ $component = $block->getData('magewire');
+
+ $context = $this->componentContextFactory->create([
+ 'component' => $component,
+ 'block' => $block,
+ ]);
+
+ $this->hydrateProperties($context->getComponent(), $snapshot['data'], $context);
+
+ return [$component, $context];
+ }
+
+ public function snapshot($component, $context = null)
+ {
+ $context ??= $this->componentContextFactory->create(['component' => $component]);
+
+ $data = $this->dehydrateProperties($component, $context);
+
+ $snapshot = [
+ 'data' => $data,
+ 'memo' => [
+ 'id' => $component->getId(),
+ 'name' => $component->getName(),
+ ...$context->getMemo()->toArray(),
+ ],
+ ];
+
+ $snapshot['checksum'] = $this->checksum->generate($snapshot);
+
+ return $snapshot;
+ }
+
+ /**
+ * @throws MethodNotFoundException
+ */
+ protected function callMethods($root, $calls, $context)
+ {
+ $returns = [];
+
+ foreach ($calls as $idx => $call) {
+ $method = $call['method'];
+ $params = $call['params'];
+
+ $earlyReturnCalled = false;
+ $earlyReturn = null;
+ $returnEarly = function ($return = null) use (&$earlyReturnCalled, &$earlyReturn) {
+ $earlyReturnCalled = true;
+ $earlyReturn = $return;
+ };
+
+ $finish = trigger('call', $root, $method, $params, $context, $returnEarly);
+
+ if ($earlyReturnCalled) {
+ $returns[] = $finish($earlyReturn);
+
+ continue;
+ }
+
+ $methods = Utils::getPublicMethodsDefinedBySubClass($root);
+
+ // Also remove "render" from the list...
+ $methods = array_values(array_diff($methods, ['render']));
+
+ // @todo: put this in a better place:
+ $methods[] = '__dispatch';
+
+ if (! in_array($method, $methods)) {
+ throw new MethodNotFoundException($method);
+ }
+
+ if (config('app.debug')) {
+ $start = microtime(true);
+ }
+ $return = wrap($root)->{$method}(...$params);
+ if (config('app.debug')) {
+ trigger('profile', 'call' . $idx, $root->getId(), [$start, microtime(true)]);
+ }
+
+ $returns[] = $finish($return);
+ }
+
+ $context->addEffect('returns', $returns);
+ }
+
+ protected function dehydrateProperties($component, $context)
+ {
+ $data = Utils::getPublicPropertiesDefinedOnSubclass($component);
+
+ foreach ($data as $key => $value) {
+ $data[$key] = $this->dehydrate($value, $context, $key);
+ }
+
+ return $data;
+ }
+
+ protected function dehydrate($target, $context, $path)
+ {
+ if (Utils::isAPrimitive($target)) {
+ return $target;
+ }
+
+ $synth = $this->propertySynth($target, $context, $path);
+
+ [ $data, $meta ] = $synth->dehydrate($target, function ($name, $child) use ($context, $path) {
+ return $this->dehydrate($child, $context, "{$path}.{$name}");
+ });
+
+ $meta['s'] = $synth::getKey();
+
+ return [$data, $meta];
+ }
+
+ protected function hydrate($valueOrTuple, $context, $path)
+ {
+ if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) {
+ return $value;
+ }
+
+ [$value, $meta] = $tuple;
+
+ if ($this->isRemoval($value) && str($path)->contains('.')) {
+ return $value;
+ }
+
+ $synth = $this->propertySynth($meta['s'], $context, $path);
+
+ return $synth->hydrate($value, $meta, function ($name, $child) use ($context, $path) {
+ return $this->hydrate($child, $context, "{$path}.{$name}");
+ });
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function getSynthesizerByKey($key, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::getKey() === $key) {
+ return ObjectManager::getInstance()->create($synth, [
+ 'context' => $context,
+ 'path' => $path
+ ]);
+ }
+ }
+
+ throw new Exception('No synthesizer found for key: "' . $key . '"');
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function getSynthesizerByTarget($target, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::match($target)) {
+ return ObjectManager::getInstance()->create($synth, [
+ 'context' => $context,
+ 'path' => $path
+ ]);
+ }
+ }
+
+ throw new Exception('Property type not supported in Magewire for property: [' . json_encode($target) . ']');
+ }
+}
diff --git a/portman/Livewire/Mechanisms/HandleRequests/HandleRequests.php b/portman/Livewire/Mechanisms/HandleRequests/HandleRequests.php
new file mode 100644
index 00000000..64ae5adc
--- /dev/null
+++ b/portman/Livewire/Mechanisms/HandleRequests/HandleRequests.php
@@ -0,0 +1,112 @@
+isMagewireRequest();
+ }
+
+ public function isMagewireRequest()
+ {
+ return $this->magewireServiceProvider->runtime()->mode()->isSubsequent();
+ }
+
+ public function request(): Http
+ {
+ return $this->request;
+ }
+
+ /**
+ * @return MagewireUpdateResult|mixed|null
+ * @throws ComponentNotFoundException
+ * @throws NoSuchEntityException
+ */
+ public function handleUpdate()
+ {
+ /** @var ComponentRequestContext[] $updates */
+ $requestPayload = $this->request->getParam('components');
+
+ $finish = trigger('request', $requestPayload);
+
+ $requestPayload = $finish($requestPayload);
+
+ $componentResponses = [];
+
+ foreach ($requestPayload as $componentPayload) {
+ $reconstruct = trigger('magewire:component:reconstruct', $componentPayload);
+
+ $block = $reconstruct();
+ $component = $block->getData('magewire');
+
+ if (! $component instanceof Component) {
+ throw new ComponentNotFoundException(
+ 'Something went wrong during block reconstruction'
+ );
+ }
+
+ /*
+ * Marks the component to indicate that it is being updated, distinguishing it from a preceding page load
+ * or refresh. This notification is crucial for informing other systems about the context of the operation.
+ */
+ store($component)->set('magewire:update', $componentPayload);
+
+ /*
+ * When the 'toHtml' method is invoked on any block with the 'magewire' argument, it initiates the
+ * rendering lifecycle. During initial (in other words: preceding) page renders, this process is
+ * automatically managed by the framework. However, on subsequent requests, it becomes necessary to
+ * manually trigger this lifecycle for the targeted block.
+ */
+ [$snapshot, $effects] = $this->magewireManager->render($block, $block->toHtml());
+
+ $componentResponses[] = [
+ 'effects' => $effects->toArray(),
+ 'snapshot' => $this->serializer->serialize($snapshot),
+ ];
+ }
+
+ $responsePayload = [
+ 'components' => $componentResponses ?? [],
+ 'assets' => [] // should be: 'assets' => SupportScriptsAndAssets::getAssets() ( = TODO)
+ ];
+
+ $finish = trigger('response', $responsePayload);
+
+ return $finish($responsePayload);
+ }
+}
diff --git a/portman/Livewire/Mechanisms/PersistentMiddleware/PersistentMiddleware.php b/portman/Livewire/Mechanisms/PersistentMiddleware/PersistentMiddleware.php
new file mode 100644
index 00000000..0fb26340
--- /dev/null
+++ b/portman/Livewire/Mechanisms/PersistentMiddleware/PersistentMiddleware.php
@@ -0,0 +1,60 @@
+extractPathAndMethodFromRequest();
+
+ $context->addMemo('path', $path);
+ // Although it's a POST request (stored in $method), Livewire still requires a GET value.
+ $context->addMemo('method', 'GET');
+ });
+
+ on('flush-state', function() {
+ // Only flush these at the end of a full request, so that child components have access to this data.
+ $this->path = null;
+ $this->method = null;
+ });
+ }
+
+ protected function extractPathAndMethodFromRequest()
+ {
+ return [$this->request->getBasePath(), $this->request->getMethod()];
+ }
+}
diff --git a/portman/Livewire/Wrapped.php b/portman/Livewire/Wrapped.php
new file mode 100644
index 00000000..71a40c1d
--- /dev/null
+++ b/portman/Livewire/Wrapped.php
@@ -0,0 +1,54 @@
+target, $method)) {
+ return value($this->fallback);
+ }
+
+ try {
+ $arguments = $this->methodsMap->getMethodParams($this->target::class, $method);
+
+ if (count($params) !== 0 && isset($params[0])) {
+ $params = array_combine(array_column($arguments, 'name'), array_intersect_key($params, $arguments));
+ }
+
+ // Remove parameters that are not required by the method.
+ $params = array_intersect_key($params, array_flip(array_column($arguments, 'name')));
+
+ return $this->target->{$method}(...$params);
+ } catch (\Throwable $e) {
+ $shouldPropagate = true;
+
+ $stopPropagation = function () use (&$shouldPropagate) {
+ $shouldPropagate = false;
+ };
+
+ trigger('exception', $this->target, $e, $stopPropagation);
+
+ $shouldPropagate && throw $e;
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Attribute.php b/portman/lib/Livewire/Attribute.php
new file mode 100644
index 00000000..9b7b4948
--- /dev/null
+++ b/portman/lib/Livewire/Attribute.php
@@ -0,0 +1,10 @@
+getId();
+ }
+
+ function setId($id)
+ {
+ $this->__id = $id;
+ }
+
+ function getId()
+ {
+ return $this->__id;
+ }
+
+ function setName($name)
+ {
+ $this->__name = $name;
+ }
+
+ function getName()
+ {
+ return $this->__name;
+ }
+
+ function skipRender($html = null)
+ {
+ store($this)->set('skipRender', $html ?: true);
+ }
+
+ function skipMount()
+ {
+ store($this)->set('skipMount', true);
+ }
+
+ function skipHydrate()
+ {
+ store($this)->set('skipHydrate', true);
+ }
+
+ function __isset($property)
+ {
+ try {
+ $value = $this->__get($property);
+
+ if (isset($value)) {
+ return true;
+ }
+ } catch(PropertyNotFoundException $ex) {}
+
+ return false;
+ }
+
+ function __get($property)
+ {
+ $value = 'noneset';
+
+ $returnValue = function ($newValue) use (&$value) {
+ $value = $newValue;
+ };
+
+ $finish = trigger('__get', $this, $property, $returnValue);
+
+ $value = $finish($value);
+
+ if ($value === 'noneset') {
+ throw new PropertyNotFoundException($property, $this->getName());
+ }
+
+ return $value;
+ }
+
+ function __unset($property)
+ {
+ trigger('__unset', $this, $property);
+ }
+
+ function __call($method, $params)
+ {
+ $value = 'noneset';
+
+ $returnValue = function ($newValue) use (&$value) {
+ $value = $newValue;
+ };
+
+ $finish = trigger('__call', $this, $method, $params, $returnValue);
+
+ $value = $finish($value);
+
+ if ($value !== 'noneset') {
+ return $value;
+ }
+
+ if (static::hasMacro($method)) {
+ return $this->macroCall($method, $params);
+ }
+
+ throw new BadMethodCallException(sprintf(
+ 'Method %s::%s does not exist.', static::class, $method
+ ));
+ }
+
+ public function tap($callback)
+ {
+ $callback($this);
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/ComponentHook.php b/portman/lib/Livewire/ComponentHook.php
new file mode 100644
index 00000000..3a869093
--- /dev/null
+++ b/portman/lib/Livewire/ComponentHook.php
@@ -0,0 +1,103 @@
+component = $component;
+ }
+
+ function callBoot(...$params) {
+ if (method_exists($this, 'boot')) $this->boot(...$params);
+ }
+
+ function callMount(...$params) {
+ if (method_exists($this, 'mount')) $this->mount(...$params);
+ }
+
+ function callHydrate(...$params) {
+ if (method_exists($this, 'hydrate')) $this->hydrate(...$params);
+ }
+
+ function callUpdate($propertyName, $fullPath, $newValue) {
+ $callbacks = [];
+
+ if (method_exists($this, 'update')) $callbacks[] = $this->update($propertyName, $fullPath, $newValue);
+
+ return function (...$params) use ($callbacks) {
+ foreach ($callbacks as $callback) {
+ if (is_callable($callback)) $callback(...$params);
+ }
+ };
+ }
+
+ function callCall($method, $params, $returnEarly) {
+ $callbacks = [];
+
+ if (method_exists($this, 'call')) $callbacks[] = $this->call($method, $params, $returnEarly);
+
+ return function (...$params) use ($callbacks) {
+ foreach ($callbacks as $callback) {
+ if (is_callable($callback)) $callback(...$params);
+ }
+ };
+ }
+
+ function callRender(...$params) {
+ $callbacks = [];
+
+ if (method_exists($this, 'render')) $callbacks[] = $this->render(...$params);
+
+ return function (...$params) use ($callbacks) {
+ foreach ($callbacks as $callback) {
+ if (is_callable($callback)) $callback(...$params);
+ }
+ };
+ }
+
+ function callDehydrate(...$params) {
+ if (method_exists($this, 'dehydrate')) $this->dehydrate(...$params);
+ }
+
+ function callDestroy(...$params) {
+ if (method_exists($this, 'destroy')) $this->destroy(...$params);
+ }
+
+ function callException(...$params) {
+ if (method_exists($this, 'exception')) $this->exception(...$params);
+ }
+
+ function getProperties()
+ {
+ return $this->component->all();
+ }
+
+ function getProperty($name)
+ {
+ return data_get($this->getProperties(), $name);
+ }
+
+ function storeSet($key, $value)
+ {
+ store($this->component)->set($key, $value);
+ }
+
+ function storePush($key, $value, $iKey = null)
+ {
+ store($this->component)->push($key, $value, $iKey);
+ }
+
+ function storeGet($key, $default = null)
+ {
+ return store($this->component)->get($key, $default);
+ }
+
+ function storeHas($key)
+ {
+ return store($this->component)->has($key);
+ }
+}
diff --git a/portman/lib/Livewire/ComponentHookRegistry.php b/portman/lib/Livewire/ComponentHookRegistry.php
new file mode 100644
index 00000000..1ac67a30
--- /dev/null
+++ b/portman/lib/Livewire/ComponentHookRegistry.php
@@ -0,0 +1,120 @@
+callBoot();
+ $hook->callMount($params, $parent);
+ });
+
+ on('hydrate', function ($component, $memo) use ($hook) {
+ if (! $hook = static::initializeHook($hook, $component)) {
+ return;
+ }
+
+ $hook->callBoot();
+ $hook->callHydrate($memo);
+ });
+ }
+
+ on('update', function ($component, $fullPath, $newValue) {
+ $propertyName = Utils::beforeFirstDot($fullPath);
+
+ return static::proxyCallToHooks($component, 'callUpdate')($propertyName, $fullPath, $newValue);
+ });
+
+ on('call', function ($component, $method, $params, $addEffect, $earlyReturn) {
+ return static::proxyCallToHooks($component, 'callCall')($method, $params, $earlyReturn);
+ });
+
+ on('render', function ($component, $view, $data) {
+ return static::proxyCallToHooks($component, 'callRender')($view, $data);
+ });
+
+ on('dehydrate', function ($component, $context) {
+ static::proxyCallToHooks($component, 'callDehydrate')($context);
+ });
+
+ on('destroy', function ($component, $context) {
+ static::proxyCallToHooks($component, 'callDestroy')($context);
+ });
+
+ on('exception', function ($target, $e, $stopPropagation) {
+ if ($target instanceof \Livewire\Component) {
+ static::proxyCallToHooks($target, 'callException')($e, $stopPropagation);
+ }
+ });
+ }
+
+ static public function initializeHook($hook, $target)
+ {
+ if (! isset(static::$components[$target])) static::$components[$target] = [];
+
+ $hook = new $hook;
+
+ $hook->setComponent($target);
+
+ // If no `skip` method has been implemented, then boot the hook anyway
+ if (method_exists($hook, 'skip') && $hook->skip()) {
+ return;
+ }
+
+ static::$components[$target][] = $hook;
+
+ return $hook;
+ }
+
+ static function proxyCallToHooks($target, $method) {
+ return function (...$params) use ($target, $method) {
+ $callbacks = [];
+
+ foreach (static::$components[$target] ?? [] as $hook) {
+ $callbacks[] = $hook->{$method}(...$params);
+ }
+
+ return function (...$forwards) use ($callbacks) {
+ foreach ($callbacks as $callback) {
+ $callback(...$forwards);
+ }
+ };
+ };
+ }
+}
diff --git a/portman/lib/Livewire/Concerns/InteractsWithProperties.php b/portman/lib/Livewire/Concerns/InteractsWithProperties.php
new file mode 100644
index 00000000..c07a120c
--- /dev/null
+++ b/portman/lib/Livewire/Concerns/InteractsWithProperties.php
@@ -0,0 +1,144 @@
+{Utils::beforeFirstDot($name)};
+
+ if (Utils::containsDots($name)) {
+ return data_get($value, Utils::afterFirstDot($name));
+ }
+
+ return $value;
+ }
+
+ public function fill($values)
+ {
+ $publicProperties = array_keys($this->all());
+
+ if ($values instanceof Model) {
+ $values = $values->toArray();
+ }
+
+ foreach ($values as $key => $value) {
+ if (in_array(Utils::beforeFirstDot($key), $publicProperties)) {
+ data_set($this, $key, $value);
+ }
+ }
+ }
+
+ public function reset(...$properties)
+ {
+ $properties = count($properties) && is_array($properties[0])
+ ? $properties[0]
+ : $properties;
+
+ // Reset all
+ if (empty($properties)) {
+ $properties = array_keys($this->all());
+ }
+
+ $freshInstance = new static;
+
+ foreach ($properties as $property) {
+ $property = str($property);
+
+ // Check if the property contains a dot which means it is actually on a nested object like a FormObject
+ if (str($property)->contains('.')) {
+ $propertyName = $property->afterLast('.');
+ $objectName = $property->before('.');
+
+ // form object reset
+ if (is_subclass_of($this->{$objectName}, Form::class)) {
+ $this->{$objectName}->reset($propertyName);
+ continue;
+ }
+
+ $object = data_get($freshInstance, $objectName, null);
+
+ if (is_object($object)) {
+ $isInitialized = (new \ReflectionProperty($object, (string) $propertyName))->isInitialized($object);
+ } else {
+ $isInitialized = false;
+ }
+ } else {
+ $isInitialized = (new \ReflectionProperty($freshInstance, (string) $property))->isInitialized($freshInstance);
+ }
+
+ // Handle resetting properties that are not initialized by default.
+ if (! $isInitialized) {
+ data_forget($this, (string) $property);
+ continue;
+ }
+
+ data_set($this, $property, data_get($freshInstance, $property));
+ }
+ }
+
+ protected function resetExcept(...$properties)
+ {
+ if (count($properties) && is_array($properties[0])) {
+ $properties = $properties[0];
+ }
+
+ $keysToReset = array_diff(array_keys($this->all()), $properties);
+
+ if($keysToReset === []) {
+ return;
+ }
+
+ $this->reset($keysToReset);
+ }
+
+ public function pull($properties = null)
+ {
+ $wantsASingleValue = is_string($properties);
+
+ $properties = is_array($properties) ? $properties : func_get_args();
+
+ $beforeReset = match (true) {
+ empty($properties) => $this->all(),
+ $wantsASingleValue => $this->getPropertyValue($properties[0]),
+ default => $this->only($properties),
+ };
+
+ $this->reset($properties);
+
+ return $beforeReset;
+ }
+
+ public function only($properties)
+ {
+ $results = [];
+
+ foreach (is_array($properties) ? $properties : func_get_args() as $property) {
+ $results[$property] = $this->hasProperty($property) ? $this->getPropertyValue($property) : null;
+ }
+
+ return $results;
+ }
+
+ public function except($properties)
+ {
+ $properties = is_array($properties) ? $properties : func_get_args();
+
+ return array_diff_key($this->all(), array_flip($properties));
+ }
+
+ public function all()
+ {
+ return Utils::getPublicPropertiesDefinedOnSubclass($this);
+ }
+}
diff --git a/portman/lib/Livewire/Drawer/BaseUtils.php b/portman/lib/Livewire/Drawer/BaseUtils.php
new file mode 100644
index 00000000..60246c91
--- /dev/null
+++ b/portman/lib/Livewire/Drawer/BaseUtils.php
@@ -0,0 +1,94 @@
+getDeclaringClass()->getName() !== \Livewire\Component::class && $property->getDeclaringClass()->getName() !== \Livewire\Volt\Component::class;
+ });
+ }
+
+ static function getPublicProperties($target, $filter = null)
+ {
+ return collect((new \ReflectionObject($target))->getProperties())
+ ->filter(function ($property) {
+ return $property->isPublic() && ! $property->isStatic() && $property->isDefault();
+ })
+ ->filter($filter ?? fn () => true)
+ ->mapWithKeys(function ($property) use ($target) {
+ // Ensures typed property is initialized in PHP >=7.4, if so, return its value,
+ // if not initialized, return null (as expected in earlier PHP Versions)
+ if (method_exists($property, 'isInitialized') && !$property->isInitialized($target)) {
+ // If a type of `array` is given with no value, let's assume users want
+ // it prefilled with an empty array...
+ $value = (method_exists($property, 'getType') && $property->getType() && method_exists($property->getType(), 'getName') && $property->getType()->getName() === 'array')
+ ? [] : null;
+ } else {
+ $value = $property->getValue($target);
+ }
+
+ return [$property->getName() => $value];
+ })
+ ->all();
+ }
+
+ static function getPublicMethodsDefinedBySubClass($target)
+ {
+ $methods = array_filter((new \ReflectionObject($target))->getMethods(), function ($method) {
+ $isInBaseComponentClass = $method->getDeclaringClass()->getName() === \Livewire\Component::class || $method->getDeclaringClass()->getName() === \Livewire\Volt\Component::class;
+
+ return $method->isPublic()
+ && ! $method->isStatic()
+ && ! $isInBaseComponentClass;
+ });
+
+ return array_map(function ($method) {
+ return $method->getName();
+ }, $methods);
+ }
+
+ static function hasAttribute($target, $property, $attributeClass) {
+ $property = static::getProperty($target, $property);
+
+ foreach ($property->getAttributes() as $attribute) {
+ $instance = $attribute->newInstance();
+
+ if ($instance instanceof $attributeClass) return true;
+ }
+
+ return false;
+ }
+
+ static function getProperty($target, $property) {
+ return (new \ReflectionObject($target))->getProperty($property);
+ }
+
+ static function propertyIsTyped($target, $property) {
+ $property = static::getProperty($target, $property);
+
+ return $property->hasType();
+ }
+
+ static function propertyIsTypedAndUninitialized($target, $property) {
+ $property = static::getProperty($target, $property);
+
+ return $property->hasType() && (! $property->isInitialized($target));
+ }
+}
diff --git a/portman/lib/Livewire/Drawer/ImplicitRouteBinding.php b/portman/lib/Livewire/Drawer/ImplicitRouteBinding.php
new file mode 100644
index 00000000..594aa85c
--- /dev/null
+++ b/portman/lib/Livewire/Drawer/ImplicitRouteBinding.php
@@ -0,0 +1,148 @@
+container = $container;
+ }
+
+ public function resolveAllParameters(Route $route, Component $component)
+ {
+ $params = $this->resolveMountParameters($route, $component);
+ $props = $this->resolveComponentProps($route, $component);
+
+ return $params->merge($props)->all();
+ }
+
+ public function resolveMountParameters(Route $route, Component $component)
+ {
+ if (! method_exists($component, 'mount')) {
+ return new Collection();
+ }
+
+ // Cache the current route action (this callback actually), just to be safe.
+ $cache = $route->getAction();
+
+ // We'll set the route action to be the "mount" method from the chosen
+ // Livewire component, to get the proper implicit bindings.
+ $route->uses(get_class($component).'@mount');
+
+ try {
+ // This is normally handled in the "SubstituteBindings" middleware, but
+ // because that middleware has already ran, we need to run them again.
+ $this->container['router']->substituteImplicitBindings($route);
+
+ $parameters = $route->resolveMethodDependencies($route->parameters(), new ReflectionMethod($component, 'mount'));
+
+ // Restore the original route action...
+ $route->setAction($cache);
+ } catch(\Exception $e) {
+ // Restore the original route action before an exception is thrown...
+ $route->setAction($cache);
+
+ throw $e;
+ }
+
+ return new Collection($parameters);
+ }
+
+ public function resolveComponentProps(Route $route, Component $component)
+ {
+ return $this->getPublicPropertyTypes($component)
+ ->intersectByKeys($route->parametersWithoutNulls())
+ ->map(function ($className, $propName) use ($route) {
+ // If typed public property, resolve the class
+ if ($className) {
+ $resolved = $this->resolveParameter($route, $propName, $className);
+
+ // We'll also pass the resolved model back to the route
+ // so that it can be used for any depending on bindings
+ $route->setParameter($propName, $resolved);
+
+ return $resolved;
+ }
+
+ // Otherwise, just return the route parameter
+ return $route->parameter($propName);
+ });
+ }
+
+ public function getPublicPropertyTypes($component)
+ {
+ return collect(Utils::getPublicPropertiesDefinedOnSubclass($component))
+ ->map(function ($value, $name) use ($component) {
+ return Reflector::getParameterClassName(new \ReflectionProperty($component, $name));
+ });
+ }
+
+ protected function resolveParameter($route, $parameterName, $parameterClassName)
+ {
+ $parameterValue = $route->parameter($parameterName);
+
+ if ($parameterValue instanceof UrlRoutable) {
+ return $parameterValue;
+ }
+
+ if($enumValue = $this->resolveEnumParameter($parameterValue, $parameterClassName)) {
+ return $enumValue;
+ }
+
+ $instance = $this->container->make($parameterClassName);
+
+ $parent = $route->parentOfParameter($parameterName);
+
+ if ($parent instanceof UrlRoutable && ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
+ $model = $parent->resolveChildRouteBinding($parameterName, $parameterValue, $route->bindingFieldFor($parameterName));
+ } else {
+ if ($route->allowsTrashedBindings()) {
+ $model = $instance->resolveSoftDeletableRouteBinding($parameterValue, $route->bindingFieldFor($parameterName));
+ } else {
+ $model = $instance->resolveRouteBinding($parameterValue, $route->bindingFieldFor($parameterName));
+ }
+ }
+
+ if (! $model) {
+ throw (new ModelNotFoundException())->setModel(get_class($instance), [$parameterValue]);
+ }
+
+ return $model;
+ }
+
+ protected function resolveEnumParameter($parameterValue, $parameterClassName)
+ {
+ if ($parameterValue instanceof BackedEnum) {
+ return $parameterValue;
+ }
+
+ if ((new ReflectionClass($parameterClassName))->isEnum()) {
+ $enumValue = $parameterClassName::tryFrom($parameterValue);
+
+ if (is_null($enumValue)) {
+ throw new BackedEnumCaseNotFoundException($parameterClassName, $parameterValue);
+ }
+
+ return $enumValue;
+ }
+
+ return null;
+ }
+}
diff --git a/portman/lib/Livewire/Drawer/Regexes.php b/portman/lib/Livewire/Drawer/Regexes.php
new file mode 100644
index 00000000..76257fc3
--- /dev/null
+++ b/portman/lib/Livewire/Drawer/Regexes.php
@@ -0,0 +1,169 @@
+
+ (?:
+ \s+
+ (?:
+ (?:
+ @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
+ )
+ |
+ (?:
+ \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ )
+ |
+ (?:
+ [\w\-:.@]+
+ (
+ =
+ (?:
+ \\\"[^\\\"]*\\\"
+ |
+ \'[^\']*\'
+ |
+ [^\'\\\"=<>]+
+ )
+ )?
+ )
+ )
+ )*
+ \s*
+ )
+ (?
+ ";
+
+ static $livewireOpeningTagOrSelfClosingTag = "
+ <
+ \s*
+ livewire[-\:]([\w\-\:\.]*)
+ (?
+ (?:
+ \s+
+ (?:
+ (?:
+ @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
+ )
+ |
+ (?:
+ \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ )
+ |
+ (?:
+ [:][$][\w]+
+ )
+ |
+ (?:
+ [\w\-:.@]+
+ (
+ =
+ (?:
+ \\\"[^\\\"]*\\\"
+ |
+ \'[^\']*\'
+ |
+ [^\'\\\"=<>]+
+ )
+ )?
+ )
+ )
+ )*
+ \s*
+ )
+ \/?>
+ ";
+
+ static $livewireSelfClosingTag = "
+ <
+ \s*
+ livewire[-\:]([\w\-\:\.]*)
+ \s*
+ (?
+ (?:
+ \s+
+ (?:
+ (?:
+ @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
+ )
+ |
+ (?:
+ \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ )
+ |
+ (?:
+ [\w\-:.@]+
+ (
+ =
+ (?:
+ \\\"[^\\\"]*\\\"
+ |
+ \'[^\']*\'
+ |
+ [^\'\\\"=<>]+
+ )
+ )?
+ )
+ )
+ )*
+ \s*
+ )
+ \/>
+ ";
+
+ static $livewireClosingTag = '<\/\s*livewire[-\:][\w\-\:\.]*\s*>';
+
+ static $slotOpeningTag = "
+ <
+ \s*
+ x[\-\:]slot
+ (?:\:(?\w+(?:-\w+)*))?
+ (?:\s+(:?)name=(?(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+)))?
+ (?
+ (?:
+ \s+
+ (?:
+ (?:
+ @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
+ )
+ |
+ (?:
+ \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ )
+ |
+ (?:
+ [\w\-:.@]+
+ (
+ =
+ (?:
+ \\\"[^\\\"]*\\\"
+ |
+ \'[^\']*\'
+ |
+ [^\'\\\"=<>]+
+ )
+ )?
+ )
+ )
+ )*
+ \s*
+ )
+ (?
+ ";
+
+ static $slotClosingTag = '<\/\s*x[\-\:]slot[^>]*>';
+
+ static $bladeDirective = "\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?";
+
+ static function specificBladeDirective($directive) {
+ return "(@?$directive(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))";
+ }
+}
diff --git a/portman/lib/Livewire/Drawer/Utils.php b/portman/lib/Livewire/Drawer/Utils.php
new file mode 100644
index 00000000..782c7268
--- /dev/null
+++ b/portman/lib/Livewire/Drawer/Utils.php
@@ -0,0 +1,202 @@
+mapWithKeys(function ($value, $key) {
+ return [$key => static::escapeStringForHtml($value)];
+ })->map(function ($value, $key) {
+ return sprintf('%s="%s"', $key, $value);
+ })->implode(' ');
+ }
+
+ static function escapeStringForHtml($subject)
+ {
+ if (is_string($subject) || is_numeric($subject)) {
+ return htmlspecialchars($subject, ENT_QUOTES|ENT_SUBSTITUTE);
+ }
+
+ return htmlspecialchars(json_encode($subject), ENT_QUOTES|ENT_SUBSTITUTE);
+ }
+
+ static function pretendResponseIsFile($file, $contentType = 'application/javascript; charset=utf-8')
+ {
+ $lastModified = filemtime($file);
+
+ return static::cachedFileResponse($file, $contentType, $lastModified,
+ fn ($headers) => response()->file($file, $headers));
+ }
+
+ static function pretendPreviewResponseIsPreviewFile($filename)
+ {
+ $file = FileUploadConfiguration::path($filename);
+ $storage = FileUploadConfiguration::storage();
+ $mimeType = FileUploadConfiguration::mimeType($filename);
+ $lastModified = FileUploadConfiguration::lastModified($file);
+
+ return self::cachedFileResponse($filename, $mimeType, $lastModified,
+ fn ($headers) => $storage->download($file, $filename, $headers));
+ }
+
+ static private function cachedFileResponse($filename, $contentType, $lastModified, $downloadCallback)
+ {
+ $expires = strtotime('+1 year');
+ $cacheControl = 'public, max-age=31536000';
+
+ if (static::matchesCache($lastModified)) {
+ return response('', 304, [
+ 'Expires' => static::httpDate($expires),
+ 'Cache-Control' => $cacheControl,
+ ]);
+ }
+
+ $headers = [
+ 'Content-Type' => $contentType,
+ 'Expires' => static::httpDate($expires),
+ 'Cache-Control' => $cacheControl,
+ 'Last-Modified' => static::httpDate($lastModified),
+ ];
+
+ if (str($filename)->endsWith('.br')) {
+ $headers['Content-Encoding'] = 'br';
+ }
+
+ return $downloadCallback($headers);
+ }
+
+ static function matchesCache($lastModified)
+ {
+ $ifModifiedSince = app(Request::class)->header('if-modified-since');
+
+ return $ifModifiedSince !== null && @strtotime($ifModifiedSince) === $lastModified;
+ }
+
+ static function httpDate($timestamp)
+ {
+ return sprintf('%s GMT', gmdate('D, d M Y H:i:s', $timestamp));
+ }
+
+ static function containsDots($subject)
+ {
+ return str_contains($subject, '.');
+ }
+
+ static function dotSegments($subject)
+ {
+ return explode('.', $subject);
+ }
+
+ static function beforeFirstDot($subject)
+ {
+ return head(explode('.', $subject));
+ }
+
+ static function afterFirstDot($subject) : string
+ {
+ return str($subject)->after('.');
+ }
+
+ static public function hasProperty($target, $property)
+ {
+ return property_exists($target, static::beforeFirstDot($property));
+ }
+
+ static public function shareWithViews($name, $value)
+ {
+ $old = app('view')->shared($name, 'notfound');
+
+ app('view')->share($name, $value);
+
+ return $revert = function () use ($name, $old) {
+ if ($old === 'notfound') {
+ unset(invade(app('view'))->shared[$name]);
+ } else {
+ app('view')->share($name, $old);
+ }
+ };
+ }
+
+ static function generateBladeView($subject, $data = [])
+ {
+ if (! is_string($subject)) {
+ return tap($subject)->with($data);
+ }
+
+ $component = new class($subject) extends \Illuminate\View\Component
+ {
+ protected $template;
+
+ public function __construct($template)
+ {
+ $this->template = $template;
+ }
+
+ public function render()
+ {
+ return $this->template;
+ }
+ };
+
+ $view = app('view')->make($component->resolveView(), $data);
+
+ return $view;
+ }
+
+ static function applyMiddleware(\Illuminate\Http\Request $request, $middleware = [])
+ {
+ $response = (new \Illuminate\Pipeline\Pipeline(app()))
+ ->send($request)
+ ->through($middleware)
+ ->then(function() {
+ return new \Illuminate\Http\Response();
+ });
+
+ if ($response instanceof \Illuminate\Http\RedirectResponse) {
+ abort($response);
+ }
+
+ return $response;
+ }
+
+ static function extractAttributeDataFromHtml($html, $attribute)
+ {
+ $data = (string) str($html)->betweenFirst($attribute.'="', '"');
+
+ return json_decode(
+ htmlspecialchars_decode($data, ENT_QUOTES|ENT_SUBSTITUTE),
+ associative: true,
+ );
+ }
+}
diff --git a/portman/lib/Livewire/EventBus.php b/portman/lib/Livewire/EventBus.php
new file mode 100644
index 00000000..bd969e3e
--- /dev/null
+++ b/portman/lib/Livewire/EventBus.php
@@ -0,0 +1,82 @@
+singleton($this::class);
+ }
+
+ function on($name, $callback) {
+ if (! isset($this->listeners[$name])) $this->listeners[$name] = [];
+
+ $this->listeners[$name][] = $callback;
+
+ return fn() => $this->off($name, $callback);
+ }
+
+ function before($name, $callback) {
+ if (! isset($this->listenersBefore[$name])) $this->listenersBefore[$name] = [];
+
+ $this->listenersBefore[$name][] = $callback;
+
+ return fn() => $this->off($name, $callback);
+ }
+
+ function after($name, $callback) {
+ if (! isset($this->listenersAfter[$name])) $this->listenersAfter[$name] = [];
+
+ $this->listenersAfter[$name][] = $callback;
+
+ return fn() => $this->off($name, $callback);
+ }
+
+ function off($name, $callback) {
+ $index = array_search($callback, $this->listeners[$name] ?? []);
+ $indexAfter = array_search($callback, $this->listenersAfter[$name] ?? []);
+ $indexBefore = array_search($callback, $this->listenersBefore[$name] ?? []);
+
+ if ($index !== false) unset($this->listeners[$name][$index]);
+ elseif ($indexAfter !== false) unset($this->listenersAfter[$name][$indexAfter]);
+ elseif ($indexBefore !== false) unset($this->listenersBefore[$name][$indexBefore]);
+ }
+
+ function trigger($name, ...$params) {
+ $middlewares = [];
+
+ $listeners = array_merge(
+ ($this->listenersBefore[$name] ?? []),
+ ($this->listeners[$name] ?? []),
+ ($this->listenersAfter[$name] ?? []),
+ );
+
+ foreach ($listeners as $callback) {
+ $result = $callback(...$params);
+
+ if ($result) {
+ $middlewares[] = $result;
+ }
+ }
+
+ return function (&$forward = null, ...$extras) use ($middlewares) {
+ foreach ($middlewares as $finisher) {
+ if ($finisher === null) continue;
+
+ $finisher = is_array($finisher) ? last($finisher) : $finisher;
+
+ $result = $finisher($forward, ...$extras);
+
+ // Only overwrite previous "forward" if something is returned from the callback.
+ $forward = $result ?? $forward;
+ }
+
+ return $forward;
+ };
+ }
+}
diff --git a/portman/lib/Livewire/Exceptions/BypassViewHandler.php b/portman/lib/Livewire/Exceptions/BypassViewHandler.php
new file mode 100644
index 00000000..6f4aee2e
--- /dev/null
+++ b/portman/lib/Livewire/Exceptions/BypassViewHandler.php
@@ -0,0 +1,8 @@
+component = $component;
+ $this->subName = $subName;
+ $this->subTarget = $subTarget;
+ $this->level = $level;
+ $this->levelName = $name;
+ }
+
+ function getComponent()
+ {
+ return $this->component;
+ }
+
+ function getSubTarget()
+ {
+ return $this->subTarget;
+ }
+
+ function getSubName()
+ {
+ return $this->subName;
+ }
+
+ function getLevel()
+ {
+ return $this->level;
+ }
+
+ function getName()
+ {
+ return $this->levelName;
+ }
+
+ function getValue()
+ {
+ if ($this->level !== AttributeLevel::PROPERTY) {
+ throw new \Exception('Can\'t get the value of a non-property attribute.');
+ }
+
+ return data_get($this->component->all(), $this->levelName);
+ }
+
+ function setValue($value, ?bool $nullable = false)
+ {
+ if ($this->level !== AttributeLevel::PROPERTY) {
+ throw new \Exception('Can\'t set the value of a non-property attribute.');
+ }
+
+ if ($enum = $this->tryingToSetStringOrIntegerToEnum($value)) {
+ if($nullable) {
+ $value = $enum::tryFrom($value);
+ }
+
+ else {
+ $value = $enum::from($value);
+ }
+ }
+
+ data_set($this->component, $this->levelName, $value);
+ }
+
+ protected function tryingToSetStringOrIntegerToEnum($subject)
+ {
+ if (! is_string($subject) && ! is_int($subject)) return;
+
+ $target = $this->subTarget ?? $this->component;
+
+ $name = $this->subName ?? $this->levelName;
+
+ $property = str($name)->before('.')->toString();
+
+ $reflection = new \ReflectionProperty($target, $property);
+
+ $type = $reflection->getType();
+
+ // If the type is available, display its name
+ if ($type instanceof \ReflectionNamedType) {
+ $name = $type->getName();
+
+ // If the type is a BackedEnum then return it's name
+ if (is_subclass_of($name, \BackedEnum::class)) {
+ return $name;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportAttributes/AttributeCollection.php b/portman/lib/Livewire/Features/SupportAttributes/AttributeCollection.php
new file mode 100644
index 00000000..17ad7e8e
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportAttributes/AttributeCollection.php
@@ -0,0 +1,55 @@
+push(tap($attribute->newInstance(), function ($attribute) use ($component, $subTarget) {
+ $attribute->__boot($component, AttributeLevel::ROOT, null, null, $subTarget);
+ }));
+ }
+
+ foreach ($reflected->getMethods() as $method) {
+ foreach ($method->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+ $instance->push(tap($attribute->newInstance(), function ($attribute) use ($component, $method, $propertyNamePrefix, $subTarget) {
+ $attribute->__boot($component, AttributeLevel::METHOD, $propertyNamePrefix . $method->getName(), $method->getName(), $subTarget);
+ }));
+ }
+ }
+
+ foreach ($reflected->getProperties() as $property) {
+ foreach ($property->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+ $instance->push(tap($attribute->newInstance(), function ($attribute) use ($component, $property, $propertyNamePrefix, $subTarget) {
+ $attribute->__boot($component, AttributeLevel::PROPERTY, $propertyNamePrefix . $property->getName(), $property->getName(), $subTarget);
+ }));
+ }
+ }
+
+ return $instance;
+ }
+
+ protected static function getClassAttributesRecursively($reflected) {
+ $attributes = [];
+
+ while ($reflected) {
+ foreach ($reflected->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+ $attributes[] = $attribute;
+ }
+
+ $reflected = $reflected->getParentClass();
+ }
+
+ return $attributes;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportAttributes/AttributeLevel.php b/portman/lib/Livewire/Features/SupportAttributes/AttributeLevel.php
new file mode 100644
index 00000000..c1c6fd23
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportAttributes/AttributeLevel.php
@@ -0,0 +1,10 @@
+attributes ??= AttributeCollection::fromComponent($this);
+ }
+
+ function setPropertyAttribute($property, $attribute)
+ {
+ $attribute->__boot($this, AttributeLevel::PROPERTY, $property);
+
+ $this->mergeOutsideAttributes(new AttributeCollection([$attribute]));
+ }
+
+ function mergeOutsideAttributes(AttributeCollection $attributes)
+ {
+ $this->attributes = $this->getAttributes()->concat($attributes);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportAttributes/SupportAttributes.php b/portman/lib/Livewire/Features/SupportAttributes/SupportAttributes.php
new file mode 100644
index 00000000..f4fce83f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportAttributes/SupportAttributes.php
@@ -0,0 +1,143 @@
+component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->each(function ($attribute) use ($params) {
+ if (method_exists($attribute, 'boot')) {
+ $attribute->boot(...$params);
+ }
+ });
+ }
+
+ function mount(...$params)
+ {
+ $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->each(function ($attribute) use ($params) {
+ if (method_exists($attribute, 'mount')) {
+ $attribute->mount(...$params);
+ }
+ });
+ }
+
+ function hydrate(...$params)
+ {
+ $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->each(function ($attribute) use ($params) {
+ if (method_exists($attribute, 'hydrate')) {
+ $attribute->hydrate(...$params);
+ }
+ });
+ }
+
+ function update($propertyName, $fullPath, $newValue)
+ {
+ $callbacks = $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->filter(fn ($attr) => $attr->getLevel() === AttributeLevel::PROPERTY)
+ // Call "update" on the root property attribute even if it's a deep update...
+ ->filter(fn ($attr) => str($fullPath)->startsWith($attr->getName() . '.') || $fullPath === $attr->getName())
+ ->map(function ($attribute) use ($fullPath, $newValue) {
+ if (method_exists($attribute, 'update')) {
+ return $attribute->update($fullPath, $newValue);
+ }
+ });
+
+ return function (...$params) use ($callbacks) {
+ foreach ($callbacks as $callback) {
+ if (is_callable($callback)) $callback(...$params);
+ }
+ };
+ }
+
+ function call($method, $params, $returnEarly)
+ {
+ $callbacks = $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->filter(fn ($attr) => $attr->getLevel() === AttributeLevel::METHOD)
+ ->filter(fn ($attr) => $attr->getName() === $method)
+ ->map(function ($attribute) use ($params, $returnEarly) {
+ if (method_exists($attribute, 'call')) {
+ return $attribute->call($params, $returnEarly);
+ }
+ });
+
+ return function (...$params) use ($callbacks) {
+ foreach ($callbacks as $callback) {
+ if (is_callable($callback)) $callback(...$params);
+ }
+ };
+ }
+
+ function render(...$params)
+ {
+ $callbacks = $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->map(function ($attribute) use ($params) {
+ if (method_exists($attribute, 'render')) {
+ return $attribute->render(...$params);
+ }
+ });
+
+ return function (...$params) use ($callbacks) {
+ foreach ($callbacks as $callback) {
+ if (is_callable($callback)) {
+ $callback(...$params);
+ }
+ }
+ };
+ }
+
+ function dehydrate(...$params)
+ {
+ $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->each(function ($attribute) use ($params) {
+ if (method_exists($attribute, 'dehydrate')) {
+ $attribute->dehydrate(...$params);
+ }
+ });
+ }
+
+ function destroy(...$params)
+ {
+ $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->each(function ($attribute) use ($params) {
+ if (method_exists($attribute, 'destroy')) {
+ $attribute->destroy(...$params);
+ }
+ });
+ }
+
+ function exception(...$params)
+ {
+ $this->component
+ ->getAttributes()
+ ->whereInstanceOf(LivewireAttribute::class)
+ ->each(function ($attribute) use ($params) {
+ if (method_exists($attribute, 'exception')) {
+ $attribute->exception(...$params);
+ }
+ });
+ }
+
+}
diff --git a/portman/lib/Livewire/Features/SupportAutoInjectedAssets/SupportAutoInjectedAssets.php b/portman/lib/Livewire/Features/SupportAutoInjectedAssets/SupportAutoInjectedAssets.php
new file mode 100644
index 00000000..1aa606e7
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportAutoInjectedAssets/SupportAutoInjectedAssets.php
@@ -0,0 +1,99 @@
+listen(RequestHandled::class, function ($handled) {
+ // If this is a successful HTML response...
+ if (! str($handled->response->headers->get('content-type'))->contains('text/html')) return;
+ if (! method_exists($handled->response, 'status') || $handled->response->status() !== 200) return;
+
+ $assetsHead = '';
+ $assetsBody = '';
+
+ // If `@assets` has been used outside of a Livewire component then we need
+ // to process those assets to be injected alongside the other assets...
+ SupportScriptsAndAssets::processNonLivewireAssets();
+
+ $assets = array_values(SupportScriptsAndAssets::getAssets());
+
+ // If there are additional head assets, inject those...
+ if (count($assets) > 0) {
+ foreach ($assets as $asset) {
+ $assetsHead .= $asset."\n";
+ }
+ }
+
+ // If we're injecting Livewire assets...
+ if (static::shouldInjectLivewireAssets()) {
+ $assetsHead .= FrontendAssets::styles()."\n";
+ $assetsBody .= FrontendAssets::scripts()."\n";
+ }
+
+ if ($assetsHead === '' && $assetsBody === '') return;
+
+ $html = $handled->response->getContent();
+
+ if (str($html)->contains('')) {
+ $originalContent = $handled->response->original;
+ $handled->response->setContent(static::injectAssets($html, $assetsHead, $assetsBody));
+ $handled->response->original = $originalContent;
+ }
+ });
+ }
+
+ protected static function shouldInjectLivewireAssets()
+ {
+ if (! static::$forceAssetInjection && config('livewire.inject_assets', true) === false) return false;
+ if ((! static::$hasRenderedAComponentThisRequest) && (! static::$forceAssetInjection)) return false;
+ if (app(FrontendAssets::class)->hasRenderedScripts) return false;
+
+ return true;
+ }
+
+ protected static function getLivewireAssets()
+ {
+ $livewireStyles = FrontendAssets::styles();
+ $livewireScripts = FrontendAssets::scripts();
+ }
+
+ public function dehydrate()
+ {
+ static::$hasRenderedAComponentThisRequest = true;
+ }
+
+ static function injectAssets($html, $assetsHead, $assetsBody)
+ {
+ $html = str($html);
+
+ if ($html->test('/<\s*\/\s*head\s*>/i') && $html->test('/<\s*\/\s*body\s*>/i')) {
+ return $html
+ ->replaceMatches('/(<\s*\/\s*head\s*>)/i', $assetsHead.'$1')
+ ->replaceMatches('/(<\s*\/\s*body\s*>)/i', $assetsBody.'$1')
+ ->toString();
+ }
+
+ return $html
+ ->replaceMatches('/(<\s*html(?:\s[^>])*>)/i', '$1'.$assetsHead)
+ ->replaceMatches('/(<\s*\/\s*html\s*>)/i', $assetsBody.'$1')
+ ->toString();
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportBladeAttributes/SupportBladeAttributes.php b/portman/lib/Livewire/Features/SupportBladeAttributes/SupportBladeAttributes.php
new file mode 100644
index 00000000..ba1fd499
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportBladeAttributes/SupportBladeAttributes.php
@@ -0,0 +1,22 @@
+whereStartsWith('wire:'.$name));
+
+ $directive = head(array_keys($entries));
+ $value = head(array_values($entries));
+
+ return new WireDirective($name, $directive, $value);
+ });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportChecksumErrorDebugging/SupportChecksumErrorDebugging.php b/portman/lib/Livewire/Features/SupportChecksumErrorDebugging/SupportChecksumErrorDebugging.php
new file mode 100644
index 00000000..17cc578d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportChecksumErrorDebugging/SupportChecksumErrorDebugging.php
@@ -0,0 +1,82 @@
+ [], 'failure' => null]));
+
+ $this->info('Monitoring for checksum errors...');
+
+ while (true) {
+ $cache = json_decode(File::get($file), true);
+
+ if ($cache['failure']) {
+ $this->info('Failure: '.$cache['failure']);
+
+ $cache['failure'] = null;
+ }
+
+ File::put($file, json_encode($cache));
+
+ sleep(1);
+ }
+
+ })->purpose('Debug checksum errors in Livewire');
+
+ on('checksum.fail', function ($checksum, $comparitor, $tamperedSnapshot) use ($file) {
+ $cache = json_decode(File::get($file), true);
+
+ if (! isset($cache['checksums'][$checksum])) return;
+
+ $canonicalSnapshot = $cache['checksums'][$checksum];
+
+ $good = $this->array_diff_assoc_recursive($canonicalSnapshot, $tamperedSnapshot);
+ $bad = $this->array_diff_assoc_recursive($tamperedSnapshot, $canonicalSnapshot);
+
+ $cache['failure'] = "\nBefore: ".json_encode($good)."\nAfter: ".json_encode($bad);
+
+ File::put($file, json_encode($cache));
+ });
+
+ on('checksum.generate', function ($checksum, $snapshot) use ($file) {
+ $cache = json_decode(File::get($file), true);
+
+ $cache['checksums'][$checksum] = $snapshot;
+
+ File::put($file, json_encode($cache));
+ });
+ }
+
+ // https://www.php.net/manual/en/function.array-diff-assoc.php#111675
+ function array_diff_assoc_recursive($array1, $array2) {
+ $difference=array();
+
+ foreach($array1 as $key => $value) {
+ if( is_array($value) ) {
+ if( !isset($array2[$key]) || !is_array($array2[$key]) ) {
+ $difference[$key] = $value;
+ } else {
+ $new_diff = $this->array_diff_assoc_recursive($value, $array2[$key]);
+ if( !empty($new_diff) )
+ $difference[$key] = $new_diff;
+ }
+ } else if( !array_key_exists($key,$array2) || $array2[$key] !== $value ) {
+ $difference[$key] = $value;
+ }
+ }
+
+ return $difference;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportCompiledWireKeys/SupportCompiledWireKeys.php b/portman/lib/Livewire/Features/SupportCompiledWireKeys/SupportCompiledWireKeys.php
new file mode 100644
index 00000000..f7020eee
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportCompiledWireKeys/SupportCompiledWireKeys.php
@@ -0,0 +1,141 @@
+ null,
+ 'index' => null,
+ 'key' => null,
+ ];
+
+ public static function provide()
+ {
+ on('flush-state', function () {
+ static::$loopStack = [];
+ static::$currentLoop = [
+ 'count' => null,
+ 'index' => null,
+ 'key' => null,
+ ];
+ });
+
+ if (! config('livewire.smart_wire_keys', true)) {
+ return;
+ }
+
+ static::registerPrecompilers();
+ }
+
+ public static function registerPrecompilers()
+ {
+ Blade::precompiler(function ($contents) {
+ $contents = static::compile($contents);
+
+ return $contents;
+ });
+ }
+
+ public static function compile($contents)
+ {
+ // Strip out all livewire tag components as we don't want to match any of them...
+ $placeholder = '<__livewire-component-placeholder__>';
+ $cleanedContents = preg_replace('/]+?\/>/is', $placeholder, $contents);
+
+ // Handle `wire:key` attributes on elements...
+ preg_match_all('/(?<=\s)wire:key\s*=\s*(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')/', $cleanedContents, $keys);
+
+ foreach ($keys[0] as $index => $key) {
+ $escapedKey = str_replace("'", "\'", $keys[1][$index]);
+ $prefix = "";
+ $contents = str_replace($key, $prefix . $key, $contents);
+ }
+
+ // Handle `wire:key` attributes on Blade components...
+ $contents = preg_replace(
+ '/(<\?php\s+\$component->withAttributes\(\[.*?\]\);\s*\?>)/s',
+ "$1\n\n",
+ $contents
+ );
+
+ return $contents;
+ }
+
+ public static function openLoop() {
+ if (static::$currentLoop['count'] === null) {
+ static::$currentLoop['count'] = 0;
+ } else {
+ static::$currentLoop['count']++;
+ }
+
+ static::$loopStack[] = static::$currentLoop;
+
+ static::$currentLoop = [
+ 'count' => null,
+ 'index' => null,
+ 'key' => null,
+ ];
+ }
+
+ public static function startLoop($index) {
+ static::$currentLoop['index'] = $index;
+ }
+
+ public static function endLoop() {
+ static::$currentLoop = [
+ 'count' => null,
+ 'index' => null,
+ 'key' => null,
+ ];
+ }
+
+ public static function closeLoop() {
+ static::$currentLoop = array_pop(static::$loopStack);
+ }
+
+ public static function processElementKey($keyString, $data)
+ {
+ $key = Blade::render($keyString, $data);
+
+ static::$currentLoop['key'] = $key;
+ }
+
+ public static function processComponentKey($component)
+ {
+ if ($component->attributes->has('wire:key')) {
+ static::$currentLoop['key'] = $component->attributes->get('wire:key');
+ }
+ }
+
+ public static function generateKey($deterministicBladeKey, $key = null)
+ {
+ $finalKey = $deterministicBladeKey;
+
+ $loops = array_merge(static::$loopStack, [static::$currentLoop]);
+
+ foreach ($loops as $loop) {
+ if (isset($loop['key']) || isset($loop['index'])) {
+ $finalKey .= isset($loop['key'])
+ ? '-' . $loop['key']
+ : '-' . $loop['index'];
+ }
+
+ if (isset($loop['count'])) {
+ $finalKey .= '-' . $loop['count'];
+ }
+ }
+
+ if (isset($key) && $key !== '') {
+ $finalKey .= '-' . $key;
+ }
+
+ return $finalKey;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportComputed/BaseComputed.php b/portman/lib/Livewire/Features/SupportComputed/BaseComputed.php
new file mode 100644
index 00000000..6ab56298
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportComputed/BaseComputed.php
@@ -0,0 +1,152 @@
+handleMagicGet(...));
+ on('__get', $this->handleMagicGet(...));
+
+ off('__unset', $this->handleMagicUnset(...));
+ on('__unset', $this->handleMagicUnset(...));
+ }
+
+ function call()
+ {
+ throw new CannotCallComputedDirectlyException(
+ $this->component->getName(),
+ $this->getName(),
+ );
+ }
+
+ protected function handleMagicGet($target, $property, $returnValue)
+ {
+ if ($target !== $this->component) return;
+ if ($this->generatePropertyName($property) !== $this->getName()) return;
+
+ if ($this->persist) {
+ $returnValue($this->handlePersistedGet());
+
+ return;
+ }
+
+ if ($this->cache) {
+ $returnValue($this->handleCachedGet());
+
+ return;
+ }
+
+ $returnValue(
+ $this->requestCachedValue ??= $this->evaluateComputed()
+ );
+ }
+
+ protected function handleMagicUnset($target, $property)
+ {
+ if ($target !== $this->component) return;
+ if ($property !== $this->getName()) return;
+
+ if ($this->persist) {
+ $this->handlePersistedUnset();
+
+ return;
+ }
+
+ if ($this->cache) {
+ $this->handleCachedUnset();
+
+ return;
+ }
+
+ unset($this->requestCachedValue);
+ }
+
+ protected function handlePersistedGet()
+ {
+ $key = $this->generatePersistedKey();
+
+ $closure = fn () => $this->evaluateComputed();
+
+ return match(Cache::supportsTags() && !empty($this->tags)) {
+ true => Cache::tags($this->tags)->remember($key, $this->seconds, $closure),
+ default => Cache::remember($key, $this->seconds, $closure)
+ };
+ }
+
+ protected function handleCachedGet()
+ {
+ $key = $this->generateCachedKey();
+
+ $closure = fn () => $this->evaluateComputed();
+
+ return match(Cache::supportsTags() && !empty($this->tags)) {
+ true => Cache::tags($this->tags)->remember($key, $this->seconds, $closure),
+ default => Cache::remember($key, $this->seconds, $closure)
+ };
+ }
+
+ protected function handlePersistedUnset()
+ {
+ $key = $this->generatePersistedKey();
+
+ Cache::forget($key);
+ }
+
+ protected function handleCachedUnset()
+ {
+ $key = $this->generateCachedKey();
+
+ Cache::forget($key);
+ }
+
+ protected function generatePersistedKey()
+ {
+ if ($this->key) return $this->key;
+
+ return 'lw_computed.'.$this->component->getId().'.'.$this->getName();
+ }
+
+ protected function generateCachedKey()
+ {
+ if ($this->key) return $this->key;
+
+ return 'lw_computed.'.$this->component->getName().'.'.$this->getName();
+ }
+
+ protected function evaluateComputed()
+ {
+ return invade($this->component)->{parent::getName()}();
+ }
+
+ public function getName()
+ {
+ return $this->generatePropertyName(parent::getName());
+ }
+
+ private function generatePropertyName($value)
+ {
+ return str($value)->camel()->toString();
+ }
+
+
+}
diff --git a/portman/lib/Livewire/Features/SupportComputed/CannotCallComputedDirectlyException.php b/portman/lib/Livewire/Features/SupportComputed/CannotCallComputedDirectlyException.php
new file mode 100644
index 00000000..e7640e67
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportComputed/CannotCallComputedDirectlyException.php
@@ -0,0 +1,15 @@
+unset('computedProperties', $property);
+ }
+ });
+ }
+
+ public static function getComputedProperties($target)
+ {
+ return collect(static::getComputedPropertyNames($target))
+ ->mapWithKeys(function ($property) use ($target) {
+ return [$property => static::getComputedProperty($target, $property)];
+ })
+ ->all();
+ }
+
+ public static function hasComputedProperty($target, $property)
+ {
+ return array_search((string) str($property)->camel(), static::getComputedPropertyNames($target)) !== false;
+ }
+
+ public static function getComputedProperty($target, $property)
+ {
+ if (! static::hasComputedProperty($target, $property)) {
+ throw new \Exception('No computed property found: $'.$property);
+ }
+
+ $method = 'get'.str($property)->studly().'Property';
+
+ store($target)->push(
+ 'computedProperties',
+ $value = store($target)->find('computedProperties', $property, fn() => wrap($target)->$method()),
+ $property,
+ );
+
+ return $value;
+ }
+
+ public static function getComputedPropertyNames($target)
+ {
+ $methodNames = SyntheticUtils::getPublicMethodsDefinedBySubClass($target);
+
+ return collect($methodNames)
+ ->filter(function ($method) {
+ return str($method)->startsWith('get')
+ && str($method)->endsWith('Property');
+ })
+ ->map(function ($method) {
+ return (string) str($method)->between('get', 'Property')->camel();
+ })
+ ->all();
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/AttributeCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/AttributeCommand.php
new file mode 100644
index 00000000..22e8b88b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/AttributeCommand.php
@@ -0,0 +1,57 @@
+baseClassNamespace = $classNamespace;
+ $this->baseTestNamespace = 'Tests\Feature\Livewire';
+
+ $classPath = static::generatePathFromNamespace($classNamespace);
+ $testPath = static::generateTestPathFromNamespace($this->baseTestNamespace);
+
+ $this->baseClassPath = rtrim($classPath, DIRECTORY_SEPARATOR).'/';
+ $this->baseViewPath = rtrim($viewPath, DIRECTORY_SEPARATOR).'/';
+ $this->baseTestPath = rtrim($testPath, DIRECTORY_SEPARATOR).'/';
+
+ if(! empty($stubSubDirectory) && str($stubSubDirectory)->startsWith('..')) {
+ $this->stubDirectory = rtrim(str($stubSubDirectory)->replaceFirst('..' . DIRECTORY_SEPARATOR, ''), DIRECTORY_SEPARATOR).'/';
+ } else {
+ $this->stubDirectory = rtrim('stubs'.DIRECTORY_SEPARATOR.$stubSubDirectory, DIRECTORY_SEPARATOR).'/';
+ }
+
+ $directories = preg_split('/[.\/(\\\\)]+/', $rawCommand);
+
+ $camelCase = str(array_pop($directories))->camel();
+ $kebabCase = str($camelCase)->kebab();
+
+ $this->component = $kebabCase;
+ $this->componentClass = str($this->component)->studly();
+
+ $this->directories = array_map([Str::class, 'studly'], $directories);
+ }
+
+ public function component()
+ {
+ return $this->component;
+ }
+
+ public function classPath()
+ {
+ return $this->baseClassPath.collect()
+ ->concat($this->directories)
+ ->push($this->classFile())
+ ->implode('/');
+ }
+
+ public function relativeClassPath() : string
+ {
+ return str($this->classPath())->replaceFirst(base_path().DIRECTORY_SEPARATOR, '');
+ }
+
+ public function classFile()
+ {
+ return $this->componentClass.'.php';
+ }
+
+ public function classNamespace()
+ {
+ return empty($this->directories)
+ ? $this->baseClassNamespace
+ : $this->baseClassNamespace.'\\'.collect()
+ ->concat($this->directories)
+ ->map([Str::class, 'studly'])
+ ->implode('\\');
+ }
+
+ public function className()
+ {
+ return $this->componentClass;
+ }
+
+ public function classContents($inline = false)
+ {
+ $stubName = $inline ? 'livewire.inline.stub' : 'livewire.stub';
+
+ if (File::exists($stubPath = base_path($this->stubDirectory.$stubName))) {
+ $template = file_get_contents($stubPath);
+ } else {
+ $template = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.$stubName);
+ }
+
+ if ($inline) {
+ $template = preg_replace('/\[quote\]/', $this->wisdomOfTheTao(), $template);
+ }
+
+ return preg_replace(
+ ['/\[namespace\]/', '/\[class\]/', '/\[view\]/'],
+ [$this->classNamespace(), $this->className(), $this->viewName()],
+ $template
+ );
+ }
+
+ public function viewPath()
+ {
+ return $this->baseViewPath.collect()
+ ->concat($this->directories)
+ ->map([Str::class, 'kebab'])
+ ->push($this->viewFile())
+ ->implode(DIRECTORY_SEPARATOR);
+ }
+
+ public function relativeViewPath() : string
+ {
+ return str($this->viewPath())->replaceFirst(base_path().'/', '');
+ }
+
+ public function viewFile()
+ {
+ return $this->component.'.blade.php';
+ }
+
+ public function viewName()
+ {
+ return collect()
+ ->when(config('livewire.view_path') !== resource_path(), function ($collection) {
+ return $collection->concat(explode('/',str($this->baseViewPath)->after(resource_path('views'))));
+ })
+ ->filter()
+ ->concat($this->directories)
+ ->map([Str::class, 'kebab'])
+ ->push($this->component)
+ ->implode('.');
+ }
+
+ public function viewContents()
+ {
+ if( ! File::exists($stubPath = base_path($this->stubDirectory.'livewire.view.stub'))) {
+ $stubPath = __DIR__.DIRECTORY_SEPARATOR.'livewire.view.stub';
+ }
+
+ return preg_replace(
+ '/\[quote\]/',
+ $this->wisdomOfTheTao(),
+ file_get_contents($stubPath)
+ );
+ }
+
+ public function testNamespace()
+ {
+ return empty($this->directories)
+ ? $this->baseTestNamespace
+ : $this->baseTestNamespace.'\\'.collect()
+ ->concat($this->directories)
+ ->map([Str::class, 'studly'])
+ ->implode('\\');
+ }
+
+ public function testClassName()
+ {
+ return $this->componentClass.'Test';
+ }
+
+ public function testFile()
+ {
+ return $this->componentClass.'Test.php';
+ }
+
+ public function testPath()
+ {
+ return $this->baseTestPath.collect()
+ ->concat($this->directories)
+ ->push($this->testFile())
+ ->implode('/');
+ }
+
+ public function relativeTestPath() : string
+ {
+ return str($this->testPath())->replaceFirst(base_path().'/', '');
+ }
+
+ public function testContents($testType = 'phpunit')
+ {
+ $stubName = $testType === 'pest' ? 'livewire.pest.stub' : 'livewire.test.stub';
+
+ if(File::exists($stubPath = base_path($this->stubDirectory.$stubName))) {
+ $template = file_get_contents($stubPath);
+ } else {
+ $template = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.$stubName);
+ }
+
+ return preg_replace(
+ ['/\[testnamespace\]/', '/\[classwithnamespace\]/', '/\[testclass\]/', '/\[class\]/'],
+ [$this->testNamespace(), $this->classNamespace() . '\\' . $this->className(), $this->testClassName(), $this->className()],
+ $template
+ );
+ }
+
+ public function wisdomOfTheTao()
+ {
+ $wisdom = require __DIR__.DIRECTORY_SEPARATOR.'the-tao.php';
+
+ return Arr::random($wisdom);
+ }
+
+ public static function generatePathFromNamespace($namespace)
+ {
+ $name = str($namespace)->finish('\\')->replaceFirst(app()->getNamespace(), '');
+ return app('path').'/'.str_replace('\\', '/', $name);
+ }
+
+ public static function generateTestPathFromNamespace($namespace)
+ {
+ return base_path(str($namespace)
+ ->replace('\\', '/', $namespace)
+ ->replaceFirst('T', 't'));
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/ComponentParserFromExistingComponent.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/ComponentParserFromExistingComponent.php
new file mode 100644
index 00000000..d67fbd93
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/ComponentParserFromExistingComponent.php
@@ -0,0 +1,45 @@
+existingParser = $existingParser;
+
+ parent::__construct($classNamespace, $viewPath, $rawCommand);
+ }
+
+ public function classContents($inline = false)
+ {
+ $originalFile = file_get_contents($this->existingParser->classPath());
+
+ $escapedClassNamespace = preg_replace('/\\\/', '\\\\\\', $this->existingParser->classNamespace());
+
+ return preg_replace_array(
+ ["/namespace {$escapedClassNamespace}/", "/class {$this->existingParser->className()}/", "/{$this->existingParser->viewName()}/"],
+ ["namespace {$this->classNamespace()}", "class {$this->className()}", $this->viewName()],
+ $originalFile
+ );
+ }
+
+ public function testContents($testType = 'phpunit')
+ {
+ $file_content = file_get_contents($this->existingParser->testPath());
+
+ $escapedTestNamespace = preg_replace('/\\\/', '\\\\\\', $this->existingParser->testNamespace());
+ $escapedClassWithNamespace = preg_replace('/\\\/', '\\\\\\', $this->existingParser->classNamespace() . '\\' . $this->existingParser->className());
+
+ $replaces = [
+ "/namespace {$escapedTestNamespace}/" => 'namespace ' . $this->testNamespace(),
+ "/use {$escapedClassWithNamespace}/" => 'use ' . $this->classNamespace() . '\\' . $this->className(),
+ "/class {$this->existingParser->testClassName()}/" => 'class ' . $this->testClassName(),
+ "/{$this->existingParser->className()}::class/" => $this->className() . '::class',
+ ];
+
+ return preg_replace(array_keys($replaces), array_values($replaces), $file_content);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/CopyCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/CopyCommand.php
new file mode 100644
index 00000000..fc923c48
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/CopyCommand.php
@@ -0,0 +1,86 @@
+parser = new ComponentParser(
+ config('livewire.class_namespace'),
+ config('livewire.view_path'),
+ $this->argument('name')
+ );
+
+ $this->newParser = new ComponentParserFromExistingComponent(
+ config('livewire.class_namespace'),
+ config('livewire.view_path'),
+ $this->argument('new-name'),
+ $this->parser
+ );
+
+ $force = $this->option('force');
+ $inline = $this->option('inline');
+ $test = $this->option('test');
+
+ $class = $this->copyClass($force, $inline);
+ if (! $inline) $view = $this->copyView($force);
+ if ($test){
+ $test = $this->copyTest($force);
+ }
+ $this->line(" COMPONENT COPIED > 🤙\n");
+ $class && $this->line("CLASS:> {$this->parser->relativeClassPath()} =>> {$this->newParser->relativeClassPath()}");
+ if (! $inline) $view && $this->line("VIEW:> {$this->parser->relativeViewPath()} =>> {$this->newParser->relativeViewPath()}");
+ if ($test) $test && $this->line("Test:> {$this->parser->relativeTestPath()} =>> {$this->newParser->relativeTestPath()}");
+ }
+
+ protected function copyTest($force)
+ {
+ if (File::exists($this->newParser->testPath()) && ! $force) {
+ $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
+ $this->line("Test already exists:> {$this->newParser->relativeTestPath()}");
+ return false;
+ }
+
+ $this->ensureDirectoryExists($this->newParser->testPath());
+
+ return File::copy("{$this->parser->testPath()}", $this->newParser->testPath());
+ }
+
+ protected function copyClass($force, $inline)
+ {
+ if (File::exists($this->newParser->classPath()) && ! $force) {
+ $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
+ $this->line("Class already exists:> {$this->newParser->relativeClassPath()}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($this->newParser->classPath());
+
+ return File::put($this->newParser->classPath(), $this->newParser->classContents($inline));
+ }
+
+ protected function copyView($force)
+ {
+ if (File::exists($this->newParser->viewPath()) && ! $force) {
+ $this->line("View already exists:> {$this->newParser->relativeViewPath()}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($this->newParser->viewPath());
+
+ return File::copy("{$this->parser->viewPath()}", $this->newParser->viewPath());
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/CpCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/CpCommand.php
new file mode 100644
index 00000000..8601cb6e
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/CpCommand.php
@@ -0,0 +1,13 @@
+setHidden(true);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/DeleteCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/DeleteCommand.php
new file mode 100644
index 00000000..6b787097
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/DeleteCommand.php
@@ -0,0 +1,91 @@
+parser = new ComponentParser(
+ config('livewire.class_namespace'),
+ config('livewire.view_path'),
+ $this->argument('name')
+ );
+
+ if (! $force = $this->option('force')) {
+ $shouldContinue = $this->confirm(
+ "Are you sure you want to delete the following files?>\n\n{$this->parser->relativeClassPath()}\n{$this->parser->relativeViewPath()}\n"
+ );
+
+ if (! $shouldContinue) {
+ return;
+ }
+ }
+
+ $inline = $this->option('inline');
+ $test = $this->option('test');
+
+ $class = $this->removeClass($force);
+ if (! $inline) $view = $this->removeView($force);
+ if ($test) $test = $this->removeTest($force);
+
+ $this->line(" COMPONENT DESTROYED > 🦖💫\n");
+ $class && $this->line("CLASS:> {$this->parser->relativeClassPath()}");
+ if (! $inline) $view && $this->line("VIEW:> {$this->parser->relativeViewPath()}");
+ if ($test) $test && $this->line("Test:> {$this->parser->relativeTestPath()}");
+ }
+
+ protected function removeTest($force = false)
+ {
+ $testPath = $this->parser->testPath();
+
+ if (! File::exists($testPath) && ! $force) {
+ $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
+ $this->line("Test doesn't exist:> {$this->parser->relativeTestPath()}");
+ return false;
+ }
+
+ File::delete($testPath);
+
+ return $testPath;
+ }
+
+ protected function removeClass($force = false)
+ {
+ $classPath = $this->parser->classPath();
+
+ if (! File::exists($classPath) && ! $force) {
+ $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
+ $this->line("Class doesn't exist:> {$this->parser->relativeClassPath()}");
+
+ return false;
+ }
+
+ File::delete($classPath);
+
+ return $classPath;
+ }
+
+ protected function removeView($force = false)
+ {
+ $viewPath = $this->parser->viewPath();
+
+ if (! File::exists($viewPath) && ! $force) {
+ $this->line("View doesn't exist:> {$this->parser->relativeViewPath()}");
+
+ return false;
+ }
+
+ File::delete($viewPath);
+
+ return $viewPath;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/FileManipulationCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/FileManipulationCommand.php
new file mode 100644
index 00000000..f5cf2695
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/FileManipulationCommand.php
@@ -0,0 +1,45 @@
+replaceFirst(app()->getNamespace(), '');
+
+ $livewireFolder = app_path($namespace->explode('\\')->implode(DIRECTORY_SEPARATOR));
+
+ return ! File::isDirectory($livewireFolder);
+ }
+
+ public function writeWelcomeMessage()
+ {
+ $asciiLogo = << _._>
+/ /o>\ \ > || () () __ >
+|_\ /_|> || || \\\// /_\ \\\ // || |~~ /_\ >
+ |>`|>`|> > || || \/ \\\_ \^/ || || \\\_ >
+EOT;
+// _._
+ // / /o\ \ || () () __
+ // |_\ /_| || || \\\// /_\ \\\ // || |~~ /_\
+// |`|`| || || \/ \\\_ \^/ || || \\\_
+ $this->line("\n".$asciiLogo."\n");
+ $this->line("\nCongratulations, you've created your first Livewire component!> 🎉🎉🎉\n");
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/FormCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/FormCommand.php
new file mode 100644
index 00000000..fd2611a6
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/FormCommand.php
@@ -0,0 +1,57 @@
+layoutPath($baseViewPath, $layout);
+
+ $relativeLayoutPath = $this->relativeLayoutPath($layoutPath);
+
+ $force = $this->option('force');
+
+ $stubPath = $this->stubPath($this->option('stub'));
+
+ if (File::exists($layoutPath) && ! $force) {
+ $this->line("View already exists:> {$relativeLayoutPath}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($layoutPath);
+
+ $result = File::copy($stubPath, $layoutPath);
+
+ if ($result) {
+ $this->line(" LAYOUT CREATED > 🤙\n");
+ $this->line("CLASS:> {$relativeLayoutPath}");
+ }
+ }
+
+ protected function stubPath($stubSubDirectory = '')
+ {
+ $stubName = 'livewire.layout.stub';
+
+ if (! empty($stubSubDirectory) && str($stubSubDirectory)->startsWith('..')) {
+ $stubDirectory = rtrim(str($stubSubDirectory)->replaceFirst('..' . DIRECTORY_SEPARATOR, ''), DIRECTORY_SEPARATOR) . '/';
+ } else {
+ $stubDirectory = rtrim('stubs' . DIRECTORY_SEPARATOR . $stubSubDirectory, DIRECTORY_SEPARATOR) . '/';
+ }
+
+ if (File::exists($stubPath = base_path($stubDirectory . $stubName))) {
+ return $stubPath;
+ }
+
+ return __DIR__ . DIRECTORY_SEPARATOR . $stubName;
+ }
+
+ protected function layoutPath($baseViewPath, $layout)
+ {
+ $directories = $layout->explode('.');
+
+ $name = Str::kebab($directories->pop());
+
+ return $baseViewPath . DIRECTORY_SEPARATOR . collect()
+ ->concat($directories)
+ ->map([Str::class, 'kebab'])
+ ->push("{$name}.blade.php")
+ ->implode(DIRECTORY_SEPARATOR);
+ }
+
+ protected function relativeLayoutPath($layoutPath)
+ {
+ return (string) str($layoutPath)->replaceFirst(base_path() . '/', '');
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MakeCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MakeCommand.php
new file mode 100644
index 00000000..b0bde04d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MakeCommand.php
@@ -0,0 +1,257 @@
+parser = new ComponentParser(
+ config('livewire.class_namespace'),
+ config('livewire.view_path'),
+ $this->argument('name'),
+ $this->option('stub')
+ );
+
+ if (!$this->isClassNameValid($name = $this->parser->className())) {
+ $this->line(" WHOOPS! > 😳 \n");
+ $this->line("Class is invalid:> {$name}");
+
+ return;
+ }
+
+ if ($this->isReservedClassName($name)) {
+ $this->line(" WHOOPS! > 😳 \n");
+ $this->line("Class is reserved:> {$name}");
+
+ return;
+ }
+
+ $force = $this->option('force');
+ $inline = $this->option('inline');
+ $test = $this->option('test') || $this->option('pest');
+ $testType = $this->option('pest') ? 'pest' : 'phpunit';
+
+ $showWelcomeMessage = $this->isFirstTimeMakingAComponent();
+
+ $class = $this->createClass($force, $inline);
+ $view = $this->createView($force, $inline);
+
+ if ($test) {
+ $test = $this->createTest($force, $testType);
+ }
+
+ if($class || $view) {
+ $this->line(" COMPONENT CREATED > 🤙\n");
+ $class && $this->line("CLASS:> {$this->parser->relativeClassPath()}");
+
+ if (! $inline) {
+ $view && $this->line("VIEW:> {$this->parser->relativeViewPath()}");
+ }
+
+ if ($test) {
+ $test && $this->line("TEST:> {$this->parser->relativeTestPath()}");
+ }
+
+ if ($showWelcomeMessage && ! app()->runningUnitTests()) {
+ $this->writeWelcomeMessage();
+ }
+ }
+ }
+
+ protected function createClass($force = false, $inline = false)
+ {
+ $classPath = $this->parser->classPath();
+
+ if (File::exists($classPath) && ! $force) {
+ $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
+ $this->line("Class already exists:> {$this->parser->relativeClassPath()}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($classPath);
+
+ File::put($classPath, $this->parser->classContents($inline));
+
+ return $classPath;
+ }
+
+ protected function createView($force = false, $inline = false)
+ {
+ if ($inline) {
+ return false;
+ }
+ $viewPath = $this->parser->viewPath();
+
+ if (File::exists($viewPath) && ! $force) {
+ $this->line("View already exists:> {$this->parser->relativeViewPath()}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($viewPath);
+
+ File::put($viewPath, $this->parser->viewContents());
+
+ return $viewPath;
+ }
+
+ protected function createTest($force = false, $testType = 'phpunit')
+ {
+ $testPath = $this->parser->testPath();
+
+ if (File::exists($testPath) && ! $force) {
+ $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
+ $this->line("Test class already exists:> {$this->parser->relativeTestPath()}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($testPath);
+
+ File::put($testPath, $this->parser->testContents($testType));
+
+ return $testPath;
+ }
+
+ public function isClassNameValid($name)
+ {
+ return preg_match("/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/", $name);
+ }
+
+ public function isReservedClassName($name)
+ {
+ return array_search(strtolower($name), $this->getReservedName()) !== false;
+ }
+
+ protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
+ {
+ if ($this->didReceiveOptions($input)) {
+ return;
+ }
+
+ if(
+ confirm(
+ label: 'Do you want to make this an inline component?',
+ default: false
+ )
+ )
+ {
+ $input->setOption('inline', true);
+ }
+
+ if(
+ $testSuite = select(
+ label: 'Do you want to create a test file?',
+ options: [
+ false => 'No',
+ 'phpunit' => 'PHPUnit',
+ 'pest' => 'Pest',
+ ],
+ )
+ )
+ {
+ $input->setOption('test', true);
+
+ if($testSuite === 'pest') {
+ $input->setOption('pest', true);
+ }
+ }
+ }
+
+ private function getReservedName()
+ {
+ return [
+ 'parent',
+ 'component',
+ 'interface',
+ '__halt_compiler',
+ 'abstract',
+ 'and',
+ 'array',
+ 'as',
+ 'break',
+ 'callable',
+ 'case',
+ 'catch',
+ 'class',
+ 'clone',
+ 'const',
+ 'continue',
+ 'declare',
+ 'default',
+ 'die',
+ 'do',
+ 'echo',
+ 'else',
+ 'elseif',
+ 'empty',
+ 'enddeclare',
+ 'endfor',
+ 'endforeach',
+ 'endif',
+ 'endswitch',
+ 'endwhile',
+ 'enum',
+ 'eval',
+ 'exit',
+ 'extends',
+ 'final',
+ 'finally',
+ 'fn',
+ 'for',
+ 'foreach',
+ 'function',
+ 'global',
+ 'goto',
+ 'if',
+ 'implements',
+ 'include',
+ 'include_once',
+ 'instanceof',
+ 'insteadof',
+ 'interface',
+ 'isset',
+ 'self',
+ 'list',
+ 'match',
+ 'namespace',
+ 'new',
+ 'or',
+ 'print',
+ 'private',
+ 'protected',
+ 'public',
+ 'readonly',
+ 'require',
+ 'require_once',
+ 'return',
+ 'static',
+ 'switch',
+ 'throw',
+ 'trait',
+ 'try',
+ 'unset',
+ 'use',
+ 'var',
+ 'while',
+ 'xor',
+ 'yield',
+ ];
+ }
+
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MakeLivewireCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MakeLivewireCommand.php
new file mode 100644
index 00000000..d31c377a
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MakeLivewireCommand.php
@@ -0,0 +1,8 @@
+parser = new ComponentParser(
+ config('livewire.class_namespace'),
+ config('livewire.view_path'),
+ $this->argument('name')
+ );
+
+ $this->newParser = new ComponentParserFromExistingComponent(
+ config('livewire.class_namespace'),
+ config('livewire.view_path'),
+ $this->argument('new-name'),
+ $this->parser
+ );
+
+ $inline = $this->option('inline');
+
+ $class = $this->renameClass();
+ if (! $inline) $view = $this->renameView();
+
+ $test = $this->renameTest();
+
+ if ($class) $this->line(" COMPONENT MOVED > 🤙\n");
+ $class && $this->line("CLASS:> {$this->parser->relativeClassPath()} =>> {$this->newParser->relativeClassPath()}");
+ if (! $inline) $view && $this->line("VIEW:> {$this->parser->relativeViewPath()} =>> {$this->newParser->relativeViewPath()}");
+ if ($test) $test && $this->line("Test:> {$this->parser->relativeTestPath()} =>> {$this->newParser->relativeTestPath()}");
+ }
+
+ protected function renameClass()
+ {
+ if (File::exists($this->newParser->classPath())) {
+ $this->line(" WHOOPS-IE-TOOTLES > 😳 \n");
+ $this->line("Class already exists:> {$this->newParser->relativeClassPath()}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($this->newParser->classPath());
+
+ File::put($this->newParser->classPath(), $this->newParser->classContents());
+
+ return File::delete($this->parser->classPath());
+ }
+
+ protected function renameView()
+ {
+ $newViewPath = $this->newParser->viewPath();
+
+ if (File::exists($newViewPath)) {
+ $this->line("View already exists:> {$this->newParser->relativeViewPath()}");
+
+ return false;
+ }
+
+ $this->ensureDirectoryExists($newViewPath);
+
+ File::move($this->parser->viewPath(), $newViewPath);
+
+ return $newViewPath;
+ }
+
+ protected function renameTest()
+ {
+ $oldTestPath = $this->parser->testPath();
+ $newTestPath = $this->newParser->testPath();
+
+ if (! File::exists($oldTestPath) || File::exists($newTestPath)) {
+ return false;
+ }
+
+ $this->ensureDirectoryExists($newTestPath);
+
+ File::put($newTestPath, $this->newParser->testContents());
+
+ File::delete($oldTestPath);
+
+ return $newTestPath;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MvCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MvCommand.php
new file mode 100644
index 00000000..d3a80991
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/MvCommand.php
@@ -0,0 +1,13 @@
+setHidden(true);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/PublishCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/PublishCommand.php
new file mode 100644
index 00000000..163c05a0
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/PublishCommand.php
@@ -0,0 +1,46 @@
+option('assets')) {
+ $this->publishAssets();
+ } elseif ($this->option('config')) {
+ $this->publishConfig();
+ } elseif ($this->option('pagination')) {
+ $this->publishPagination();
+ } else {
+ $this->publishConfig();
+ $this->publishPagination();
+ }
+ }
+
+ public function publishAssets()
+ {
+ $this->call('vendor:publish', ['--tag' => 'livewire:assets', '--force' => true]);
+ }
+
+ public function publishConfig()
+ {
+ $this->call('vendor:publish', ['--tag' => 'livewire:config', '--force' => true]);
+ }
+
+ public function publishPagination()
+ {
+ $this->call('vendor:publish', ['--tag' => 'livewire:pagination', '--force' => true]);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/RmCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/RmCommand.php
new file mode 100644
index 00000000..8894b942
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/RmCommand.php
@@ -0,0 +1,13 @@
+setHidden(true);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/S3CleanupCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/S3CleanupCommand.php
new file mode 100644
index 00000000..a93fd3a4
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/S3CleanupCommand.php
@@ -0,0 +1,98 @@
+error("Configuration ['livewire.temporary_file_upload.disk'] is not set to a disk with an S3 driver.");
+
+ return;
+ }
+
+ $driver = FileUploadConfiguration::storage()->getDriver();
+
+ // Flysystem V2+ doesn't allow direct access to adapter, so we need to invade instead.
+ $adapter = invade($driver)->adapter;
+
+ // Flysystem V2+ doesn't allow direct access to client, so we need to invade instead.
+ $client = invade($adapter)->client;
+
+ // Flysystem V2+ doesn't allow direct access to bucket, so we need to invade instead.
+ $bucket = invade($adapter)->bucket;
+
+ $prefix = FileUploadConfiguration::path();
+
+ $rules[] = [
+ 'Filter' => [
+ 'Prefix' => $prefix,
+ ],
+ 'Expiration' => [
+ 'Days' => 1,
+ ],
+ 'Status' => 'Enabled',
+ ];
+
+ $rules = $this->mergeRulesWithExistingConfiguration($client, $bucket, $prefix, $rules);
+
+ try {
+ $client->putBucketLifecycleConfiguration([
+ 'Bucket' => $bucket,
+ 'LifecycleConfiguration' => [
+ 'Rules' => $rules
+ ],
+ ]);
+ } catch (\Exception $e) {
+ $this->error('Failed to configure S3 bucket ['.$bucket.'] to automatically cleanup files older than 24hrs!');
+ $this->error($e->getMessage());
+
+ return;
+ }
+
+ $this->info('Livewire temporary S3 upload directory ['.$prefix.'] set to automatically cleanup files older than 24hrs!');
+ }
+
+ private function checkIfLivewireConfigurationIsAlreadySet(array $existingConfigurationRules, string $bucket, S3Client $client, string $prefix) {
+ $existingConfigurationHasLivewire = collect($existingConfigurationRules)->contains('Filter.Prefix', $prefix);
+
+ if($existingConfigurationHasLivewire) {
+ $this->info('Livewire temporary S3 upload directory ['.$prefix.'] already set to automatically cleanup files older than 24hrs!');
+ $this->info('No changes made to S3 bucket ['.$bucket.'] configuration.');
+ exit;
+ }
+ }
+
+ private function mergeRulesWithExistingConfiguration(S3Client $client, string $bucket, string $prefix, array $rules): array
+ {
+ try {
+ $existingConfiguration = $client->getBucketLifecycleConfiguration([
+ 'Bucket' => $bucket,
+ ]);
+ } catch (\Exception $e) {
+ // if no configuration exists, we'll just ignore the error and continue.
+ $existingConfiguration = null;
+ }
+
+ if ($existingConfiguration) {
+ $this->checkIfLivewireConfigurationIsAlreadySet($existingConfiguration['Rules'], $bucket, $client, $prefix);
+ $existingConfiguration = $existingConfiguration['Rules'];
+ $rules = array_merge($existingConfiguration, $rules);
+ }
+
+ return $rules;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/StubsCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/StubsCommand.php
new file mode 100644
index 00000000..19743bbc
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/StubsCommand.php
@@ -0,0 +1,61 @@
+makeDirectory($stubsPath);
+ }
+
+ file_put_contents(
+ $stubsPath.'/livewire.stub',
+ file_get_contents(__DIR__.'/livewire.stub')
+ );
+
+ file_put_contents(
+ $stubsPath.'/livewire.inline.stub',
+ file_get_contents(__DIR__.'/livewire.inline.stub')
+ );
+
+ file_put_contents(
+ $stubsPath.'/livewire.view.stub',
+ file_get_contents(__DIR__.'/livewire.view.stub')
+ );
+
+ file_put_contents(
+ $stubsPath.'/livewire.test.stub',
+ file_get_contents(__DIR__.'/livewire.test.stub')
+ );
+
+ file_put_contents(
+ $stubsPath.'/livewire.pest.stub',
+ file_get_contents(__DIR__.'/livewire.pest.stub')
+ );
+
+ file_put_contents(
+ $stubsPath.'/livewire.form.stub',
+ file_get_contents(__DIR__.'/livewire.form.stub')
+ );
+
+ file_put_contents(
+ $stubsPath.'/livewire.attribute.stub',
+ file_get_contents(__DIR__.'/livewire.attribute.stub')
+ );
+
+ $this->info('Stubs published successfully.');
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/TouchCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/TouchCommand.php
new file mode 100644
index 00000000..a3a2e157
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/TouchCommand.php
@@ -0,0 +1,13 @@
+setHidden(true);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToEntangleDirectives.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToEntangleDirectives.php
new file mode 100644
index 00000000..43f806b6
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToEntangleDirectives.php
@@ -0,0 +1,32 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The @entangle(...) directive is now deferred by default.',
+ before: '@entangle(...)',
+ after: '@entangle(...).live',
+ pattern: '/@entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)(?!\.(?:defer|live))/',
+ replacement: '@entangle($1).live',
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: 'The $wire.entangle function is now deferred by default and has been changed to $wire.$entangle.',
+ before: '$wire.entangle(...)',
+ after: '$wire.$entangle(..., true)',
+ pattern: '/\$wire\.entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)(?!\.(?:defer))/',
+ replacement: '$wire.$entangle($1, true)',
+ directories: ['resources/views', 'resources/js']
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToWireModelDirectives.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToWireModelDirectives.php
new file mode 100644
index 00000000..e9e6c362
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToWireModelDirectives.php
@@ -0,0 +1,22 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The wire:model directive is now deferred by default.',
+ before: 'wire:model',
+ after: 'wire:model.live',
+ pattern: '/wire:model(?!\.(?:defer|lazy|live))/',
+ replacement: 'wire:model.live',
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeDefaultLayoutView.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeDefaultLayoutView.php
new file mode 100644
index 00000000..bcac0adc
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeDefaultLayoutView.php
@@ -0,0 +1,47 @@
+hasOldLayout())
+ {
+ $console->line(' The Livewire default layout has changed. >');
+ $console->newLine();
+
+ $console->line('When rendering full-page components Livewire would use the "layouts.app" view as the default layout. This has been changed to "components.layouts.app".');
+
+ $choice = $console->choice('Would you like to migrate or keep the old layout?', [
+ 'migrate',
+ 'keep',
+ ], 'migrate');
+
+ if($choice == 'keep') {
+ $console->line('Keeping the old default layout...');
+
+ $this->publishConfigIfMissing($console);
+
+ $console->line('Setting the default layout to "layouts.app"...');
+
+ $this->patternReplacement('/components\.layouts\.app/', 'layouts.app', 'config');
+
+ return $next($console);
+ }
+
+ $console->line('Setting the default layout to "components.layouts.app"...');
+
+ $this->patternReplacement('/layouts\.app/', 'components.layouts.app', 'config');
+ }
+
+ return $next($console);
+ }
+
+ protected function hasOldLayout()
+ {
+ return config('livewire.class_namespace') === 'layouts.app' || $this->filesystem()->exists('resources/views/layouts/app.blade.php');
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeDefaultNamespace.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeDefaultNamespace.php
new file mode 100644
index 00000000..99b56f99
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeDefaultNamespace.php
@@ -0,0 +1,115 @@
+hasOldNamespace())
+ {
+ $console->line(' The Livewire namespace has changed. >');
+ $console->newLine();
+
+ $console->line('The App\\Http\\Livewire> namespace was detected and is no longer the default in Livewire v3. Livewire v3 now uses the App\\Livewire> namespace.');
+
+ $choice = $console->choice('Would you like to migrate or keep the old namespace?', [
+ 'migrate',
+ 'keep',
+ ], 'migrate');
+
+ if($choice === 'keep') {
+ $console->line('Keeping the old namespace...');
+
+ $this->publishConfigIfMissing($console);
+
+ $console->line('Setting the default namespace to "App\\Http\\Livewire"...');
+
+ $config = $this->filesystem()->get('config/livewire.php');
+ $config = str_replace('App\\\\Livewire', 'App\\\\Http\\\\Livewire', $config);
+ $this->filesystem()->put('config/livewire.php', $config);
+
+ return $next($console);
+ }
+
+ $componentNames = [];
+
+ $results = collect($this->filesystem()->allFiles('app/Http/Livewire'))
+ ->filter(function($file) {
+ return str($file)->endsWith('.php');
+ })
+ ->map(function($file) {
+ return str($file)->after('app/Http/Livewire/')->before('.php')->__toString();
+ })->map(function($component) use (&$componentNames) {
+
+ // Track component names to update namespace references later on.
+ $componentNames[] = $component;
+
+ $parser = new ComponentParser(
+ 'App\\Http\\Livewire',
+ config('livewire.view_path'),
+ $component,
+ );
+
+ $newParser = new ComponentParserFromExistingComponent(
+ 'App\\Livewire',
+ config('livewire.view_path'),
+ $component,
+ $parser
+ );
+
+ if ($this->filesystem()->exists($newParser->relativeClassPath())) {
+ return ['Skipped', $component, 'Already exists'];
+ }
+
+ if($this->filesystem()->directoryMissing(dirname($newParser->relativeClassPath()))) {
+ $this->filesystem()->createDirectory(dirname($newParser->relativeClassPath()));
+ }
+
+ $this->filesystem()->put($newParser->relativeClassPath(), $newParser->classContents());
+ $this->filesystem()->delete($parser->relativeClassPath());
+
+ return ['Migrated', $component];
+ });
+
+ foreach($componentNames as $name) {
+ $name = str($name)->replace('/', '\\\\')->toString();
+
+ // Update any namespace references
+ $this->patternReplacement(
+ pattern: "/App\\\Http\\\Livewire\\\({$name})/",
+ replacement: 'App\Livewire\\\$1',
+ directories: [
+ 'app',
+ 'resources/views',
+ 'routes',
+ 'tests',
+ ]
+ );
+ }
+
+ // Update vite config
+ $this->patternReplacement(
+ pattern: '/App\/Http\/Livewire/',
+ replacement: 'App/Livewire',
+ mode: 'manual',
+ files: 'vite.config.js'
+ );
+
+ $console->table(
+ ['Status', 'Component', 'Remark'], $results
+ );
+ }
+
+ return $next($console);
+ }
+
+ protected function hasOldNamespace()
+ {
+ return $this->filesystem()->exists('app/Http/Livewire') || config('livewire.class_namespace') === 'App\\Http\\Livewire';
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeForgetComputedToUnset.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeForgetComputedToUnset.php
new file mode 100644
index 00000000..8176113b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeForgetComputedToUnset.php
@@ -0,0 +1,39 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The forgetComputed component method is replaced by PHP\'s unset function',
+ before: '$this->forgetComputed(\'title\')',
+ after: 'unset($this->title)',
+ pattern: '/\$this->forgetComputed\((.*?)\);/',
+ replacement: function($matches) {
+ $replacement = '';
+
+ if(isset($matches[1])) {
+ preg_match_all('/(?:\'|")(.*?)(?:\'|")/', $matches[1], $keys);
+
+ $replacement .= 'unset(';
+ foreach($keys[1] ?? [] as $key) {
+ $replacement .= '$this->' . $key . ', ';
+ }
+ $replacement = rtrim($replacement, ', ');
+ $replacement .= ');';
+ }
+
+ // Leave unchanged if no replacement was possible.
+ return $replacement ?: $matches[0];
+ },
+ directories: 'app',
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeLazyToBlurModifierOnWireModelDirectives.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeLazyToBlurModifierOnWireModelDirectives.php
new file mode 100644
index 00000000..46ba20ab
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeLazyToBlurModifierOnWireModelDirectives.php
@@ -0,0 +1,22 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The wire:model.lazy modifier is now wire:model.blur.',
+ before: 'wire:model.lazy',
+ after: 'wire:model.blur',
+ pattern: '/wire:model.lazy/',
+ replacement: 'wire:model.blur',
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeTestAssertionMethods.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeTestAssertionMethods.php
new file mode 100644
index 00000000..dd5b0f6f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeTestAssertionMethods.php
@@ -0,0 +1,53 @@
+interactiveReplacement(
+ console: $console,
+ title: 'assertEmitted is now assertDispatched.',
+ before: 'assertEmitted',
+ after: 'assertDispatched',
+ pattern: '/assertEmitted\((.*)\)/',
+ replacement: 'assertDispatched($1)',
+ directories: 'tests'
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: 'assertEmittedTo is now assertDispatchedTo.',
+ before: 'assertEmittedTo',
+ after: 'assertDispatchedTo',
+ pattern: '/assertEmittedTo\((.*)\)/',
+ replacement: 'assertDispatchedTo($1)',
+ directories: 'tests'
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: 'assertNotEmitted is now assertNotDispatched.',
+ before: 'assertNotEmitted',
+ after: 'assertNotDispatched',
+ pattern: '/assertNotEmitted\((.*)\)/',
+ replacement: 'assertNotDispatched($1)',
+ directories: 'tests'
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: 'assertEmittedUp is no longer available.',
+ before: 'assertEmittedUp',
+ after: '',
+ pattern: '/->assertEmittedUp\(.*\)/',
+ replacement: '',
+ directories: 'tests'
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeWireLoadDirectiveToWireInit.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeWireLoadDirectiveToWireInit.php
new file mode 100644
index 00000000..7bdc07b5
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ChangeWireLoadDirectiveToWireInit.php
@@ -0,0 +1,23 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The livewire:load is now livewire:init.',
+ before: 'livewire:load',
+ after: 'livewire:init',
+ pattern: '/livewire:load/',
+ replacement: 'livewire:init',
+ directories: ['resources/views', 'resources/js']
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ClearViewCache.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ClearViewCache.php
new file mode 100644
index 00000000..5a5c96b8
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ClearViewCache.php
@@ -0,0 +1,15 @@
+call('view:clear');
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromEntangleDirectives.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromEntangleDirectives.php
new file mode 100644
index 00000000..ffae62f8
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromEntangleDirectives.php
@@ -0,0 +1,22 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The @entangle(...) directive is now deferred by default.',
+ before: '@entangle(...).defer',
+ after: '@entangle(...)',
+ pattern: '/@entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)\.defer/',
+ replacement: '@entangle($1)',
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromWireModelDirectives.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromWireModelDirectives.php
new file mode 100644
index 00000000..0e56d4c8
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromWireModelDirectives.php
@@ -0,0 +1,22 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The wire:model directive is now deferred by default.',
+ before: 'wire:model.defer',
+ after: 'wire:model',
+ pattern: '/wire:model\.defer/',
+ replacement: 'wire:model',
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemovePrefetchModifierFromWireClickDirective.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemovePrefetchModifierFromWireClickDirective.php
new file mode 100644
index 00000000..afef29e3
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemovePrefetchModifierFromWireClickDirective.php
@@ -0,0 +1,22 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The wire:click.prefetch modifier has been removed.',
+ before: 'wire:click.prefetch',
+ after: 'wire:click',
+ pattern: '/wire:click\.prefetch/',
+ replacement: 'wire:click',
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemovePreventModifierFromWireSubmitDirective.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemovePreventModifierFromWireSubmitDirective.php
new file mode 100644
index 00000000..3077eaec
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RemovePreventModifierFromWireSubmitDirective.php
@@ -0,0 +1,22 @@
+interactiveReplacement(
+ console: $console,
+ title: 'The wire:submit directive now prevents submission by default.',
+ before: 'wire:submit.prevent',
+ after: 'wire:submit',
+ pattern: '/wire:submit\.prevent/',
+ replacement: 'wire:submit',
+ );
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceEmitWithDispatch.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceEmitWithDispatch.php
new file mode 100644
index 00000000..7872500c
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceEmitWithDispatch.php
@@ -0,0 +1,126 @@
+newLine(2);
+ $console->line(' Partial Manual Upgrade: Event dispatching >');
+ $console->newLine();
+ $console->line('In v2 you could use the emit() and dispatchBrowserEvent() methods in PHP.');
+ $console->line('For version 3, Livewire has unified these two methods into a single method: dispatch()');
+ $console->line('This step is partially automated, given parameters must now be named parameters, you will have to do this manually.');
+ $console->confirm('Ready to continue?');
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: '$this->emit is now $this->dispatch.',
+ before: '$this->emit(\'post-created\');',
+ after: '$this->dispatch(\'post-created\')',
+ pattern: '/\$this->emit\((.*)\)/',
+ replacement: '$this->dispatch($1)',
+ directories: ['app', 'tests']
+ );
+
+ $this->manualUpgradeWarning(
+ console: $console,
+ warning: 'Please update the named parameters manually',
+ before: '$this->dispatch(\'post-created\', $post->id);',
+ after: '$this->dispatch(\'post-created\', postId: $post->id);'
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: '$this->emitTo() is now $this->dispatch()->to().',
+ before: '$this->emitTo(\'foo\', \'post-created\');',
+ after: '$this->dispatch(\'post-created\')->to(\'foo\')',
+ pattern: '/\$this->emitTo\((["\'][a-z0-9-.]*["\']),\s?([|"\'][a-zA-Z0-9-.]*["\'])(?:[,])?\s?(.*)\);/',
+ replacement: function($matches) {
+ $component = $matches[1];
+ $eventName = $matches[2];
+ $eventData = $matches[3];
+
+ if (empty($eventData)) {
+ return "\$this->dispatch({$eventName})->to($component);";
+ }
+
+ return "\$this->dispatch({$eventName}, $eventData)->to($component);";
+ },
+ directories: ['app', 'tests']
+ );
+
+ $this->manualUpgradeWarning(
+ console: $console,
+ warning: 'Please update the named parameters manually',
+ before: '$this->dispatch(\'post-created\', $post->id)->to(\'foo\');',
+ after: '$this->dispatch(\'post-created\', postId: $post->id)->to(\'foo\');'
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: '$this->emitSelf() is now $this->dispatch()->self().',
+ before: '$this->emitSelf(\'post-created\');',
+ after: '$this->dispatch(\'post-created\')->self();',
+ pattern: '/\$this->emitSelf\((.*)\)/',
+ replacement: '\$this->dispatch($1)->self()',
+ directories: ['app', 'tests']
+ );
+
+ $this->manualUpgradeWarning(
+ console: $console,
+ warning: 'Please update the named parameters manually',
+ before: '$this->dispatch(\'post-created\', $post->id)->self();',
+ after: '$this->dispatch(\'post-created\', postId: $post->id)->self();'
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: '$this->dispatchBrowserEvent() is now $this->dispatch().',
+ before: '$this->dispatchBrowserEvent(\'post-created\');',
+ after: '$this->dispatch(\'post-created\');',
+ pattern: '/\$this->dispatchBrowserEvent\((.*)\)/',
+ replacement: '\$this->dispatch($1)',
+ directories: ['app', 'tests']
+ );
+
+ $this->manualUpgradeWarning(
+ console: $console,
+ warning: 'Please update the named parameters manually',
+ before: '$this->dispatch(\'post-created\', [\'postId\' => $post->id]);',
+ after: '$this->dispatch(\'post-created\', postId: $post->id);'
+ );
+
+ $this->interactiveReplacement(
+ console: $console,
+ title: 'The $emit helper is now $dispatch.',
+ before: '$emit(\'post-created\');',
+ after: '$dispatch(\'post-created\')',
+ pattern: '/\$emit\((.*)\)/',
+ replacement: '\$dispatch($1)',
+ directories: ['resources']
+ );
+
+ $this->manualUpgradeWarning(
+ console: $console,
+ warning: 'Please update the named parameters manually',
+ before: '$dispatch(\'post-created\', 1);',
+ after: '$dispatch(\'post-created\', {postId: 1});'
+ );
+
+ $this->manualUpgradeWarning(
+ console: $console,
+ warning: 'The concept of `emitUp` has been removed entirely. Events are now dispatched as actual browser events and therefore "bubble up" by default.',
+ before: ['$this->emitUp(\'post-created\');', '$emitUp(\'post-created\', 1)'],
+ after: ['', ''],
+ );
+
+ if($console->confirm('Continue?', true))
+ {
+ return $next($console);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceTemporaryUploadedFileNamespace.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceTemporaryUploadedFileNamespace.php
new file mode 100644
index 00000000..98b55c1a
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceTemporaryUploadedFileNamespace.php
@@ -0,0 +1,24 @@
+interactiveReplacement(
+ console: $console,
+ title: 'Livewire\TemporaryUploadedFile is now Livewire\Features\SupportFileUploads\TemporaryUploadedFile.',
+ before: 'Livewire\TemporaryUploadedFile',
+ after: 'Livewire\Features\SupportFileUploads\TemporaryUploadedFile',
+ pattern: '/Livewire\\\\TemporaryUploadedFile/',
+ replacement: "Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile",
+ directories: ['app', 'tests', 'resources/views']
+ );
+ if ($console->confirm('Continue?', true)) {
+ return $next($console);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RepublishNavigation.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RepublishNavigation.php
new file mode 100644
index 00000000..e689ff46
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/RepublishNavigation.php
@@ -0,0 +1,29 @@
+filesystem()->directoryExists('resources/views/vendor/livewire')) {
+ $console->line(' The Livewire pagination views have changed. >');
+ $console->newLine();
+
+ $console->line('Republishing of the pagination views is required.');
+
+ $confirm = $console->confirm('Do you want to republish the pagination views?', true);
+
+ if($confirm) {
+ $console->call('vendor:publish', [
+ '--tag' => 'livewire:pagination',
+ '--force' => true,
+ ]);
+ }
+ }
+
+ return $next($console);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ThirdPartyUpgradeNotice.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ThirdPartyUpgradeNotice.php
new file mode 100644
index 00000000..d4ebfc68
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/ThirdPartyUpgradeNotice.php
@@ -0,0 +1,23 @@
+line(' Third-party package upgrade 🚀 >');
+ $console->newLine();
+ $console->comment('!! Please be aware that the following upgrade steps are registered by third-parties !!');
+ $console->newLine();
+ $console->newLine();
+ $console->line('You can abort this command at any time by pressing ctrl+c.');
+
+ if($console->confirm('Ready to continue?', true))
+ {
+ return $next($console);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeAlpineInstructions.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeAlpineInstructions.php
new file mode 100644
index 00000000..6f38fdd8
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeAlpineInstructions.php
@@ -0,0 +1,34 @@
+line(' Manual Upgrade: Remove Alpine references >');
+ $console->newLine();
+ $console->line('Livewire version 3 ships with AlpineJS by default.');
+ $console->line('If you use Alpine in your Livewire application, you will need to remove it and any of the plugins listed below so that Livewire\'s built-in version doesn\'t conflict with it.');
+ $console->line('Livewire version 3 now also ships with the following Alpine plugins:');
+ $console->line('- Intersect');
+ $console->line('- Collapse');
+ $console->line('- Persist');
+ $console->line('- Morph');
+ $console->line('- Focus');
+ $console->line('- Mask');
+ $console->newLine();
+ $console->line('If you were accessing Alpine via JS bundle you can now import Livewire\'s ESM module instead and call Livewire.start() when ready, for example:');
+ $console->newLine();
+ $console->line('import { Livewire, Alpine } from \'../../vendor/livewire/livewire/dist/livewire.esm\';');
+ $console->line('Alpine.plugin(yourCustomPlugin);');
+ $console->line('Livewire.start();');
+
+ if($console->confirm('Continue?', true))
+ {
+ return $next($console);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeConfigInstructions.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeConfigInstructions.php
new file mode 100644
index 00000000..fd204d7d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeConfigInstructions.php
@@ -0,0 +1,24 @@
+line(' Manual Upgrade: New configuration >');
+ $console->newLine();
+ $console->line('Livewire V3 has both added and removed certain configuration items.');
+ $console->line('If your application has a published configuration file `config/livewire.php`, you will need to update it to account for the following changes.');
+ $console->line('Added options: legacy_model_binding, inject_assets, inject_morph_markers, and navigate');
+ $console->line('Removed options: app_url, middleware_group, manifest_path, back_button_cache');
+ $console->newLine();
+
+ if($console->confirm('Continue?', true))
+ {
+ return $next($console);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeIntroduction.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeIntroduction.php
new file mode 100644
index 00000000..2f247a61
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeIntroduction.php
@@ -0,0 +1,27 @@
+line(' LIVEWIRE v2 to v3 UPGRADE 🚀 >');
+ $console->newLine();
+ $console->comment('!! Running this command multiple times may result in incorrect replacements !!');
+ $console->newLine();
+ $console->line('This command will help you upgrade from Livewire v2 to v3.');
+ $console->newLine();
+ $console->line('Files will be modified in-place, so make sure you have a backup of your project before continuing.>');
+ $console->newLine();
+ $console->newLine();
+ $console->line('You can abort this command at any time by pressing ctrl+c.');
+
+ if($console->confirm('Ready to continue?', true))
+ {
+ return $next($console);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeStep.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeStep.php
new file mode 100644
index 00000000..d053e2f5
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeStep.php
@@ -0,0 +1,142 @@
+ 'local',
+ 'root' => base_path(),
+ ]);
+ }
+
+ public function publishConfigIfMissing($console): bool
+ {
+ if($this->filesystem()->missing('config/livewire.php')) {
+ $console->line('Publishing Livewire config file...');
+ $console->newLine();
+
+ $console->call('vendor:publish', [
+ '--tag' => 'livewire:config',
+ ]);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public function manualUpgradeWarning($console, $warning, $before, $after)
+ {
+ $console->newLine();
+ $console->error($warning);
+ $console->newLine();
+
+ $this->beforeAfterView(
+ console: $console,
+ before: $before,
+ after: $after,
+ );
+
+ $console->confirm('Ready to continue?');
+ }
+
+ public function beforeAfterView($console, $before, $after, $title = 'Before/After example')
+ {
+ $console->table(
+ [$title],
+ [
+ array_map(fn($line) => "- {$line}>", Arr::wrap($before)),
+ array_map(fn($line) => "+ {$line}>", Arr::wrap($after))
+ ],
+ );
+ }
+
+ public function interactiveReplacement(Command $console, $title, $before, $after, $pattern, $replacement, $directories = ['resources/views'])
+ {
+ $console->newLine(4);
+ $console->line(" {$title} >");
+ $console->newLine();
+ $console->line('Please review the example below and confirm if you would like to apply this change.');
+ $console->newLine();
+
+ $this->beforeAfterView($console, $before, $after);
+
+ $confirm = $console->confirm('Would you like to apply these changes?', true);
+
+ if ($confirm) {
+ $console->newLine();
+
+ $replacements = $this->patternReplacement($pattern, $replacement, $directories);
+
+ if($replacements->isEmpty())
+ {
+ $console->line('No occurrences of were found.');
+ }
+
+ if($replacements->isNotEmpty()) {
+ $console->table(['File', 'Occurrences'], $replacements);
+ }
+ }
+
+ $console->newLine(4);
+ }
+
+ public function patternReplacement(
+ $pattern,
+ $replacement,
+ $directories = 'resources/views',
+ $files = [],
+ $mode = 'auto')
+ {
+ // If the mode is auto, we'll just get all the files in the directories
+ if($mode == 'auto') {
+ $files = collect(Arr::wrap($directories))->map(function($directory) {
+ return collect($this->filesystem()->allFiles($directory))->map(function ($path) {
+ return [
+ 'path' => $path,
+ 'content' => $this->filesystem()->get($path),
+ ];
+ });
+ })->flatten(1);
+ }
+
+ // If the mode is manual, we'll just use the files passed in
+ if($mode == 'manual') {
+ $files = collect(Arr::wrap($files))->map(function($path) {
+ return [
+ 'path' => $path,
+ 'content' => $this->filesystem()->get($path),
+ ];
+ });
+ }
+
+ return $files->map(function($file) use ($pattern, $replacement) {
+ if($replacement instanceof Closure) {
+ $file['content'] = preg_replace_callback($pattern, $replacement, $file['content'], -1, $count);
+ } else {
+ $file['content'] = preg_replace($pattern, $replacement, $file['content'], -1, $count);
+ }
+ $file['occurrences'] = $count;
+
+ return $count > 0 ? $file : null;
+ })
+ ->filter()
+ ->values()
+ ->map(function($file) {
+ $this->filesystem()->put($file['path'], $file['content']);
+
+ return [
+ $file['path'], $file['occurrences'],
+ ];
+ });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/UpgradeCommand.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/UpgradeCommand.php
new file mode 100644
index 00000000..15b19cd4
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/UpgradeCommand.php
@@ -0,0 +1,84 @@
+send($this)->through(collect([
+ UpgradeIntroduction::class,
+
+ // Automated steps
+ ChangeDefaultNamespace::class,
+ ChangeDefaultLayoutView::class,
+ AddLiveModifierToWireModelDirectives::class,
+ RemoveDeferModifierFromWireModelDirectives::class,
+ ChangeLazyToBlurModifierOnWireModelDirectives::class,
+ AddLiveModifierToEntangleDirectives::class,
+ RemoveDeferModifierFromEntangleDirectives::class,
+ RemovePreventModifierFromWireSubmitDirective::class,
+ RemovePrefetchModifierFromWireClickDirective::class,
+ ChangeWireLoadDirectiveToWireInit::class,
+ RepublishNavigation::class,
+ ChangeTestAssertionMethods::class,
+ ChangeForgetComputedToUnset::class,
+ ReplaceTemporaryUploadedFileNamespace::class,
+
+ // Partially automated steps
+ ReplaceEmitWithDispatch::class,
+
+ // Manual steps
+ UpgradeConfigInstructions::class,
+ UpgradeAlpineInstructions::class,
+
+ // Third-party steps
+ ... static::$thirdPartyUpgradeSteps,
+
+ ClearViewCache::class,
+ ])->when($this->option('run-only'), function($collection) {
+ return $collection->filter(fn($step) => str($step)->afterLast('\\')->kebab()->is($this->option('run-only')));
+ })->toArray())
+ ->thenReturn();
+ }
+
+ public static function addThirdPartyUpgradeStep($step)
+ {
+ if(empty(static::$thirdPartyUpgradeSteps)) {
+ static::$thirdPartyUpgradeSteps[] = ThirdPartyUpgradeNotice::class;
+ }
+
+ static::$thirdPartyUpgradeSteps[] = $step;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.attribute.stub b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.attribute.stub
new file mode 100644
index 00000000..8c299d2d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.attribute.stub
@@ -0,0 +1,11 @@
+
+ {{-- [quote] --}}
+
+ HTML;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.layout.stub b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.layout.stub
new file mode 100644
index 00000000..68ba6384
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.layout.stub
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ {{ $title ?? 'Page Title' }}
+
+
+ {{ $slot }}
+
+
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.pest.stub b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.pest.stub
new file mode 100644
index 00000000..a937e61b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.pest.stub
@@ -0,0 +1,9 @@
+assertStatus(200);
+});
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.stub b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.stub
new file mode 100644
index 00000000..75398851
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.stub
@@ -0,0 +1,13 @@
+assertStatus(200);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.view.stub b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.view.stub
new file mode 100644
index 00000000..8f4e3fa9
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/livewire.view.stub
@@ -0,0 +1,3 @@
+
+ {{-- [quote] --}}
+
diff --git a/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/the-tao.php b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/the-tao.php
new file mode 100644
index 00000000..85360330
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportConsoleCommands/Commands/the-tao.php
@@ -0,0 +1,21 @@
+runningInConsole()) return;
+
+ static::commands([
+ Commands\MakeLivewireCommand::class, // make:livewire
+ Commands\MakeCommand::class, // livewire:make
+ Commands\FormCommand::class, // livewire:form
+ Commands\AttributeCommand::class, // livewire:attribute
+ Commands\TouchCommand::class, // livewire:touch
+ Commands\CopyCommand::class, // livewire:copy
+ Commands\CpCommand::class, // livewire:cp
+ Commands\DeleteCommand::class, // livewire:delete
+ Commands\LayoutCommand::class, // livewire:layout
+ Commands\RmCommand::class, // livewire:rm
+ Commands\MoveCommand::class, // livewire:move
+ Commands\MvCommand::class, // livewire:mv
+ Commands\StubsCommand::class, // livewire:stubs
+ Commands\S3CleanupCommand::class, // livewire:configure-s3-upload-cleanup
+ Commands\PublishCommand::class, // livewire:publish
+ Commands\UpgradeCommand::class, // livewire:upgrade
+ ]);
+ }
+
+ static function commands($commands)
+ {
+ $commands = is_array($commands) ? $commands : func_get_args();
+
+ Artisan::starting(fn(Artisan $artisan) => $artisan->resolveCommands($commands));
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php b/portman/lib/Livewire/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php
new file mode 100644
index 00000000..ad5a4608
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php
@@ -0,0 +1,35 @@
+headers->add([
+ 'Pragma' => 'no-cache',
+ 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT',
+ 'Cache-Control' => 'no-cache, must-revalidate, no-store, max-age=0, private',
+ ]);
+
+ // We do flush this in the `SupportDisablingBackButtonCache` hook, but we
+ // need to do it here as well to ensure that unit tests still work...
+ SupportDisablingBackButtonCache::$disableBackButtonCache = false;
+ }
+
+ return $response;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportDisablingBackButtonCache/HandlesDisablingBackButtonCache.php b/portman/lib/Livewire/Features/SupportDisablingBackButtonCache/HandlesDisablingBackButtonCache.php
new file mode 100644
index 00000000..9be1a22b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportDisablingBackButtonCache/HandlesDisablingBackButtonCache.php
@@ -0,0 +1,16 @@
+make(\Illuminate\Contracts\Http\Kernel::class);
+
+ if ($kernel->hasMiddleware(DisableBackButtonCacheMiddleware::class)) {
+ return;
+ }
+
+ $kernel->pushMiddleware(DisableBackButtonCacheMiddleware::class);
+ }
+
+ public function boot()
+ {
+ static::$disableBackButtonCache = true;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportEntangle/SupportEntangle.php b/portman/lib/Livewire/Features/SupportEntangle/SupportEntangle.php
new file mode 100644
index 00000000..8a8a286d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportEntangle/SupportEntangle.php
@@ -0,0 +1,18 @@
+window.Livewire.find('{{ \$__livewire->getId() }}').entangle('{{ {$expression}->value() }}'){{ {$expression}->hasModifier('live') ? '.live' : '' }}window.Livewire.find('{{ \$__livewire->getId() }}').entangle('{{ {$expression} }}')
+ EOT;
+ });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportEvents/BaseOn.php b/portman/lib/Livewire/Features/SupportEvents/BaseOn.php
new file mode 100644
index 00000000..7b150989
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportEvents/BaseOn.php
@@ -0,0 +1,26 @@
+event) as $event) {
+ store($this->component)->push(
+ 'listenersFromAttributes',
+ $this->getName() ?? '$refresh',
+ $event,
+ );
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportEvents/Event.php b/portman/lib/Livewire/Features/SupportEvents/Event.php
new file mode 100644
index 00000000..86558f50
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportEvents/Event.php
@@ -0,0 +1,51 @@
+name = $name;
+ $this->params = $params;
+ }
+
+ public function self()
+ {
+ $this->self = true;
+
+ return $this;
+ }
+
+ public function component($name)
+ {
+ $this->component = $name;
+
+ return $this;
+ }
+
+ public function to($name)
+ {
+ return $this->component($name);
+ }
+
+ public function serialize()
+ {
+ $output = [
+ 'name' => $this->name,
+ 'params' => $this->params,
+ ];
+
+ if ($this->self) $output['self'] = true;
+ if ($this->component) $output['to'] = app(ComponentRegistry::class)->getName($this->component);
+
+ return $output;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportEvents/HandlesEvents.php b/portman/lib/Livewire/Features/SupportEvents/HandlesEvents.php
new file mode 100644
index 00000000..c630e525
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportEvents/HandlesEvents.php
@@ -0,0 +1,23 @@
+listeners;
+ }
+
+ public function dispatch($event, ...$params)
+ {
+ $event = new Event($event, $params);
+
+ store($this)->push('dispatched', $event);
+
+ return $event;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportEvents/SupportEvents.php b/portman/lib/Livewire/Features/SupportEvents/SupportEvents.php
new file mode 100644
index 00000000..90289f5a
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportEvents/SupportEvents.php
@@ -0,0 +1,122 @@
+component);
+
+ if (! in_array($name, $names)) {
+ throw new EventHandlerDoesNotExist($name);
+ }
+
+ $method = static::getListenerMethodName($this->component, $name);
+
+ $returnEarly(
+ wrap($this->component)->$method(...$params)
+ );
+
+ // Here we have to manually check to see if the event listener method
+ // is "renderless" as it's normal "call" hook doesn't get run when
+ // the method is called as an event listener...
+ $isRenderless = $this->component->getAttributes()
+ ->filter(fn ($i) => is_subclass_of($i, BaseRenderless::class))
+ ->filter(fn ($i) => $i->getName() === $method)
+ ->filter(fn ($i) => $i->getLevel() === AttributeLevel::METHOD)
+ ->count() > 0;
+
+ if ($isRenderless) $this->component->skipRender();
+ }
+ }
+
+ function dehydrate($context)
+ {
+ if ($context->mounting) {
+ $listeners = static::getListenerEventNames($this->component);
+
+ $listeners && $context->addEffect('listeners', $listeners);
+ }
+
+ $dispatches = $this->getServerDispatchedEvents($this->component);
+
+ $dispatches && $context->addEffect('dispatches', $dispatches);
+ }
+
+ static function getListenerEventNames($component)
+ {
+ $listeners = static::getComponentListeners($component);
+
+ return collect($listeners)
+ ->map(fn ($value, $key) => is_numeric($key) ? $value : $key)
+ ->values()
+ ->toArray();
+ }
+
+ static function getListenerMethodName($component, $name)
+ {
+ $listeners = static::getComponentListeners($component);
+
+ foreach ($listeners as $event => $method) {
+ if (is_numeric($event)) $event = $method;
+
+ if ($name === $event) return $method;
+ }
+
+ throw new \Exception('Event method not found');
+ }
+
+ static function getComponentListeners($component)
+ {
+ $fromClass = invade($component)->getListeners();
+
+ $fromAttributes = store($component)->get('listenersFromAttributes', []);
+
+ $listeners = array_merge($fromClass, $fromAttributes);
+
+ return static::replaceDynamicEventNamePlaceholders($listeners, $component);
+ }
+
+ function getServerDispatchedEvents($component)
+ {
+ return collect(store($component)->get('dispatched', []))
+ ->map(fn ($event) => $event->serialize())
+ ->toArray();
+ }
+
+ static function replaceDynamicEventNamePlaceholders($listeners, $component)
+ {
+ foreach ($listeners as $event => $method) {
+ if (is_numeric($event)) continue;
+
+ $replaced = static::replaceDynamicPlaceholders($event, $component);
+
+ unset($listeners[$event]);
+
+ $listeners[$replaced] = $method;
+ }
+
+ return $listeners;
+ }
+
+ static function replaceDynamicPlaceholders($event, $component)
+ {
+ return preg_replace_callback('/\{(.*)\}/U', function ($matches) use ($component) {
+ return data_get($component, $matches[1], function () use ($matches) {
+ throw new \Exception('Unable to evaluate dynamic event name placeholder: '.$matches[0]);
+ });
+ }, $event);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportEvents/TestsEvents.php b/portman/lib/Livewire/Features/SupportEvents/TestsEvents.php
new file mode 100644
index 00000000..1911e14f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportEvents/TestsEvents.php
@@ -0,0 +1,90 @@
+call('__dispatch', $event, $parameters);
+ }
+
+ public function fireEvent($event, ...$parameters)
+ {
+ return $this->dispatch($event, ...$parameters);
+ }
+
+ public function assertDispatched($event, ...$params)
+ {
+ $result = $this->testDispatched($event, $params);
+
+ PHPUnit::assertTrue($result['test'], "Failed asserting that an event [{$event}] was fired{$result['assertionSuffix']}");
+
+ return $this;
+ }
+
+ public function assertNotDispatched($event, ...$params)
+ {
+ $result = $this->testDispatched($event, $params);
+
+ PHPUnit::assertFalse($result['test'], "Failed asserting that an event [{$event}] was not fired{$result['assertionSuffix']}");
+
+ return $this;
+ }
+
+ public function assertDispatchedTo($target, $event, ...$params)
+ {
+ $this->assertDispatched($event, ...$params);
+ $result = $this->testDispatchedTo($target, $event);
+
+ PHPUnit::assertTrue($result, "Failed asserting that an event [{$event}] was fired to {$target}.");
+
+ return $this;
+ }
+
+ protected function testDispatched($value, $params)
+ {
+ $assertionSuffix = '.';
+
+ if (empty($params)) {
+ $test = collect(data_get($this->effects, 'dispatches'))->contains('name', '=', $value);
+ } elseif (isset($params[0]) && ! is_string($params[0]) && is_callable($params[0])) {
+ $event = collect(data_get($this->effects, 'dispatches'))->first(function ($item) use ($value) {
+ return $item['name'] === $value;
+ });
+
+ $test = $event && $params[0]($event['name'], $event['params']);
+ } else {
+ $test = (bool) collect(data_get($this->effects, 'dispatches'))->first(function ($item) use ($value, $params) {
+ $commonParams = array_intersect_key($item['params'], $params);
+
+ ksort($commonParams);
+ ksort($params);
+
+ return $item['name'] === $value
+ && $commonParams === $params;
+ });
+
+ $encodedParams = json_encode($params);
+ $assertionSuffix = " with parameters: {$encodedParams}";
+ }
+
+ return [
+ 'test' => $test,
+ 'assertionSuffix' => $assertionSuffix,
+ ];
+ }
+
+ protected function testDispatchedTo($target, $value)
+ {
+ $name = app(ComponentRegistry::class)->getName($target);
+
+ return (bool) collect(data_get($this->effects, 'dispatches'))->first(function ($item) use ($name, $value) {
+ return $item['name'] === $value
+ && $item['to'] === $name;
+ });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportEvents/fake-echo.js b/portman/lib/Livewire/Features/SupportEvents/fake-echo.js
new file mode 100644
index 00000000..0dcd8943
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportEvents/fake-echo.js
@@ -0,0 +1,108 @@
+
+window.fakeEchoListeners = []
+
+class FakeChannel {
+ constructor(channel) {
+ this.channel = channel
+
+ this.type = 'public'
+ }
+
+ listen(eventName, callback) {
+ window.fakeEchoListeners.push({
+ channel: this.channel,
+ event: eventName,
+ type: this.type,
+ callback,
+ })
+
+ return this
+ }
+
+ stopListening(eventName, callback) {
+ window.fakeEchoListeners = window.fakeEchoListeners.filter(i => {
+ if (callback) {
+ return ! (i.event === eventName && i.callback === callback)
+ }
+
+ return ! (i.event === eventName)
+ })
+
+
+ return this
+ }
+}
+
+class FakePrivateChannel extends FakeChannel {
+ constructor(channel) {
+ super(channel)
+
+ this.type = 'private'
+ }
+
+ whisper(eventName, data) {
+ return this
+ }
+}
+
+class FakePresenceChannel extends FakeChannel {
+ constructor(channel) {
+ super(channel)
+
+ this.type = 'presence'
+ }
+
+ here(callback) {
+ return this
+ }
+
+ joining(callback) {
+ return this
+ }
+
+ whisper(eventName, data) {
+ return this
+ }
+
+ leaving(callback) {
+ return this
+ }
+}
+
+class FakeEcho {
+ join(channel) {
+ return new FakePresenceChannel(channel);
+ }
+
+ channel(channel) {
+ return new FakeChannel(channel);
+ }
+
+ private(channel) {
+ return new FakePrivateChannel(channel);
+ }
+
+ encryptedPrivate(channel) {
+ return new FakePrivateChannel(channel);
+ }
+
+ socketId() {
+ return 'fake-socked-id'
+ }
+
+ // For dusk to trigger listeners...
+
+ fakeTrigger({ channel, event, type, payload = {} }) {
+ window.fakeEchoListeners.filter(listener => {
+ if (event !== listener.event) return false
+ if (channel !== listener.channel) return false
+ if (type !== undefined && type !== listener.type) return false
+
+ return true
+ }).forEach(({ callback }) => {
+ callback(payload)
+ })
+ }
+}
+
+window.Echo = new FakeEcho
diff --git a/portman/lib/Livewire/Features/SupportFileDownloads/SupportFileDownloads.php b/portman/lib/Livewire/Features/SupportFileDownloads/SupportFileDownloads.php
new file mode 100644
index 00000000..cea7de06
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileDownloads/SupportFileDownloads.php
@@ -0,0 +1,90 @@
+toResponse(request());
+ }
+
+ if ($this->valueIsntAFileResponse($return)) return;
+
+ $response = $return;
+
+ $name = $this->getFilenameFromContentDispositionHeader(
+ $response->headers->get('Content-Disposition')
+ );
+
+ $binary = $this->captureOutput(function () use ($response) {
+ $response->sendContent();
+ });
+
+ $content = base64_encode($binary);
+
+ $this->storeSet('download', [
+ 'name' => $name,
+ 'content' => $content,
+ 'contentType' => $response->headers->get('Content-Type'),
+ ]);
+ };
+ }
+
+ function dehydrate($context)
+ {
+ if (! $download = $this->storeGet('download')) return;
+
+ $context->addEffect('download', $download);
+ }
+
+ function valueIsntAFileResponse($value)
+ {
+ return ! $value instanceof StreamedResponse
+ && ! $value instanceof BinaryFileResponse;
+ }
+
+ function captureOutput($callback)
+ {
+ ob_start();
+
+ $callback();
+
+ return ob_get_clean();
+ }
+
+ function getFilenameFromContentDispositionHeader($header)
+ {
+ /**
+ * The following conditionals are here to allow for quoted and
+ * non quoted filenames in the Content-Disposition header.
+ *
+ * Both of these values should return the correct filename without quotes.
+ *
+ * Content-Disposition: attachment; filename=filename.jpg
+ * Content-Disposition: attachment; filename="test file.jpg"
+ */
+
+ // Support foreign-language filenames (japanese, greek, etc..)...
+ if (preg_match('/filename\*=utf-8\'\'(.+)$/i', $header, $matches)) {
+ return rawurldecode($matches[1]);
+ }
+
+ if (preg_match('/.*?filename="(.+?)"/', $header, $matches)) {
+ return $matches[1];
+ }
+
+ if (preg_match('/.*?filename=([^; ]+)/', $header, $matches)) {
+ return $matches[1];
+ }
+
+ return 'download';
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileDownloads/TestsFileDownloads.php b/portman/lib/Livewire/Features/SupportFileDownloads/TestsFileDownloads.php
new file mode 100644
index 00000000..0d70f26f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileDownloads/TestsFileDownloads.php
@@ -0,0 +1,49 @@
+effects, 'download');
+
+ if ($filename) {
+ PHPUnit::assertEquals(
+ $filename,
+ data_get($downloadEffect, 'name')
+ );
+ } else {
+ PHPUnit::assertNotNull($downloadEffect);
+ }
+
+ if ($content) {
+ $downloadedContent = data_get($this->effects, 'download.content');
+
+ PHPUnit::assertEquals(
+ $content,
+ base64_decode($downloadedContent)
+ );
+ }
+
+ if ($contentType) {
+ PHPUnit::assertEquals(
+ $contentType,
+ data_get($this->effects, 'download.contentType')
+ );
+ }
+
+ return $this;
+ }
+
+ public function assertNoFileDownloaded()
+ {
+ $downloadEffect = data_get($this->effects, 'download');
+
+ PHPUnit::assertNull($downloadEffect);
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/FileNotPreviewableException.php b/portman/lib/Livewire/Features/SupportFileUploads/FileNotPreviewableException.php
new file mode 100644
index 00000000..89e93ce6
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/FileNotPreviewableException.php
@@ -0,0 +1,17 @@
+guessExtension()}\" is not previewable. See the livewire.temporary_file_upload.preview_mimes config."
+ );
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/FilePreviewController.php b/portman/lib/Livewire/Features/SupportFileUploads/FilePreviewController.php
new file mode 100644
index 00000000..9dbf0fb0
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/FilePreviewController.php
@@ -0,0 +1,24 @@
+ new Middleware($middleware), static::$middleware);
+ }
+
+ public function handle($filename)
+ {
+ abort_unless(request()->hasValidSignature(), 401);
+
+ return Utils::pretendPreviewResponseIsPreviewFile($filename);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/FileUploadConfiguration.php b/portman/lib/Livewire/Features/SupportFileUploads/FileUploadConfiguration.php
new file mode 100644
index 00000000..650ec6a0
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/FileUploadConfiguration.php
@@ -0,0 +1,127 @@
+runningUnitTests()) {
+ // We want to "fake" the first time in a test run, but not again because
+ // Storage::fake() wipes the storage directory every time its called.
+ rescue(
+ // If the storage disk is not found (meaning it's the first time),
+ // this will throw an error and trip the second callback.
+ fn() => Storage::disk($disk),
+ fn() => Storage::fake($disk),
+ // swallows the error that is thrown on the first try
+ report: false
+ );
+ }
+
+ return Storage::disk($disk);
+ }
+
+ public static function disk()
+ {
+ if (app()->runningUnitTests()) {
+ return 'tmp-for-tests';
+ }
+
+ return config('livewire.temporary_file_upload.disk') ?: config('filesystems.default');
+ }
+
+ public static function diskConfig()
+ {
+ return config('filesystems.disks.'.static::disk());
+ }
+
+ public static function isUsingS3()
+ {
+ $diskBeforeTestFake = config('livewire.temporary_file_upload.disk') ?: config('filesystems.default');
+
+ return config('filesystems.disks.'.strtolower($diskBeforeTestFake).'.driver') === 's3';
+ }
+
+ public static function isUsingGCS()
+ {
+ $diskBeforeTestFake = config('livewire.temporary_file_upload.disk') ?: config('filesystems.default');
+
+ return config('filesystems.disks.'.strtolower($diskBeforeTestFake).'.driver') === 'gcs';
+ }
+
+ public static function normalizeRelativePath($path)
+ {
+ return (new WhitespacePathNormalizer)->normalizePath($path);
+ }
+
+ public static function directory()
+ {
+ return static::normalizeRelativePath(config('livewire.temporary_file_upload.directory') ?: 'livewire-tmp');
+ }
+
+ protected static function s3Root()
+ {
+ if (! static::isUsingS3()) return '';
+
+ $diskConfig = static::diskConfig();
+
+ if (! is_array($diskConfig)) return '';
+
+ $root = $diskConfig['root'] ?? null;
+
+ return $root !== null ? static::normalizeRelativePath($root) : '';
+ }
+
+ public static function path($path = '', $withS3Root = true)
+ {
+ $prefix = $withS3Root ? static::s3Root() : '';
+ $directory = static::directory();
+ $path = static::normalizeRelativePath($path);
+
+ return $prefix.($prefix ? '/' : '').$directory.($path ? '/' : '').$path;
+ }
+
+ public static function mimeType($filename)
+ {
+ $mimeType = static::storage()->mimeType(static::path($filename));
+
+ return $mimeType === 'image/svg' ? 'image/svg+xml' : $mimeType;
+ }
+
+ public static function lastModified($filename)
+ {
+ return static::storage()->lastModified($filename);
+ }
+
+ public static function middleware()
+ {
+ return config('livewire.temporary_file_upload.middleware') ?: 'throttle:60,1';
+ }
+
+ public static function shouldCleanupOldUploads()
+ {
+ return config('livewire.temporary_file_upload.cleanup', true);
+ }
+
+ public static function rules()
+ {
+ $rules = config('livewire.temporary_file_upload.rules');
+
+ if (is_null($rules)) return ['required', 'file', 'max:12288'];
+
+ if (is_array($rules)) return $rules;
+
+ return explode('|', $rules);
+ }
+
+ public static function maxUploadTime()
+ {
+ return config('livewire.temporary_file_upload.max_upload_time') ?: 5;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/FileUploadController.php b/portman/lib/Livewire/Features/SupportFileUploads/FileUploadController.php
new file mode 100644
index 00000000..41011ab8
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/FileUploadController.php
@@ -0,0 +1,55 @@
+ new Middleware($middleware), $middleware);
+ }
+
+ public function handle()
+ {
+ abort_unless(request()->hasValidSignature(), 401);
+
+ $disk = FileUploadConfiguration::disk();
+
+ $filePaths = $this->validateAndStore(request('files'), $disk);
+
+ return ['paths' => $filePaths];
+ }
+
+ public function validateAndStore($files, $disk)
+ {
+ Validator::make(['files' => $files], [
+ 'files.*' => FileUploadConfiguration::rules()
+ ])->validate();
+
+ $fileHashPaths = collect($files)->map(function ($file) use ($disk) {
+ $filename = TemporaryUploadedFile::generateHashNameWithOriginalNameEmbedded($file);
+
+ return $file->storeAs('/'.FileUploadConfiguration::path(), $filename, [
+ 'disk' => $disk
+ ]);
+ });
+
+ // Strip out the temporary upload directory from the paths.
+ return $fileHashPaths->map(function ($path) { return str_replace(FileUploadConfiguration::path('/'), '', $path); });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/FileUploadSynth.php b/portman/lib/Livewire/Features/SupportFileUploads/FileUploadSynth.php
new file mode 100644
index 00000000..488a9130
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/FileUploadSynth.php
@@ -0,0 +1,66 @@
+dehydratePropertyFromWithFileUploads($target), []];
+ }
+
+ public function dehydratePropertyFromWithFileUploads($value)
+ {
+ if (TemporaryUploadedFile::canUnserialize($value)) {
+ return TemporaryUploadedFile::unserializeFromLivewireRequest($value);
+ }
+
+ if ($value instanceof TemporaryUploadedFile) {
+ return $value->serializeForLivewireResponse();
+ }
+
+ if (is_array($value) && isset(array_values($value)[0])) {
+ $isValid = true;
+
+ foreach ($value as $key => $arrayValue) {
+ if (!($arrayValue instanceof TemporaryUploadedFile) || !is_numeric($key)) {
+ $isValid = false;
+ break;
+ }
+ }
+
+ if ($isValid) {
+ return array_values($value)[0]::serializeMultipleForLivewireResponse($value);
+ }
+ }
+
+ if (is_array($value)) {
+ foreach ($value as $key => $item) {
+ $value[$key] = $this->dehydratePropertyFromWithFileUploads($item);
+ }
+ }
+
+ if ($value instanceof \Livewire\Wireable) {
+ $keys = array_keys(get_object_vars($value));
+
+ foreach ($keys as $key) {
+ $value->{$key} = $this->dehydratePropertyFromWithFileUploads($value->{$key});
+ }
+ }
+
+ return $value;
+ }
+
+ function hydrate($value) {
+ if (TemporaryUploadedFile::canUnserialize($value)) {
+ return TemporaryUploadedFile::unserializeFromLivewireRequest($value);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/GenerateSignedUploadUrl.php b/portman/lib/Livewire/Features/SupportFileUploads/GenerateSignedUploadUrl.php
new file mode 100644
index 00000000..8369bc88
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/GenerateSignedUploadUrl.php
@@ -0,0 +1,73 @@
+addMinutes(FileUploadConfiguration::maxUploadTime())
+ );
+ }
+
+ public function forS3($file, $visibility = 'private')
+ {
+ $storage = FileUploadConfiguration::storage();
+
+ $driver = $storage->getDriver();
+
+ // Flysystem V2+ doesn't allow direct access to adapter, so we need to invade instead.
+ $adapter = invade($driver)->adapter;
+
+ // Flysystem V2+ doesn't allow direct access to client, so we need to invade instead.
+ $client = invade($adapter)->client;
+
+ // Flysystem V2+ doesn't allow direct access to bucket, so we need to invade instead.
+ $bucket = invade($adapter)->bucket;
+
+ $fileType = $file->getMimeType();
+ $fileHashName = TemporaryUploadedFile::generateHashNameWithOriginalNameEmbedded($file);
+ $path = FileUploadConfiguration::path($fileHashName);
+
+ $command = $client->getCommand('putObject', array_filter([
+ 'Bucket' => $bucket,
+ 'Key' => $path,
+ 'ACL' => $visibility,
+ 'ContentType' => $fileType ?: 'application/octet-stream',
+ 'CacheControl' => null,
+ 'Expires' => null,
+ ]));
+
+ $signedRequest = $client->createPresignedRequest(
+ $command,
+ '+' . FileUploadConfiguration::maxUploadTime() . ' minutes'
+ );
+
+ $uri = $signedRequest->getUri();
+
+ if (filled($url = $storage->getConfig()['temporary_url'] ?? null)) {
+ $uri = invade($storage)->replaceBaseUrl($uri, $url);
+ }
+
+ return [
+ 'path' => $fileHashName,
+ 'url' => (string) $uri,
+ 'headers' => $this->headers($signedRequest, $fileType),
+ ];
+ }
+
+ protected function headers($signedRequest, $fileType)
+ {
+ return array_merge(
+ $signedRequest->getHeaders(),
+ [
+ 'Content-Type' => $fileType ?: 'application/octet-stream'
+ ]
+ );
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/MissingFileUploadsTraitException.php b/portman/lib/Livewire/Features/SupportFileUploads/MissingFileUploadsTraitException.php
new file mode 100644
index 00000000..c478f78b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/MissingFileUploadsTraitException.php
@@ -0,0 +1,18 @@
+getName()}] component class."
+ );
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/S3DoesntSupportMultipleFileUploads.php b/portman/lib/Livewire/Features/SupportFileUploads/S3DoesntSupportMultipleFileUploads.php
new file mode 100644
index 00000000..b777c539
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/S3DoesntSupportMultipleFileUploads.php
@@ -0,0 +1,18 @@
+runningUnitTests()) {
+ // Don't actually generate S3 signedUrls during testing.
+ GenerateSignedUploadUrlFacade::swap(new class extends GenerateSignedUploadUrl {
+ public function forS3($file, $visibility = '') { return []; }
+ });
+ }
+
+ app('livewire')->propertySynthesizer([
+ FileUploadSynth::class,
+ ]);
+
+ on('call', function ($component, $method, $params, $addEffect, $earlyReturn) {
+ if ($method === '_startUpload') {
+ if (! method_exists($component, $method)) {
+ throw new MissingFileUploadsTraitException($component);
+ }
+ }
+ });
+
+ Route::post('/livewire/upload-file', [FileUploadController::class, 'handle'])
+ ->name('livewire.upload-file');
+
+ Route::get('/livewire/preview-file/{filename}', [FilePreviewController::class, 'handle'])
+ ->name('livewire.preview-file');
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/TemporaryUploadedFile.php b/portman/lib/Livewire/Features/SupportFileUploads/TemporaryUploadedFile.php
new file mode 100644
index 00000000..78d2bbc5
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/TemporaryUploadedFile.php
@@ -0,0 +1,250 @@
+disk = $disk;
+ $this->storage = Storage::disk($this->disk);
+ $this->path = FileUploadConfiguration::path($path, false);
+
+ $tmpFile = tmpfile();
+
+ parent::__construct(stream_get_meta_data($tmpFile)['uri'], $this->path);
+
+ // While running tests, update the last modified timestamp to the current
+ // Carbon timestamp (which respects time traveling), because otherwise
+ // cleanupOldUploads() will mess up with the filesystem...
+ if (app()->runningUnitTests())
+ {
+ @touch($this->path(), now()->timestamp);
+ }
+ }
+
+ public function getPath(): string
+ {
+ return $this->storage->path(FileUploadConfiguration::directory());
+ }
+
+ public function isValid(): bool
+ {
+ return true;
+ }
+
+ public function getSize(): int
+ {
+ if (app()->runningUnitTests() && str($this->getFilename())->contains('-size=')) {
+ return (int) str($this->getFilename())->between('-size=', '.')->__toString();
+ }
+
+ return (int) $this->storage->size($this->path);
+ }
+
+ public function getMimeType(): string
+ {
+ if (app()->runningUnitTests() && str($this->getFilename())->contains('-mimeType=')) {
+ $escapedMimeType = str($this->getFilename())->between('-mimeType=', '-');
+
+ // MimeTypes contain slashes, but we replaced them with underscores in `SupportTesting\Testable`
+ // to ensure the filename is valid, so we now need to revert that.
+ return (string) $escapedMimeType->replace('_', '/');
+ }
+
+ $mimeType = $this->storage->mimeType($this->path);
+
+ // Flysystem V2.0+ removed guess mimeType from extension support, so it has been re-added back
+ // in here to ensure the correct mimeType is returned when using faked files in tests
+ if (in_array($mimeType, ['application/octet-stream', 'inode/x-empty', 'application/x-empty'])) {
+ $detector = new FinfoMimeTypeDetector();
+
+ $mimeType = $detector->detectMimeTypeFromPath($this->path) ?: 'text/plain';
+ }
+
+ return $mimeType;
+ }
+
+ public function getFilename(): string
+ {
+ return $this->getName($this->path);
+ }
+
+ public function getRealPath(): string
+ {
+ return $this->storage->path($this->path);
+ }
+
+ public function getPathname(): string
+ {
+ return $this->getRealPath();
+ }
+
+ public function getClientOriginalName(): string
+ {
+ return $this->extractOriginalNameFromFilePath($this->path);
+ }
+
+ public function dimensions()
+ {
+ stream_copy_to_stream($this->storage->readStream($this->path), $tmpFile = tmpfile());
+
+ return @getimagesize(stream_get_meta_data($tmpFile)['uri']);
+ }
+
+ public function temporaryUrl()
+ {
+ if (!$this->isPreviewable()) {
+ throw new FileNotPreviewableException($this);
+ }
+
+ if ((FileUploadConfiguration::isUsingS3() or FileUploadConfiguration::isUsingGCS()) && ! app()->runningUnitTests()) {
+ return $this->storage->temporaryUrl(
+ $this->path,
+ now()->addDay()->endOfHour(),
+ ['ResponseContentDisposition' => 'attachment; filename="' . urlencode($this->getClientOriginalName()) . '"']
+ );
+ }
+
+ if (method_exists($this->storage->getAdapter(), 'getTemporaryUrl')) {
+ // This will throw an error because it's not used with S3.
+ return $this->storage->temporaryUrl($this->path, now()->addDay());
+ }
+
+ return URL::temporarySignedRoute(
+ 'livewire.preview-file', now()->addMinutes(30)->endOfHour(), ['filename' => $this->getFilename()]
+ );
+ }
+
+ public function isPreviewable()
+ {
+ $supportedPreviewTypes = config('livewire.temporary_file_upload.preview_mimes', [
+ 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
+ 'mov', 'avi', 'wmv', 'mp3', 'm4a',
+ 'jpg', 'jpeg', 'mpga', 'webp', 'wma',
+ ]);
+
+ return in_array($this->guessExtension(), $supportedPreviewTypes);
+ }
+
+ public function readStream()
+ {
+ return $this->storage->readStream($this->path);
+ }
+
+ public function exists()
+ {
+ return $this->storage->exists($this->path);
+ }
+
+ public function get()
+ {
+ return $this->storage->get($this->path);
+ }
+
+ public function delete()
+ {
+ return $this->storage->delete($this->path);
+ }
+
+ public function storeAs($path, $name = null, $options = [])
+ {
+ $options = $this->parseOptions($options);
+
+ $disk = Arr::pull($options, 'disk') ?: $this->disk;
+
+ $newPath = trim($path.'/'.$name, '/');
+
+ Storage::disk($disk)->put(
+ $newPath, $this->storage->readStream($this->path), $options
+ );
+
+ return $newPath;
+ }
+
+ public static function generateHashNameWithOriginalNameEmbedded($file)
+ {
+ $hash = str()->random(30);
+ $meta = str('-meta'.base64_encode($file->getClientOriginalName()).'-')->replace('/', '_');
+ $extension = '.'.$file->getClientOriginalExtension();
+
+ return $hash.$meta.$extension;
+ }
+
+ public function hashName($path = null)
+ {
+ if (app()->runningUnitTests() && str($this->getFilename())->contains('-hash=')) {
+ return str($this->getFilename())->between('-hash=', '-mimeType')->value();
+ }
+
+ return parent::hashName($path);
+ }
+
+ public function extractOriginalNameFromFilePath($path)
+ {
+ return base64_decode(head(explode('-', last(explode('-meta', str($path)->replace('_', '/'))))));
+ }
+
+ public static function createFromLivewire($filePath)
+ {
+ return new static($filePath, FileUploadConfiguration::disk());
+ }
+
+ public static function canUnserialize($subject)
+ {
+ if (is_string($subject)) {
+ return (string) str($subject)->startsWith(['livewire-file:', 'livewire-files:']);
+ }
+
+ if (is_array($subject)) {
+ return collect($subject)->contains(function ($value) {
+ return static::canUnserialize($value);
+ });
+ }
+
+ return false;
+ }
+
+ public static function unserializeFromLivewireRequest($subject)
+ {
+ if (is_string($subject)) {
+ if (str($subject)->startsWith('livewire-file:')) {
+ return static::createFromLivewire(str($subject)->after('livewire-file:'));
+ }
+
+ if (str($subject)->startsWith('livewire-files:')) {
+ $paths = json_decode(str($subject)->after('livewire-files:'), true);
+
+ return collect($paths)->map(function ($path) { return static::createFromLivewire($path); })->toArray();
+ }
+ }
+
+ if (is_array($subject)) {
+ foreach ($subject as $key => $value) {
+ $subject[$key] = static::unserializeFromLivewireRequest($value);
+ }
+ }
+
+ return $subject;
+ }
+
+ public function serializeForLivewireResponse()
+ {
+ return 'livewire-file:'.$this->getFilename();
+ }
+
+ public static function serializeMultipleForLivewireResponse($files)
+ {
+ return 'livewire-files:'.json_encode(collect($files)->map->getFilename());
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/WithFileUploads.php b/portman/lib/Livewire/Features/SupportFileUploads/WithFileUploads.php
new file mode 100644
index 00000000..aba17958
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFileUploads/WithFileUploads.php
@@ -0,0 +1,128 @@
+create($fileInfo[0]['name'], $fileInfo[0]['size'] / 1024, $fileInfo[0]['type']);
+
+ $this->dispatch('upload:generatedSignedUrlForS3', name: $name, payload: GenerateSignedUploadUrl::forS3($file))->self();
+
+ return;
+ }
+
+ $this->dispatch('upload:generatedSignedUrl', name: $name, url: GenerateSignedUploadUrl::forLocal())->self();
+ }
+
+ function _finishUpload($name, $tmpPath, $isMultiple, $append = false)
+ {
+ if (FileUploadConfiguration::shouldCleanupOldUploads()) {
+ $this->cleanupOldUploads();
+ }
+
+ if ($isMultiple) {
+ $file = collect($tmpPath)->map(function ($i) {
+ return TemporaryUploadedFile::createFromLivewire($i);
+ })->toArray();
+ $this->dispatch('upload:finished', name: $name, tmpFilenames: collect($file)->map->getFilename()->toArray())->self();
+
+ if ($append) {
+ $existing = $this->getPropertyValue($name);
+ if ($existing instanceof \Illuminate\Support\Collection) {
+ $file = $existing->merge($file);
+ } elseif (is_array($existing)) {
+ $file = array_merge($existing, $file);
+ }
+ }
+ } else {
+ $file = TemporaryUploadedFile::createFromLivewire($tmpPath[0]);
+ $this->dispatch('upload:finished', name: $name, tmpFilenames: [$file->getFilename()])->self();
+
+ // If the property is an array, but the upload ISNT set to "multiple"
+ // then APPEND the upload to the array, rather than replacing it.
+ if (is_array($value = $this->getPropertyValue($name))) {
+ $file = array_merge($value, [$file]);
+ }
+ }
+
+ app('livewire')->updateProperty($this, $name, $file);
+ }
+
+ function _uploadErrored($name, $errorsInJson, $isMultiple) {
+ $this->dispatch('upload:errored', name: $name)->self();
+
+ if (is_null($errorsInJson)) {
+ // Handle any translations/custom names
+ $translator = app()->make('translator');
+
+ $attribute = $translator->get("validation.attributes.{$name}");
+ if ($attribute === "validation.attributes.{$name}") $attribute = $name;
+
+ $message = trans('validation.uploaded', ['attribute' => $attribute]);
+ if ($message === 'validation.uploaded') $message = "The {$name} failed to upload.";
+
+ throw ValidationException::withMessages([$name => $message]);
+ }
+
+ $errorsInJson = $isMultiple
+ ? str_ireplace('files', $name, $errorsInJson)
+ : str_ireplace('files.0', $name, $errorsInJson);
+
+ $errors = json_decode($errorsInJson, true)['errors'];
+
+ throw (ValidationException::withMessages($errors));
+ }
+
+ function _removeUpload($name, $tmpFilename)
+ {
+ $uploads = $this->getPropertyValue($name);
+
+ if (is_array($uploads) && isset($uploads[0]) && $uploads[0] instanceof TemporaryUploadedFile) {
+ $this->dispatch('upload:removed', name: $name, tmpFilename: $tmpFilename)->self();
+
+ app('livewire')->updateProperty($this, $name, array_values(array_filter($uploads, function ($upload) use ($tmpFilename) {
+ if ($upload->getFilename() === $tmpFilename) {
+ $upload->delete();
+ return false;
+ }
+
+ return true;
+ })));
+ } elseif ($uploads instanceof TemporaryUploadedFile && $uploads->getFilename() === $tmpFilename) {
+ $uploads->delete();
+
+ $this->dispatch('upload:removed', name: $name, tmpFilename: $tmpFilename)->self();
+
+ app('livewire')->updateProperty($this, $name, null);
+ }
+ }
+
+ protected function cleanupOldUploads()
+ {
+ if (FileUploadConfiguration::isUsingS3()) return;
+
+ $storage = FileUploadConfiguration::storage();
+
+ foreach ($storage->allFiles(FileUploadConfiguration::path()) as $filePathname) {
+ // On busy websites, this cleanup code can run in multiple threads causing part of the output
+ // of allFiles() to have already been deleted by another thread.
+ if (! $storage->exists($filePathname)) continue;
+
+ $yesterdaysStamp = now()->subDay()->timestamp;
+ if ($yesterdaysStamp > $storage->lastModified($filePathname)) {
+ $storage->delete($filePathname);
+ }
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image.png b/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image.png
new file mode 100644
index 00000000..99caba2a
Binary files /dev/null and b/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image.png differ
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image2.png b/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image2.png
new file mode 100644
index 00000000..99caba2a
Binary files /dev/null and b/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image2.png differ
diff --git a/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image_big.jpg b/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image_big.jpg
new file mode 100644
index 00000000..d73d0cf9
Binary files /dev/null and b/portman/lib/Livewire/Features/SupportFileUploads/browser_test_image_big.jpg differ
diff --git a/portman/lib/Livewire/Features/SupportFormObjects/Form.php b/portman/lib/Livewire/Features/SupportFormObjects/Form.php
new file mode 100644
index 00000000..85878515
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFormObjects/Form.php
@@ -0,0 +1,198 @@
+component; }
+ public function getPropertyName() { return $this->propertyName; }
+
+ public function validate($rules = null, $messages = [], $attributes = [])
+ {
+ try {
+ return $this->parentValidate($rules, $messages, $attributes);
+ } catch (ValidationException $e) {
+ invade($e->validator)->messages = $this->prefixErrorBag(invade($e->validator)->messages);
+ invade($e->validator)->failedRules = $this->prefixArray(invade($e->validator)->failedRules);
+
+ throw $e;
+ }
+ }
+
+ public function validateOnly($field, $rules = null, $messages = [], $attributes = [], $dataOverrides = [])
+ {
+ try {
+ return $this->parentValidateOnly($field, $rules, $messages, $attributes, $dataOverrides);
+ } catch (ValidationException $e) {
+ invade($e->validator)->messages = $this->prefixErrorBag(invade($e->validator)->messages)->merge(
+ $this->getComponent()->getErrorBag()
+ );
+
+ invade($e->validator)->failedRules = $this->prefixArray(invade($e->validator)->failedRules);
+
+ throw $e;
+ }
+ }
+
+ protected function runSubValidators()
+ {
+ // This form object IS the sub-validator.
+ // Let's skip it...
+ }
+
+ protected function prefixErrorBag($bag)
+ {
+ $raw = $bag->toArray();
+
+ $raw = Arr::prependKeysWith($raw, $this->getPropertyName().'.');
+
+ return new MessageBag($raw);
+ }
+
+ protected function prefixArray($array)
+ {
+ return Arr::prependKeysWith($array, $this->getPropertyName().'.');
+ }
+
+ public function addError($key, $message)
+ {
+ $this->component->addError($this->propertyName . '.' . $key, $message);
+ }
+
+ public function resetErrorBag($field = null)
+ {
+ $fields = (array) $field;
+
+ foreach ($fields as $idx => $field) {
+ $fields[$idx] = $this->propertyName . '.' . $field;
+ }
+
+ $this->getComponent()->resetErrorBag($fields);
+ }
+
+ public function all()
+ {
+ return $this->toArray();
+ }
+
+ public function only($properties)
+ {
+ $results = [];
+
+ foreach (is_array($properties) ? $properties : func_get_args() as $property) {
+ $results[$property] = $this->hasProperty($property) ? $this->getPropertyValue($property) : null;
+ }
+
+ return $results;
+ }
+
+ public function except($properties)
+ {
+ $properties = is_array($properties) ? $properties : func_get_args();
+
+ return array_diff_key($this->all(), array_flip($properties));
+ }
+
+ public function hasProperty($prop)
+ {
+ return property_exists($this, Utils::beforeFirstDot($prop));
+ }
+
+ public function getPropertyValue($name)
+ {
+ $value = $this->{Utils::beforeFirstDot($name)};
+
+ if (Utils::containsDots($name)) {
+ return data_get($value, Utils::afterFirstDot($name));
+ }
+
+ return $value;
+ }
+
+ public function fill($values)
+ {
+ $publicProperties = array_keys($this->all());
+
+ if ($values instanceof Model) {
+ $values = $values->toArray();
+ }
+
+ foreach ($values as $key => $value) {
+ if (in_array(Utils::beforeFirstDot($key), $publicProperties)) {
+ data_set($this, $key, $value);
+ }
+ }
+ }
+
+ public function reset(...$properties)
+ {
+ $properties = count($properties) && is_array($properties[0])
+ ? $properties[0]
+ : $properties;
+
+ if (empty($properties)) $properties = array_keys($this->all());
+
+ $freshInstance = new static($this->getComponent(), $this->getPropertyName());
+
+ foreach ($properties as $property) {
+ data_set($this, $property, data_get($freshInstance, $property));
+ }
+ }
+
+ public function resetExcept(...$properties)
+ {
+ if (count($properties) && is_array($properties[0])) {
+ $properties = $properties[0];
+ }
+
+ $keysToReset = array_diff(array_keys($this->all()), $properties);
+
+ if($keysToReset === []) {
+ return;
+ }
+
+ $this->reset($keysToReset);
+ }
+
+ public function pull($properties = null)
+ {
+ $wantsASingleValue = is_string($properties);
+
+ $properties = is_array($properties) ? $properties : func_get_args();
+
+ $beforeReset = match (true) {
+ empty($properties) => $this->all(),
+ $wantsASingleValue => $this->getPropertyValue($properties[0]),
+ default => $this->only($properties),
+ };
+
+ $this->reset($properties);
+
+ return $beforeReset;
+ }
+
+ public function toArray()
+ {
+ return Utils::getPublicProperties($this);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFormObjects/FormObjectSynth.php b/portman/lib/Livewire/Features/SupportFormObjects/FormObjectSynth.php
new file mode 100644
index 00000000..fc3ab757
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFormObjects/FormObjectSynth.php
@@ -0,0 +1,69 @@
+toArray();
+
+ foreach ($data as $key => $child) {
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [$data, ['class' => get_class($target)]];
+ }
+
+ function hydrate($data, $meta, $hydrateChild)
+ {
+ $form = new $meta['class']($this->context->component, $this->path);
+
+ $callBootMethod = static::bootFormObject($this->context->component, $form, $this->path);
+
+ foreach ($data as $key => $child) {
+ if ($child === null && Utils::propertyIsTypedAndUninitialized($form, $key)) {
+ continue;
+ }
+
+ $form->$key = $hydrateChild($key, $child);
+ }
+
+ $callBootMethod();
+
+ return $form;
+ }
+
+ function set(&$target, $key, $value)
+ {
+ if ($value === null && Utils::propertyIsTyped($target, $key) && ! Utils::getProperty($target, $key)->getType()->allowsNull()) {
+ unset($target->$key);
+ } else {
+ $target->$key = $value;
+ }
+ }
+
+ public static function bootFormObject($component, $form, $path)
+ {
+ $component->mergeOutsideAttributes(
+ AttributeCollection::fromComponent($component, $form, $path . '.')
+ );
+
+ return function () use ($form) {
+ wrap($form)->boot();
+ };
+ }
+}
+
diff --git a/portman/lib/Livewire/Features/SupportFormObjects/HandlesFormObjects.php b/portman/lib/Livewire/Features/SupportFormObjects/HandlesFormObjects.php
new file mode 100644
index 00000000..aa0f67ec
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFormObjects/HandlesFormObjects.php
@@ -0,0 +1,19 @@
+all() as $key => $value) {
+ if ($value instanceof Form) {
+ $forms[] = $value;
+ }
+ }
+
+ return $forms;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportFormObjects/SupportFormObjects.php b/portman/lib/Livewire/Features/SupportFormObjects/SupportFormObjects.php
new file mode 100644
index 00000000..d1956938
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportFormObjects/SupportFormObjects.php
@@ -0,0 +1,107 @@
+propertySynthesizer(
+ FormObjectSynth::class
+ );
+ }
+
+ function boot()
+ {
+ $this->initializeFormObjects();
+ }
+
+ public function update($formName, $fullPath, $newValue)
+ {
+ $form = $this->getProperty($formName);
+
+ if (! $form instanceof Form) {
+ return;
+ }
+
+ if (! str($fullPath)->contains('.')) {
+ return;
+ }
+
+ $path = str($fullPath)->after('.')->__toString();
+
+ $name = str($path);
+
+ $propertyName = $name->studly()->before('.');
+ $keyAfterFirstDot = $name->contains('.') ? $name->after('.')->__toString() : null;
+ $keyAfterLastDot = $name->contains('.') ? $name->afterLast('.')->__toString() : null;
+
+ $beforeMethod = 'updating'.$propertyName;
+ $afterMethod = 'updated'.$propertyName;
+
+ $beforeNestedMethod = $name->contains('.')
+ ? 'updating'.$name->replace('.', '_')->studly()
+ : false;
+
+ $afterNestedMethod = $name->contains('.')
+ ? 'updated'.$name->replace('.', '_')->studly()
+ : false;
+
+ $this->callFormHook($form, 'updating', [$path, $newValue]);
+
+ $this->callFormHook($form, $beforeMethod, [$newValue, $keyAfterFirstDot]);
+
+ $this->callFormHook($form, $beforeNestedMethod, [$newValue, $keyAfterLastDot]);
+
+ return function () use ($form, $path, $afterMethod, $afterNestedMethod, $keyAfterFirstDot, $keyAfterLastDot, $newValue) {
+ $this->callFormHook($form, 'updated', [$path, $newValue]);
+
+ $this->callFormHook($form, $afterMethod, [$newValue, $keyAfterFirstDot]);
+
+ $this->callFormHook($form, $afterNestedMethod, [$newValue, $keyAfterLastDot]);
+ };
+ }
+
+ protected function initializeFormObjects()
+ {
+ foreach ((new ReflectionClass($this->component))->getProperties() as $property) {
+ // Public properties only...
+ if ($property->isPublic() !== true) continue;
+ // Uninitialized properties only...
+ if ($property->isInitialized($this->component)) continue;
+
+ $type = $property->getType();
+
+ if (! $type instanceof ReflectionNamedType) continue;
+
+ $typeName = $type->getName();
+
+ // "Form" object property types only...
+ if (! is_subclass_of($typeName, Form::class)) continue;
+
+ $form = new $typeName(
+ $this->component,
+ $name = $property->getName()
+ );
+
+ $callBootMethod = FormObjectSynth::bootFormObject($this->component, $form, $name);
+
+ $property->setValue($this->component, $form);
+
+ $callBootMethod();
+ }
+ }
+
+ protected function callFormHook($form, $name, $params = [])
+ {
+ if (method_exists($form, $name)) {
+ wrap($form)->$name(...$params);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportIsolating/BaseIsolate.php b/portman/lib/Livewire/Features/SupportIsolating/BaseIsolate.php
new file mode 100644
index 00000000..7785992f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportIsolating/BaseIsolate.php
@@ -0,0 +1,11 @@
+shouldIsolate()) {
+ $context->addMemo('isolate', true);
+ }
+ }
+
+ public function shouldIsolate()
+ {
+ return $this->component->getAttributes()
+ ->filter(fn ($i) => is_subclass_of($i, BaseIsolate::class))
+ ->count() > 0;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportJsEvaluation/BaseJs.php b/portman/lib/Livewire/Features/SupportJsEvaluation/BaseJs.php
new file mode 100644
index 00000000..c88e7735
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportJsEvaluation/BaseJs.php
@@ -0,0 +1,22 @@
+getName();
+
+ $stringifiedMethod = $this->component->$name();
+
+ $context->pushEffect('js', $stringifiedMethod, $name);
+ }
+}
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportJsEvaluation/HandlesJsEvaluation.php b/portman/lib/Livewire/Features/SupportJsEvaluation/HandlesJsEvaluation.php
new file mode 100644
index 00000000..cfd88aea
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportJsEvaluation/HandlesJsEvaluation.php
@@ -0,0 +1,13 @@
+push('js', compact('expression', 'params'));
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportJsEvaluation/SupportJsEvaluation.php b/portman/lib/Livewire/Features/SupportJsEvaluation/SupportJsEvaluation.php
new file mode 100644
index 00000000..e8e8cb39
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportJsEvaluation/SupportJsEvaluation.php
@@ -0,0 +1,17 @@
+component)->has('js')) return;
+
+ $context->addEffect('xjs', store($this->component)->get('js'));
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLazyLoading/BaseLazy.php b/portman/lib/Livewire/Features/SupportLazyLoading/BaseLazy.php
new file mode 100644
index 00000000..b20972b2
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLazyLoading/BaseLazy.php
@@ -0,0 +1,13 @@
+defaults['lazy'] = $enabled;
+
+ return $this;
+ });
+ }
+
+ public function mount($params)
+ {
+ $hasLazyParam = isset($params['lazy']);
+ $lazyProperty = $params['lazy'] ?? false;
+ $isolate = true;
+
+ $reflectionClass = new \ReflectionClass($this->component);
+ $lazyAttribute = $reflectionClass->getAttributes(\Livewire\Attributes\Lazy::class)[0] ?? null;
+
+ // If Livewire::withoutLazyLoading()...
+ if (static::$disableWhileTesting) return;
+ // If `:lazy="false"` disable lazy loading...
+ if ($hasLazyParam && ! $lazyProperty) return;
+ // If no lazy loading is included at all...
+ if (! $hasLazyParam && ! $lazyAttribute) return;
+
+ if ($lazyAttribute) {
+ $attribute = $lazyAttribute->newInstance();
+
+ $isolate = $attribute->isolate;
+ }
+
+ $this->component->skipMount();
+
+ store($this->component)->set('isLazyLoadMounting', true);
+ store($this->component)->set('isLazyIsolated', $isolate);
+
+ $this->component->skipRender(
+ $this->generatePlaceholderHtml($params)
+ );
+ }
+
+ public function hydrate($memo)
+ {
+ if (! isset($memo['lazyLoaded'])) return;
+ if ($memo['lazyLoaded'] === true) return;
+
+ $this->component->skipHydrate();
+
+ store($this->component)->set('isLazyLoadHydrating', true);
+ }
+
+ function dehydrate($context)
+ {
+ if (store($this->component)->get('isLazyLoadMounting') === true) {
+ $context->addMemo('lazyLoaded', false);
+ $context->addMemo('lazyIsolated', store($this->component)->get('isLazyIsolated'));
+ } elseif (store($this->component)->get('isLazyLoadHydrating') === true) {
+ $context->addMemo('lazyLoaded', true);
+ }
+ }
+
+ function call($method, $params, $returnEarly)
+ {
+ if ($method !== '__lazyLoad') return;
+
+ [ $encoded ] = $params;
+
+ $mountParams = $this->resurrectMountParams($encoded);
+
+ $this->callMountLifecycleMethod($mountParams);
+
+ $returnEarly();
+ }
+
+ public function generatePlaceholderHtml($params)
+ {
+ $this->registerContainerComponent();
+
+ $container = app('livewire')->new('__mountParamsContainer');
+
+ $container->forMount = array_diff_key($params, array_flip(['lazy']));
+
+ $context = new ComponentContext($container, mounting: true);
+
+ trigger('dehydrate', $container, $context);
+
+ $snapshot = app('livewire')->snapshot($container, $context);
+
+ $encoded = base64_encode(json_encode($snapshot));
+
+ $placeholder = $this->getPlaceholderView($this->component, $params);
+
+ $finish = trigger('render.placeholder', $this->component, $placeholder, $params);
+
+ $viewContext = new ViewContext;
+
+ $html = $placeholder->render(function ($view) use ($viewContext) {
+ // Extract leftover slots, sections, and pushes before they get flushed...
+ $viewContext->extractFromEnvironment($view->getFactory());
+ });
+
+ $html = Utils::insertAttributesIntoHtmlRoot($html, [
+ ((isset($params['lazy']) and $params['lazy'] === 'on-load') ? 'x-init' : 'x-intersect') => '$wire.__lazyLoad(\''.$encoded.'\')',
+ ]);
+
+ $replaceHtml = function ($newHtml) use (&$html) {
+ $html = $newHtml;
+ };
+
+ $html = $finish($html, $replaceHtml, $viewContext);
+
+ return $html;
+ }
+
+ protected function getPlaceholderView($component, $params)
+ {
+ $globalPlaceholder = config('livewire.lazy_placeholder');
+
+ $placeholderHtml = $globalPlaceholder
+ ? view($globalPlaceholder)->render()
+ : '
';
+
+ $viewOrString = wrap($component)->withFallback($placeholderHtml)->placeholder($params);
+
+ $properties = Utils::getPublicPropertiesDefinedOnSubclass($component);
+
+ $view = Utils::generateBladeView($viewOrString, $properties);
+
+ return $view;
+ }
+
+ function resurrectMountParams($encoded)
+ {
+ $snapshot = json_decode(base64_decode($encoded), associative: true);
+
+ $this->registerContainerComponent();
+
+ [ $container ] = app('livewire')->fromSnapshot($snapshot);
+
+ return $container->forMount;
+ }
+
+ function callMountLifecycleMethod($params)
+ {
+ $hook = new SupportLifecycleHooks;
+
+ $hook->setComponent($this->component);
+
+ $hook->mount($params);
+ }
+
+ public function registerContainerComponent()
+ {
+ app('livewire')->component('__mountParamsContainer', new class extends Component {
+ public $forMount;
+ });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLegacyModels/CannotBindToModelDataWithoutValidationRuleException.php b/portman/lib/Livewire/Features/SupportLegacyModels/CannotBindToModelDataWithoutValidationRuleException.php
new file mode 100644
index 00000000..6dd4ea81
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLegacyModels/CannotBindToModelDataWithoutValidationRuleException.php
@@ -0,0 +1,17 @@
+getQueueableClass();
+
+ $meta = [];
+
+ $meta['keys'] = $target->modelKeys();
+ $meta['class'] = $class;
+ $meta['modelClass'] = $modelClass;
+
+ if ($modelClass && ($connection = $this->getConnection($target)) !== $modelClass::make()->getConnectionName()) {
+ $meta['connection'] = $connection;
+ }
+
+ $relations = $target->getQueueableRelations();
+
+ if (count($relations)) {
+ $meta['relations'] = $relations;
+ }
+
+ $rules = $this->getRules($this->context);
+
+ if (empty($rules)) return [[], $meta];
+
+ $data = $this->getDataFromCollection($target, $rules);
+
+ foreach ($data as $key => $child) {
+
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [ $data, $meta ];
+ }
+
+ public function hydrate($data, $meta, $hydrateChild)
+ {
+ if (isset($meta['__child_from_parent'])) {
+ $collection = $meta['__child_from_parent'];
+
+ unset($meta['__child_from_parent']);
+ } else {
+ $collection = $this->loadCollection($meta);
+ }
+
+ if (isset($meta['relations'])) {
+ $collection->loadMissing($meta['relations']);
+ }
+
+ if (count($data)) {
+ foreach ($data as $key => $childData) {
+ $childData[1]['__child_from_parent'] = $collection->get($key);
+
+ $data[$key] = $hydrateChild($key, $childData);
+ }
+
+ return $collection::wrap($data);
+ }
+
+ return $collection;
+ }
+
+ public function get(&$target, $key)
+ {
+ return $target->get($key);
+ }
+
+ public function set(&$target, $key, $value, $pathThusFar, $fullPath)
+ {
+ if (SupportLegacyModels::missingRuleFor($this->context->component, $fullPath)) {
+ throw new CannotBindToModelDataWithoutValidationRuleException($fullPath, $this->context->component->getName());
+ }
+
+ $target->put($key, $value);
+ }
+
+ public function methods($target)
+ {
+ return [];
+ }
+
+ public function call($target, $method, $params, $addEffect)
+ {
+ }
+
+ protected function getRules($context)
+ {
+ $key = $this->path ?? null;
+
+ if (is_null($key)) return [];
+
+ return SupportLegacyModels::getRulesFor($context->component, $key);
+ }
+
+ protected function getConnection(EloquentCollection $collection)
+ {
+ if ($collection->isEmpty()) {
+ return;
+ }
+
+ $connection = $collection->first()->getConnectionName();
+
+ $collection->each(function ($model) use ($connection) {
+ // If there is no connection name, it must be a new model so continue.
+ if (is_null($model->getConnectionName())) {
+ return;
+ }
+
+ if ($model->getConnectionName() !== $connection) {
+ throw new LogicException('Livewire can\'t dehydrate an Eloquent Collection with models from different connections.');
+ }
+ });
+
+ return $connection;
+ }
+
+ protected function getDataFromCollection(EloquentCollection $collection, $rules)
+ {
+ return $this->filterData($collection->all(), $rules);
+ }
+
+ protected function filterData($data, $rules)
+ {
+ return array_filter($data, function ($key) use ($rules) {
+ return array_key_exists('*', $rules);
+ }, ARRAY_FILTER_USE_KEY);
+ }
+
+ protected function loadCollection($meta)
+ {
+ if (isset($meta['keys']) && count($meta['keys']) >= 0 && ! empty($meta['modelClass'])) {
+ $model = new $meta['modelClass'];
+
+ if (isset($meta['connection'])) {
+ $model->setConnection($meta['connection']);
+ }
+
+ $query = $model->newQueryForRestoration($meta['keys']);
+
+ if (isset($meta['relations'])) {
+ $query->with($meta['relations']);
+ }
+
+ $query->useWritePdo();
+
+ $collection = $query->get();
+
+ $collection = $collection->keyBy->getKey();
+
+ return new $meta['class'](
+ collect($meta['keys'])->map(function ($id) use ($collection) {
+ return $collection[$id] ?? null;
+ })->filter()
+ );
+ }
+
+ return new $meta['class']();
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLegacyModels/EloquentModelSynth.php b/portman/lib/Livewire/Features/SupportLegacyModels/EloquentModelSynth.php
new file mode 100644
index 00000000..dbd7b8a3
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLegacyModels/EloquentModelSynth.php
@@ -0,0 +1,227 @@
+getMorphClass();
+ } catch (ClassMorphViolationException $e) {
+ // If the model is not using morph classes, this exception is thrown
+ $alias = $class;
+ }
+
+ $meta = [];
+
+ if ($target->exists) {
+ $meta['key'] = $target->getKey();
+ }
+
+ $meta['class'] = $alias;
+
+ if ($target->getConnectionName() !== $class::make()->getConnectionName()) {
+ $meta['connection'] = $target->getConnectionName();
+ }
+
+ $relations = $target->getQueueableRelations();
+
+ if (count($relations)) {
+ $meta['relations'] = $relations;
+ }
+
+ $rules = $this->getRules($this->context);
+
+ if (empty($rules)) return [[], $meta];
+
+ $data = $this->getDataFromModel($target, $rules);
+
+ foreach ($data as $key => $child) {
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [$data, $meta];
+ }
+
+ public function hydrate($data, $meta, $hydrateChild)
+ {
+ if (! is_iterable($data)) return null;
+
+ if (isset($meta['__child_from_parent'])) {
+ $model = $meta['__child_from_parent'];
+
+ unset($meta['__child_from_parent']);
+ } else {
+ $model = $this->loadModel($meta);
+ }
+
+ if (isset($meta['relations'])) {
+ foreach($meta['relations'] as $relationKey) {
+ if (! isset($data[$relationKey])) continue;
+
+ $data[$relationKey][1]['__child_from_parent'] = $model->getRelation($relationKey);
+
+ $model->setRelation($relationKey, $hydrateChild($relationKey, $data[$relationKey]));
+
+ unset($data[$relationKey]);
+ }
+ }
+
+ foreach ($data as $key => $child) {
+ $data[$key] = $hydrateChild($key, $child);
+ }
+
+ $this->setDataOnModel($model, $data);
+
+ return $model;
+ }
+
+ public function get(&$target, $key)
+ {
+ return $target->$key;
+ }
+
+ public function set(Model &$target, $key, $value, $pathThusFar, $fullPath)
+ {
+ if (SupportLegacyModels::missingRuleFor($this->context->component, $fullPath)) {
+ throw new CannotBindToModelDataWithoutValidationRuleException($fullPath, $this->context->component->getName());
+ }
+
+ if ($target->relationLoaded($key)) {
+ return $target->setRelation($key, $value);
+ }
+
+ if (array_key_exists($key, $target->getCasts()) && enum_exists($target->getCasts()[$key]) && $value === '') {
+ $value = null;
+ }
+
+ $target->$key = $value;
+ }
+
+ public function methods($target)
+ {
+ return [];
+ }
+
+ public function call($target, $method, $params, $addEffect)
+ {
+ }
+
+ protected function getRules($context)
+ {
+ $key = $this->path ?? null;
+
+ if (is_null($key)) return [];
+
+ if ($context->component) {
+ return SupportLegacyModels::getRulesFor($this->context->component, $key);
+ }
+ }
+
+ protected function getDataFromModel(Model $model, $rules)
+ {
+ return [
+ ...$this->filterAttributes($this->getAttributes($model), $rules),
+ ...$this->filterRelations($model->getRelations(), $rules),
+ ];
+ }
+
+ protected function getAttributes($model)
+ {
+ $attributes = $model->attributesToArray();
+
+ foreach ($model->getCasts() as $key => $cast) {
+ if (! class_exists($cast)) continue;
+
+ if (
+ in_array(CastsAttributes::class, class_implements($cast))
+ && isset($attributes[$key])
+ ) {
+ $attributes[$key] = $model->getAttributes()[$key];
+ }
+ }
+
+ return $attributes;
+ }
+
+ protected function filterAttributes($data, $rules)
+ {
+ $filteredAttributes = [];
+
+ foreach($rules as $key => $rule) {
+ // If the rule is an array, take the key instead
+ if (is_array($rule)) {
+ $rule = $key;
+ }
+
+ // If someone has created an empty model, the attribute may not exist
+ // yet, so use data_get so it will still be sent to the front end.
+ $filteredAttributes[$rule] = data_get($data, $rule);
+ }
+
+ return $filteredAttributes;
+ }
+
+ protected function filterRelations($data, $rules)
+ {
+ return array_filter($data, function ($key) use ($rules) {
+ return array_key_exists($key, $rules) || in_array($key, $rules);
+ }, ARRAY_FILTER_USE_KEY);
+ }
+
+ protected function loadModel($meta): ?Model
+ {
+ $class = $meta['class'];
+
+ // If no alias found, this returns `null`
+ $aliasClass = Relation::getMorphedModel($class);
+
+ if (! is_null($aliasClass)) {
+ $class = $aliasClass;
+ }
+
+ if (isset($meta['key'])) {
+ $model = new $class;
+
+ if (isset($meta['connection'])) {
+ $model->setConnection($meta['connection']);
+ }
+
+ $query = $model->newQueryForRestoration($meta['key']);
+
+ if (isset($meta['relations'])) {
+ $query->with($meta['relations']);
+ }
+
+ $model = $query->first();
+ } else {
+ $model = new $class();
+ }
+
+ return $model;
+ }
+
+ protected function setDataOnModel(Model $model, $data)
+ {
+ foreach ($data as $key => $value) {
+ $model->$key = $value;
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLegacyModels/SupportLegacyModels.php b/portman/lib/Livewire/Features/SupportLegacyModels/SupportLegacyModels.php
new file mode 100644
index 00000000..9a95d122
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLegacyModels/SupportLegacyModels.php
@@ -0,0 +1,169 @@
+missingRuleFor() method on component. (inside ModelSynth)
+ */
+class SupportLegacyModels extends ComponentHook
+{
+ protected static $rules;
+
+ static function provide()
+ {
+ // Only enable this feature if config option is set to `true`.
+ if (config('livewire.legacy_model_binding', false) !== true) return;
+
+ app('livewire')->propertySynthesizer([
+ EloquentModelSynth::class,
+ EloquentCollectionSynth::class,
+ ]);
+
+ on('flush-state', function() {
+ static::flushRules();
+ });
+ }
+
+ static function flushRules()
+ {
+ static::$rules = null;
+ }
+
+ static function hasRuleFor($component, $path) {
+ $path = str($path)->explode('.');
+
+ $has = false;
+ $end = false;
+
+ $segmentRules = static::getRules($component);
+
+ foreach ($path as $key => $segment) {
+ if ($end) {
+ throw new \LogicException('Something went wrong');
+ }
+
+ if (!is_numeric($segment) && array_key_exists($segment, $segmentRules)) {
+ $segmentRules = $segmentRules[$segment];
+ $has = true;
+ continue;
+ }
+
+ if (is_numeric($segment) && array_key_exists('*', $segmentRules)) {
+ $segmentRules = $segmentRules['*'];
+ $has = true;
+ continue;
+ }
+
+ if (is_numeric($segment) && in_array('*', $segmentRules, true)) {
+ $has = true;
+ $end = true;
+ continue;
+ }
+
+ if (in_array($segment, $segmentRules, true)) {
+ $has = true;
+ $end = true;
+ continue;
+ }
+
+ $has = false;
+ }
+
+ return $has;
+ }
+
+ static function missingRuleFor($component, $path) {
+ return ! static::hasRuleFor($component, $path);
+ }
+
+ static function getRules($component) {
+ return static::$rules[$component->getId()] ??= static::processRules($component);
+ }
+
+ static function getRulesFor($component, $key)
+ {
+ $rules = static::getRules($component);
+
+ $propertyWithStarsInsteadOfNumbers = static::ruleWithNumbersReplacedByStars($key);
+
+ return static::dataGetWithoutWildcardSupport(
+ $rules,
+ $propertyWithStarsInsteadOfNumbers,
+ data_get($rules, $key, []),
+ );
+ }
+
+ static function dataGetWithoutWildcardSupport($array, $key, $default)
+ {
+ $segments = explode('.', $key);
+
+ $first = array_shift($segments);
+
+ if (! isset($array[$first])) {
+ return value($default);
+ }
+
+ $value = $array[$first];
+
+ if (count($segments) > 0) {
+ return static::dataGetWithoutWildcardSupport($value, implode('.', $segments), $default);
+ }
+
+ return $value;
+ }
+
+ static function ruleWithNumbersReplacedByStars($dotNotatedProperty)
+ {
+ // Convert foo.0.bar.1 -> foo.*.bar.*
+ return (string) str($dotNotatedProperty)
+ // Replace all numeric indexes with an array wildcard: (.0., .10., .007.) => .*.
+ // In order to match overlapping numerical indexes (foo.1.2.3.4.name),
+ // We need to use a positive look-behind, that's technically all the magic here.
+ // For better understanding, see: https://regexr.com/5d1n3
+ ->replaceMatches('/(?<=(\.))\d+\./', '*.')
+ // Replace all numeric indexes at the end of the name with an array wildcard
+ // (Same as the previous regex, but ran only at the end of the string)
+ // For better undestanding, see: https://regexr.com/5d1n6
+ ->replaceMatches('/\.\d+$/', '.*');
+ }
+
+ protected static function processRules($component)
+ {
+ $rules = array_keys($component->getRules());
+
+ return static::convertDotNotationToArrayNotation($rules);
+ }
+
+ protected static function convertDotNotationToArrayNotation($rules)
+ {
+ return static::recusivelyProcessDotNotation($rules);
+ }
+
+ protected static function recusivelyProcessDotNotation($rules)
+ {
+ $singleRules = [];
+ $groupedRules = [];
+
+ foreach ($rules as $key => $value) {
+ $value = str($value);
+
+ if (!$value->contains('.')) {
+ $singleRules[] = (string) $value;
+
+ continue;
+ }
+
+ $groupedRules[(string) $value->before('.')][] = (string) $value->after('.');
+ }
+
+ foreach ($groupedRules as $key => $value) {
+ $groupedRules[$key] = static::recusivelyProcessDotNotation($value);
+ }
+
+ return $singleRules + $groupedRules;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLifecycleHooks/DirectlyCallingLifecycleHooksNotAllowedException.php b/portman/lib/Livewire/Features/SupportLifecycleHooks/DirectlyCallingLifecycleHooksNotAllowedException.php
new file mode 100644
index 00000000..e664286c
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLifecycleHooks/DirectlyCallingLifecycleHooksNotAllowedException.php
@@ -0,0 +1,17 @@
+component)->has('skipMount')) { return; }
+
+ $this->callHook('boot');
+ $this->callTraitHook('boot');
+
+ $this->callTraitHook('initialize');
+
+ $this->callHook('mount', $params);
+ $this->callTraitHook('mount', $params);
+
+ $this->callHook('booted');
+ $this->callTraitHook('booted');
+ }
+
+ public function hydrate()
+ {
+ if (store($this->component)->has('skipHydrate')) { return; }
+
+ $this->callHook('boot');
+ $this->callTraitHook('boot');
+
+ $this->callTraitHook('initialize');
+
+ $this->callHook('hydrate');
+ $this->callTraitHook('hydrate');
+
+ // Call "hydrateXx" hooks for each property...
+ foreach ($this->getProperties() as $property => $value) {
+ $this->callHook('hydrate'.str($property)->studly(), [$value]);
+ }
+
+ $this->callHook('booted');
+ $this->callTraitHook('booted');
+ }
+
+ public function update($propertyName, $fullPath, $newValue)
+ {
+ $name = str($fullPath);
+
+ $propertyName = $name->studly()->before('.');
+ $keyAfterFirstDot = $name->contains('.') ? $name->after('.')->__toString() : null;
+ $keyAfterLastDot = $name->contains('.') ? $name->afterLast('.')->__toString() : null;
+
+ $beforeMethod = 'updating'.$propertyName;
+ $afterMethod = 'updated'.$propertyName;
+
+ $beforeNestedMethod = $name->contains('.')
+ ? 'updating'.$name->replace('.', '_')->studly()
+ : false;
+
+ $afterNestedMethod = $name->contains('.')
+ ? 'updated'.$name->replace('.', '_')->studly()
+ : false;
+
+ $this->callHook('updating', [$fullPath, $newValue]);
+ $this->callTraitHook('updating', [$fullPath, $newValue]);
+
+ $this->callHook($beforeMethod, [$newValue, $keyAfterFirstDot]);
+
+ $this->callHook($beforeNestedMethod, [$newValue, $keyAfterLastDot]);
+
+ return function () use ($fullPath, $afterMethod, $afterNestedMethod, $keyAfterFirstDot, $keyAfterLastDot, $newValue) {
+ $this->callHook('updated', [$fullPath, $newValue]);
+ $this->callTraitHook('updated', [$fullPath, $newValue]);
+
+ $this->callHook($afterMethod, [$newValue, $keyAfterFirstDot]);
+
+ $this->callHook($afterNestedMethod, [$newValue, $keyAfterLastDot]);
+ };
+ }
+
+ public function call($methodName, $params, $returnEarly)
+ {
+ $protectedMethods = [
+ 'mount',
+ 'exception',
+ 'hydrate*',
+ 'dehydrate*',
+ 'updating*',
+ 'updated*',
+ ];
+
+ throw_if(
+ str($methodName)->is($protectedMethods),
+ new DirectlyCallingLifecycleHooksNotAllowedException($methodName, $this->component->getName())
+ );
+
+ $this->callTraitHook('call', ['methodName' => $methodName, 'params' => $params, 'returnEarly' => $returnEarly]);
+ }
+
+ public function exception($e, $stopPropagation)
+ {
+ $this->callHook('exception', ['e' => $e, 'stopPropagation' => $stopPropagation]);
+ $this->callTraitHook('exception', ['e' => $e, 'stopPropagation' => $stopPropagation]);
+ }
+
+ public function render($view, $data)
+ {
+ $this->callHook('rendering', ['view' => $view, 'data' => $data]);
+ $this->callTraitHook('rendering', ['view' => $view, 'data' => $data]);
+
+ return function ($html) use ($view) {
+ $this->callHook('rendered', ['view' => $view, 'html' => $html]);
+ $this->callTraitHook('rendered', ['view' => $view, 'html' => $html]);
+ };
+ }
+
+ public function dehydrate()
+ {
+ $this->callHook('dehydrate');
+ $this->callTraitHook('dehydrate');
+
+ // Call "dehydrateXx" hooks for each property...
+ foreach ($this->getProperties() as $property => $value) {
+ $this->callHook('dehydrate'.str($property)->studly(), [$value]);
+ }
+ }
+
+ public function callHook($name, $params = [])
+ {
+ if (method_exists($this->component, $name)) {
+ wrap($this->component)->__call($name, $params);
+ }
+ }
+
+ function callTraitHook($name, $params = [])
+ {
+ foreach (class_uses_recursive($this->component) as $trait) {
+ $method = $name.class_basename($trait);
+
+ if (method_exists($this->component, $method)) {
+ wrap($this->component)->$method(...$params);
+ }
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLocales/SupportLocales.php b/portman/lib/Livewire/Features/SupportLocales/SupportLocales.php
new file mode 100644
index 00000000..5fba921c
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLocales/SupportLocales.php
@@ -0,0 +1,18 @@
+setLocale($locale);
+ }
+
+ function dehydrate($context)
+ {
+ $context->addMemo('locale', app()->getLocale());
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLockedProperties/BaseLocked.php b/portman/lib/Livewire/Features/SupportLockedProperties/BaseLocked.php
new file mode 100644
index 00000000..20060a71
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLockedProperties/BaseLocked.php
@@ -0,0 +1,14 @@
+getName());
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportLockedProperties/CannotUpdateLockedPropertyException.php b/portman/lib/Livewire/Features/SupportLockedProperties/CannotUpdateLockedPropertyException.php
new file mode 100644
index 00000000..77010cec
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportLockedProperties/CannotUpdateLockedPropertyException.php
@@ -0,0 +1,13 @@
+property.']'
+ );
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportModels/EloquentCollectionSynth.php b/portman/lib/Livewire/Features/SupportModels/EloquentCollectionSynth.php
new file mode 100644
index 00000000..eb5b686b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportModels/EloquentCollectionSynth.php
@@ -0,0 +1,92 @@
+getQueueableClass();
+
+ /**
+ * `getQueueableClass` above checks all models are the same and
+ * then returns the class. We then instantiate a model object
+ * so we can call `getMorphClass()` on it.
+ *
+ * If no alias is found, this just returns the class name
+ */
+ $modelAlias = $modelClass ? (new $modelClass)->getMorphClass() : null;
+
+ $meta = [];
+
+ $serializedCollection = (array) $this->getSerializedPropertyValue($target);
+
+ $meta['keys'] = $serializedCollection['id'];
+ $meta['class'] = $class;
+ $meta['modelClass'] = $modelAlias;
+
+ return [
+ null,
+ $meta
+ ];
+ }
+
+ function hydrate($data, $meta, $hydrateChild)
+ {
+ $class = $meta['class'];
+
+ $modelClass = $meta['modelClass'];
+
+ // If no alias found, this returns `null`
+ $modelAlias = Relation::getMorphedModel($modelClass);
+
+ if (! is_null($modelAlias)) {
+ $modelClass = $modelAlias;
+ }
+
+ $keys = $meta['keys'] ?? [];
+
+ if (count($keys) === 0) {
+ return new $class();
+ }
+
+ // We are using Laravel's method here for restoring the collection, which ensures
+ // that all models in the collection are restored in one query, preventing n+1
+ // issues and also only restores models that exist.
+ $collection = (new $modelClass)->newQueryForRestoration($keys)->useWritePdo()->get();
+
+ $collection = $collection->keyBy->getKey();
+
+ return new $meta['class'](
+ collect($meta['keys'])->map(function ($id) use ($collection) {
+ return $collection[$id] ?? null;
+ })->filter()
+ );
+ }
+
+ function get(&$target, $key) {
+ throw new \Exception('Can\'t access model properties directly');
+ }
+
+ function set(&$target, $key, $value, $pathThusFar, $fullPath) {
+ throw new \Exception('Can\'t set model properties directly');
+ }
+
+ function call($target, $method, $params, $addEffect) {
+ throw new \Exception('Can\'t call model methods directly');
+ }
+}
\ No newline at end of file
diff --git a/portman/lib/Livewire/Features/SupportModels/ModelSynth.php b/portman/lib/Livewire/Features/SupportModels/ModelSynth.php
new file mode 100644
index 00000000..a14abf74
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportModels/ModelSynth.php
@@ -0,0 +1,81 @@
+getMorphClass();
+ } catch (ClassMorphViolationException $e) {
+ // If the model is not using morph classes, this exception is thrown
+ $alias = $class;
+ }
+
+ $serializedModel = $target->exists
+ ? (array) $this->getSerializedPropertyValue($target)
+ : null;
+
+ $meta = ['class' => $alias];
+
+ // If the model doesn't exist as it's an empty model or has been
+ // recently deleted, then we don't want to include any key.
+ if ($serializedModel) $meta['key'] = $serializedModel['id'];
+
+
+ return [
+ null,
+ $meta,
+ ];
+ }
+
+ function hydrate($data, $meta) {
+ $class = $meta['class'];
+
+ // If no alias found, this returns `null`
+ $aliasClass = Relation::getMorphedModel($class);
+
+ if (! is_null($aliasClass)) {
+ $class = $aliasClass;
+ }
+
+ // If no key is provided then an empty model is returned
+ if (! array_key_exists('key', $meta)) {
+ return new $class;
+ }
+
+ $key = $meta['key'];
+
+ $model = (new $class)->newQueryForRestoration($key)->useWritePdo()->firstOrFail();
+
+ return $model;
+ }
+
+ function get(&$target, $key) {
+ throw new \Exception('Can\'t access model properties directly');
+ }
+
+ function set(&$target, $key, $value, $pathThusFar, $fullPath) {
+ throw new \Exception('Can\'t set model properties directly');
+ }
+
+ function call($target, $method, $params, $addEffect) {
+ throw new \Exception('Can\'t call model methods directly');
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportModels/SupportModels.php b/portman/lib/Livewire/Features/SupportModels/SupportModels.php
new file mode 100644
index 00000000..7a8b1691
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportModels/SupportModels.php
@@ -0,0 +1,16 @@
+propertySynthesizer([
+ ModelSynth::class,
+ EloquentCollectionSynth::class,
+ ]);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportMorphAwareBladeCompilation/SupportMorphAwareBladeCompilation.php b/portman/lib/Livewire/Features/SupportMorphAwareBladeCompilation/SupportMorphAwareBladeCompilation.php
new file mode 100644
index 00000000..2a250e99
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportMorphAwareBladeCompilation/SupportMorphAwareBladeCompilation.php
@@ -0,0 +1,564 @@
+ '@endif',
+ '@unless' => '@endunless',
+ '@error' => '@enderror',
+ '@isset' => '@endisset',
+ '@empty' => '@endempty',
+ '@auth' => '@endauth',
+ '@guest' => '@endguest',
+ '@switch' => '@endswitch',
+ '@foreach' => '@endforeach',
+ '@forelse' => '@endforelse',
+ '@while' => '@endwhile',
+ '@for' => '@endfor',
+ ];
+
+ Blade::precompiler(function ($entire) use ($directives) {
+ $conditions = \Livewire\invade(app('blade.compiler'))->conditions;
+
+ foreach (array_keys($conditions) as $conditionalDirective) {
+ $directives['@'.$conditionalDirective] = '@end'.$conditionalDirective;
+ }
+
+ $entire = static::compileDirectives($entire, $directives);
+
+ return $entire;
+ });
+ }
+
+ /*
+ * This method is a modified version of the Blade compiler's `compileStatements` method.
+ * It finds all directives in the template, gets the expression if it has parentheses
+ * and prefixes the opening directives and suffixes the closing directives.
+ */
+ public static function compileDirectives($template, $directives)
+ {
+ $openings = array_keys($directives);
+ $closings = array_values($directives);
+
+ $openingDirectivesPattern = static::directivesPattern($openings);
+ $closingDirectivesPattern = static::directivesPattern($closings);
+ // This is for an `@empty` inside a `@forelse` loop, not `@empty()` conditional directive...
+ $loopEmptyDirectivePattern = '/@empty(?!\s*\()/mUxi';
+
+ // First, let's match ALL blade directives on the page, not just conditionals...
+ preg_match_all(
+ '/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( [\S\s]*? ) \))?/x',
+ $template,
+ $matches,
+ PREG_OFFSET_CAPTURE
+ );
+
+ if (empty($matches[0])) {
+ return $template;
+ }
+
+ // Find all tags which shouldn't have directives processed inside them...
+ $ignoredTags = ['script', 'style'];
+ $excludedRanges = static::findIgnoredTagRanges($template, $ignoredTags);
+
+ for ($i = count($matches[0]) - 1; $i >= 0; $i--) {
+ $match = [
+ $matches[0][$i][0],
+ $matches[1][$i][0],
+ $matches[2][$i][0],
+ $matches[3][$i][0] ?: null,
+ $matches[4][$i][0] ?: null,
+ ];
+
+ $matchPosition = $matches[0][$i][1];
+
+ // If the blade directive is escaped with an extra `@` then we don't want to process it...
+ if (str($match[1])->startsWith('@')) {
+ continue;
+ }
+
+ // Skip directives inside ignored tags...
+ if (static::isInExcludedRange($matchPosition, $excludedRanges)) {
+ continue;
+ }
+
+ // Here we check to see if we have properly found the closing parenthesis by
+ // regex pattern or not, and will recursively continue on to the next ")"
+ // then check again until the tokenizer confirms we find the right one.
+ while (
+ isset($match[4])
+ && str($match[0])->endsWith(')')
+ && ! static::hasEvenNumberOfParentheses($match[0])
+ ) {
+ // Use position-based approach to find the text after the current match,
+ // rather than searching for the match string (which could find an earlier
+ // occurrence if the same pattern appears multiple times in the template)...
+ $afterPosition = $matchPosition + strlen($match[0]);
+
+ if ($afterPosition >= strlen($template)) {
+ break;
+ }
+
+ $after = substr($template, $afterPosition);
+
+ $rest = str($after)->before(')');
+
+ if (
+ isset($matches[0][$i - 1])
+ && str($rest.')')->contains($matches[0][$i - 1][0])
+ ) {
+ unset($matches[0][$i - 1]);
+ $i--;
+ }
+
+ $match[0] = $match[0].$rest.')';
+ $match[3] = $match[3].$rest.')';
+ $match[4] = $match[4].$rest;
+ }
+
+ // Now we can check to see if the current Blade directive is a conditional,
+ // and if so, prefix/suffix it with HTML comment morph markers...
+ if (preg_match($openingDirectivesPattern, $match[0])) {
+ $template = static::prefixOpeningDirective($match[0], $template, $matchPosition);
+ } elseif (preg_match($closingDirectivesPattern, $match[0])) {
+ $template = static::suffixClosingDirective($match[0], $template, $matchPosition);
+ } elseif (preg_match($loopEmptyDirectivePattern, $match[0])) {
+ $template = static::suffixLoopEmptyDirective($match[0], $template, $matchPosition);
+ }
+ }
+
+ return $template;
+ }
+
+ protected static function prefixOpeningDirective($found, $template, $position)
+ {
+ $foundEscaped = preg_quote($found, '/');
+
+ $livewireCheckOpeningTag = '';
+
+ $livewireCheckClosingTag = '';
+
+ $prefix = '';
+
+ $suffix = '';
+
+ if (static::$shouldInjectConditionalMarkers) {
+ $prefix = '';
+ }
+
+ if (static::$shouldInjectLoopMarkers && static::isLoop($found)) {
+ $prefix .= '';
+
+ $suffix .= 'index); ?>';
+ }
+
+ if ($prefix === '' && $suffix === '') {
+ return $template;
+ }
+
+ if ($prefix !== '') {
+ $prefix = $livewireCheckOpeningTag.$prefix.$livewireCheckClosingTag;
+ }
+
+ if ($suffix !== '') {
+ $suffix = $livewireCheckOpeningTag.$suffix.$livewireCheckClosingTag;
+ }
+
+ $prefixEscaped = preg_quote($prefix);
+
+ $suffixEscaped = preg_quote($suffix);
+
+ $pattern = "/(?';
+
+ $livewireCheckClosingTag = '';
+
+ $prefix = '';
+
+ $suffix = '';
+
+ if (static::$shouldInjectConditionalMarkers) {
+ $suffix = '';
+ }
+
+ if (static::$shouldInjectLoopMarkers && static::isEndLoop($found)) {
+ $prefix .= '';
+
+ $suffix .= '';
+ }
+
+ if ($prefix === '' && $suffix === '') {
+ return $template;
+ }
+
+ if ($prefix !== '') {
+ $prefix = $livewireCheckOpeningTag.$prefix.$livewireCheckClosingTag;
+ }
+
+ if ($suffix !== '') {
+ $suffix = $livewireCheckOpeningTag.$suffix.$livewireCheckClosingTag;
+ }
+
+ $prefixEscaped = preg_quote($prefix);
+
+ $suffixEscaped = preg_quote($suffix);
+
+ $pattern = "/";
+
+ // If the prefix is not empty, then add it to the pattern...
+ if ($prefixEscaped !== '') {
+ $pattern .= "(?';
+
+ $livewireCheckClosingTag = '';
+
+ $prefix = '';
+
+ $suffix = '';
+
+ if (static::$shouldInjectLoopMarkers) {
+ $prefix = '';
+
+ $suffix = '';
+ }
+
+ if ($prefix === '' && $suffix === '') {
+ return $template;
+ }
+
+ if ($prefix !== '') {
+ $prefix = $livewireCheckOpeningTag.$prefix.$livewireCheckClosingTag;
+ }
+
+ if ($suffix !== '') {
+ $suffix = $livewireCheckOpeningTag.$suffix.$livewireCheckClosingTag;
+ }
+
+ $prefixEscaped = preg_quote($prefix);
+
+ $suffixEscaped = preg_quote($suffix);
+
+ $pattern = "/(?sortBy(fn ($directive) => strlen($directive), descending: true)
+ // Only match directives that are an exact match and not ones that
+ // simply start with the provided directive here...
+ ->map(fn ($directive) => $directive.'(?![a-zA-Z])')
+ // @empty is a special case in that it can be used as a standalone directive
+ // and also within a @forelese statement. We only want to target when it's standalone
+ // by enforcing @empty has an opening parenthesis after it when matching...
+ ->map(fn ($directive) => str($directive)->startsWith('@empty') ? $directive.'[^\S\r\n]*\(' : $directive)
+ ->join('|')
+ .')';
+
+ // Blade directives: (@if|@foreach|...)
+ $pattern = '/'.$directivesPattern.'/mUxi';
+
+ return $pattern;
+ }
+
+ protected static function hasEvenNumberOfParentheses(string $expression)
+ {
+ $tokens = token_get_all('`...
+ preg_match_all(
+ '/<('.$tagsPattern.')(?:\s[^>]*)?>|<\/('.$tagsPattern.')>/i',
+ $template,
+ $tagMatches,
+ PREG_OFFSET_CAPTURE
+ );
+
+ $stack = [];
+
+ foreach ($tagMatches[0] as $tagMatch) {
+ $tag = $tagMatch[0];
+ $position = $tagMatch[1];
+
+ // Check if it's an opening tag...
+ if (preg_match('/<('.$tagsPattern.')/i', $tag, $typeMatch)) {
+ $type = strtolower($typeMatch[1]);
+ $stack[] = ['type' => $type, 'start' => $position];
+ }
+ // Check if it's a closing tag...
+ elseif (preg_match('/<\/('.$tagsPattern.')>/i', $tag, $typeMatch)) {
+ $type = strtolower($typeMatch[1]);
+
+ // Find the matching opening tag...
+ for ($i = count($stack) - 1; $i >= 0; $i--) {
+ if ($stack[$i]['type'] === $type) {
+ $start = $stack[$i]['start'];
+ $end = $position + strlen($tag);
+ $ranges[] = [$start, $end];
+ array_splice($stack, $i, 1);
+ break;
+ }
+ }
+ }
+ }
+
+ return $ranges;
+ }
+
+ protected static function isInExcludedRange(int $position, array $ranges): bool
+ {
+ foreach ($ranges as [$start, $end]) {
+ if ($position >= $start && $position < $end) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected static function replaceMatchIfNotInsideAHtmlTag(string $template, int $position, string $pattern, string $found, string $prefix, string $suffix): string
+ {
+ // Clamp the match position to the template bounds...
+ $templateLength = strlen($template);
+ $position = max(0, min($templateLength, (int) $position));
+
+ // Check if we're inside an HTML tag by looking backwards...
+ if (static::isInsideHtmlTag($template, $position)) {
+ return $template;
+ }
+
+ $before = substr($template, 0, $position);
+ $after = substr($template, $position);
+
+ if (! preg_match($pattern, $after, $afterMatch, PREG_OFFSET_CAPTURE) || $afterMatch[0][1] !== 0) {
+ return $template;
+ }
+
+ // Remove the match from the beginning of the after string...
+ $after = substr($after, strlen($afterMatch[0][0]));
+
+ return $before.$prefix.$found.$suffix.$after;
+ }
+
+ /**
+ * Check if the given position in the template is inside an unclosed HTML tag.
+ *
+ * This looks backwards from the position to find the most recent '<' that could
+ * start an HTML tag, then checks if there's a closing '>' between that '<' and
+ * the position. It correctly handles '>' characters inside quoted strings and
+ * Blade expressions (parentheses/braces).
+ */
+ protected static function isInsideHtmlTag(string $template, int $position): bool
+ {
+ $before = substr($template, 0, $position);
+
+ // Search backwards through '<' occurrences to find an HTML tag opener...
+ $searchFrom = strlen($before);
+
+ while ($searchFrom > 0) {
+ // Find last '<' before $searchFrom...
+ $bracketPos = strrpos(substr($before, 0, $searchFrom), '<');
+
+ if ($bracketPos === false) {
+ return false;
+ }
+
+ $segment = substr($before, $bracketPos);
+
+ // Skip PHP tags (' that closes an HTML tag.
+ *
+ * This ignores '>' characters that appear inside:
+ * - Single or double quoted strings (attribute values)
+ * - Parentheses (Blade directive expressions like @if($x > 0))
+ * - Braces (Blade echo like {{ $x > 0 }})
+ */
+ protected static function hasClosingBracketInSegment(string $segment): bool
+ {
+ $length = strlen($segment);
+ $inSingleQuote = false;
+ $inDoubleQuote = false;
+ $parenDepth = 0;
+ $braceDepth = 0;
+
+ for ($i = 1; $i < $length; $i++) {
+ $char = $segment[$i];
+ $prevChar = $segment[$i - 1];
+
+ // Track quote state (but not if escaped)...
+ if (! $inDoubleQuote && $char === "'" && $prevChar !== '\\') {
+ $inSingleQuote = ! $inSingleQuote;
+ } elseif (! $inSingleQuote && $char === '"' && $prevChar !== '\\') {
+ $inDoubleQuote = ! $inDoubleQuote;
+ } elseif (! $inSingleQuote && ! $inDoubleQuote) {
+ // Track nesting depth when not inside quotes...
+ if ($char === '(') {
+ $parenDepth++;
+ } elseif ($char === ')') {
+ $parenDepth = max(0, $parenDepth - 1);
+ } elseif ($char === '{') {
+ $braceDepth++;
+ } elseif ($char === '}') {
+ $braceDepth = max(0, $braceDepth - 1);
+ } elseif ($char === '>' && $parenDepth === 0 && $braceDepth === 0) {
+ // Found a top-level '>' that closes the tag...
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportMultipleRootElementDetection/MultipleRootElementsDetectedException.php b/portman/lib/Livewire/Features/SupportMultipleRootElementDetection/MultipleRootElementsDetectedException.php
new file mode 100644
index 00000000..e192a10a
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportMultipleRootElementDetection/MultipleRootElementsDetectedException.php
@@ -0,0 +1,15 @@
+getName() . ']');
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportMultipleRootElementDetection/SupportMultipleRootElementDetection.php b/portman/lib/Livewire/Features/SupportMultipleRootElementDetection/SupportMultipleRootElementDetection.php
new file mode 100644
index 00000000..c669392b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportMultipleRootElementDetection/SupportMultipleRootElementDetection.php
@@ -0,0 +1,51 @@
+warnAgainstMoreThanOneRootElement($component, $html);
+
+ };
+ });
+ }
+
+ function warnAgainstMoreThanOneRootElement($component, $html)
+ {
+ $count = $this->getRootElementCount($html);
+
+ if ($count > 1) {
+ throw new MultipleRootElementsDetectedException($component);
+ }
+ }
+
+ function getRootElementCount($html)
+ {
+ $dom = new \DOMDocument();
+
+ @$dom->loadHTML($html);
+
+ $body = $dom->getElementsByTagName('body')->item(0);
+
+ $count = 0;
+
+ foreach ($body->childNodes as $child) {
+ if ($child->nodeType == XML_ELEMENT_NODE) {
+ if ($child->tagName === 'script') continue;
+
+ $count++;
+ }
+ }
+
+ return $count;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportNavigate/SupportNavigate.php b/portman/lib/Livewire/Features/SupportNavigate/SupportNavigate.php
new file mode 100644
index 00000000..846051d0
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/SupportNavigate.php
@@ -0,0 +1,33 @@
+forceAssetInjection(); ?>';
+ });
+
+ Blade::directive('endpersist', function ($expression) {
+ return '
';
+ });
+
+ app('livewire')->useScriptTagAttributes([
+ 'data-navigate-once' => true,
+ ]);
+
+ Vite::useScriptTagAttributes([
+ 'data-navigate-track' => 'reload',
+ ]);
+
+ Vite::useStyleTagAttributes([
+ 'data-navigate-track' => 'reload',
+ ]);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/changed-layout.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/changed-layout.blade.php
new file mode 100644
index 00000000..2e11daeb
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/changed-layout.blade.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/changed-tracked-layout.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/changed-tracked-layout.blade.php
new file mode 100644
index 00000000..ca54436b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/changed-tracked-layout.blade.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/html-attributes1.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/html-attributes1.blade.php
new file mode 100644
index 00000000..42d3999d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/html-attributes1.blade.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/html-attributes2.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/html-attributes2.blade.php
new file mode 100644
index 00000000..9ed3a8a8
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/html-attributes2.blade.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/layout-with-navigate-outside.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/layout-with-navigate-outside.blade.php
new file mode 100644
index 00000000..c326730c
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/layout-with-navigate-outside.blade.php
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Go to second page (outside)
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/layout-with-noscript.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/layout-with-noscript.blade.php
new file mode 100644
index 00000000..8903ecde
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/layout-with-noscript.blade.php
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/layout.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/layout.blade.php
new file mode 100644
index 00000000..e496e85d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/layout.blade.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/navbar-sidebar.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/navbar-sidebar.blade.php
new file mode 100644
index 00000000..07c1ded7
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/navbar-sidebar.blade.php
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ Document
+
+
+
+
+
+ @persist('nav')
+
+ @endpersist
+
+
+
+ {{ $slot }}
+
+
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/test-navigate-asset.js b/portman/lib/Livewire/Features/SupportNavigate/test-views/test-navigate-asset.js
new file mode 100644
index 00000000..a9c56bdf
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/test-navigate-asset.js
@@ -0,0 +1,8 @@
+
+if (! window._lw_dusk_asset_count) {
+ window._lw_dusk_asset_count = 1
+} else {
+ window._lw_dusk_asset_count++
+}
+
+
diff --git a/portman/lib/Livewire/Features/SupportNavigate/test-views/tracked-layout.blade.php b/portman/lib/Livewire/Features/SupportNavigate/test-views/tracked-layout.blade.php
new file mode 100644
index 00000000..420f6c6e
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNavigate/test-views/tracked-layout.blade.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ {{ $slot }}
+
+ @stack('scripts')
+
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportNestedComponentListeners/SupportNestedComponentListeners.php b/portman/lib/Livewire/Features/SupportNestedComponentListeners/SupportNestedComponentListeners.php
new file mode 100644
index 00000000..0bea1638
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNestedComponentListeners/SupportNestedComponentListeners.php
@@ -0,0 +1,62 @@
+ $value) {
+ if (str($key)->startsWith('@')) {
+ // any kebab-cased parameters passed in will have been converted to camelCase
+ // so we need to convert back to kebab to ensure events are valid in html
+ $fullEvent = str($key)->after('@')->kebab();
+ $attributeKey = 'x-on:'.$fullEvent;
+ $attributeValue = "\$wire.\$parent.".$value;
+
+ store($this->component)->push('attributes', $attributeValue, $attributeKey);
+ }
+ }
+ }
+
+ public function render($view, $data)
+ {
+ return function ($html, $replaceHtml) {
+ $attributes = store($this->component)->get('attributes', false);
+
+ if (! $attributes) return;
+
+ $replaceHtml(Utils::insertAttributesIntoHtmlRoot($html, $attributes));
+ };
+ }
+
+ public function dehydrate($context)
+ {
+ $attributes = store($this->component)->get('attributes', false);
+
+ if (! $attributes) return;
+
+ $attributes && $context->addMemo('attributes', $attributes);
+ }
+
+ public function hydrate($memo)
+ {
+ if (! isset($memo['attributes'])) return;
+
+ $attributes = $memo['attributes'];
+
+ // Store the attributes for later dehydration...
+ store($this->component)->set('attributes', $attributes);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportNestingComponents/SupportNestingComponents.php b/portman/lib/Livewire/Features/SupportNestingComponents/SupportNestingComponents.php
new file mode 100644
index 00000000..54815299
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportNestingComponents/SupportNestingComponents.php
@@ -0,0 +1,120 @@
+{$tag}>";
+
+ static::setParentChild($parent, $key, $tag, $childId);
+
+ $hijack($finish($html));
+ }
+ });
+
+ on('mount', function ($component, $params, $key, $parent) {
+ $start = null;
+ if ($parent && config('app.debug')) $start = microtime(true);
+
+ static::setParametersToMatchingProperties($component, $params);
+
+ return function ($html) use ($component, $key, $parent, $start) {
+ if ($parent) {
+ if (config('app.debug')) trigger('profile', 'child:'.$component->getId(), $parent->getId(), [$start, microtime(true)]);
+
+ preg_match('/<([a-zA-Z0-9\-]*)/', $html, $matches, PREG_OFFSET_CAPTURE);
+ $tag = $matches[1][0];
+ static::setParentChild($parent, $key, $tag, $component->getId());
+ }
+ };
+ });
+ }
+
+ function hydrate($memo)
+ {
+ $children = $memo['children'];
+
+ static::setPreviouslyRenderedChildren($this->component, $children);
+
+ $this->ifThisComponentIsAChildThatHasBeenRemovedByTheParent(function () {
+ // Let's skip its render so that we aren't wasting extra rendering time
+ // on a component that has already been thrown-away by its parent...
+ $this->component->skipRender();
+ });
+ }
+
+ function dehydrate($context)
+ {
+ $skipRender = $this->storeGet('skipRender');
+
+ if ($skipRender) $this->keepRenderedChildren();
+
+ $this->storeRemovedChildrenToReferenceWhenThoseChildrenHydrateSoWeCanSkipTheirRenderAndAvoideUselessWork();
+
+ $context->addMemo('children', $this->getChildren());
+ }
+
+ function getChildren() { return $this->storeGet('children', []); }
+ function setChild($key, $tag, $id) { $this->storePush('children', [$tag, $id], $key); }
+
+ static function setParentChild($parent, $key, $tag, $id) { store($parent)->push('children', [$tag, $id], $key); }
+ static function setPreviouslyRenderedChildren($component, $children) { store($component)->set('previousChildren', $children); }
+ static function hasPreviouslyRenderedChild($parent, $key) {
+ return array_key_exists($key, store($parent)->get('previousChildren', []));
+ }
+
+ static function getPreviouslyRenderedChild($parent, $key)
+ {
+ return store($parent)->get('previousChildren')[$key];
+ }
+
+ function keepRenderedChildren()
+ {
+ $this->storeSet('children', $this->storeGet('previousChildren'));
+ }
+
+ static function setParametersToMatchingProperties($component, $params)
+ {
+ // Assign all public component properties that have matching parameters.
+ collect(array_intersect_key($params, Utils::getPublicPropertiesDefinedOnSubclass($component)))
+ ->each(function ($value, $property) use ($component) {
+ $component->{$property} = $value;
+ });
+ }
+
+ protected function storeRemovedChildrenToReferenceWhenThoseChildrenHydrateSoWeCanSkipTheirRenderAndAvoideUselessWork()
+ {
+ // Get a list of children that we're "removed" in this request...
+ $removedChildren = array_diff_key($this->storeGet('previousChildren', []), $this->getChildren());
+
+ foreach ($removedChildren as $key => $child) {
+ store()->push('removedChildren', $key, $child[1]);
+ }
+ }
+
+ protected function ifThisComponentIsAChildThatHasBeenRemovedByTheParent($callback)
+ {
+ $removedChildren = store()->get('removedChildren', []);
+
+ if (isset($removedChildren[$this->component->getId()])) {
+ $callback();
+
+ store()->unset('removedChildren', $this->component->getId());
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportPageComponents/BaseLayout.php b/portman/lib/Livewire/Features/SupportPageComponents/BaseLayout.php
new file mode 100644
index 00000000..8de9e7a6
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPageComponents/BaseLayout.php
@@ -0,0 +1,14 @@
+mount($this::class, $params);
+ });
+
+ $layoutConfig = $layoutConfig ?: new PageComponentConfig;
+
+ $layoutConfig->normalizeViewNameAndParamsForBladeComponents();
+
+ $response = response(SupportPageComponents::renderContentsIntoLayout($html, $layoutConfig));
+
+ if (is_callable($layoutConfig->response)) {
+ call_user_func($layoutConfig->response, $response);
+ }
+
+ return $response;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportPageComponents/MissingLayoutException.php b/portman/lib/Livewire/Features/SupportPageComponents/MissingLayoutException.php
new file mode 100644
index 00000000..317ab7ab
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPageComponents/MissingLayoutException.php
@@ -0,0 +1,13 @@
+view = $view ?: config('livewire.layout');
+ $this->viewContext = new ViewContext;
+ }
+
+ function mergeParams($toMerge)
+ {
+ $this->params = array_merge($toMerge, $this->params);
+ }
+
+ function normalizeViewNameAndParamsForBladeComponents()
+ {
+ // If a user passes the class name of a Blade component to the
+ // layout macro (or uses inside their config), we need to
+ // convert it to it's "view" name so Blade doesn't break.
+ $view = $this->view;
+ $params = $this->params;
+
+ $attributes = $params['attributes'] ?? [];
+ unset($params['attributes']);
+
+ if (is_subclass_of($view, \Illuminate\View\Component::class)) {
+ $layout = app()->makeWith($view, $params);
+ $view = $layout->resolveView()->name();
+ $params = array_merge($params, $layout->resolveView()->getData());
+ } else {
+ $layout = new AnonymousComponent($view, $params);
+ }
+
+ $layout->withAttributes($attributes);
+
+ $params = array_merge($params, $layout->data());
+
+ $this->view = $view;
+ $this->params = $params;
+
+ // Remove default slot if present...
+ if (isset($this->slots['default'])) unset($this->slots['default']);
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportPageComponents/SupportPageComponents.php b/portman/lib/Livewire/Features/SupportPageComponents/SupportPageComponents.php
new file mode 100644
index 00000000..3dc58276
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPageComponents/SupportPageComponents.php
@@ -0,0 +1,246 @@
+layoutConfig)) $this->layoutConfig = new PageComponentConfig;
+
+ $this->layoutConfig->mergeParams($data);
+
+ return $this;
+ });
+
+ View::macro('section', function ($section) {
+ if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;
+
+ $this->layoutConfig->slotOrSection = $section;
+
+ return $this;
+ });
+
+ View::macro('title', function ($title) {
+ if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;
+
+ $this->layoutConfig->mergeParams(['title' => $title]);
+
+ return $this;
+ });
+
+ View::macro('slot', function ($slot) {
+ if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;
+
+ $this->layoutConfig->slotOrSection = $slot;
+
+ return $this;
+ });
+
+ View::macro('extends', function ($view, $params = []) {
+ if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;
+
+ $this->layoutConfig->type = 'extends';
+ $this->layoutConfig->slotOrSection = 'content';
+ $this->layoutConfig->view = $view;
+ $this->layoutConfig->mergeParams($params);
+
+ return $this;
+ });
+
+ View::macro('layout', function ($view, $params = []) {
+ if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;
+
+ $this->layoutConfig->type = 'component';
+ $this->layoutConfig->slotOrSection = 'slot';
+ $this->layoutConfig->view = $view;
+ $this->layoutConfig->mergeParams($params);
+
+ return $this;
+ });
+
+ View::macro('response', function (callable $callback) {
+ if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;
+
+ $this->layoutConfig->response = $callback;
+
+ return $this;
+ });
+ }
+
+ static function interceptTheRenderOfTheComponentAndRetreiveTheLayoutConfiguration($callback)
+ {
+ $layoutConfig = null;
+ $slots = [];
+
+ // Only run this handler once for the parent-most component. Otherwise child components
+ // will run this handler too and override the configured layout...
+ $handler = once(function ($target, $view, $data) use (&$layoutConfig, &$slots) {
+ $layoutAttr = $target->getAttributes()->whereInstanceOf(BaseLayout::class)->first();
+ $titleAttr = $target->getAttributes()->whereInstanceOf(BaseTitle::class)->first();
+
+ if ($layoutAttr) {
+ $view->layout($layoutAttr->name, $layoutAttr->params);
+ }
+
+ if ($titleAttr) {
+ $view->title($titleAttr->content);
+ }
+
+ $layoutConfig = $view->layoutConfig ?? new PageComponentConfig;
+
+ return function ($html, $replace, $viewContext) use ($view, $layoutConfig) {
+ // Gather up any slots and sections declared in the component template and store them
+ // to be later forwarded into the layout component itself...
+ $layoutConfig->viewContext = $viewContext;
+ };
+ });
+
+ on('render', $handler);
+ on('render.placeholder', $handler);
+
+ $callback();
+
+ off('render', $handler);
+ off('render.placeholder', $handler);
+
+ return $layoutConfig;
+ }
+
+ static function gatherMountMethodParamsFromRouteParameters($component)
+ {
+ // This allows for route parameters like "slug" in /post/{slug},
+ // to be passed into a Livewire component's mount method...
+ $route = request()->route();
+
+ if (! $route) return [];
+
+ try {
+ $params = (new ImplicitRouteBinding(app()))
+ ->resolveAllParameters($route, new $component);
+ } catch (ModelNotFoundException $exception) {
+ if (method_exists($route,'getMissing') && $route->getMissing()) {
+ abort(
+ $route->getMissing()(request())
+ );
+ }
+
+ throw $exception;
+ }
+
+ return $params;
+ }
+
+ static function renderContentsIntoLayout($content, $layoutConfig)
+ {
+ try {
+ if ($layoutConfig->type === 'component') {
+ return Blade::render(<<<'HTML'
+ viewContext->mergeIntoNewEnvironment($__env); ?>
+
+ @component($layout->view, $layout->params)
+ @slot($layout->slotOrSection)
+ {!! $content !!}
+ @endslot
+
+ viewContext->slots[-1] ?? [] as $name => $slot) {
+ $__env->slot($name, attributes: $slot->attributes->getAttributes());
+ echo $slot->toHtml();
+ $__env->endSlot();
+ }
+ ?>
+ @endcomponent
+ HTML, [
+ 'content' => $content,
+ 'layout' => $layoutConfig,
+ ]);
+ } else {
+ return Blade::render(<<<'HTML'
+ viewContext->mergeIntoNewEnvironment($__env); ?>
+
+ @extends($layout->view, $layout->params)
+
+ @section($layout->slotOrSection)
+ {!! $content !!}
+ @endsection
+ HTML, [
+ 'content' => $content,
+ 'layout' => $layoutConfig,
+ ]);
+ }
+ } catch (\Illuminate\View\ViewException $e) {
+ $layout = $layoutConfig->view;
+
+ if (str($e->getMessage())->startsWith('View ['.$layout.'] not found.')) {
+ throw new MissingLayoutException($layout);
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ // This needs to exist so that authorization middleware (and other middleware) have access
+ // to implicit route bindings based on the Livewire page component. Otherwise, Laravel
+ // has no implicit binding references because the action is __invoke with no params
+ protected static function resolvePageComponentRouteBindings()
+ {
+ // This method was introduced into Laravel 10.37.1 for this exact purpose...
+ if (static::canSubstituteImplicitBindings()) {
+ app('router')->substituteImplicitBindingsUsing(function ($container, $route, $default) {
+ // If the current route is a Livewire page component...
+ if ($componentClass = static::routeActionIsAPageComponent($route)) {
+ // Resolve and set all page component parameters to the current route...
+ (new \Livewire\Drawer\ImplicitRouteBinding($container))
+ ->resolveAllParameters($route, new $componentClass);
+ } else {
+ // Otherwise, run the default Laravel implicit binding system...
+ $default();
+ }
+ });
+ }
+ }
+
+ public static function canSubstituteImplicitBindings()
+ {
+ return method_exists(app('router'), 'substituteImplicitBindingsUsing');
+ }
+
+ protected static function routeActionIsAPageComponent($route)
+ {
+ $action = $route->action;
+
+ if (! $action) return false;
+
+ $uses = $action['uses'] ?? false;
+
+ if (! $uses) return;
+
+ if (is_string($uses)) {
+ $class = str($uses)->before('@')->toString();
+ $method = str($uses)->after('@')->toString();
+
+ if (is_subclass_of($class, \Livewire\Component::class) && $method === '__invoke') {
+ return $class;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportPagination/HandlesPagination.php b/portman/lib/Livewire/Features/SupportPagination/HandlesPagination.php
new file mode 100644
index 00000000..1bc13081
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPagination/HandlesPagination.php
@@ -0,0 +1,74 @@
+paginators)->mapWithKeys(function ($page, $pageName) {
+ return ['paginators.'.$pageName => ['history' => true, 'as' => $pageName, 'keep' => false]];
+ })->toArray();
+ }
+
+ public function getPage($pageName = 'page')
+ {
+ return $this->paginators[$pageName] ?? Paginator::resolveCurrentPage($pageName);
+ }
+
+ public function previousPage($pageName = 'page')
+ {
+ $this->setPage(max(($this->paginators[$pageName] ?? 1) - 1, 1), $pageName);
+ }
+
+ public function nextPage($pageName = 'page')
+ {
+ $this->setPage(($this->paginators[$pageName] ?? 1) + 1, $pageName);
+ }
+
+ public function gotoPage($page, $pageName = 'page')
+ {
+ $this->setPage($page, $pageName);
+ }
+
+ public function resetPage($pageName = 'page')
+ {
+ $this->setPage(1, $pageName);
+ }
+
+ public function setPage($page, $pageName = 'page')
+ {
+ if (is_numeric($page)) {
+ $page = (int) ($page <= 0 ? 1 : $page);
+ }
+
+ $beforePaginatorMethod = 'updatingPaginators';
+ $afterPaginatorMethod = 'updatedPaginators';
+
+ $beforeMethod = 'updating' . ucfirst(Str::camel($pageName));
+ $afterMethod = 'updated' . ucfirst(Str::camel($pageName));
+
+ if (method_exists($this, $beforePaginatorMethod)) {
+ $this->{$beforePaginatorMethod}($page, $pageName);
+ }
+
+ if (method_exists($this, $beforeMethod)) {
+ $this->{$beforeMethod}($page, null);
+ }
+
+ $this->paginators[$pageName] = $page;
+
+ if (method_exists($this, $afterPaginatorMethod)) {
+ $this->{$afterPaginatorMethod}($page, $pageName);
+ }
+
+ if (method_exists($this, $afterMethod)) {
+ $this->{$afterMethod}($page, null);
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportPagination/PaginationUrl.php b/portman/lib/Livewire/Features/SupportPagination/PaginationUrl.php
new file mode 100644
index 00000000..2845b671
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPagination/PaginationUrl.php
@@ -0,0 +1,17 @@
+pushQueryStringEffect($context);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportPagination/SupportPagination.php b/portman/lib/Livewire/Features/SupportPagination/SupportPagination.php
new file mode 100644
index 00000000..01eccd91
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPagination/SupportPagination.php
@@ -0,0 +1,161 @@
+provide(function () {
+ $this->loadViewsFrom(__DIR__.'/views', 'livewire');
+
+ $paths = [__DIR__.'/views' => resource_path('views/vendor/livewire')];
+
+ $this->publishes($paths, 'livewire');
+ $this->publishes($paths, 'livewire:pagination');
+ });
+ }
+
+ protected $restoreOverriddenPaginationViews;
+
+ function skip()
+ {
+ return ! in_array(WithPagination::class, class_uses_recursive($this->component));
+ }
+
+ function boot()
+ {
+ $this->setPathResolvers();
+
+ $this->setPageResolvers();
+
+ $this->overrideDefaultPaginationViews();
+ }
+
+ function destroy()
+ {
+ ($this->restoreOverriddenPaginationViews)();
+ }
+
+ function overrideDefaultPaginationViews()
+ {
+ $oldDefaultView = Paginator::$defaultView;
+ $oldDefaultSimpleView = Paginator::$defaultSimpleView;
+
+ $this->restoreOverriddenPaginationViews = function () use ($oldDefaultView, $oldDefaultSimpleView) {
+ Paginator::defaultView($oldDefaultView);
+ Paginator::defaultSimpleView($oldDefaultSimpleView);
+ };
+
+ Paginator::defaultView($this->paginationView());
+ Paginator::defaultSimpleView($this->paginationSimpleView());
+ }
+
+ protected function setPathResolvers()
+ {
+ // Setting the path resolver here on the default paginator also works for the cursor paginator...
+ Paginator::currentPathResolver(function () {
+ return Livewire::originalPath();
+ });
+ }
+
+ protected function setPageResolvers()
+ {
+ CursorPaginator::currentCursorResolver(function ($pageName) {
+ $this->ensurePaginatorIsInitialized($pageName, defaultPage: '');
+
+ return Cursor::fromEncoded($this->component->paginators[$pageName]);
+ });
+
+ Paginator::currentPageResolver(function ($pageName) {
+ $this->ensurePaginatorIsInitialized($pageName);
+
+ return (int) $this->component->paginators[$pageName];
+ });
+ }
+
+ protected function ensurePaginatorIsInitialized($pageName, $defaultPage = 1)
+ {
+ if (isset($this->component->paginators[$pageName])) return;
+
+ $queryStringDetails = $this->getQueryStringDetails($pageName);
+
+ $this->component->paginators[$pageName] = $this->resolvePage($queryStringDetails['as'], $defaultPage);
+
+ $shouldSkipUrlTracking = in_array(
+ WithoutUrlPagination::class, class_uses_recursive($this->component)
+ );
+
+ if ($shouldSkipUrlTracking) return;
+
+ $this->addUrlHook($pageName, $queryStringDetails);
+ }
+
+ protected function getQueryStringDetails($pageName)
+ {
+ $pageNameQueryString = data_get($this->getQueryString(), 'paginators.' . $pageName);
+
+ $pageNameQueryString['as'] ??= $pageName;
+ $pageNameQueryString['history'] ??= true;
+ $pageNameQueryString['keep'] ??= false;
+
+ return $pageNameQueryString;
+ }
+
+ protected function resolvePage($alias, $default)
+ {
+ return request()->query($alias, $default);
+ }
+
+ protected function addUrlHook($pageName, $queryStringDetails)
+ {
+ $key = 'paginators.' . $pageName;
+ $alias = $queryStringDetails['as'];
+ $history = $queryStringDetails['history'];
+ $keep = $queryStringDetails['keep'];
+
+ $attribute = new PaginationUrl(as: $alias, history: $history, keep: $keep);
+
+ $this->component->setPropertyAttribute($key, $attribute);
+
+ // We need to manually call this in case it's a Lazy component,
+ // in which case the `mount()` lifecycle hook isn't called.
+ // This means it can be called twice, but that's fine...
+ $attribute->setPropertyFromQueryString();
+ }
+
+ protected function paginationView()
+ {
+ if (method_exists($this->component, 'paginationView')) {
+ return $this->component->paginationView();
+ }
+
+ return 'livewire::' . (property_exists($this->component, 'paginationTheme') ? invade($this->component)->paginationTheme : config('livewire.pagination_theme', 'tailwind'));
+ }
+
+ protected function paginationSimpleView()
+ {
+ if (method_exists($this->component, 'paginationSimpleView')) {
+ return $this->component->paginationSimpleView();
+ }
+
+ return 'livewire::simple-' . (property_exists($this->component, 'paginationTheme') ? invade($this->component)->paginationTheme : config('livewire.pagination_theme', 'tailwind'));
+ }
+
+ protected function getQueryString()
+ {
+ $supportQueryStringHook = ComponentHookRegistry::getHook($this->component, SupportQueryString::class);
+
+ return $supportQueryStringHook->getQueryString();
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportPagination/WithoutUrlPagination.php b/portman/lib/Livewire/Features/SupportPagination/WithoutUrlPagination.php
new file mode 100644
index 00000000..db54d68e
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPagination/WithoutUrlPagination.php
@@ -0,0 +1,8 @@
+
+ @if ($paginator->hasPages())
+
+
+
+
+
+
+
+
+ {!! __('Showing') !!}
+ {{ $paginator->firstItem() }}
+ {!! __('to') !!}
+ {{ $paginator->lastItem() }}
+ {!! __('of') !!}
+ {{ $paginator->total() }}
+ {!! __('results') !!}
+
+
+
+
+
+
+
+
+ @endif
+
diff --git a/portman/lib/Livewire/Features/SupportPagination/views/simple-bootstrap.blade.php b/portman/lib/Livewire/Features/SupportPagination/views/simple-bootstrap.blade.php
new file mode 100644
index 00000000..44bdce01
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPagination/views/simple-bootstrap.blade.php
@@ -0,0 +1,53 @@
+@php
+if (! isset($scrollTo)) {
+ $scrollTo = 'body';
+}
+
+$scrollIntoViewJsSnippet = ($scrollTo !== false)
+ ? <<
+ @if ($paginator->hasPages())
+
+
+
+ @endif
+
diff --git a/portman/lib/Livewire/Features/SupportPagination/views/simple-tailwind.blade.php b/portman/lib/Livewire/Features/SupportPagination/views/simple-tailwind.blade.php
new file mode 100644
index 00000000..d7f2560d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPagination/views/simple-tailwind.blade.php
@@ -0,0 +1,56 @@
+@php
+if (! isset($scrollTo)) {
+ $scrollTo = 'body';
+}
+
+$scrollIntoViewJsSnippet = ($scrollTo !== false)
+ ? <<
+ @if ($paginator->hasPages())
+
+
+ {{-- Previous Page Link --}}
+ @if ($paginator->onFirstPage())
+
+ {!! __('pagination.previous') !!}
+
+ @else
+ @if(method_exists($paginator,'getCursorName'))
+
+ {!! __('pagination.previous') !!}
+
+ @else
+
+ {!! __('pagination.previous') !!}
+
+ @endif
+ @endif
+
+
+
+ {{-- Next Page Link --}}
+ @if ($paginator->hasMorePages())
+ @if(method_exists($paginator,'getCursorName'))
+
+ {!! __('pagination.next') !!}
+
+ @else
+
+ {!! __('pagination.next') !!}
+
+ @endif
+ @else
+
+ {!! __('pagination.next') !!}
+
+ @endif
+
+
+ @endif
+
diff --git a/portman/lib/Livewire/Features/SupportPagination/views/tailwind.blade.php b/portman/lib/Livewire/Features/SupportPagination/views/tailwind.blade.php
new file mode 100644
index 00000000..9ad7f28a
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportPagination/views/tailwind.blade.php
@@ -0,0 +1,126 @@
+@php
+if (! isset($scrollTo)) {
+ $scrollTo = 'body';
+}
+
+$scrollIntoViewJsSnippet = ($scrollTo !== false)
+ ? <<
+ @if ($paginator->hasPages())
+
+
+
+ @if ($paginator->onFirstPage())
+
+ {!! __('pagination.previous') !!}
+
+ @else
+
+ {!! __('pagination.previous') !!}
+
+ @endif
+
+
+
+ @if ($paginator->hasMorePages())
+
+ {!! __('pagination.next') !!}
+
+ @else
+
+ {!! __('pagination.next') !!}
+
+ @endif
+
+
+
+
+
+
+ {!! __('Showing') !!}
+ {{ $paginator->firstItem() }}
+ {!! __('to') !!}
+ {{ $paginator->lastItem() }}
+ {!! __('of') !!}
+ {{ $paginator->total() }}
+ {!! __('results') !!}
+
+
+
+
+
+
+ {{-- Previous Page Link --}}
+ @if ($paginator->onFirstPage())
+
+
+
+
+
+
+
+ @else
+
+
+
+
+
+ @endif
+
+
+ {{-- Pagination Elements --}}
+ @foreach ($elements as $element)
+ {{-- "Three Dots" Separator --}}
+ @if (is_string($element))
+
+ {{ $element }}
+
+ @endif
+
+ {{-- Array Of Links --}}
+ @if (is_array($element))
+ @foreach ($element as $page => $url)
+
+ @if ($page == $paginator->currentPage())
+
+ {{ $page }}
+
+ @else
+
+ {{ $page }}
+
+ @endif
+
+ @endforeach
+ @endif
+ @endforeach
+
+
+ {{-- Next Page Link --}}
+ @if ($paginator->hasMorePages())
+
+
+
+
+
+ @else
+
+
+
+
+
+
+
+ @endif
+
+
+
+
+
+ @endif
+
diff --git a/portman/lib/Livewire/Features/SupportQueryString/BaseUrl.php b/portman/lib/Livewire/Features/SupportQueryString/BaseUrl.php
new file mode 100644
index 00000000..97ef44ae
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportQueryString/BaseUrl.php
@@ -0,0 +1,158 @@
+nullable = $this->determineNullability();
+
+ $this->setPropertyFromQueryString();
+ }
+
+ public function dehydrate($context)
+ {
+ if (! $context->mounting) return;
+
+ $this->pushQueryStringEffect($context);
+ }
+
+ protected function determineNullability()
+ {
+ // It's nullable if they passed it in like: #[Url(nullable: true)]
+ if ($this->nullable !== null) return $this->nullable;
+
+ $reflectionClass = new ReflectionClass($this->getSubTarget() ?? $this->getComponent());
+
+ // It's nullable if there's a nullable typehint like: public ?string $foo;
+ if ($this->getSubName() && $reflectionClass->hasProperty($this->getSubName())) {
+ $property = $reflectionClass->getProperty($this->getSubName());
+
+ return $property->getType()?->allowsNull() ?? false;
+ }
+
+ return false;
+ }
+
+ public function setPropertyFromQueryString()
+ {
+ if ($this->as === null && $this->isOnFormObjectProperty()) {
+ $this->as = $this->getSubName();
+ }
+
+ $nonExistentValue = uniqid('__no_exist__', true);
+
+ $initialValue = $this->getFromUrlQueryString($this->urlName(), $nonExistentValue);
+
+ if ($initialValue === $nonExistentValue) return;
+
+ $decoded = is_array($initialValue)
+ ? json_decode(json_encode($initialValue), true)
+ : json_decode($initialValue ?? '', true);
+
+ // If only part of an array is present in the query string,
+ // we want to merge instead of override the value...
+ if (is_array($decoded) && is_array($original = $this->getValue())) {
+ $decoded = $this->recursivelyMergeArraysWithoutAppendingDuplicateValues($original, $decoded);
+ }
+
+ // Handle empty strings differently depending on if this
+ // field is considered "nullable" by typehint or API.
+ if ($initialValue === null) {
+ $value = $this->nullable ? null : '';
+ } else {
+ $value = $decoded === null ? $initialValue : $decoded;
+ }
+
+ $this->setValue($value, $this->nullable);
+ }
+
+ protected function recursivelyMergeArraysWithoutAppendingDuplicateValues(&$array1, &$array2)
+ {
+ $merged = $array1;
+
+ foreach ($array2 as $key => &$value) {
+ if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
+ $merged[$key] = $this->recursivelyMergeArraysWithoutAppendingDuplicateValues($merged[$key], $value);
+ } else {
+ $merged[$key] = $value;
+ }
+ }
+
+ return $merged;
+ }
+
+ public function pushQueryStringEffect($context)
+ {
+ $queryString = [
+ 'as' => $this->as,
+ 'use' => $this->history ? 'push' : 'replace',
+ 'alwaysShow' => $this->keep,
+ 'except' => $this->except,
+ ];
+
+ $context->pushEffect('url', $queryString, $this->getName());
+ }
+
+ public function isOnFormObjectProperty()
+ {
+ $subTarget = $this->getSubTarget();
+
+ return $subTarget && is_subclass_of($subTarget, Form::class);
+ }
+
+ public function urlName()
+ {
+ return $this->as ?? $this->getName();
+ }
+
+ public function getFromUrlQueryString($name, $default = null)
+ {
+ if (! app('livewire')->isLivewireRequest()) {
+ $value = request()->query($this->urlName(), $default);
+
+ // If the property is present in the querystring without a value, then Laravel returns
+ // the $default value. We want to return null in this case, so we can differentiate
+ // between "not present" and "present with no value". If the request is a Livewire
+ // request, we don't have that issue as we use PHP's parse_str function.
+ if (array_key_exists($name, request()->query()) && $value === $default) {
+ return null;
+ }
+
+ return $value;
+ }
+
+ // If this is a subsequent ajax request, we can't use Laravel's standard "request()->query()"...
+ return $this->getFromRefererUrlQueryString(
+ request()->header('Referer'),
+ $name,
+ $default
+ );
+ }
+
+ public function getFromRefererUrlQueryString($url, $key, $default = null)
+ {
+ $parsedUrl = parse_url($url ?? '');
+ $query = [];
+
+ if (isset($parsedUrl['query'])) {
+ parse_str($parsedUrl['query'], $query);
+ }
+
+ return $query[$key] ?? $default;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportQueryString/SupportQueryString.php b/portman/lib/Livewire/Features/SupportQueryString/SupportQueryString.php
new file mode 100644
index 00000000..47d52e22
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportQueryString/SupportQueryString.php
@@ -0,0 +1,62 @@
+getQueryString()) return;
+
+ foreach ($queryString as $key => $value) {
+ $key = is_string($key) ? $key : $value;
+ $alias = $value['as'] ?? $key;
+ $history = $value['history'] ?? true;
+ $keep = $value['alwaysShow'] ?? $value['keep'] ?? false;
+ $except = $value['except'] ?? null;
+
+ $this->component->setPropertyAttribute($key, new BaseUrl(as: $alias, history: $history, keep: $keep, except: $except));
+ }
+ }
+
+ public function getQueryString()
+ {
+ if (isset($this->queryString)) return $this->queryString;
+
+ $component = $this->component;
+
+ $componentQueryString = [];
+
+ if (method_exists($component, 'queryString')) $componentQueryString = invade($component)->queryString();
+ elseif (property_exists($component, 'queryString')) $componentQueryString = invade($component)->queryString;
+
+ return $this->queryString = collect(class_uses_recursive($class = $component::class))
+ ->map(function ($trait) use ($class, $component) {
+ $member = 'queryString' . class_basename($trait);
+
+ if (method_exists($class, $member)) {
+ return invade($component)->{$member}();
+ }
+
+ if (property_exists($class, $member)) {
+ return invade($component)->{$member};
+ }
+
+ return [];
+ })
+ ->values()
+ ->mapWithKeys(function ($value) {
+ return $value;
+ })
+ ->merge($componentQueryString)
+ ->toArray();
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportReactiveProps/BaseReactive.php b/portman/lib/Livewire/Features/SupportReactiveProps/BaseReactive.php
new file mode 100644
index 00000000..594cc3e2
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportReactiveProps/BaseReactive.php
@@ -0,0 +1,45 @@
+getName();
+
+ store($this->component)->push('reactiveProps', $property);
+
+ $this->originalValueHash = crc32(json_encode($this->getValue()));
+ }
+
+ public function hydrate()
+ {
+ if (SupportReactiveProps::hasPassedInProps($this->component->getId())) {
+ $updatedValue = SupportReactiveProps::getPassedInProp(
+ $this->component->getId(), $this->getName()
+ );
+
+ $this->setValue($updatedValue);
+ }
+
+ $this->originalValueHash = crc32(json_encode($this->getValue()));
+ }
+
+ public function dehydrate($context)
+ {
+ if ($this->originalValueHash !== crc32(json_encode($this->getValue()))) {
+ throw new CannotMutateReactivePropException($this->component->getName(), $this->getName());
+ }
+
+ $context->pushMemo('props', $this->getName());
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportReactiveProps/CannotMutateReactivePropException.php b/portman/lib/Livewire/Features/SupportReactiveProps/CannotMutateReactivePropException.php
new file mode 100644
index 00000000..ed1f5c9e
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportReactiveProps/CannotMutateReactivePropException.php
@@ -0,0 +1,13 @@
+ static::$pendingChildParams = []);
+
+ on('mount.stub', function ($tag, $id, $params, $parent, $key) {
+ static::$pendingChildParams[$id] = $params;
+ });
+ }
+
+ static function hasPassedInProps($id) {
+ return isset(static::$pendingChildParams[$id]);
+ }
+
+ static function getPassedInProp($id, $name) {
+ $params = static::$pendingChildParams[$id] ?? [];
+
+ return $params[$name] ?? null;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportRedirects/HandlesRedirects.php b/portman/lib/Livewire/Features/SupportRedirects/HandlesRedirects.php
new file mode 100644
index 00000000..4ea4d793
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportRedirects/HandlesRedirects.php
@@ -0,0 +1,36 @@
+set('redirect', $url);
+
+ if ($navigate) store($this)->set('redirectUsingNavigate', true);
+
+ $shouldSkipRender = ! config('livewire.render_on_redirect', false);
+
+ $shouldSkipRender && $this->skipRender();
+ }
+
+ public function redirectRoute($name, $parameters = [], $absolute = true, $navigate = false)
+ {
+ $this->redirect(route($name, $parameters, $absolute), $navigate);
+ }
+
+ public function redirectIntended($default = '/', $navigate = false)
+ {
+ $url = session()->pull('url.intended', $default);
+
+ $this->redirect($url, $navigate);
+ }
+
+ public function redirectAction($name, $parameters = [], $absolute = true, $navigate = false)
+ {
+ $this->redirect(action($name, $parameters, $absolute), $navigate);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportRedirects/Redirector.php b/portman/lib/Livewire/Features/SupportRedirects/Redirector.php
new file mode 100644
index 00000000..e89ee68c
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportRedirects/Redirector.php
@@ -0,0 +1,46 @@
+component->redirect($this->generator->to($path, [], $secure));
+
+ return $this;
+ }
+
+ public function away($path, $status = 302, $headers = [])
+ {
+ return $this->to($path, $status, $headers);
+ }
+
+ public function with($key, $value = null)
+ {
+ $key = is_array($key) ? $key : [$key => $value];
+
+ foreach ($key as $k => $v) {
+ $this->session->flash($k, $v);
+ }
+
+ return $this;
+ }
+
+ public function component(Component $component)
+ {
+ $this->component = $component;
+
+ return $this;
+ }
+
+ public function response($to)
+ {
+ return $this->createRedirect($to, 302, []);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportRedirects/SupportRedirects.php b/portman/lib/Livewire/Features/SupportRedirects/SupportRedirects.php
new file mode 100644
index 00000000..e434c36f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportRedirects/SupportRedirects.php
@@ -0,0 +1,71 @@
+has('session.store')) {
+ session()->forget(session()->get('_flash.new'));
+ }
+ });
+
+ on('flush-state', function () {
+ static::$atLeastOneMountedComponentHasRedirected = false;
+ });
+ }
+
+ public function boot()
+ {
+ // Put Laravel's redirector aside and replace it with our own custom one.
+ static::$redirectorCacheStack[] = app('redirect');
+
+ app()->bind('redirect', function () {
+ $redirector = app(Redirector::class)->component($this->component);
+
+ if (app()->has('session.store')) {
+ $redirector->setSession(app('session.store'));
+ }
+
+ return $redirector;
+ });
+ }
+
+ public function dehydrate($context)
+ {
+ // Put the old redirector back into the container.
+ app()->instance('redirect', array_pop(static::$redirectorCacheStack));
+
+ $to = $this->storeGet('redirect');
+ $usingNavigate = $this->storeGet('redirectUsingNavigate');
+
+ if (is_subclass_of($to, Component::class)) {
+ $to = url()->action($to);
+ }
+
+ if ($to && ! app(HandleRequests::class)->isLivewireRequest()) {
+ abort(redirect($to));
+ }
+
+ if (! $to) return;
+
+ $context->addEffect('redirect', $to);
+ $usingNavigate && $context->addEffect('redirectUsingNavigate', true);
+
+ if (! $context->isMounting()) {
+ static::$atLeastOneMountedComponentHasRedirected = true;
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportRedirects/TestsRedirects.php b/portman/lib/Livewire/Features/SupportRedirects/TestsRedirects.php
new file mode 100644
index 00000000..bf0ae31f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportRedirects/TestsRedirects.php
@@ -0,0 +1,74 @@
+action($uri);
+ }
+
+ if (! app('livewire')->isLivewireRequest()) {
+ $this->lastState->getResponse()->assertRedirect($uri);
+
+ return $this;
+ }
+
+ PHPUnit::assertArrayHasKey(
+ 'redirect',
+ $this->effects,
+ 'Component did not perform a redirect.'
+ );
+
+ if (! is_null($uri)) {
+ PHPUnit::assertSame(url($uri), url($this->effects['redirect']));
+ }
+
+ return $this;
+ }
+
+ public function assertRedirectContains($uri)
+ {
+ if (is_subclass_of($uri, Component::class)) {
+ $uri = url()->action($uri);
+ }
+
+ if (! app('livewire')->isLivewireRequest()) {
+ $this->lastState->getResponse()->assertRedirectContains($uri);
+
+ return $this;
+ }
+
+ PHPUnit::assertArrayHasKey(
+ 'redirect',
+ $this->effects,
+ 'Component did not perform a redirect.'
+ );
+
+ PHPUnit::assertTrue(
+ Str::contains($this->effects['redirect'], $uri), 'Redirect location ['.$this->effects['redirect'].'] does not contain ['.$uri.'].'
+ );
+
+ return $this;
+ }
+
+ public function assertRedirectToRoute($name, $parameters = [])
+ {
+ $uri = route($name, $parameters);
+
+ return $this->assertRedirect($uri);
+ }
+
+ public function assertNoRedirect()
+ {
+ PHPUnit::assertTrue(! isset($this->effects['redirect']));
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportReleaseTokens/HandlesReleaseTokens.php b/portman/lib/Livewire/Features/SupportReleaseTokens/HandlesReleaseTokens.php
new file mode 100644
index 00000000..1c318ff6
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportReleaseTokens/HandlesReleaseTokens.php
@@ -0,0 +1,14 @@
+getClass($snapshot['memo']['name']);
+
+ if (!isset($snapshot['memo']['release']) || $snapshot['memo']['release'] !== static::generate($componentClass)) {
+ throw new LivewireReleaseTokenMismatchException;
+ }
+ }
+
+ static function generate($componentOrComponentClass): string
+ {
+ $livewireReleaseToken = static::$LIVEWIRE_RELEASE_TOKEN;
+ $appReleaseToken = app('config')->get('livewire.release_token', '');
+ $componentReleaseToken = method_exists($componentOrComponentClass, 'releaseToken') ? $componentOrComponentClass::releaseToken() : '';
+
+ return $livewireReleaseToken . '-' . $appReleaseToken . '-' . $componentReleaseToken;
+ }
+}
\ No newline at end of file
diff --git a/portman/lib/Livewire/Features/SupportReleaseTokens/SupportReleaseTokens.php b/portman/lib/Livewire/Features/SupportReleaseTokens/SupportReleaseTokens.php
new file mode 100644
index 00000000..65b68b48
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportReleaseTokens/SupportReleaseTokens.php
@@ -0,0 +1,23 @@
+addMemo('release', ReleaseToken::generate($component));
+ });
+
+ // Verify the release token before verifying the snapshot checksum, as we don't want to trigger a checksum
+ // failure if the release token doesn't match as there may be intentional changes in the snapshot...
+ on('checksum.verify', function ($checksum, $snapshot) {
+ ReleaseToken::verify($snapshot);
+ });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportScriptsAndAssets/SupportScriptsAndAssets.php b/portman/lib/Livewire/Features/SupportScriptsAndAssets/SupportScriptsAndAssets.php
new file mode 100644
index 00000000..3e8e9ef6
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportScriptsAndAssets/SupportScriptsAndAssets.php
@@ -0,0 +1,170 @@
+ $assets) {
+ if (! in_array($key, $alreadyRunAssetKeys)) {
+
+ // These will get injected into the HTML if it's an initial page load...
+ static::$renderedAssets[$key] = $assets;
+
+ $alreadyRunAssetKeys[] = $key;
+ }
+ }
+ }
+
+ public static function getUniqueBladeCompileTimeKey()
+ {
+ // Rather than using random strings as compile-time keys for blade directives,
+ // we want something more detereminstic to protect against problems that arise
+ // from using load-balancers and such.
+ // Therefore, we create a key based on the currently compiling view path and
+ // number of already compiled directives here...
+ $viewPath = crc32(app('blade.compiler')->getPath() ?? '');
+
+ if (! isset(static::$countersByViewPath[$viewPath])) static::$countersByViewPath[$viewPath] = 0;
+
+ $key = $viewPath.'-'.static::$countersByViewPath[$viewPath];
+
+ static::$countersByViewPath[$viewPath]++;
+
+ return $key;
+ }
+
+ static function provide()
+ {
+ on('flush-state', function () {
+ static::$alreadyRunAssetKeys = [];
+ static::$countersByViewPath = [];
+ static::$renderedAssets = [];
+ static::$nonLivewireAssets = [];
+ });
+
+ Blade::directive('script', function () {
+ $key = static::getUniqueBladeCompileTimeKey();
+
+ return <<
+ PHP;
+ });
+
+ Blade::directive('endscript', function () {
+ return <<push('scripts', \$__output, \$__scriptKey)
+ ?>
+ PHP;
+ });
+
+ Blade::directive('assets', function () {
+ $key = static::getUniqueBladeCompileTimeKey();
+
+ return <<
+ PHP;
+ });
+
+ Blade::directive('endassets', function () {
+ return <<push('assets', \$__output, \$__assetKey);
+ } else {
+ \Livewire\Features\SupportScriptsAndAssets\SupportScriptsAndAssets::\$nonLivewireAssets[\$__assetKey] = \$__output;
+ }
+ }
+ ?>
+ PHP;
+ });
+ }
+
+ function hydrate($memo) {
+ // Store the "scripts" and "assets" memos so they can be re-added later (persisted between requests)...
+ if (isset($memo['scripts'])) {
+ store($this->component)->set('forwardScriptsToDehydrateMemo', $memo['scripts']);
+ }
+
+ if (isset($memo['assets'])) {
+ store($this->component)->set('forwardAssetsToDehydrateMemo', $memo['assets']);
+ }
+ }
+
+ function dehydrate($context)
+ {
+ $alreadyRunScriptKeys = store($this->component)->get('forwardScriptsToDehydrateMemo', []);
+
+ // Add any scripts to the payload that haven't been run yet for this component....
+ foreach (store($this->component)->get('scripts', []) as $key => $script) {
+ if (! in_array($key, $alreadyRunScriptKeys)) {
+ $context->pushEffect('scripts', $script, $key);
+ $alreadyRunScriptKeys[] = $key;
+ }
+ }
+
+ $context->addMemo('scripts', $alreadyRunScriptKeys);
+
+ // Add any assets to the payload that haven't been run yet for the entire page...
+
+ $alreadyRunAssetKeys = store($this->component)->get('forwardAssetsToDehydrateMemo', []);
+
+ foreach (store($this->component)->get('assets', []) as $key => $assets) {
+ if (! in_array($key, $alreadyRunAssetKeys)) {
+
+ // These will either get injected into the HTML if it's an initial page load
+ // or they will be added to the "assets" key in an ajax payload...
+ static::$renderedAssets[$key] = $assets;
+
+ $alreadyRunAssetKeys[] = $key;
+ }
+ }
+
+ $context->addMemo('assets', $alreadyRunAssetKeys);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportScriptsAndAssets/non-livewire-asset.js b/portman/lib/Livewire/Features/SupportScriptsAndAssets/non-livewire-asset.js
new file mode 100644
index 00000000..15bc962a
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportScriptsAndAssets/non-livewire-asset.js
@@ -0,0 +1,2 @@
+
+document.querySelector('[dusk="foo"]').textContent = 'non livewire evaluated'
diff --git a/portman/lib/Livewire/Features/SupportScriptsAndAssets/test.js b/portman/lib/Livewire/Features/SupportScriptsAndAssets/test.js
new file mode 100644
index 00000000..3dddc4c1
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportScriptsAndAssets/test.js
@@ -0,0 +1,2 @@
+
+document.querySelector('[dusk="foo"]').textContent = 'evaluated'
diff --git a/portman/lib/Livewire/Features/SupportSession/BaseSession.php b/portman/lib/Livewire/Features/SupportSession/BaseSession.php
new file mode 100644
index 00000000..0cd16c5a
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportSession/BaseSession.php
@@ -0,0 +1,62 @@
+exists()) return;
+
+ $fromSession = $this->read();
+
+ $this->setValue($fromSession);
+ }
+
+ public function dehydrate($context)
+ {
+ $this->write();
+ }
+
+ protected function exists()
+ {
+ return Session::exists($this->key());
+ }
+
+ protected function read()
+ {
+ return Session::get($this->key());
+ }
+
+ protected function write()
+ {
+ Session::put($this->key(), $this->getValue());
+ }
+
+ protected function key()
+ {
+ if (! $this->key) {
+ return (string) 'lw' . crc32($this->component->getName() . $this->getName());
+ }
+
+ return self::replaceDynamicPlaceholders($this->key, $this->component);
+ }
+
+ static function replaceDynamicPlaceholders($key, $component)
+ {
+ return preg_replace_callback('/\{(.*)\}/U', function ($matches) use ($component) {
+ return data_get($component, $matches[1], function () use ($matches) {
+ throw new \Exception('Unable to evaluate dynamic session key placeholder: '.$matches[0]);
+ });
+ }, $key);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportStreaming/HandlesStreaming.php b/portman/lib/Livewire/Features/SupportStreaming/HandlesStreaming.php
new file mode 100644
index 00000000..c1567cba
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportStreaming/HandlesStreaming.php
@@ -0,0 +1,15 @@
+stream($to, $content, $replace);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportStreaming/SupportStreaming.php b/portman/lib/Livewire/Features/SupportStreaming/SupportStreaming.php
new file mode 100644
index 00000000..ce31f987
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportStreaming/SupportStreaming.php
@@ -0,0 +1,42 @@
+ $name, 'content' => $content, 'replace' => $replace]);
+ }
+
+ public static function ensureStreamResponseStarted()
+ {
+ if (static::$response) return;
+
+ static::$response = response()->stream(null , 200, [
+ 'Cache-Control' => 'no-cache',
+ 'Content-Type' => 'text/event-stream',
+ 'X-Accel-Buffering' => 'no',
+ 'X-Livewire-Stream' => true,
+ ]);
+
+ static::$response->sendHeaders();
+ }
+
+ public static function streamContent($body)
+ {
+ echo json_encode(['stream' => true, 'body' => $body, 'endStream' => true]);
+
+ if (ob_get_level() > 0) {
+ ob_flush();
+ }
+
+ flush();
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTeleporting/SupportTeleporting.php b/portman/lib/Livewire/Features/SupportTeleporting/SupportTeleporting.php
new file mode 100644
index 00000000..f089c2e4
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTeleporting/SupportTeleporting.php
@@ -0,0 +1,20 @@
+">';
+ });
+
+ Blade::directive('endteleport', function () {
+ return '';
+ });
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/ComponentState.php b/portman/lib/Livewire/Features/SupportTesting/ComponentState.php
new file mode 100644
index 00000000..6c40b452
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/ComponentState.php
@@ -0,0 +1,79 @@
+component;
+ }
+
+ function getSnapshot()
+ {
+ return $this->snapshot;
+ }
+
+ function getSnapshotData()
+ {
+ return $this->untupleify($this->snapshot['data']);
+ }
+
+ function getEffects()
+ {
+ return $this->effects;
+ }
+
+ function getView()
+ {
+ return $this->view;
+ }
+
+ function getResponse()
+ {
+ return $this->response;
+ }
+
+ function untupleify($payload) {
+ $value = Utils::isSyntheticTuple($payload) ? $payload[0] : $payload;
+
+ if (is_array($value)) {
+ foreach ($value as $key => $child) {
+ $value[$key] = $this->untupleify($child);
+ }
+ }
+
+ return $value;
+ }
+
+ function getHtml($stripInitialData = false)
+ {
+ $html = $this->html;
+
+ if ($stripInitialData) {
+ $removeMe = (string) str($html)->betweenFirst(
+ 'wire:snapshot="', '"'
+ );
+
+ $html = str_replace($removeMe, '', $html);
+
+ $removeMe = (string) str($html)->betweenFirst(
+ 'wire:effects="', '"'
+ );
+
+ $html = str_replace($removeMe, '', $html);
+ }
+
+ return $html;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/DuskBrowserMacros.php b/portman/lib/Livewire/Features/SupportTesting/DuskBrowserMacros.php
new file mode 100644
index 00000000..49504069
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/DuskBrowserMacros.php
@@ -0,0 +1,660 @@
+resolver->format($selector);
+
+ $actual = $this->resolver->findOrFail($selector)->getAttribute($attribute);
+
+ PHPUnit::assertNull(
+ $actual,
+ "Did not see expected attribute [{$attribute}] within element [{$fullSelector}]."
+ );
+
+ return $this;
+ };
+ }
+
+ public function assertNotVisible()
+ {
+ return function ($selector) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $fullSelector = $this->resolver->format($selector);
+
+ PHPUnit::assertFalse(
+ $this->resolver->findOrFail($selector)->isDisplayed(),
+ "Element [{$fullSelector}] is visible."
+ );
+
+ return $this;
+ };
+ }
+
+ public function assertNotPresent()
+ {
+ return function ($selector) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $fullSelector = $this->resolver->format($selector);
+
+ PHPUnit::assertTrue(
+ is_null($this->resolver->find($selector)),
+ "Element [{$fullSelector}] is present."
+ );
+
+ return $this;
+ };
+ }
+
+ public function assertHasClass()
+ {
+ return function ($selector, $className) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $fullSelector = $this->resolver->format($selector);
+
+ PHPUnit::assertContains(
+ $className,
+ explode(' ', $this->attribute($selector, 'class')),
+ "Element [{$fullSelector}] missing class [{$className}]."
+ );
+
+ return $this;
+ };
+ }
+
+ public function assertScript()
+ {
+ return function ($js, $expects = true) {
+ /** @var \Laravel\Dusk\Browser $this */
+ PHPUnit::assertEquals($expects, head($this->script(
+ str($js)->start('return ')
+ )));
+
+ return $this;
+ };
+ }
+
+ public function runScript()
+ {
+ return function ($js) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $this->script([$js]);
+
+ return $this;
+ };
+ }
+
+ public function scrollTo()
+ {
+ return function ($selector) {
+ $this->browser->scrollTo($selector);
+ return $this;
+ };
+ }
+
+ public function assertNotInViewPort()
+ {
+ return function ($selector) {
+ /** @var \Laravel\Dusk\Browser $this */
+ return $this->assertInViewPort($selector, invert: true);
+ };
+ }
+
+ public function assertInViewPort()
+ {
+ return function ($selector, $invert = false) {
+ /** @var \Laravel\Dusk\Browser $this */
+
+ $fullSelector = $this->resolver->format($selector);
+
+ $result = $this->script(
+ 'const rect = document.querySelector(\''.$fullSelector.'\').getBoundingClientRect();
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
+ );',
+ $selector
+ )[0];
+
+ PHPUnit::assertEquals($invert ? false : true, $result);
+
+ return $this;
+ };
+ }
+
+ public function assertClassMissing()
+ {
+ return function ($selector, $className) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $fullSelector = $this->resolver->format($selector);
+
+ PHPUnit::assertNotContains(
+ $className,
+ explode(' ', $this->attribute($selector, 'class')),
+ "Element [{$fullSelector}] has class [{$className}]."
+ );
+
+ return $this;
+ };
+ }
+
+ public function waitForLivewireToLoad()
+ {
+ return function () {
+ /** @var \Laravel\Dusk\Browser $this */
+ return $this->waitUsing(6, 25, function () {
+ return $this->driver->executeScript('return !! window.Livewire.initialRenderIsFinished');
+ });
+ };
+ }
+
+ public function waitForLivewire()
+ {
+ return function ($callback = null) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $id = str()->random();
+
+ $this->script([
+ "window.duskIsWaitingForLivewireRequest{$id} = true",
+ "window.Livewire.hook('request', ({ respond, succeed, fail }) => {
+ window.duskIsWaitingForLivewireRequest{$id} = true
+
+ let handle = () => {
+ queueMicrotask(() => {
+ console.log('test')
+ delete window.duskIsWaitingForLivewireRequest{$id}
+ })
+ }
+
+ succeed(handle)
+ fail(handle)
+ })",
+ ]);
+
+ if ($callback) {
+ $callback($this);
+
+ return $this->waitUsing(6, 25, function () use ($id) {
+ return $this->driver->executeScript("return window.duskIsWaitingForLivewireRequest{$id} === undefined");
+ }, 'Livewire request was never triggered');
+ }
+
+ // If no callback is passed, make ->waitForLivewire a higher-order method.
+ return new class($this, $id) {
+ protected $browser;
+ protected $id;
+
+ public function __construct($browser, $id) { $this->browser = $browser; $this->id = $id; }
+
+ public function __call($method, $params)
+ {
+ return tap($this->browser->{$method}(...$params), function ($browser) {
+ $browser->waitUsing(6, 25, function () use ($browser) {
+ return $browser->driver->executeScript("return window.duskIsWaitingForLivewireRequest{$this->id} === undefined");
+ }, 'Livewire request was never triggered');
+ });
+ }
+ };
+ };
+ }
+
+ public function waitForNoLivewire()
+ {
+ return function ($callback = null) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $id = str()->random();
+
+ $this->script([
+ "window.duskIsWaitingForLivewireRequest{$id} = true",
+ "window.Livewire.hook('request', ({ respond, succeed, fail }) => {
+ window.duskIsWaitingForLivewireRequest{$id} = true
+
+ let handle = () => {
+ queueMicrotask(() => {
+ delete window.duskIsWaitingForLivewireRequest{$id}
+ })
+ }
+
+ succeed(handle)
+ fail(handle)
+ })",
+ ]);
+
+ if ($callback) {
+ $callback($this);
+
+ return $this->waitUsing(6, 25, function () use ($id) {
+ return $this->driver->executeScript("return window.duskIsWaitingForLivewireRequest{$id}");
+ }, 'Livewire request was triggered');
+ }
+
+ // If no callback is passed, make ->waitForNoLivewire a higher-order method.
+ return new class($this, $id) {
+ protected $browser;
+ protected $id;
+
+ public function __construct($browser, $id) { $this->browser = $browser; $this->id = $id; }
+
+ public function __call($method, $params)
+ {
+ return tap($this->browser->{$method}(...$params), function ($browser) {
+ $browser->waitUsing(6, 25, function () use ($browser) {
+ return $browser->driver->executeScript("return window.duskIsWaitingForLivewireRequest{$this->id}");
+ }, 'Livewire request was triggered');
+ });
+ }
+ };
+ };
+ }
+
+ public function waitForNavigate()
+ {
+ return function ($callback = null) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $id = str()->random();
+
+ $this->script([
+ "window.duskIsWaitingForLivewireNavigate{$id} = true",
+ "window.handler{$id} = () => {
+ window.duskIsWaitingForLivewireNavigate{$id} = true
+
+ document.removeEventListener('livewire:navigated', window.handler{$id})
+
+ queueMicrotask(() => {
+ delete window.duskIsWaitingForLivewireNavigate{$id}
+ })
+ }",
+ "document.addEventListener('livewire:navigated', window.handler{$id})",
+ ]);
+
+ if ($callback) {
+ $callback($this);
+
+ return $this->waitUsing(6, 25, function () use ($id) {
+ return $this->driver->executeScript("return window.duskIsWaitingForLivewireNavigate{$id} === undefined");
+ }, 'Livewire navigate was never triggered');
+ }
+
+ // If no callback is passed, make ->waitForNavigate a higher-order method.
+ return new class($this, $id) {
+ protected $browser;
+ protected $id;
+ public function __construct($browser, $id) { $this->browser = $browser; $this->id = $id; }
+
+ public function __call($method, $params)
+ {
+ return tap($this->browser->{$method}(...$params), function ($browser) {
+ $browser->waitUsing(6, 25, function () use ($browser) {
+ return $browser->driver->executeScript("return window.duskIsWaitingForLivewireNavigate{$this->id} === undefined");
+ }, 'Livewire navigate was never triggered');
+ });
+ }
+ };
+ };
+ }
+
+ public function waitForNavigateRequest()
+ {
+ return function ($callback = null) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $id = str()->random();
+
+ $this->script([
+ "window.duskIsWaitingForLivewireNavigateRequestStarted{$id} = false",
+ "window.duskIsWaitingForLivewireNavigateRequestFinished{$id} = true",
+ 'let cleanupRequest = () => {}',
+ "cleanupRequest = Livewire.hook('navigate.request', () => {
+ window.duskIsWaitingForLivewireNavigateRequestStarted{$id} = true
+
+ cleanupRequest()
+ })",
+ "window.handler{$id} = () => {
+ if (! window.duskIsWaitingForLivewireNavigateRequestStarted{$id}) {
+ return
+ }
+
+ window.duskIsWaitingForLivewireNavigateRequestFinished{$id} = true
+
+ document.removeEventListener('livewire:navigated', window.handler{$id})
+
+ queueMicrotask(() => {
+ delete window.duskIsWaitingForLivewireNavigateRequestStarted{$id}
+ delete window.duskIsWaitingForLivewireNavigateRequestFinished{$id}
+ })
+ }",
+ "document.addEventListener('livewire:navigated', window.handler{$id})",
+ ]);
+
+ if ($callback) {
+ $callback($this);
+
+ return $this->waitUsing(6, 25, function () use ($id) {
+ return $this->driver->executeScript("return window.duskIsWaitingForLivewireNavigateRequestFinished{$id} === undefined");
+ }, 'Livewire navigate request was never completed');
+ }
+
+ // If no callback is passed, make ->waitForNavigate a higher-order method.
+ return new class($this, $id)
+ {
+ protected $browser;
+ protected $id;
+
+ public function __construct($browser, $id)
+ {
+ $this->browser = $browser;
+ $this->id = $id;
+ }
+
+ public function __call($method, $params)
+ {
+ return tap($this->browser->{$method}(...$params), function ($browser) {
+ $browser->waitUsing(6, 25, function () use ($browser) {
+ return $browser->driver->executeScript("return window.duskIsWaitingForLivewireNavigateRequestFinished{$this->id} === undefined");
+ }, 'Livewire navigate request was never completed');
+ });
+ }
+ };
+ };
+ }
+
+ public function waitForNoNavigateRequest()
+ {
+ return function ($callback = null) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $id = str()->random();
+
+ $this->script([
+ "window.duskIsWaitingForLivewireNavigateRequestStarted{$id} = true",
+ 'let cleanupRequest = () => {}',
+ "cleanupRequest = Livewire.hook('navigate.request', () => {
+ window.duskIsWaitingForLivewireNavigateRequestStarted{$id} = true
+
+ cleanupRequest()
+
+ queueMicrotask(() => {
+ delete window.duskIsWaitingForLivewireNavigateRequestStarted{$id}
+ })
+ })",
+ ]);
+
+ if ($callback) {
+ $callback($this);
+
+ return $this->waitUsing(6, 25, function () use ($id) {
+ return $this->driver->executeScript("return window.duskIsWaitingForLivewireNavigateRequestStarted{$id}");
+ }, 'Livewire navigate request was completed');
+ }
+
+ // If no callback is passed, make ->waitForNavigate a higher-order method.
+ return new class($this, $id)
+ {
+ protected $browser;
+ protected $id;
+
+ public function __construct($browser, $id)
+ {
+ $this->browser = $browser;
+ $this->id = $id;
+ }
+
+ public function __call($method, $params)
+ {
+ return tap($this->browser->{$method}(...$params), function ($browser) {
+ $browser->waitUsing(6, 25, function () use ($browser) {
+ return $browser->driver->executeScript("return window.duskIsWaitingForLivewireNavigateRequestStarted{$this->id}");
+ }, 'Livewire navigate request was completed');
+ });
+ }
+ };
+ };
+ }
+
+ public function waitForNavigatePrefetchRequest()
+ {
+ return function ($callback = null) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $id = str()->random();
+
+ $this->script([
+ "window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id} = true",
+ 'let cleanupPrefetchRequest = () => {}',
+ "cleanupPrefetchRequest = Livewire.hook('navigate.request', () => {
+ window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id} = true
+
+ cleanupPrefetchRequest()
+
+ queueMicrotask(() => {
+ delete window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id}
+ })
+ })",
+ ]);
+
+ if ($callback) {
+ $callback($this);
+
+ return $this->waitUsing(6, 25, function () use ($id) {
+ return $this->driver->executeScript("return window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id} === undefined");
+ }, 'Livewire navigate prefetch request was never triggered');
+ }
+
+ // If no callback is passed, make ->waitForNavigatePrefetchRequest a higher-order method.
+ return new class($this, $id)
+ {
+ protected $browser;
+ protected $id;
+
+ public function __construct($browser, $id)
+ {
+ $this->browser = $browser;
+ $this->id = $id;
+ }
+
+ public function __call($method, $params)
+ {
+ return tap($this->browser->{$method}(...$params), function ($browser) {
+ $browser->waitUsing(6, 25, function () use ($browser) {
+ return $browser->driver->executeScript("return window.duskIsWaitingForLivewireNavigatePrefetchRequest{$this->id} === undefined");
+ }, 'Livewire navigate prefetch request was never triggered');
+ });
+ }
+ };
+ };
+ }
+
+ public function waitForNoNavigatePrefetchRequest()
+ {
+ // 60ms is the minimum delay for a hover event to trigger a prefetch plus a buffer...
+ return function ($callback = null, $prefetchDelay = 70) {
+ /** @var \Laravel\Dusk\Browser $this */
+ $id = str()->random();
+
+ $this->script([
+ "window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id} = true",
+ "Livewire.hook('navigate.request', () => {
+ window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id} = true
+
+ queueMicrotask(() => {
+ delete window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id}
+ })
+ })",
+ ]);
+
+ if ($callback) {
+ $callback($this);
+
+ // Wait for the specified prefetch delay before checking
+ $this->pause($prefetchDelay);
+
+ return $this->waitUsing(6, 25, function () use ($id) {
+ return $this->driver->executeScript("return window.duskIsWaitingForLivewireNavigatePrefetchRequest{$id}");
+ }, 'Livewire navigate prefetch request was triggered');
+ }
+
+ // If no callback is passed, make ->waitForNoNavigatePrefetchRequest a higher-order method.
+ return new class($this, $id, $prefetchDelay)
+ {
+ protected $browser;
+ protected $id;
+ protected $prefetchDelay;
+
+ public function __construct($browser, $id, $prefetchDelay)
+ {
+ $this->browser = $browser;
+ $this->id = $id;
+ $this->prefetchDelay = $prefetchDelay;
+ }
+
+ public function __call($method, $params)
+ {
+ return tap($this->browser->{$method}(...$params), function ($browser) {
+ // Wait for the specified prefetch delay before checking
+ $browser->pause($this->prefetchDelay);
+
+ $browser->waitUsing(6, 25, function () use ($browser) {
+ return $browser->driver->executeScript("return window.duskIsWaitingForLivewireNavigatePrefetchRequest{$this->id}");
+ }, 'Livewire navigate prefetch request was triggered');
+ });
+ }
+ };
+ };
+ }
+
+ public function online()
+ {
+ return function () {
+ /** @var \Laravel\Dusk\Browser $this */
+ return tap($this)->script("window.dispatchEvent(new Event('online'))");
+ };
+ }
+
+ public function offline()
+ {
+ return function () {
+ /** @var \Laravel\Dusk\Browser $this */
+ return tap($this)->script("window.dispatchEvent(new Event('offline'))");
+ };
+ }
+
+ public function selectMultiple()
+ {
+ return function ($field, $values = []) {
+ $element = $this->resolver->resolveForSelection($field);
+
+ $options = $element->findElements(WebDriverBy::tagName('option'));
+
+ if (empty($values)) {
+ $maxSelectValues = sizeof($options) - 1;
+ $minSelectValues = rand(0, $maxSelectValues);
+ foreach (range($minSelectValues, $maxSelectValues) as $optValue) {
+ $options[$optValue]->click();
+ }
+ } else {
+ foreach ($options as $option) {
+ $optValue = (string)$option->getAttribute('value');
+ if (in_array($optValue, $values)) {
+ $option->click();
+ }
+ }
+ }
+
+ return $this;
+ };
+ }
+
+ public function assertConsoleLogHasWarning()
+ {
+ return function($expectedMessage){
+ $logs = $this->driver->manage()->getLog('browser');
+
+ $containsError = false;
+
+ foreach ($logs as $log) {
+ if (! isset($log['message']) || ! isset($log['level']) || $log['level'] !== 'WARNING') continue;
+
+
+ if(str($log['message'])->contains($expectedMessage)) {
+ $containsError = true;
+ }
+ }
+
+ PHPUnit::assertTrue($containsError, "Console log error message \"{$expectedMessage}\" not found");
+
+ return $this;
+ };
+ }
+
+ public function assertConsoleLogMissingWarning()
+ {
+ return function($expectedMessage){
+ $logs = $this->driver->manage()->getLog('browser');
+
+ $containsError = false;
+
+ foreach ($logs as $log) {
+ if (! isset($log['message']) || ! isset($log['level']) || $log['level'] !== 'WARNING') continue;
+
+
+ if(str($log['message'])->contains($expectedMessage)) {
+ $containsError = true;
+ }
+ }
+
+ PHPUnit::assertFalse($containsError, "Console log error message \"{$expectedMessage}\" was found");
+
+ return $this;
+ };
+ }
+
+ public function assertConsoleLogHasNoErrors()
+ {
+ return function(){
+ $logs = $this->driver->manage()->getLog('browser');
+
+ $errors = [];
+ foreach ($logs as $log) {
+ if (! isset($log['message']) || ! isset($log['level']) || $log['level'] !== 'SEVERE') continue;
+
+ // Ignore favicon.ico
+ if(str($log['message'])->contains('favicon.ico')) continue;
+
+ $errors[] = $log['message'];
+ }
+
+ PHPUnit::assertEmpty($errors, "Console log contained errors: " . implode(", ", $errors));
+
+ return $this;
+ };
+ }
+
+ public function assertConsoleLogHasErrors()
+ {
+ return function(){
+ $logs = $this->driver->manage()->getLog('browser');
+
+ $errors = [];
+ foreach ($logs as $log) {
+ if (! isset($log['message']) || ! isset($log['level']) || $log['level'] !== 'SEVERE') continue;
+
+ // Ignore favicon.ico
+ if(str($log['message'])->contains('favicon.ico')) continue;
+
+ $errors[] = $log['message'];
+ }
+
+ PHPUnit::assertNotEmpty($errors, "Console log contained no errors");
+
+ return $this;
+ };
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/DuskTestable.php b/portman/lib/Livewire/Features/SupportTesting/DuskTestable.php
new file mode 100644
index 00000000..5aadc982
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/DuskTestable.php
@@ -0,0 +1,195 @@
+middleware('web');
+
+ on('browser.testCase.setUp', function ($testCase) {
+ static::$currentTestCase = $testCase;
+ static::$isTestProcess = true;
+
+ $tweakApplication = $testCase::tweakApplicationHook();
+
+ invade($testCase)->beforeServingApplication(function ($app, $config) use ($tweakApplication) {
+ $config->set('app.debug', true);
+
+ if (is_callable($tweakApplication)) $tweakApplication();
+
+ static::loadTestComponents();
+ });
+ });
+
+ on('browser.testCase.tearDown', function () {
+ static::wipeRuntimeComponentRegistration();
+
+ static::$browser && static::$browser->quit();
+
+ static::$currentTestCase = null;
+ });
+
+ if (isset($_SERVER['CI']) && class_exists(\Orchestra\Testbench\Dusk\Options::class)) {
+ \Orchestra\Testbench\Dusk\Options::withoutUI();
+ }
+
+ \Laravel\Dusk\Browser::mixin(new DuskBrowserMacros);
+ }
+
+ /**
+ * @return Browser
+ */
+ static function create($components, $params = [], $queryParams = [])
+ {
+ if (static::$shortCircuitCreateCall) {
+ throw new class ($components) extends \Exception {
+ public $components;
+ public $isDuskShortcircuit = true;
+ function __construct($components) {
+ $this->components = $components;
+ }
+ };
+ }
+
+ $components = (array) $components;
+
+ $firstComponent = array_shift($components);
+
+ $id = 'a'.str()->random(10);
+
+ $components = [$id => $firstComponent, ...$components];
+
+ return static::createBrowser($id, $components, $params, $queryParams)->visit('/livewire-dusk/'.$id.'?'.Arr::query($queryParams));
+ }
+
+ static function createBrowser($id, $components, $params = [], $queryParams = [])
+ {
+ if (static::$shortCircuitCreateCall) {
+ throw new class ($components) extends \Exception {
+ public $components;
+ public $isDuskShortcircuit = true;
+ function __construct($components) {
+ $this->components = $components;
+ }
+ };
+ }
+
+ [$class, $method] = static::findTestClassAndMethodThatCalledThis();
+
+ static::registerComponentsForNextTest([$id, $class, $method]);
+
+ $testCase = invade(static::$currentTestCase);
+
+ return static::$browser = $testCase->newBrowser($testCase->createWebDriver());
+ }
+
+ static function actingAs(\Illuminate\Contracts\Auth\Authenticatable $user, $driver = null)
+ {
+ //
+ }
+
+ static function findTestClassAndMethodThatCalledThis()
+ {
+ $traces = debug_backtrace(options: DEBUG_BACKTRACE_IGNORE_ARGS, limit: 10);
+
+ foreach ($traces as $trace) {
+ if (is_subclass_of($trace['class'], TestCase::class)) {
+ return [$trace['class'], $trace['function']];
+ }
+ }
+
+ throw new \Exception;
+ }
+
+ static function loadTestComponents()
+ {
+ if (static::$isTestProcess) return;
+
+ $tmp = __DIR__ . '/_runtime_components.json';
+
+ if (file_exists($tmp)) {
+ // We can't just "require" this file because of race conditions...
+ [$id, $testClass, $method] = json_decode(file_get_contents($tmp), associative: true);
+
+ if (! method_exists($testClass, $method)) return;
+
+ static::$shortCircuitCreateCall = true;
+
+ $components = null;
+
+ try {
+ if (\Orchestra\Testbench\phpunit_version_compare('10.0', '>=')) {
+ (new $testClass($method))->$method();
+ } else {
+ (new $testClass())->$method();
+ }
+ } catch (\Exception $e) {
+ if (! $e->isDuskShortcircuit) throw $e;
+ $components = $e->components;
+ }
+
+ $components = is_array($components) ? $components : [$components];
+
+ $firstComponent = array_shift($components);
+
+ $components = [$id => $firstComponent, ...$components];
+
+ static::$shortCircuitCreateCall = false;
+
+ foreach ($components as $name => $class) {
+ if (is_object($class)) $class = $class::class;
+
+ if (is_numeric($name)) {
+ app('livewire')->component($class);
+ } else {
+ app('livewire')->component($name, $class);
+ }
+ }
+ }
+ }
+
+ static function registerComponentsForNextTest($components)
+ {
+ $tmp = __DIR__ . '/_runtime_components.json';
+
+ file_put_contents($tmp, json_encode($components, JSON_PRETTY_PRINT));
+ }
+
+ static function wipeRuntimeComponentRegistration()
+ {
+ $tmp = __DIR__ . '/_runtime_components.json';
+
+ file_exists($tmp) && unlink($tmp);
+ }
+
+ function breakIntoATinkerShell($browsers, $e)
+ {
+ $sh = new \Psy\Shell();
+
+ $sh->add(new \Laravel\Dusk\Console\DuskCommand($this, $e));
+
+ $sh->setScopeVariables([
+ 'browsers' => $browsers,
+ ]);
+
+ $sh->addInput('dusk');
+
+ $sh->setBoundObject($this);
+
+ $sh->run();
+
+ return $sh->getScopeVariables(false);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/InitialRender.php b/portman/lib/Livewire/Features/SupportTesting/InitialRender.php
new file mode 100644
index 00000000..5453f304
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/InitialRender.php
@@ -0,0 +1,72 @@
+makeInitialRequest($name, $params, $fromQueryString, $cookies, $headers);
+ }
+
+ function makeInitialRequest($name, $params, $fromQueryString = [], $cookies = [], $headers = [])
+ {
+ $uri = '/livewire-unit-test-endpoint/'.str()->random(20);
+
+ $this->registerRouteBeforeExistingRoutes($uri, function () use ($name, $params) {
+ return \Illuminate\Support\Facades\Blade::render('@livewire($name, $params)', [
+ 'name' => $name,
+ 'params' => $params,
+ ]);
+ });
+
+ [$response, $componentInstance, $componentView] = $this->extractComponentAndBladeView(function () use ($uri, $fromQueryString, $cookies, $headers) {
+ return $this->requestBroker->temporarilyDisableExceptionHandlingAndMiddleware(function ($requestBroker) use ($uri, $fromQueryString, $cookies, $headers) {
+ return $requestBroker->addHeaders($headers)->call('GET', $uri, $fromQueryString, $cookies);
+ });
+ });
+
+ app('livewire')->flushState();
+
+ $html = $response->getContent();
+
+ // Set "original" to Blade view for assertions like "assertViewIs()"...
+ $response->original = $componentView;
+
+ $snapshot = Utils::extractAttributeDataFromHtml($html, 'wire:snapshot');
+ $effects = Utils::extractAttributeDataFromHtml($html, 'wire:effects');
+
+ return new ComponentState($componentInstance, $response, $componentView, $html, $snapshot, $effects);
+ }
+
+ private function registerRouteBeforeExistingRoutes($path, $closure)
+ {
+ // To prevent this route from overriding wildcard routes registered within the application,
+ // We have to make sure that this route is registered before other existing routes.
+ $livewireTestingRoute = new \Illuminate\Routing\Route(['GET', 'HEAD'], $path, $closure);
+
+ $existingRoutes = app('router')->getRoutes();
+
+ // Make an empty collection.
+ $runningCollection = new \Illuminate\Routing\RouteCollection;
+
+ // Add this testing route as the first one.
+ $runningCollection->add($livewireTestingRoute);
+
+ // Now add the existing routes after it.
+ foreach ($existingRoutes as $route) {
+ $runningCollection->add($route);
+ }
+
+ // Now set this route collection as THE route collection for the app.
+ app('router')->setRoutes($runningCollection);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/MakesAssertions.php b/portman/lib/Livewire/Features/SupportTesting/MakesAssertions.php
new file mode 100644
index 00000000..b38418cd
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/MakesAssertions.php
@@ -0,0 +1,198 @@
+html($stripInitialData)
+ );
+ }
+
+ return $this;
+ }
+
+ function assertDontSee($values, $escape = true, $stripInitialData = true)
+ {
+ foreach (Arr::wrap($values) as $value) {
+ PHPUnit::assertStringNotContainsString(
+ $escape ? e($value): $value,
+ $this->html($stripInitialData)
+ );
+ }
+
+ return $this;
+ }
+
+ function assertSeeHtml($values)
+ {
+ foreach (Arr::wrap($values) as $value) {
+ PHPUnit::assertStringContainsString(
+ $value,
+ $this->html()
+ );
+ }
+
+ return $this;
+ }
+
+ function assertSeeHtmlInOrder($values)
+ {
+ PHPUnit::assertThat(
+ $values,
+ new SeeInOrder($this->html())
+ );
+
+ return $this;
+ }
+
+ function assertDontSeeHtml($values)
+ {
+ foreach (Arr::wrap($values) as $value) {
+ PHPUnit::assertStringNotContainsString(
+ $value,
+ $this->html()
+ );
+ }
+
+ return $this;
+ }
+
+ function assertSeeText($value, $escape = true)
+ {
+ $value = Arr::wrap($value);
+
+ $values = $escape ? array_map('e', ($value)) : $value;
+
+ $content = $this->html();
+
+ tap(strip_tags($content), function ($content) use ($values) {
+ foreach ($values as $value) {
+ PHPUnit::assertStringContainsString((string) $value, $content);
+ }
+ });
+
+ return $this;
+ }
+
+ function assertDontSeeText($value, $escape = true)
+ {
+ $value = Arr::wrap($value);
+
+ $values = $escape ? array_map('e', ($value)) : $value;
+
+ $content = $this->html();
+
+ tap(strip_tags($content), function ($content) use ($values) {
+ foreach ($values as $value) {
+ PHPUnit::assertStringNotContainsString((string) $value, $content);
+ }
+ });
+
+ return $this;
+ }
+
+ function assertSet($name, $value, $strict = false)
+ {
+ $actual = $this->get($name);
+
+ if (! is_string($value) && is_callable($value)) {
+ PHPUnit::assertTrue($value($actual));
+ } else {
+ $strict ? PHPUnit::assertSame($value, $actual) : PHPUnit::assertEquals($value, $actual);
+ }
+
+ return $this;
+ }
+
+ function assertNotSet($name, $value, $strict = false)
+ {
+ $actual = $this->get($name);
+
+ $strict ? PHPUnit::assertNotSame($value, $actual) : PHPUnit::assertNotEquals($value, $actual);
+
+ return $this;
+ }
+
+ function assertSetStrict($name, $value)
+ {
+ $this->assertSet($name, $value, true);
+
+ return $this;
+ }
+
+ function assertNotSetStrict($name, $value)
+ {
+ $this->assertNotSet($name, $value, true);
+
+ return $this;
+ }
+
+ function assertCount($name, $value)
+ {
+ PHPUnit::assertCount($value, $this->get($name));
+
+ return $this;
+ }
+
+ function assertSnapshotSet($name, $value, $strict = false)
+ {
+ $data = $this->lastState->getSnapshotData();
+
+ if (is_callable($value)) {
+ PHPUnit::assertTrue($value(data_get($data, $name)));
+ } else {
+ $strict ? PHPUnit::assertSame($value, data_get($data, $name)) : PHPUnit::assertEquals($value, data_get($data, $name));
+ }
+
+ return $this;
+ }
+
+ function assertSnapshotNotSet($name, $value, $strict = false)
+ {
+ $data = $this->lastState->getSnapshotData();
+
+ if (is_callable($value)) {
+ PHPUnit::assertFalse($value(data_get($data, $name)));
+ } else {
+ $strict ? PHPUnit::assertNotSame($value, data_get($data, $name)) : PHPUnit::assertNotEquals($value, data_get($data, $name));
+ }
+
+ return $this;
+ }
+
+ function assertSnapshotSetStrict($name, $value)
+ {
+ $this->assertSnapshotSet($name, $value, true);
+
+ return $this;
+ }
+
+ function assertSnapshotNotSetStrict($name, $value)
+ {
+ $this->assertSnapshotNotSet($name, $value, true);
+
+ return $this;
+ }
+
+ public function assertReturned($value)
+ {
+ $data = data_get($this->lastState->getEffects(), 'returns.0');
+
+ if (is_callable($value)) {
+ PHPUnit::assertTrue($value($data));
+ } else {
+ PHPUnit::assertEquals($value, $data);
+ }
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/Render.php b/portman/lib/Livewire/Features/SupportTesting/Render.php
new file mode 100644
index 00000000..299085c4
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/Render.php
@@ -0,0 +1,30 @@
+app = $app;
+ }
+
+ function temporarilyDisableExceptionHandlingAndMiddleware($callback)
+ {
+ $cachedHandler = app(ExceptionHandler::class);
+
+ $cachedShouldSkipMiddleware = $this->app->shouldSkipMiddleware();
+
+ $this->withoutExceptionHandling([HttpException::class, AuthorizationException::class])->withoutMiddleware();
+
+ $result = $callback($this);
+
+ $this->app->instance(ExceptionHandler::class, $cachedHandler);
+
+ if (! $cachedShouldSkipMiddleware) {
+ unset($this->app['middleware.disable']);
+ }
+
+ return $result;
+ }
+
+ function withoutHandling($except = [])
+ {
+ return $this->withoutExceptionHandling($except);
+ }
+
+ function addHeaders(array $headers)
+ {
+ $this->serverVariables = $this->transformHeadersToServerVars($headers);
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/ShowDuskComponent.php b/portman/lib/Livewire/Features/SupportTesting/ShowDuskComponent.php
new file mode 100644
index 00000000..e5c074ac
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/ShowDuskComponent.php
@@ -0,0 +1,13 @@
+call(app('livewire')->new($class));
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/SubsequentRender.php b/portman/lib/Livewire/Features/SupportTesting/SubsequentRender.php
new file mode 100644
index 00000000..207b1d89
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/SubsequentRender.php
@@ -0,0 +1,77 @@
+makeSubsequentRequest($calls, $updates, $cookies);
+ }
+
+ function makeSubsequentRequest($calls = [], $updates = [], $cookies = []) {
+ $uri = app('livewire')->getUpdateUri();
+
+ $encodedSnapshot = json_encode($this->lastState->getSnapshot());
+
+ $payload = [
+ 'components' => [
+ [
+ 'snapshot' => $encodedSnapshot,
+ 'calls' => $calls,
+ 'updates' => $updates,
+ ],
+ ],
+ ];
+
+ [$response, $componentInstance, $componentView] = $this->extractComponentAndBladeView(function () use ($uri, $payload, $cookies) {
+ return $this->requestBroker->temporarilyDisableExceptionHandlingAndMiddleware(function ($requestBroker) use ($uri, $payload, $cookies) {
+ return $requestBroker->addHeaders(['X-Livewire' => true])->call('POST', $uri, $payload, $cookies);
+ });
+ });
+
+ app('livewire')->flushState();
+
+ if (! $response->isOk()) {
+ return new ComponentState(
+ $componentInstance,
+ $response,
+ null,
+ '',
+ [],
+ [],
+ );
+ }
+
+ $json = $response->json();
+
+ // Set "original" to Blade view for assertions like "assertViewIs()"...
+ $response->original = $componentView;
+
+ $componentResponsePayload = $json['components'][0];
+
+ $snapshot = json_decode($componentResponsePayload['snapshot'], true);
+
+ $effects = $componentResponsePayload['effects'];
+
+ // If no new HTML has been rendered, let's forward the last known HTML...
+ $html = $effects['html'] ?? $this->lastState->getHtml(stripInitialData: true);
+ $view = $componentView ?? $this->lastState->getView();
+
+ return new ComponentState(
+ $componentInstance,
+ $response,
+ $view,
+ $html,
+ $snapshot,
+ $effects,
+ );
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/SupportTesting.php b/portman/lib/Livewire/Features/SupportTesting/SupportTesting.php
new file mode 100644
index 00000000..ed1c5900
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/SupportTesting.php
@@ -0,0 +1,111 @@
+environment('testing')) return;
+
+ if (class_exists('Laravel\Dusk\Browser')) {
+ DuskTestable::provide();
+ }
+
+ static::registerTestingMacros();
+ }
+
+ function dehydrate($context)
+ {
+ $target = $this->component;
+
+ $errors = $target->getErrorBag();
+
+ if (! $errors->isEmpty()) {
+ $this->storeSet('testing.errors', $errors);
+ }
+ }
+
+ function hydrate()
+ {
+ $this->storeSet('testing.validator', null);
+ }
+
+ function exception($e, $stopPropagation) {
+ if (! $e instanceof ValidationException) return;
+
+ $this->storeSet('testing.validator', $e->validator);
+ }
+
+ protected static function registerTestingMacros()
+ {
+ // Usage: $this->assertSeeLivewire('counter');
+ \Illuminate\Testing\TestResponse::macro('assertSeeLivewire', function ($component) {
+ if (is_subclass_of($component, Component::class)) {
+ $component = app(ComponentRegistry::class)->getName($component);
+ }
+ $escapedComponentName = trim(htmlspecialchars(json_encode(['name' => $component])), '{}');
+
+ \PHPUnit\Framework\Assert::assertStringContainsString(
+ $escapedComponentName,
+ $this->getContent(),
+ 'Cannot find Livewire component ['.$component.'] rendered on page.'
+ );
+
+ return $this;
+ });
+
+ // Usage: $this->assertDontSeeLivewire('counter');
+ \Illuminate\Testing\TestResponse::macro('assertDontSeeLivewire', function ($component) {
+ if (is_subclass_of($component, Component::class)) {
+ $component = app(ComponentRegistry::class)->getName($component);
+ }
+ $escapedComponentName = trim(htmlspecialchars(json_encode(['name' => $component])), '{}');
+
+ \PHPUnit\Framework\Assert::assertStringNotContainsString(
+ $escapedComponentName,
+ $this->getContent(),
+ 'Found Livewire component ['.$component.'] rendered on page.'
+ );
+
+ return $this;
+ });
+
+ if (class_exists(\Illuminate\Testing\TestView::class)) {
+ \Illuminate\Testing\TestView::macro('assertSeeLivewire', function ($component) {
+ if (is_subclass_of($component, Component::class)) {
+ $component = app(ComponentRegistry::class)->getName($component);
+ }
+ $escapedComponentName = trim(htmlspecialchars(json_encode(['name' => $component])), '{}');
+
+ \PHPUnit\Framework\Assert::assertStringContainsString(
+ $escapedComponentName,
+ $this->rendered,
+ 'Cannot find Livewire component ['.$component.'] rendered on page.'
+ );
+
+ return $this;
+ });
+
+ \Illuminate\Testing\TestView::macro('assertDontSeeLivewire', function ($component) {
+ if (is_subclass_of($component, Component::class)) {
+ $component = app(ComponentRegistry::class)->getName($component);
+ }
+ $escapedComponentName = trim(htmlspecialchars(json_encode(['name' => $component])), '{}');
+
+ \PHPUnit\Framework\Assert::assertStringNotContainsString(
+ $escapedComponentName,
+ $this->rendered,
+ 'Found Livewire component ['.$component.'] rendered on page.'
+ );
+
+ return $this;
+ });
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportTesting/Testable.php b/portman/lib/Livewire/Features/SupportTesting/Testable.php
new file mode 100644
index 00000000..e9bc791d
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportTesting/Testable.php
@@ -0,0 +1,405 @@
+|object $name
+ *
+ * @return string
+ */
+ static function normalizeAndRegisterComponentName($name)
+ {
+ if (is_array($otherComponents = $name)) {
+ $name = array_shift($otherComponents);
+
+ foreach ($otherComponents as $key => $value) {
+ if (is_numeric($key)) {
+ app('livewire')->isDiscoverable($name) || app('livewire')->component($value);
+ } else {
+ app('livewire')->component($key, $value);
+ }
+ }
+ } elseif (is_object($name)) {
+ $anonymousClassComponent = $name;
+
+ $name = str()->random(10);
+
+ app('livewire')->component($name, $anonymousClassComponent);
+ } else {
+ app('livewire')->isDiscoverable($name) || app('livewire')->component($name);
+ }
+
+ return $name;
+ }
+
+ /**
+ * @param ?string $driver
+ *
+ * @return void
+ */
+ static function actingAs(\Illuminate\Contracts\Auth\Authenticatable $user, $driver = null)
+ {
+ if (isset($user->wasRecentlyCreated) && $user->wasRecentlyCreated) {
+ $user->wasRecentlyCreated = false;
+ }
+
+ auth()->guard($driver)->setUser($user);
+
+ auth()->shouldUse($driver);
+ }
+
+ function id() {
+ return $this->lastState->getComponent()->getId();
+ }
+
+ /**
+ * @param string $key
+ */
+ function get($key)
+ {
+ return data_get($this->lastState->getComponent(), $key);
+ }
+
+ /**
+ * @param bool $stripInitialData
+ *
+ * @return string
+ */
+ function html($stripInitialData = false)
+ {
+ return $this->lastState->getHtml($stripInitialData);
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ function updateProperty($name, $value = null)
+ {
+ return $this->set($name, $value);
+ }
+
+ /**
+ * @param array $values
+ *
+ * @return $this
+ */
+ function fill($values)
+ {
+ foreach ($values as $name => $value) {
+ $this->set($name, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ function toggle($name)
+ {
+ return $this->set($name, ! $this->get($name));
+ }
+
+ /**
+ * @param string|array $name
+ *
+ * @return $this
+ */
+ function set($name, $value = null)
+ {
+ if (is_array($name)) {
+ foreach ($name as $key => $value) {
+ $this->setProperty($key, $value);
+ }
+ } else {
+ $this->setProperty($name, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ function setProperty($name, $value)
+ {
+ if ($value instanceof \Illuminate\Http\UploadedFile) {
+ return $this->upload($name, [$value]);
+ } elseif (is_array($value) && isset($value[0]) && $value[0] instanceof \Illuminate\Http\UploadedFile) {
+ return $this->upload($name, $value, $isMultiple = true);
+ } elseif ($value instanceof BackedEnum) {
+ $value = $value->value;
+ }
+
+ return $this->update(updates: [$name => $value]);
+ }
+
+ /**
+ * @param string $method
+ *
+ * @return $this
+ */
+ function runAction($method, ...$params)
+ {
+ return $this->call($method, ...$params);
+ }
+
+ /**
+ * @param string $method
+ *
+ * @return $this
+ */
+ function call($method, ...$params)
+ {
+ if ($method === '$refresh') {
+ return $this->commit();
+ }
+
+ if ($method === '$set') {
+ return $this->set(...$params);
+ }
+
+ return $this->update(calls: [
+ [
+ 'method' => $method,
+ 'params' => $params,
+ 'path' => '',
+ ]
+ ]);
+ }
+
+ /**
+ * @return $this
+ */
+ function commit()
+ {
+ return $this->update();
+ }
+
+ /**
+ * @return $this
+ */
+ function refresh()
+ {
+ return $this->update();
+ }
+
+ /**
+ * @param array $calls
+ * @param array $updates
+ *
+ * @return $this
+ */
+ function update($calls = [], $updates = [])
+ {
+ $newState = SubsequentRender::make(
+ $this->requestBroker,
+ $this->lastState,
+ $calls,
+ $updates,
+ app('request')->cookies->all()
+ );
+
+ $this->lastState = $newState;
+
+ return $this;
+ }
+
+ /**
+ * @todo Move me outta here and into the file upload folder somehow...
+ *
+ * @param string $name
+ * @param array $files
+ * @param bool $isMultiple
+ *
+ * @return $this
+ */
+ function upload($name, $files, $isMultiple = false)
+ {
+ // This method simulates the calls Livewire's JavaScript
+ // normally makes for file uploads.
+ $this->call(
+ '_startUpload',
+ $name,
+ collect($files)->map(function ($file) {
+ return [
+ 'name' => $file->name,
+ 'size' => $file->getSize(),
+ 'type' => $file->getMimeType(),
+ ];
+ })->toArray(),
+ $isMultiple,
+ );
+
+ // This is where either the pre-signed S3 url or the regular Livewire signed
+ // upload url would do its thing and return a hashed version of the uploaded
+ // file in a tmp directory.
+ $storage = \Livewire\Features\SupportFileUploads\FileUploadConfiguration::storage();
+ try {
+ $fileHashes = (new \Livewire\Features\SupportFileUploads\FileUploadController)->validateAndStore($files, \Livewire\Features\SupportFileUploads\FileUploadConfiguration::disk());
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ $this->call('_uploadErrored', $name, json_encode(['errors' => $e->errors()]), $isMultiple);
+
+ return $this;
+ }
+
+ // We are going to encode the original file size, mimeType and hashName in the filename
+ // so when we create a new TemporaryUploadedFile instance we can fake the
+ // same file size, mimeType and hashName set for the original file upload.
+ $newFileHashes = collect($files)->zip($fileHashes)->mapSpread(function ($file, $fileHash) {
+ // MimeTypes contain slashes, so we replace them with underscores to ensure the filename is valid.
+ $escapedMimeType = (string) str($file->getMimeType())->replace('/', '_');
+
+ return (string) str($fileHash)->replaceFirst('.', "-hash={$file->hashName()}-mimeType={$escapedMimeType}-size={$file->getSize()}.");
+ })->toArray();
+
+ collect($fileHashes)->zip($newFileHashes)->mapSpread(function ($fileHash, $newFileHash) use ($storage) {
+ $storage->move('/'.\Livewire\Features\SupportFileUploads\FileUploadConfiguration::path($fileHash), '/'.\Livewire\Features\SupportFileUploads\FileUploadConfiguration::path($newFileHash));
+ });
+
+ // Now we finish the upload with a final call to the Livewire component
+ // with the temporarily uploaded file path.
+ $this->call('_finishUpload', $name, $newFileHashes, $isMultiple);
+
+ return $this;
+ }
+
+ /**
+ * @param string $key
+ */
+ function viewData($key)
+ {
+ return $this->lastState->getView()->getData()[$key];
+ }
+
+ function getData()
+ {
+ return $this->lastState->getSnapshotData();
+ }
+
+ function instance()
+ {
+ return $this->lastState->getComponent();
+ }
+
+ /**
+ * @return \Livewire\Component
+ */
+ function invade()
+ {
+ return \Livewire\invade($this->lastState->getComponent());
+ }
+
+ /**
+ * @return $this
+ */
+ function dump()
+ {
+ dump($this->lastState->getHtml());
+
+ return $this;
+ }
+
+ /**
+ * @return void
+ */
+ function dd()
+ {
+ dd($this->lastState->getHtml());
+ }
+
+ /**
+ * @return $this
+ */
+ function tap($callback)
+ {
+ $callback($this);
+
+ return $this;
+ }
+
+ /**
+ * @param string $property
+ */
+ function __get($property)
+ {
+ if ($property === 'effects') return $this->lastState->getEffects();
+ if ($property === 'snapshot') return $this->lastState->getSnapshot();
+ if ($property === 'target') return $this->lastState->getComponent();
+
+ return $this->instance()->$property;
+ }
+
+ /**
+ * @param string $method
+ *
+ * @return $this
+ */
+ function __call($method, $params)
+ {
+ if (static::hasMacro($method)) {
+ return $this->macroCall($method, $params);
+ }
+
+ $this->lastState->getResponse()->{$method}(...$params);
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportValidation/BaseRule.php b/portman/lib/Livewire/Features/SupportValidation/BaseRule.php
new file mode 100644
index 00000000..f69c24a0
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportValidation/BaseRule.php
@@ -0,0 +1,11 @@
+subTarget ?: $this->component;
+ $name = $this->subTarget ? $this->getSubName() : $this->getName();
+
+ $rules = [];
+
+ if (is_null($this->rule)) {
+ // Allow "Rule" to be used without a given validation rule. It's purpose is to instead
+ // trigger validation on property updates...
+ } elseif (is_array($this->rule) && count($this->rule) > 0 && ! is_numeric(array_keys($this->rule)[0])) {
+ // Support setting rules by key-value for this and other properties:
+ // For example, #[Validate(['foo' => 'required', 'foo.*' => 'required'])]
+ $rules = $this->rule;
+ } else {
+ $rules[$this->getSubName()] = $this->rule;
+ }
+
+ if ($this->attribute) {
+ if (is_array($this->attribute)) {
+ $target = $this->subTarget ?? $this->component;
+ $target->addValidationAttributesFromOutside($this->attribute);
+ } else {
+ $target->addValidationAttributesFromOutside([$name => $this->attribute]);
+ }
+ }
+
+ if ($this->as) {
+ if (is_array($this->as)) {
+ $as = $this->translate
+ ? array_map(fn ($i) => trans($i), $this->as)
+ : $this->as;
+
+ $target->addValidationAttributesFromOutside($as);
+ } else {
+ $target->addValidationAttributesFromOutside([$name => $this->translate ? trans($this->as) : $this->as]);
+ }
+ }
+
+ if ($this->message) {
+ if (is_array($this->message)) {
+ $messages = $this->translate
+ ? array_map(fn ($i) => trans($i), $this->message)
+ : $this->message;
+
+ $target->addMessagesFromOutside($messages);
+ } else {
+ // If a single message was provided, apply it to the first given rule.
+ // There should have only been one rule provided in this case anyways...
+ $rule = head(array_values($rules));
+
+ // In the case of "min:5" or something, we only want "min"...
+ $rule = (string) str($rule)->before(':');
+
+ $target->addMessagesFromOutside([$name.'.'.$rule => $this->translate ? trans($this->message) : $this->message]);
+ }
+ }
+
+ $target->addRulesFromOutside($rules);
+ }
+
+ function update($fullPath, $newValue)
+ {
+ if ($this->onUpdate === false) return;
+
+ return function () {
+ // If this attribute is added to a "form object", we want to run
+ // the validateOnly method on the form object, not the base component...
+ $target = $this->subTarget ?: $this->component;
+ $name = $this->subTarget ? $this->getSubName() : $this->getName();
+
+ // Here we have to run the form object validator from the context
+ // of the base "wrapped" component so that validation works...
+ wrap($this->component)->tap(function () use ($target, $name) {
+ $target->validateOnly($name);
+ });
+ };
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportValidation/HandlesValidation.php b/portman/lib/Livewire/Features/SupportValidation/HandlesValidation.php
new file mode 100644
index 00000000..f96930fb
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportValidation/HandlesValidation.php
@@ -0,0 +1,523 @@
+rulesFromOutside[] = $rules;
+ }
+
+ public function addMessagesFromOutside($messages)
+ {
+ $this->messagesFromOutside[] = $messages;
+ }
+
+ public function addValidationAttributesFromOutside($validationAttributes)
+ {
+ $this->validationAttributesFromOutside[] = $validationAttributes;
+ }
+
+ public function getErrorBag()
+ {
+ if (! store($this)->has('errorBag')) {
+ $previouslySharedErrors = app('view')->getShared()['errors'] ?? new ViewErrorBag;
+ $this->setErrorBag($previouslySharedErrors->getMessages());
+ }
+
+ return store($this)->get('errorBag');
+ }
+
+ public function addError($name, $message)
+ {
+ return $this->getErrorBag()->add($name, $message);
+ }
+
+ public function setErrorBag($bag)
+ {
+ return store($this)->set('errorBag', $bag instanceof MessageBag
+ ? $bag
+ : new MessageBag($bag)
+ );
+ }
+
+ public function resetErrorBag($field = null)
+ {
+ $fields = (array) $field;
+
+ if (empty($fields)) {
+ $errorBag = new MessageBag;
+
+ $this->setErrorBag($errorBag);
+
+ return $errorBag;
+ }
+
+ $this->setErrorBag(
+ $this->errorBagExcept($fields)
+ );
+ }
+
+ public function clearValidation($field = null)
+ {
+ $this->resetErrorBag($field);
+ }
+
+ public function resetValidation($field = null)
+ {
+ $this->resetErrorBag($field);
+ }
+
+ public function errorBagExcept($field)
+ {
+ $fields = (array) $field;
+
+ return new MessageBag(
+ collect($this->getErrorBag())
+ ->reject(function ($messages, $messageKey) use ($fields) {
+ return collect($fields)->some(function ($field) use ($messageKey) {
+ return str($messageKey)->is($field);
+ });
+ })
+ ->toArray()
+ );
+ }
+
+ public function getRules()
+ {
+ $rulesFromComponent = [];
+
+ if (method_exists($this, 'rules')) $rulesFromComponent = $this->rules();
+ else if (property_exists($this, 'rules')) $rulesFromComponent = $this->rules;
+
+ $rulesFromOutside = array_merge_recursive(
+ ...array_map(
+ fn($i) => value($i),
+ $this->rulesFromOutside
+ )
+ );
+
+ return array_merge($rulesFromComponent, $rulesFromOutside);
+ }
+
+ protected function getMessages()
+ {
+ $messages = [];
+
+ if (method_exists($this, 'messages')) $messages = $this->messages();
+ elseif (property_exists($this, 'messages')) $messages = $this->messages;
+
+ $messagesFromOutside = array_merge(
+ ...array_map(
+ fn($i) => value($i),
+ $this->messagesFromOutside
+ )
+ );
+
+ return array_merge($messages, $messagesFromOutside);
+ }
+
+ protected function getValidationAttributes()
+ {
+ $validationAttributes = [];
+
+ if (method_exists($this, 'validationAttributes')) $validationAttributes = $this->validationAttributes();
+ elseif (property_exists($this, 'validationAttributes')) $validationAttributes = $this->validationAttributes;
+
+ $validationAttributesFromOutside = array_merge(
+ ...array_map(
+ fn($i) => value($i),
+ $this->validationAttributesFromOutside
+ )
+ );
+
+ return array_merge($validationAttributes, $validationAttributesFromOutside);
+ }
+
+ protected function getValidationCustomValues()
+ {
+ if (method_exists($this, 'validationCustomValues')) return $this->validationCustomValues();
+ if (property_exists($this, 'validationCustomValues')) return $this->validationCustomValues;
+
+ return [];
+ }
+
+ public function rulesForModel($name)
+ {
+ if (empty($this->getRules())) return collect();
+
+ return collect($this->getRules())
+ ->filter(function ($value, $key) use ($name) {
+ return Utils::beforeFirstDot($key) === $name;
+ });
+ }
+
+ public function hasRuleFor($dotNotatedProperty)
+ {
+ $propertyWithStarsInsteadOfNumbers = $this->ruleWithNumbersReplacedByStars($dotNotatedProperty);
+
+ // If property has numeric indexes in it,
+ if ($dotNotatedProperty !== $propertyWithStarsInsteadOfNumbers) {
+ return collect($this->getRules())->keys()->contains($propertyWithStarsInsteadOfNumbers);
+ }
+
+ return collect($this->getRules())
+ ->keys()
+ ->map(function ($key) {
+ return (string) str($key)->before('.*');
+ })->contains($dotNotatedProperty);
+ }
+
+ public function ruleWithNumbersReplacedByStars($dotNotatedProperty)
+ {
+ // Convert foo.0.bar.1 -> foo.*.bar.*
+ return (string) str($dotNotatedProperty)
+ // Replace all numeric indexes with an array wildcard: (.0., .10., .007.) => .*.
+ // In order to match overlapping numerical indexes (foo.1.2.3.4.name),
+ // We need to use a positive look-behind, that's technically all the magic here.
+ // For better understanding, see: https://regexr.com/5d1n3
+ ->replaceMatches('/(?<=(\.))\d+\./', '*.')
+ // Replace all numeric indexes at the end of the name with an array wildcard
+ // (Same as the previous regex, but ran only at the end of the string)
+ // For better undestanding, see: https://regexr.com/5d1n6
+ ->replaceMatches('/\.\d+$/', '.*');
+ }
+
+ public function missingRuleFor($dotNotatedProperty)
+ {
+ return ! $this->hasRuleFor($dotNotatedProperty);
+ }
+
+ public function withValidator($callback)
+ {
+ $this->withValidatorCallback = $callback;
+
+ return $this;
+ }
+
+ protected function checkRuleMatchesProperty($rules, $data)
+ {
+ collect($rules)
+ ->keys()
+ ->each(function($ruleKey) use ($data) {
+ throw_unless(
+ array_key_exists(Utils::beforeFirstDot($ruleKey), $data),
+ new \Exception('No property found for validation: ['.$ruleKey.']')
+ );
+ });
+ }
+
+ public function validate($rules = null, $messages = [], $attributes = [])
+ {
+ $isUsingGlobalRules = is_null($rules);
+
+ [$rules, $messages, $attributes] = $this->providedOrGlobalRulesMessagesAndAttributes($rules, $messages, $attributes);
+
+ $data = $this->prepareForValidation(
+ $this->getDataForValidation($rules)
+ );
+
+ $this->checkRuleMatchesProperty($rules, $data);
+
+ $ruleKeysToShorten = $this->getModelAttributeRuleKeysToShorten($data, $rules);
+
+ $data = $this->unwrapDataForValidation($data);
+
+ $validator = Validator::make($data, $rules, $messages, $attributes);
+
+ if ($this->withValidatorCallback) {
+ call_user_func($this->withValidatorCallback, $validator);
+
+ $this->withValidatorCallback = null;
+ }
+
+ $this->shortenModelAttributesInsideValidator($ruleKeysToShorten, $validator);
+
+ $customValues = $this->getValidationCustomValues();
+
+ if (! empty($customValues)) {
+ $validator->addCustomValues($customValues);
+ }
+
+ if ($this->isRootComponent() && $isUsingGlobalRules) {
+ $validatedData = $this->withFormObjectValidators($validator, fn () => $validator->validate(), fn ($form) => $form->validate());
+ } else {
+ $validatedData = $validator->validate();
+ }
+
+ $this->resetErrorBag();
+
+ return $validatedData;
+ }
+
+ protected function isRootComponent()
+ {
+ // Because this trait is used for form objects as well...
+ return $this instanceof \Livewire\Component;
+ }
+
+ protected function withFormObjectValidators($validator, $validateSelf, $validateForm)
+ {
+ $cumulativeErrors = new MessageBag;
+ $cumulativeData = [];
+ $formExceptions = [];
+
+ // First, run sub-validators...
+ foreach ($this->getFormObjects() as $form) {
+ try {
+ // Only run sub-validator if the sub-validator has rules...
+ if (filled($form->getRules())) {
+ $cumulativeData = array_merge($cumulativeData, $validateForm($form));
+ }
+ } catch (ValidationException $e) {
+ $cumulativeErrors->merge($e->validator->errors());
+
+ $formExceptions[] = $e;
+ }
+ }
+
+ // Now run main validator...
+ try {
+ $cumulativeData = array_merge($cumulativeData, $validateSelf());
+ } catch (ValidationException $e) {
+ // If the main validator has errors, merge them with subs and rethrow...
+ $e->validator->errors()->merge($cumulativeErrors);
+
+ throw $e;
+ }
+
+ // If main validation passed, go through other sub-validation exceptions
+ // and throw the first one with the cumulative messages...
+ foreach ($formExceptions as $e) {
+ $exceptionErrorKeys = $e->validator->errors()->keys();
+ $remainingErrors = Arr::except($cumulativeErrors->messages(), $exceptionErrorKeys);
+ $e->validator->errors()->merge($remainingErrors);
+
+ throw $e;
+ }
+
+ // All validation has passed, we can return the data...
+ return $cumulativeData;
+ }
+
+ public function validateOnly($field, $rules = null, $messages = [], $attributes = [], $dataOverrides = [])
+ {
+ $property = (string) str($field)->before('.');
+
+ // If validating a field in a form object, defer validation to that form object...
+ if (
+ $this->isRootComponent()
+ && ($form = $this->all()[$property] ?? false) instanceof Form
+ ) {
+ $stripPrefix = (string) str($field)->after('.');
+ return $form->validateOnly($stripPrefix, $rules, $messages, $attributes, $dataOverrides);
+ }
+
+ $isUsingGlobalRules = is_null($rules);
+
+ [$rules, $messages, $attributes] = $this->providedOrGlobalRulesMessagesAndAttributes($rules, $messages, $attributes);
+
+ // Loop through rules and swap any wildcard '*' with keys from field, then filter down to only
+ // rules that match the field, but return the rules without wildcard characters replaced,
+ // so that custom attributes and messages still work as they need wildcards to work.
+ $rulesForField = collect($rules)
+ ->filter(function($value, $rule) use ($field) {
+ if(! str($field)->is($rule)) {
+ return false;
+ }
+
+ $fieldArray = str($field)->explode('.');
+ $ruleArray = str($rule)->explode('.');
+
+ for($i = 0; $i < count($fieldArray); $i++) {
+ if(isset($ruleArray[$i]) && $ruleArray[$i] === '*') {
+ $ruleArray[$i] = $fieldArray[$i];
+ }
+ }
+
+ $rule = $ruleArray->join('.');
+
+ return $field === $rule;
+ });
+
+ $ruleForField = $rulesForField->keys()->first();
+
+ $rulesForField = $rulesForField->toArray();
+
+ $ruleKeysForField = array_keys($rulesForField);
+
+ $data = array_merge($this->getDataForValidation($rules), $dataOverrides);
+
+ $data = $this->prepareForValidation($data);
+
+ $this->checkRuleMatchesProperty($rules, $data);
+
+ $ruleKeysToShorten = $this->getModelAttributeRuleKeysToShorten($data, $rules);
+
+ $data = $this->unwrapDataForValidation($data);
+
+ // If a matching rule is found, then filter collections down to keys specified in the field,
+ // while leaving all other data intact. If a key isn't specified and instead there is a
+ // wildcard '*' then leave that whole collection intact. This ensures that any rules
+ // that depend on other fields/ properties still work.
+ if ($ruleForField) {
+ $ruleArray = str($ruleForField)->explode('.');
+ $fieldArray = str($field)->explode('.');
+
+ $data = $this->filterCollectionDataDownToSpecificKeys($data, $ruleArray, $fieldArray);
+ }
+
+ $validator = Validator::make($data, $rulesForField, $messages, $attributes);
+
+ if ($this->withValidatorCallback) {
+ call_user_func($this->withValidatorCallback, $validator);
+
+ $this->withValidatorCallback = null;
+ }
+
+ $this->shortenModelAttributesInsideValidator($ruleKeysToShorten, $validator);
+
+ $customValues = $this->getValidationCustomValues();
+ if (!empty($customValues)) {
+ $validator->addCustomValues($customValues);
+ }
+
+ try {
+ $result = $validator->validate();
+ } catch (ValidationException $e) {
+ $messages = $e->validator->getMessageBag();
+
+ invade($e->validator)->messages = $messages->merge(
+ $this->errorBagExcept($ruleKeysForField)
+ );
+
+ throw $e;
+ }
+
+ $this->resetErrorBag($ruleKeysForField);
+
+ return $result;
+ }
+
+ protected function filterCollectionDataDownToSpecificKeys($data, $ruleKeys, $fieldKeys)
+ {
+
+ // Filter data down to specified keys in collections, but leave all other data intact
+ if (count($ruleKeys)) {
+ $ruleKey = $ruleKeys->shift();
+ $fieldKey = $fieldKeys->shift();
+
+ if ($fieldKey == '*') {
+ // If the specified field has a '*', then loop through the collection and keep the whole collection intact.
+ foreach ($data as $key => $value) {
+ $data[$key] = $this->filterCollectionDataDownToSpecificKeys($value, $ruleKeys, $fieldKeys);
+ }
+ } else {
+ // Otherwise filter collection down to a specific key
+ $keyData = data_get($data, $fieldKey, null);
+
+ if ($ruleKey == '*') {
+ $data = [];
+ }
+
+ data_set($data, $fieldKey, $this->filterCollectionDataDownToSpecificKeys($keyData, $ruleKeys, $fieldKeys));
+ }
+ }
+
+ return $data;
+ }
+
+ protected function getModelAttributeRuleKeysToShorten($data, $rules)
+ {
+ // If a model ($foo) is a property, and the validation rule is
+ // "foo.bar", then set the attribute to just "bar", so that
+ // the validation message is shortened and more readable.
+
+ $toShorten = [];
+
+ foreach ($rules as $key => $value) {
+ $propertyName = Utils::beforeFirstDot($key);
+
+ if ($data[$propertyName] instanceof Model) {
+ $toShorten[] = $key;
+ }
+ }
+
+ return $toShorten;
+ }
+
+ protected function shortenModelAttributesInsideValidator($ruleKeys, $validator)
+ {
+ foreach ($ruleKeys as $key) {
+ if (str($key)->snake()->replace('_', ' ')->is($validator->getDisplayableAttribute($key))) {
+ $validator->addCustomAttributes([$key => $validator->getDisplayableAttribute(Utils::afterFirstDot($key))]);
+ }
+ }
+ }
+
+ protected function providedOrGlobalRulesMessagesAndAttributes($rules, $messages, $attributes)
+ {
+ $rules = is_null($rules) ? $this->getRules() : $rules;
+
+ // Before we warn the user about not providing validation rules,
+ // Let's make sure there are no form objects that contain them...
+ $allRules = $rules;
+
+ if ($this->isRootComponent()) {
+ foreach ($this->getFormObjects() as $form) {
+ $allRules = array_merge($allRules, $form->getRules());
+ }
+ }
+
+ throw_if(empty($allRules), new MissingRulesException($this));
+
+ $messages = empty($messages) ? $this->getMessages() : $messages;
+ $attributes = empty($attributes) ? $this->getValidationAttributes() : $attributes;
+
+ return [$rules, $messages, $attributes];
+ }
+
+ protected function getDataForValidation($rules)
+ {
+ return Utils::getPublicPropertiesDefinedOnSubclass($this);
+ }
+
+ protected function unwrapDataForValidation($data)
+ {
+ return collect($data)->map(function ($value) {
+
+ $synth = app('livewire')->findSynth($value, $this);
+
+ if ($synth && method_exists($synth, 'unwrapForValidation')) return $synth->unwrapForValidation($value);
+ else if ($value instanceof Arrayable) return $value->toArray();
+
+ return $value;
+ })->all();
+ }
+
+ protected function prepareForValidation($attributes)
+ {
+ return $attributes;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportValidation/SupportValidation.php b/portman/lib/Livewire/Features/SupportValidation/SupportValidation.php
new file mode 100644
index 00000000..2cfdaf0f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportValidation/SupportValidation.php
@@ -0,0 +1,54 @@
+component->setErrorBag(
+ $memo['errors'] ?? []
+ );
+ }
+
+ function render($view, $data)
+ {
+ $errors = (new ViewErrorBag)->put('default', $this->component->getErrorBag());
+
+ $revert = Utils::shareWithViews('errors', $errors);
+
+ return function () use ($revert) {
+ // After the component has rendered, let's revert our global
+ // sharing of the "errors" variable with blade views...
+ $revert();
+ };
+ }
+
+ function dehydrate($context)
+ {
+ $errors = $this->component->getErrorBag()->toArray();
+
+ // Only persist errors that were born from properties on the component
+ // and not from custom validators (Validator::make) that were run.
+ $context->addMemo('errors', collect($errors)
+ ->filter(function ($value, $key) {
+ return Utils::hasProperty($this->component, $key);
+ })
+ ->toArray()
+ );
+ }
+
+ function exception($e, $stopPropagation)
+ {
+ if (! $e instanceof ValidationException) return;
+
+ $this->component->setErrorBag($e->validator->errors());
+
+ $stopPropagation();
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportValidation/TestsValidation.php b/portman/lib/Livewire/Features/SupportValidation/TestsValidation.php
new file mode 100644
index 00000000..fbf0a39e
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportValidation/TestsValidation.php
@@ -0,0 +1,135 @@
+target)->get('testing.validator');
+
+ if ($validator) return $validator->errors();
+
+ $errors = store($this->target)->get('testing.errors');
+
+ if ($errors) return $errors;
+
+ return new MessageBag;
+ }
+
+ function failedRules()
+ {
+ $validator = store($this->target)->get('testing.validator');
+
+ return $validator ? $validator->failed() : [];
+ }
+
+ public function assertHasErrors($keys = [])
+ {
+ $errors = $this->errors();
+
+ PHPUnit::assertTrue($errors->isNotEmpty(), 'Component has no errors.');
+
+ $keys = (array) $keys;
+
+ foreach ($keys as $key => $value) {
+ if (is_int($key)) {
+ $this->makeErrorAssertion($value);
+ } else {
+ $this->makeErrorAssertion($key, $value);
+ }
+ }
+
+ return $this;
+ }
+
+ protected function makeErrorAssertion($key = null, $value = null) {
+ $errors = $this->errors();
+
+ $messages = $errors->get($key);
+
+ $failed = $this->failedRules() ?: [];
+ $failedRules = array_keys(Arr::get($failed, $key, []));
+ $failedRules = array_map(function (string $rule) {
+ if (is_a($rule, ValidationRule::class, true) || is_a($rule, Rule::class, true)) {
+ return $rule;
+ }
+
+ return Str::snake($rule);
+ }, $failedRules);
+
+ PHPUnit::assertTrue($errors->isNotEmpty(), 'Component has no errors.');
+
+ if (is_null($value)) {
+ PHPUnit::assertTrue($errors->has($key), "Component missing error: $key");
+ } elseif ($value instanceof Closure) {
+ PHPUnit::assertTrue($value($failedRules, $messages));
+ } elseif (is_array($value)) {
+ foreach ((array) $value as $ruleOrMessage) {
+ $this->assertErrorMatchesRuleOrMessage($failedRules, $messages, $key, $ruleOrMessage);
+ }
+ } else {
+ $this->assertErrorMatchesRuleOrMessage($failedRules, $messages, $key, $value);
+ }
+
+ return $this;
+ }
+
+ protected function assertErrorMatchesRuleOrMessage($rules, $messages, $key, $ruleOrMessage)
+ {
+ if (Str::contains($ruleOrMessage, ':')){
+ $ruleOrMessage = Str::before($ruleOrMessage, ':');
+ }
+
+ if (in_array($ruleOrMessage, $rules)) {
+ PHPUnit::assertTrue(true);
+
+ return;
+ }
+
+ // If the provided rule/message isn't a failed rule, let's check to see if it's a message...
+ PHPUnit::assertContains($ruleOrMessage, $messages, "Component has no matching failed rule or error message [{$ruleOrMessage}] for [{$key}] attribute.");
+ }
+
+
+ public function assertHasNoErrors($keys = [])
+ {
+ $errors = $this->errors();
+
+ if (empty($keys)) {
+ PHPUnit::assertTrue($errors->isEmpty(), 'Component has errors: "'.implode('", "', $errors->keys()).'"');
+
+ return $this;
+ }
+
+ $keys = (array) $keys;
+
+ foreach ($keys as $key => $value) {
+ if (is_int($key)) {
+ PHPUnit::assertFalse($errors->has($value), "Component has error: $value");
+ } else {
+ $failed = $this->failedRules() ?: [];
+ $rules = array_keys(Arr::get($failed, $key, []));
+
+ foreach ((array) $value as $rule) {
+ if (Str::contains($rule, ':')){
+ $rule = Str::before($rule, ':');
+ }
+
+ PHPUnit::assertNotContains(Str::studly($rule), $rules, "Component has [{$rule}] errors for [{$key}] attribute.");
+ }
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportWireCurrent/test-views/navbar-sidebar.blade.php b/portman/lib/Livewire/Features/SupportWireCurrent/test-views/navbar-sidebar.blade.php
new file mode 100644
index 00000000..f5ed4e8f
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportWireCurrent/test-views/navbar-sidebar.blade.php
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ Document
+
+
+
+
+
+ @persist('nav')
+
+ @endpersist
+
+
+
+ {{ $slot }}
+
+
+
diff --git a/portman/lib/Livewire/Features/SupportWireLoading/browser_test_image.png b/portman/lib/Livewire/Features/SupportWireLoading/browser_test_image.png
new file mode 100644
index 00000000..99caba2a
Binary files /dev/null and b/portman/lib/Livewire/Features/SupportWireLoading/browser_test_image.png differ
diff --git a/portman/lib/Livewire/Features/SupportWireModelingNestedComponents/BaseModelable.php b/portman/lib/Livewire/Features/SupportWireModelingNestedComponents/BaseModelable.php
new file mode 100644
index 00000000..c7a1ab6b
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportWireModelingNestedComponents/BaseModelable.php
@@ -0,0 +1,55 @@
+ $value) {
+ if (str($key)->startsWith('wire:model')) {
+ $outer = $value;
+ store($this->component)->push('bindings-directives', $key, $value);
+ break;
+ }
+ }
+
+ if ($outer === null) return;
+
+ $inner = $this->getName();
+
+ store($this->component)->push('bindings', $inner, $outer);
+
+ $this->setValue(data_get($parent, $outer));
+ }
+
+ // This update hook is for the following scenario:
+ // An modelable value has changed in the browser.
+ // A network request is triggered from the parent.
+ // The request contains both parent and child component updates.
+ // The parent finishes it's request and the "updated" value is
+ // overridden in the parent's lifecycle (ex. a form field being reset).
+ // Without this hook, the child's value will not honor that change
+ // and will instead still be updated to the old value, even though
+ // the parent changed the bound value. This hook detects if the parent
+ // has provided a value during this request and ensures that it is the
+ // final value for the child's request...
+ function update($fullPath, $newValue)
+ {
+ if (store($this->component)->get('hasBeenSeeded', false)) {
+ $oldValue = $this->getValue();
+
+ return function () use ($oldValue) {
+ $this->setValue($oldValue);
+ };
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportWireModelingNestedComponents/SupportWireModelingNestedComponents.php b/portman/lib/Livewire/Features/SupportWireModelingNestedComponents/SupportWireModelingNestedComponents.php
new file mode 100644
index 00000000..a039dd21
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportWireModelingNestedComponents/SupportWireModelingNestedComponents.php
@@ -0,0 +1,94 @@
+ static::$outersByComponentId = []);
+
+ // On a subsequent request, a parent encounters a child component
+ // with wire:model on it, and that child has already been mounted
+ // in a previous request, capture the value being passed in so we
+ // can later set the child's property if it exists in this request.
+ on('mount.stub', function ($tag, $id, $params, $parent, $key) {
+ $outer = collect($params)->first(function ($value, $key) {
+ return str($key)->startsWith('wire:model');
+ });
+
+ if (! $outer) return;
+
+ static::$outersByComponentId[$id] = [$outer => data_get($parent, $outer)];
+ });
+ }
+
+ public function hydrate($memo)
+ {
+ if (! isset($memo['bindings'])) return;
+
+ $bindings = $memo['bindings'];
+ $directives = $memo['bindingsDirectives'];
+
+ // Store the bindings for later dehydration...
+ store($this->component)->set('bindings', $bindings);
+ store($this->component)->set('bindings-directives', $directives);
+
+ // If this child's parent already rendered its stub, retrieve
+ // the memo'd value and set it.
+ if (! isset(static::$outersByComponentId[$memo['id']])) return;
+
+ $outers = static::$outersByComponentId[$memo['id']];
+
+ foreach ($bindings as $outer => $inner) {
+ store($this->component)->set('hasBeenSeeded', true);
+
+ $this->component->$inner = $outers[$outer];
+ }
+ }
+
+ public function render($view, $data)
+ {
+ return function ($html, $replaceHtml) {
+ $bindings = store($this->component)->get('bindings', false);
+ $directives = store($this->component)->get('bindings-directives', false);
+
+ if (! $bindings) return;
+
+ // Currently we can only support a single wire:model bound value,
+ // so we'll just get the first one. But in the future we will
+ // likely want to support named bindings, so we'll keep
+ // this value as an array.
+ $outer = array_keys($bindings)[0];
+ $inner = array_values($bindings)[0];
+ $directive = array_values($directives)[0];
+
+ // Attach the necessary Alpine directives so that the child and
+ // parent's JS, ephemeral, values are bound.
+ $replaceHtml(Utils::insertAttributesIntoHtmlRoot($html, [
+ $directive => '$parent.'.$outer,
+ 'x-modelable' => '$wire.'.$inner,
+ ]));
+ };
+ }
+
+ public function dehydrate($context)
+ {
+ $bindings = store($this->component)->get('bindings', false);
+
+ if (! $bindings) return;
+
+ $directives = store($this->component)->get('bindings-directives');
+
+ // Add the bindings metadata to the paylad for later reference...
+ $context->addMemo('bindings', $bindings);
+ $context->addMemo('bindingsDirectives', $directives);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportWireables/SupportWireables.php b/portman/lib/Livewire/Features/SupportWireables/SupportWireables.php
new file mode 100644
index 00000000..f60fa4c4
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportWireables/SupportWireables.php
@@ -0,0 +1,13 @@
+propertySynthesizer(WireableSynth::class);
+ }
+}
diff --git a/portman/lib/Livewire/Features/SupportWireables/WireableSynth.php b/portman/lib/Livewire/Features/SupportWireables/WireableSynth.php
new file mode 100644
index 00000000..b2629b67
--- /dev/null
+++ b/portman/lib/Livewire/Features/SupportWireables/WireableSynth.php
@@ -0,0 +1,47 @@
+toLivewire();
+ }
+
+ function dehydrate($target, $dehydrateChild)
+ {
+ $data = $target->toLivewire();
+
+ foreach ($data as $key => $child) {
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [
+ $data,
+ ['class' => get_class($target)],
+ ];
+ }
+
+ function hydrate($value, $meta, $hydrateChild) {
+ foreach ($value as $key => $child) {
+ $value[$key] = $hydrateChild($key, $child);
+ }
+
+ return $meta['class']::fromLivewire($value);
+ }
+
+ function set(&$target, $key, $value) {
+ $target->{$key} = $value;
+ }
+}
diff --git a/portman/lib/Livewire/Form.php b/portman/lib/Livewire/Form.php
new file mode 100644
index 00000000..aac5d5ae
--- /dev/null
+++ b/portman/lib/Livewire/Form.php
@@ -0,0 +1,10 @@
+getParameters() as $parameter) {
+ static::substituteNameBindingForCallParameter($parameter, $parameters, $paramIndex);
+ static::substituteImplicitBindingForCallParameter($container, $parameter, $parameters);
+ static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
+ }
+
+ return array_values(array_merge($dependencies, $parameters));
+ }
+
+ protected static function substituteNameBindingForCallParameter($parameter, array &$parameters, int &$paramIndex)
+ {
+ // check if we have a candidate for name/value binding
+ if (! array_key_exists($paramIndex, $parameters)) {
+ return;
+ }
+
+ if ($parameter->isVariadic()) {
+ // this last param will pick up the rest - reindex any remaining parameters
+ $parameters = array_merge(
+ array_filter($parameters, function ($key) { return ! is_int($key); }, ARRAY_FILTER_USE_KEY),
+ array_values(array_filter($parameters, function ($key) { return is_int($key); }, ARRAY_FILTER_USE_KEY))
+ );
+
+ return;
+ }
+
+ // stop if this one is due for dependency injection
+ if (! is_null($className = static::getClassForDependencyInjection($parameter)) && ! $parameters[$paramIndex] instanceof $className) {
+ return;
+ }
+
+ if (! array_key_exists($paramName = $parameter->getName(), $parameters)) {
+ // have a parameter value that is bound by sequential order
+ // and not yet bound by name, so bind it to parameter name
+
+ $parameters[$paramName] = $parameters[$paramIndex];
+ unset($parameters[$paramIndex]);
+ $paramIndex++;
+ }
+ }
+
+ protected static function substituteImplicitBindingForCallParameter($container, $parameter, array &$parameters)
+ {
+ $paramName = $parameter->getName();
+
+ // check if we have a candidate for implicit binding
+ if (is_null($className = static::getClassForImplicitBinding($parameter))) {
+ return;
+ }
+
+ // Check if the value we have for this param is an instance
+ // of the desired class, attempt implicit binding if not
+ if (array_key_exists($paramName, $parameters) && ! $parameters[$paramName] instanceof $className) {
+ $parameters[$paramName] = static::getImplicitBinding($container, $className, $parameters[$paramName]);
+ } elseif (array_key_exists($className, $parameters) && ! $parameters[$className] instanceof $className) {
+ $parameters[$className] = static::getImplicitBinding($container, $className, $parameters[$className]);
+ }
+ }
+
+ protected static function getClassForDependencyInjection($parameter)
+ {
+ $className = static::getParameterClassName($parameter);
+
+ if (is_null($className)) return null;
+
+ if (static::isEnum($parameter)) return null;
+
+ if (! static::implementsInterface($parameter)) return $className;
+
+ return null;
+ }
+
+ protected static function getClassForImplicitBinding($parameter)
+ {
+ $className = static::getParameterClassName($parameter);
+
+ if (is_null($className)) return null;
+
+ if (static::isEnum($parameter)) return $className;
+
+ if (static::implementsInterface($parameter)) return $className;
+
+ return null;
+ }
+
+ protected static function getImplicitBinding($container, $className, $value)
+ {
+ if (is_null($value)) {
+ return null;
+ }
+
+ if ((new ReflectionClass($className))->isEnum()) {
+ return $className::tryFrom($value);
+ }
+
+ $model = $container->make($className)->resolveRouteBinding($value);
+
+ if (! $model) {
+ throw (new ModelNotFoundException)->setModel($className, [$value]);
+ }
+
+ return $model;
+ }
+
+ public static function getParameterClassName($parameter)
+ {
+ $type = $parameter->getType();
+
+ if (! $type) return null;
+
+ if (! $type instanceof ReflectionNamedType) return null;
+
+ return (! $type->isBuiltin()) ? $type->getName() : null;
+ }
+
+ public static function implementsInterface($parameter)
+ {
+ return (new ReflectionClass($parameter->getType()->getName()))->implementsInterface(ImplicitlyBindable::class);
+ }
+
+ public static function isEnum($parameter)
+ {
+ return (new ReflectionClass($parameter->getType()->getName()))->isEnum();
+ }
+}
diff --git a/portman/lib/Livewire/Livewire.php b/portman/lib/Livewire/Livewire.php
new file mode 100644
index 00000000..c152981e
--- /dev/null
+++ b/portman/lib/Livewire/Livewire.php
@@ -0,0 +1,43 @@
+provider = $provider;
+ }
+
+ function provide($callback)
+ {
+ \Closure::bind($callback, $this->provider, $this->provider::class)();
+ }
+
+ function component($name, $class = null)
+ {
+ app(ComponentRegistry::class)->component($name, $class);
+ }
+
+ function componentHook($hook)
+ {
+ ComponentHookRegistry::register($hook);
+ }
+
+ function propertySynthesizer($synth)
+ {
+ app(HandleComponents::class)->registerPropertySynthesizer($synth);
+ }
+
+ function directive($name, $callback)
+ {
+ app(ExtendBlade::class)->livewireOnlyDirective($name, $callback);
+ }
+
+ function precompiler($callback)
+ {
+ app(ExtendBlade::class)->livewireOnlyPrecompiler($callback);
+ }
+
+ function new($name, $id = null)
+ {
+ return app(ComponentRegistry::class)->new($name, $id);
+ }
+
+ function isDiscoverable($componentNameOrClass)
+ {
+ return app(ComponentRegistry::class)->isDiscoverable($componentNameOrClass);
+ }
+
+ function resolveMissingComponent($resolver)
+ {
+ return app(ComponentRegistry::class)->resolveMissingComponent($resolver);
+ }
+
+ function mount($name, $params = [], $key = null)
+ {
+ return app(HandleComponents::class)->mount($name, $params, $key);
+ }
+
+ function snapshot($component, $context = null)
+ {
+ return app(HandleComponents::class)->snapshot($component, $context);
+ }
+
+ function fromSnapshot($snapshot)
+ {
+ return app(HandleComponents::class)->fromSnapshot($snapshot);
+ }
+
+ function listen($eventName, $callback) {
+ return on($eventName, $callback);
+ }
+
+ function current()
+ {
+ return last(app(HandleComponents::class)::$componentStack);
+ }
+
+ function findSynth($keyOrTarget, $component)
+ {
+ return app(HandleComponents::class)->findSynth($keyOrTarget, $component);
+ }
+
+ function update($snapshot, $diff, $calls)
+ {
+ return app(HandleComponents::class)->update($snapshot, $diff, $calls);
+ }
+
+ function updateProperty($component, $path, $value)
+ {
+ $dummyContext = new ComponentContext($component, false);
+
+ $updatedHook = app(HandleComponents::class)->updateProperty($component, $path, $value, $dummyContext);
+
+ $updatedHook();
+ }
+
+ function isLivewireRequest()
+ {
+ return app(HandleRequests::class)->isLivewireRequest();
+ }
+
+ function componentHasBeenRendered()
+ {
+ return SupportAutoInjectedAssets::$hasRenderedAComponentThisRequest;
+ }
+
+ function forceAssetInjection()
+ {
+ SupportAutoInjectedAssets::$forceAssetInjection = true;
+ }
+
+ function setUpdateRoute($callback)
+ {
+ return app(HandleRequests::class)->setUpdateRoute($callback);
+ }
+
+ function getUpdateUri()
+ {
+ return app(HandleRequests::class)->getUpdateUri();
+ }
+
+ function setScriptRoute($callback)
+ {
+ return app(FrontendAssets::class)->setScriptRoute($callback);
+ }
+
+ function useScriptTagAttributes($attributes)
+ {
+ return app(FrontendAssets::class)->useScriptTagAttributes($attributes);
+ }
+
+ protected $queryParamsForTesting = [];
+
+ protected $cookiesForTesting = [];
+
+ protected $headersForTesting = [];
+
+ function withUrlParams($params)
+ {
+ return $this->withQueryParams($params);
+ }
+
+ function withQueryParams($params)
+ {
+ $this->queryParamsForTesting = $params;
+
+ return $this;
+ }
+
+ function withCookie($name, $value)
+ {
+ $this->cookiesForTesting[$name] = $value;
+
+ return $this;
+ }
+
+ function withCookies($cookies)
+ {
+ $this->cookiesForTesting = array_merge($this->cookiesForTesting, $cookies);
+
+ return $this;
+ }
+
+ function withHeaders($headers)
+ {
+ $this->headersForTesting = array_merge($this->headersForTesting, $headers);
+
+ return $this;
+ }
+
+ function withoutLazyLoading()
+ {
+ SupportLazyLoading::disableWhileTesting();
+
+ return $this;
+ }
+
+ function test($name, $params = [])
+ {
+ return Testable::create(
+ $name,
+ $params,
+ $this->queryParamsForTesting,
+ $this->cookiesForTesting,
+ $this->headersForTesting,
+ );
+ }
+
+ function visit($name)
+ {
+ return DuskTestable::create($name, $params = [], $this->queryParamsForTesting);
+ }
+
+ function actingAs(\Illuminate\Contracts\Auth\Authenticatable $user, $driver = null)
+ {
+ Testable::actingAs($user, $driver);
+
+ return $this;
+ }
+
+ function isRunningServerless()
+ {
+ return in_array($_ENV['SERVER_SOFTWARE'] ?? null, [
+ 'vapor',
+ 'bref',
+ ]);
+ }
+
+ function addPersistentMiddleware($middleware)
+ {
+ app(PersistentMiddleware::class)->addPersistentMiddleware($middleware);
+ }
+
+ function setPersistentMiddleware($middleware)
+ {
+ app(PersistentMiddleware::class)->setPersistentMiddleware($middleware);
+ }
+
+ function getPersistentMiddleware()
+ {
+ return app(PersistentMiddleware::class)->getPersistentMiddleware();
+ }
+
+ function flushState()
+ {
+ trigger('flush-state');
+ }
+
+ function originalUrl()
+ {
+ if ($this->isLivewireRequest()) {
+ return url()->to($this->originalPath());
+ }
+
+ return url()->current();
+ }
+
+ function originalPath()
+ {
+ if ($this->isLivewireRequest()) {
+ $snapshot = json_decode(request('components.0.snapshot'), true);
+
+ return data_get($snapshot, 'memo.path', 'POST');
+ }
+
+ return request()->path();
+ }
+
+ function originalMethod()
+ {
+ if ($this->isLivewireRequest()) {
+ $snapshot = json_decode(request('components.0.snapshot'), true);
+
+ return data_get($snapshot, 'memo.method', 'POST');
+ }
+
+ return request()->method();
+ }
+}
diff --git a/portman/lib/Livewire/LivewireServiceProvider.php b/portman/lib/Livewire/LivewireServiceProvider.php
new file mode 100644
index 00000000..41340450
--- /dev/null
+++ b/portman/lib/Livewire/LivewireServiceProvider.php
@@ -0,0 +1,129 @@
+registerLivewireSingleton();
+ $this->registerConfig();
+ $this->bootEventBus();
+ $this->registerMechanisms();
+ }
+
+ public function boot()
+ {
+ $this->bootMechanisms();
+ $this->bootFeatures();
+ }
+
+ protected function registerLivewireSingleton()
+ {
+ $this->app->alias(LivewireManager::class, 'livewire');
+
+ $this->app->singleton(LivewireManager::class);
+
+ app('livewire')->setProvider($this);
+ }
+
+ protected function registerConfig()
+ {
+ $config = __DIR__.'/../config/livewire.php';
+
+ $this->publishes([$config => base_path('config/livewire.php')], ['livewire', 'livewire:config']);
+
+ $this->mergeConfigFrom($config, 'livewire');
+ }
+
+ protected function bootEventBus()
+ {
+ app(EventBus::class)->boot();
+ }
+
+ protected function getMechanisms()
+ {
+ return [
+ Mechanisms\PersistentMiddleware\PersistentMiddleware::class,
+ Mechanisms\HandleComponents\HandleComponents::class,
+ Mechanisms\HandleRequests\HandleRequests::class,
+ Mechanisms\FrontendAssets\FrontendAssets::class,
+ Mechanisms\ExtendBlade\ExtendBlade::class,
+ Mechanisms\CompileLivewireTags\CompileLivewireTags::class,
+ Mechanisms\ComponentRegistry::class,
+ Mechanisms\RenderComponent::class,
+ Mechanisms\DataStore::class,
+ ];
+ }
+
+ protected function registerMechanisms()
+ {
+ foreach ($this->getMechanisms() as $mechanism) {
+ app($mechanism)->register();
+ }
+ }
+
+ protected function bootMechanisms()
+ {
+ if (class_exists(AboutCommand::class) && class_exists(InstalledVersions::class)) {
+ AboutCommand::add('Livewire', [
+ 'Livewire' => InstalledVersions::getPrettyVersion('livewire/livewire'),
+ ]);
+ }
+
+ foreach ($this->getMechanisms() as $mechanism) {
+ app($mechanism)->boot();
+ }
+ }
+
+ protected function bootFeatures()
+ {
+ foreach([
+ Features\SupportWireModelingNestedComponents\SupportWireModelingNestedComponents::class,
+ Features\SupportMultipleRootElementDetection\SupportMultipleRootElementDetection::class,
+ Features\SupportMorphAwareBladeCompilation\SupportMorphAwareBladeCompilation::class,
+ Features\SupportDisablingBackButtonCache\SupportDisablingBackButtonCache::class,
+ Features\SupportNestedComponentListeners\SupportNestedComponentListeners::class,
+ Features\SupportAutoInjectedAssets\SupportAutoInjectedAssets::class,
+ Features\SupportComputed\SupportLegacyComputedPropertySyntax::class,
+ Features\SupportNestingComponents\SupportNestingComponents::class,
+ Features\SupportCompiledWireKeys\SupportCompiledWireKeys::class,
+ Features\SupportScriptsAndAssets\SupportScriptsAndAssets::class,
+ Features\SupportBladeAttributes\SupportBladeAttributes::class,
+ Features\SupportConsoleCommands\SupportConsoleCommands::class,
+ Features\SupportPageComponents\SupportPageComponents::class,
+ Features\SupportReactiveProps\SupportReactiveProps::class,
+ Features\SupportReleaseTokens\SupportReleaseTokens::class,
+ Features\SupportFileDownloads\SupportFileDownloads::class,
+ Features\SupportJsEvaluation\SupportJsEvaluation::class,
+ Features\SupportQueryString\SupportQueryString::class,
+ Features\SupportFileUploads\SupportFileUploads::class,
+ Features\SupportTeleporting\SupportTeleporting::class,
+ Features\SupportLazyLoading\SupportLazyLoading::class,
+ Features\SupportFormObjects\SupportFormObjects::class,
+ Features\SupportAttributes\SupportAttributes::class,
+ Features\SupportPagination\SupportPagination::class,
+ Features\SupportValidation\SupportValidation::class,
+ Features\SupportIsolating\SupportIsolating::class,
+ Features\SupportRedirects\SupportRedirects::class,
+ Features\SupportStreaming\SupportStreaming::class,
+ Features\SupportNavigate\SupportNavigate::class,
+ Features\SupportEntangle\SupportEntangle::class,
+ Features\SupportLocales\SupportLocales::class,
+ Features\SupportTesting\SupportTesting::class,
+ Features\SupportModels\SupportModels::class,
+ Features\SupportEvents\SupportEvents::class,
+
+ // Some features we want to have priority over others...
+ Features\SupportLifecycleHooks\SupportLifecycleHooks::class,
+ Features\SupportLegacyModels\SupportLegacyModels::class,
+ Features\SupportWireables\SupportWireables::class,
+ ] as $feature) {
+ app('livewire')->componentHook($feature);
+ }
+
+ ComponentHookRegistry::boot();
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/CompileLivewireTags/CompileLivewireTags.php b/portman/lib/Livewire/Mechanisms/CompileLivewireTags/CompileLivewireTags.php
new file mode 100644
index 00000000..f50ed376
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/CompileLivewireTags/CompileLivewireTags.php
@@ -0,0 +1,13 @@
+precompiler(new LivewireTagPrecompiler);
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/CompileLivewireTags/LivewireTagPrecompiler.php b/portman/lib/Livewire/Mechanisms/CompileLivewireTags/LivewireTagPrecompiler.php
new file mode 100644
index 00000000..1b182df1
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/CompileLivewireTags/LivewireTagPrecompiler.php
@@ -0,0 +1,92 @@
+getAttributesFromAttributeString($matches['attributes']);
+
+ $keys = array_keys($attributes);
+ $values = array_values($attributes);
+ $attributesCount = count($attributes);
+
+ for ($i=0; $i < $attributesCount; $i++) {
+ if ($keys[$i] === ':' && $values[$i] === 'true') {
+ if (isset($values[$i + 1]) && $values[$i + 1] === 'true') {
+ $attributes[$keys[$i + 1]] = '$'.$keys[$i + 1];
+ unset($attributes[':']);
+ }
+ }
+ }
+
+ // Convert all kebab-cased to camelCase.
+ $attributes = collect($attributes)->mapWithKeys(function ($value, $key) {
+ // Skip snake_cased attributes.
+ if (str($key)->contains('_')) return [$key => $value];
+
+ return [(string) str($key)->camel() => $value];
+ })->toArray();
+
+ // Convert all snake_cased attributes to camelCase, and merge with
+ // existing attributes so both snake and camel are available.
+ $attributes = collect($attributes)->mapWithKeys(function ($value, $key) {
+ // Skip snake_cased attributes
+ if (! str($key)->contains('_')) return [$key => false];
+
+ return [(string) str($key)->camel() => $value];
+ })->filter()->merge($attributes)->toArray();
+
+ $component = $matches[1];
+
+ if ($component === 'styles') return '@livewireStyles';
+ if ($component === 'scripts') return '@livewireScripts';
+ if ($component === 'dynamic-component' || $component === 'is') {
+ if (! isset($attributes['component']) && ! isset($attributes['is'])) {
+ throw new ComponentAttributeMissingOnDynamicComponentException;
+ }
+
+ // Does not need quotes as resolved with quotes already.
+ $component = $attributes['component'] ?? $attributes['is'];
+
+ unset($attributes['component'], $attributes['is']);
+ } else {
+ // Add single quotes to the component name to compile it as string in quotes
+ $component = "'{$component}'";
+ }
+
+ return $this->componentString($component, $attributes);
+ }, $value);
+ }
+
+ protected function componentString(string $component, array $attributes)
+ {
+ if (isset($attributes['key']) || isset($attributes['wire:key'])) {
+ $key = $attributes['key'] ?? $attributes['wire:key'];
+ unset($attributes['key'], $attributes['wire:key']);
+
+ return "@livewire({$component}, [".$this->attributesToString($attributes, escapeBound: false)."], key({$key}))";
+ }
+
+ return "@livewire({$component}, [".$this->attributesToString($attributes, escapeBound: false).'])';
+ }
+
+ protected function attributesToString(array $attributes, $escapeBound = true)
+ {
+ return collect($attributes)
+ ->map(function (string $value, string $attribute) use ($escapeBound) {
+ return $escapeBound && isset($this->boundAttributes[$attribute]) && $value !== 'true' && ! is_numeric($value)
+ ? "'{$attribute}' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute({$value})"
+ : "'{$attribute}' => {$value}";
+ })
+ ->implode(',');
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/ComponentRegistry.php b/portman/lib/Livewire/Mechanisms/ComponentRegistry.php
new file mode 100644
index 00000000..e3b848c0
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/ComponentRegistry.php
@@ -0,0 +1,217 @@
+nonAliasedClasses[] = $name;
+ } else {
+ $this->aliases[$name] = $class;
+ }
+ }
+
+ function new($nameOrClass, $id = null)
+ {
+ [$class, $name] = $this->getNameAndClass($nameOrClass);
+
+ $component = new $class;
+
+ $component->setId($id ?: str()->random(20));
+
+ $component->setName($name);
+
+ // // Parameters passed in automatically set public properties by the same name...
+ // foreach ($params as $key => $value) {
+ // if (! property_exists($component, $key)) continue;
+
+ // // Typed properties shouldn't be set back to "null". It will throw an error...
+ // if ((new \ReflectionProperty($component, $key))->getType() && is_null($value)) continue;
+
+ // $component->$key = $value;
+ // }
+
+ return $component;
+ }
+
+ function isDiscoverable($classOrName)
+ {
+ if (is_object($classOrName)) {
+ $classOrName = get_class($classOrName);
+ }
+
+ if (class_exists($name = $classOrName)) {
+ $name = $this->generateNameFromClass($classOrName);
+ }
+
+ $class = $this->generateClassFromName($name);
+
+ if (is_subclass_of($class, Component::class)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function getName($nameOrClassOrComponent)
+ {
+ [$class, $name] = $this->getNameAndClass($nameOrClassOrComponent);
+
+ return $name;
+ }
+
+ function getClass($nameOrClassOrComponent)
+ {
+ [$class, $name] = $this->getNameAndClass($nameOrClassOrComponent);
+
+ return $class;
+ }
+
+ function resolveMissingComponent($resolver)
+ {
+ $this->missingComponentResolvers[] = $resolver;
+ }
+
+ protected function getNameAndClass($nameComponentOrClass)
+ {
+ // If a component itself was passed in, just take the class name...
+ $nameOrClass = is_object($nameComponentOrClass) ? $nameComponentOrClass::class : $nameComponentOrClass;
+
+ // If a component class was passed in, use that...
+ if (is_subclass_of($nameOrClass, Component::class)) {
+ $class = $nameOrClass;
+ // Otherwise, assume it was a simple name...
+ } else {
+ $class = $this->nameToClass($nameOrClass);
+
+ // If class can't be found, see if there is an index component in a subfolder...
+ if(! class_exists($class)) {
+ $class = $class . '\\Index';
+ }
+
+ if(! class_exists($class)) {
+ foreach ($this->missingComponentResolvers as $resolve) {
+ if ($resolved = $resolve($nameOrClass)) {
+ $this->component($nameOrClass, $resolved);
+
+ $class = $this->aliases[$nameOrClass];
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Now that we have a class, we can check that it's actually a Livewire component...
+ if (! is_subclass_of($class, Component::class)) {
+ throw new ComponentNotFoundException(
+ "Unable to find component: [{$nameOrClass}]"
+ );
+ }
+
+ // Convert it to a name even if a name was passed in to make sure we're using deterministic names...
+ $name = $this->classToName($class);
+
+ return [$class, $name];
+ }
+
+ protected function nameToClass($name)
+ {
+ // Check the aliases...
+ if (isset($this->aliases[$name])) {
+ if (is_object($this->aliases[$name])) return $this->aliases[$name]::class;
+
+ return $this->aliases[$name];
+ }
+
+ // Hash check the non-aliased classes...
+ foreach ($this->nonAliasedClasses as $class) {
+ if (crc32($class) === $name) {
+ return $class;
+ }
+ }
+
+ // Reverse generate a class from a name...
+ return $this->generateClassFromName($name);
+ }
+
+ protected function classToName($class)
+ {
+ // Check the aliases...
+ $resolvedAliases = array_map(fn ($i) => is_object($i) ? get_class($i) : $i, $this->aliases);
+
+ if ($name = array_search($class, $resolvedAliases)) return $name;
+
+ // Check existance in non-aliased classes and hash...
+ foreach ($this->nonAliasedClasses as $oneOff) {
+ if (crc32($oneOff) === $hash = crc32($class)) {
+ return $hash;
+ }
+ }
+
+ // Generate name from class...
+ return $this->generateNameFromClass($class);
+ }
+
+ protected function generateClassFromName($name)
+ {
+ $rootNamespace = config('livewire.class_namespace');
+
+ $class = collect(str($name)->explode('.'))
+ ->map(fn ($segment) => (string) str($segment)->studly())
+ ->join('\\');
+
+ if (empty($rootNamespace)) {
+ return $class;
+ }
+
+ return '\\' . $rootNamespace . '\\' . $class;
+ }
+
+ protected function generateNameFromClass($class)
+ {
+ $namespace = str_replace(
+ ['/', '\\'],
+ '.',
+ trim(trim(config('livewire.class_namespace')), '\\')
+ );
+
+ $class = str_replace(
+ ['/', '\\'],
+ '.',
+ trim(trim($class, '/'), '\\')
+ );
+
+ $namespace = collect(explode('.', $namespace))
+ ->map(fn ($i) => \Illuminate\Support\Str::kebab($i))
+ ->implode('.');
+
+ $fullName = str(collect(explode('.', $class))
+ ->map(fn ($i) => \Illuminate\Support\Str::kebab($i))
+ ->implode('.'));
+
+ if ($fullName->startsWith('.')) {
+ $fullName = $fullName->substr(1);
+ }
+
+ // If using an index component in a sub folder, remove the '.index' so the name is the subfolder name...
+ if ($fullName->endsWith('.index')) {
+ $fullName = $fullName->replaceLast('.index', '');
+ }
+
+ if ($fullName->startsWith($namespace)) {
+ return (string) $fullName->substr(strlen($namespace) + 1);
+ }
+
+ return (string) $fullName;
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/DataStore.php b/portman/lib/Livewire/Mechanisms/DataStore.php
new file mode 100644
index 00000000..e24afbab
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/DataStore.php
@@ -0,0 +1,116 @@
+lookup = new WeakMap;
+ }
+
+ function set($instance, $key, $value)
+ {
+ if (! isset($this->lookup[$instance])) {
+ $this->lookup[$instance] = [];
+ }
+
+ $this->lookup[$instance][$key] = $value;
+ }
+
+ function has($instance, $key, $iKey = null) {
+ if (! isset($this->lookup[$instance])) {
+ return false;
+ }
+
+ if (! isset($this->lookup[$instance][$key])) {
+ return false;
+ }
+
+ if ($iKey !== null) {
+ return !! ($this->lookup[$instance][$key][$iKey] ?? false);
+ }
+
+ return true;
+ }
+
+ function get($instance, $key, $default = null)
+ {
+ if (! isset($this->lookup[$instance])) {
+ return value($default);
+ }
+
+ if (! isset($this->lookup[$instance][$key])) {
+ return value($default);
+ }
+
+ return $this->lookup[$instance][$key];
+ }
+
+ function find($instance, $key, $iKey = null, $default = null)
+ {
+ if (! isset($this->lookup[$instance])) {
+ return value($default);
+ }
+
+ if (! isset($this->lookup[$instance][$key])) {
+ return value($default);
+ }
+
+ if ($iKey !== null && ! isset($this->lookup[$instance][$key][$iKey])) {
+ return value($default);
+ }
+
+ return $iKey !== null
+ ? $this->lookup[$instance][$key][$iKey]
+ : $this->lookup[$instance][$key];
+ }
+
+ function push($instance, $key, $value, $iKey = null)
+ {
+ if (! isset($this->lookup[$instance])) {
+ $this->lookup[$instance] = [];
+ }
+
+ if (! isset($this->lookup[$instance][$key])) {
+ $this->lookup[$instance][$key] = [];
+ }
+
+ if ($iKey) {
+ $this->lookup[$instance][$key][$iKey] = $value;
+ } else {
+ $this->lookup[$instance][$key][] = $value;
+ }
+ }
+
+ function unset($instance, $key, $iKey = null)
+ {
+ if (! isset($this->lookup[$instance])) {
+ return;
+ }
+
+ if (! isset($this->lookup[$instance][$key])) {
+ return;
+ }
+
+ if ($iKey !== null) {
+ // Set a local variable to avoid the "indirect modification" error.
+ $keyValue = $this->lookup[$instance][$key];
+
+ unset($keyValue[$iKey]);
+
+ $this->lookup[$instance][$key] = $keyValue;
+ } else {
+ // Set a local variable to avoid the "indirect modification" error.
+ $instanceValue = $this->lookup[$instance];
+
+ unset($instanceValue[$key]);
+
+ $this->lookup[$instance] = $instanceValue;
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/ExtendBlade/DeterministicBladeKeys.php b/portman/lib/Livewire/Mechanisms/ExtendBlade/DeterministicBladeKeys.php
new file mode 100644
index 00000000..12195527
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/ExtendBlade/DeterministicBladeKeys.php
@@ -0,0 +1,48 @@
+currentPathHash) {
+ throw new \Exception('Latest compiled component path not found.');
+ }
+
+ $path = $this->currentPathHash;
+ $count = $this->counter();
+
+ // $key = "lw-[hash of Blade view path]-[current @livewire directive count]"
+ return 'lw-' . $this->currentPathHash . '-' . $count;
+ }
+
+ public function counter()
+ {
+ if (! isset($this->countersByPath[$this->currentPathHash])) {
+ $this->countersByPath[$this->currentPathHash] = 0;
+ }
+
+ return $this->countersByPath[$this->currentPathHash]++;
+ }
+
+ public function hookIntoCompile(BladeCompiler $compiler, $viewContent)
+ {
+ $path = $compiler->getPath();
+
+ // If there is no path this means this Blade is being compiled
+ // with ->compileString(...) directly instead of ->compile()
+ // therefore we'll generate a hash of the contents instead
+ if ($path === null) {
+ $path = $viewContent;
+ }
+
+ $this->currentPathHash = crc32($path);
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/ExtendBlade/ExtendBlade.php b/portman/lib/Livewire/Mechanisms/ExtendBlade/ExtendBlade.php
new file mode 100644
index 00000000..e035baa7
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/ExtendBlade/ExtendBlade.php
@@ -0,0 +1,145 @@
+ "window.Livewire.find('{{ \$_instance->getId() }}')");
+
+ on('render', function ($target, $view) {
+ $this->startLivewireRendering($target);
+
+ $undo = $this->livewireifyBladeCompiler();
+
+ $this->renderCounter++;
+
+ return function ($html) use ($view, $undo, $target) {
+ $this->endLivewireRendering();
+
+ $this->renderCounter--;
+
+ if ($this->renderCounter === 0) {
+ $undo();
+ }
+
+ return $html;
+ };
+ });
+
+ // This is a custom view engine that gets used when rendering
+ // Livewire views. Things like letting certain exceptions bubble
+ // to the handler, and registering custom directives like: "@this".
+ app()->make('view.engine.resolver')->register('blade', function () {
+ return new ExtendedCompilerEngine(app('blade.compiler'));
+ });
+
+ app()->singleton(DeterministicBladeKeys::class);
+
+ // Reset this singleton between tests and Octane requests...
+ on('flush-state', function () {
+ app()->singleton(DeterministicBladeKeys::class);
+
+ static::$livewireComponents = [];
+ });
+
+ // We're using "precompiler" as a hook for the point in time when
+ // Laravel compiles a Blade view...
+ app('blade.compiler')->precompiler(function ($value) {
+ app(DeterministicBladeKeys::class)->hookIntoCompile(app('blade.compiler'), $value);
+
+ return $value;
+ });
+ }
+
+ function livewireOnlyDirective($name, $handler)
+ {
+ $this->directives[$name] = $handler;
+ }
+
+ function livewireOnlyPrecompiler($handler)
+ {
+ $this->precompilers[] = $handler;
+ }
+
+ function livewireifyBladeCompiler() {
+ $removals = [];
+
+ if ($this->renderCounter === 0) {
+ $customDirectives = app('blade.compiler')->getCustomDirectives();
+ $precompilers = invade(app('blade.compiler'))->precompilers;
+
+ foreach ($this->directives as $name => $handler) {
+ if (! isset($customDirectives[$name])) {
+ $customDirectives[$name] = $handler;
+
+ invade(app('blade.compiler'))->customDirectives = $customDirectives;
+
+ $removals[] = function () use ($name) {
+ $customDirectives = app('blade.compiler')->getCustomDirectives();
+
+ unset($customDirectives[$name]);
+
+ invade(app('blade.compiler'))->customDirectives = $customDirectives;
+ };
+ }
+ }
+
+ foreach ($this->precompilers as $handler) {
+ if (array_search($handler, $precompilers) === false) {
+ array_unshift($precompilers, $handler);
+
+ invade(app('blade.compiler'))->precompilers = $precompilers;
+
+ $removals[] = function () use ($handler) {
+ $precompilers = invade(app('blade.compiler'))->precompilers;
+
+ $index = array_search($handler, $precompilers);
+
+ if ($index === false) return;
+
+ unset($precompilers[$index]);
+
+ invade(app('blade.compiler'))->precompilers = $precompilers;
+ };
+ }
+ }
+ }
+
+ return function () use ($removals) {
+ while ($removals) array_pop($removals)();
+ };
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php b/portman/lib/Livewire/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php
new file mode 100644
index 00000000..dfd21ac2
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php
@@ -0,0 +1,76 @@
+handleViewException($e, $obLevel);
+ }
+
+ return ltrim(ob_get_clean());
+ }
+
+ // Errors thrown while a view is rendering are caught by the Blade
+ // compiler and wrapped in an "ErrorException". This makes Livewire errors
+ // harder to read, AND causes issues like `abort(404)` not actually working.
+ protected function handleViewException(\Throwable $e, $obLevel)
+ {
+ if ($this->shouldBypassExceptionForLivewire($e, $obLevel)) {
+ // This is because there is no "parent::parent::".
+ \Illuminate\View\Engines\PhpEngine::handleViewException($e, $obLevel);
+
+ return;
+ }
+
+ parent::handleViewException($e, $obLevel);
+ }
+
+ public function shouldBypassExceptionForLivewire(\Throwable $e, $obLevel)
+ {
+ $uses = array_flip(class_uses_recursive($e));
+
+ return (
+ // Don't wrap "abort(403)".
+ $e instanceof \Illuminate\Auth\Access\AuthorizationException
+ // Don't wrap "abort(404)".
+ || $e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ // Don't wrap "abort(500)".
+ || $e instanceof \Symfony\Component\HttpKernel\Exception\HttpException
+ // Don't wrap most Livewire exceptions.
+ || isset($uses[\Livewire\Exceptions\BypassViewHandler::class])
+ );
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/FrontendAssets/FrontendAssets.php b/portman/lib/Livewire/Mechanisms/FrontendAssets/FrontendAssets.php
new file mode 100644
index 00000000..16238753
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/FrontendAssets/FrontendAssets.php
@@ -0,0 +1,276 @@
+setScriptRoute(function ($handle) {
+ return config('app.debug')
+ ? Route::get('/livewire/livewire.js', $handle)
+ : Route::get('/livewire/livewire.min.js', $handle);
+ });
+
+ Route::get('/livewire/livewire.min.js.map', [static::class, 'maps']);
+
+ Blade::directive('livewireScripts', [static::class, 'livewireScripts']);
+ Blade::directive('livewireScriptConfig', [static::class, 'livewireScriptConfig']);
+ Blade::directive('livewireStyles', [static::class, 'livewireStyles']);
+
+ app('livewire')->provide(function() {
+ $this->publishes(
+ [
+ __DIR__.'/../../../dist' => public_path('vendor/livewire'),
+ ],
+ 'livewire:assets',
+ );
+ });
+
+ on('flush-state', function () {
+ $instance = app(static::class);
+
+ $instance->hasRenderedScripts = false;
+ $instance->hasRenderedStyles = false;
+ });
+ }
+
+ function useScriptTagAttributes($attributes)
+ {
+ $this->scriptTagAttributes = array_merge($this->scriptTagAttributes, $attributes);
+ }
+
+ function setScriptRoute($callback)
+ {
+ $route = $callback([self::class, 'returnJavaScriptAsFile']);
+
+ $this->javaScriptRoute = $route;
+ }
+
+ public static function livewireScripts($expression)
+ {
+ return '{!! \Livewire\Mechanisms\FrontendAssets\FrontendAssets::scripts('.$expression.') !!}';
+ }
+
+ public static function livewireScriptConfig($expression)
+ {
+ return '{!! \Livewire\Mechanisms\FrontendAssets\FrontendAssets::scriptConfig('.$expression.') !!}';
+ }
+
+ public static function livewireStyles($expression)
+ {
+ return '{!! \Livewire\Mechanisms\FrontendAssets\FrontendAssets::styles('.$expression.') !!}';
+ }
+
+ public function returnJavaScriptAsFile()
+ {
+ return Utils::pretendResponseIsFile(
+ config('app.debug')
+ ? __DIR__.'/../../../dist/livewire.js'
+ : __DIR__.'/../../../dist/livewire.min.js'
+ );
+ }
+
+ public function maps()
+ {
+ return Utils::pretendResponseIsFile(__DIR__.'/../../../dist/livewire.min.js.map');
+ }
+
+ /**
+ * @return string
+ */
+ public static function styles($options = [])
+ {
+ app(static::class)->hasRenderedStyles = true;
+
+ $nonce = static::nonce($options);
+ $nonce = $nonce ? "{$nonce} data-livewire-style" : '';
+
+ $progressBarColor = config('livewire.navigate.progress_bar_color', '#2299dd');
+
+ // Note: the attribute selectors are "doubled" so that they don't get overriden when Tailwind's CDN loads a script tag
+ // BELOW the one Livewire injects...
+ $html = <<
+
+ HTML;
+
+ return static::minify($html);
+ }
+
+ /**
+ * @return string
+ */
+ public static function scripts($options = [])
+ {
+ app(static::class)->hasRenderedScripts = true;
+
+ $debug = config('app.debug');
+
+ $scripts = static::js($options);
+
+ // HTML Label.
+ $html = $debug ? [''] : [];
+
+ $html[] = $scripts;
+
+ return implode("\n", $html);
+ }
+
+ public static function js($options)
+ {
+ // Use the default endpoint...
+ $url = app(static::class)->javaScriptRoute->uri;
+
+ // Use the configured one...
+ $url = config('livewire.asset_url') ?: $url;
+
+ // Use the legacy passed in one...
+ $url = $options['asset_url'] ?? $url;
+
+ // Use the new passed in one...
+ $url = $options['url'] ?? $url;
+
+ $url = rtrim($url, '/');
+
+ $url = (string) str($url)->when(! str($url)->isUrl(), fn($url) => $url->start('/'));
+
+ // Add the build manifest hash to it...
+ $manifest = json_decode(file_get_contents(__DIR__.'/../../../dist/manifest.json'), true);
+ $versionHash = $manifest['/livewire.js'];
+ $url = "{$url}?id={$versionHash}";
+
+ $token = app()->has('session.store') ? csrf_token() : '';
+
+ $assetWarning = null;
+
+ $nonce = static::nonce($options);
+
+ [$url, $assetWarning] = static::usePublishedAssetsIfAvailable($url, $manifest, $nonce);
+
+ $progressBar = config('livewire.navigate.show_progress_bar', true) ? '' : 'data-no-progress-bar';
+
+ $updateUri = app('livewire')->getUpdateUri();
+
+ $extraAttributes = Utils::stringifyHtmlAttributes(
+ app(static::class)->scriptTagAttributes,
+ );
+
+ return <<
+ HTML;
+ }
+
+ public static function scriptConfig($options = [])
+ {
+ app(static::class)->hasRenderedScripts = true;
+
+ $nonce = static::nonce($options);
+
+ $progressBar = config('livewire.navigate.show_progress_bar', true) ? '' : 'data-no-progress-bar';
+
+ $attributes = json_encode([
+ 'csrf' => app()->has('session.store') ? csrf_token() : '',
+ 'uri' => app('livewire')->getUpdateUri(),
+ 'progressBar' => $progressBar,
+ 'nonce' => isset($options['nonce']) ? $options['nonce'] : '',
+ ]);
+
+ return <<window.livewireScriptConfig = {$attributes};
+ HTML;
+ }
+
+ protected static function usePublishedAssetsIfAvailable($url, $manifest, $nonce)
+ {
+ $assetWarning = null;
+
+ // Check to see if static assets have been published...
+ if (! file_exists(public_path('vendor/livewire/manifest.json'))) {
+ return [$url, $assetWarning];
+ }
+
+ $publishedManifest = json_decode(file_get_contents(public_path('vendor/livewire/manifest.json')), true);
+ $versionedFileName = $publishedManifest['/livewire.js'];
+
+ $fileName = config('app.debug') ? '/livewire.js' : '/livewire.min.js';
+
+ $versionedFileName = "{$fileName}?id={$versionedFileName}";
+
+ $assertUrl = config('livewire.asset_url')
+ ?? (app('livewire')->isRunningServerless()
+ ? rtrim(config('app.asset_url'), '/')."/vendor/livewire$versionedFileName"
+ : url("vendor/livewire{$versionedFileName}")
+ );
+
+ $url = $assertUrl;
+
+ if ($manifest !== $publishedManifest) {
+ $assetWarning = <<
+ console.warn('Livewire: The published Livewire assets are out of date\\n See: https://livewire.laravel.com/docs/installation#publishing-livewires-frontend-assets')
+ \n
+ HTML;
+ }
+
+ return [$url, $assetWarning];
+ }
+
+ protected static function minify($subject)
+ {
+ return preg_replace('~(\v|\t|\s{2,})~m', '', $subject);
+ }
+
+ protected static function nonce($options = [])
+ {
+ $nonce = $options['nonce'] ?? Vite::cspNonce();
+
+ return $nonce ? "nonce=\"{$nonce}\"" : '';
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/BaseRenderless.php b/portman/lib/Livewire/Mechanisms/HandleComponents/BaseRenderless.php
new file mode 100644
index 00000000..a8a15fc3
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/BaseRenderless.php
@@ -0,0 +1,14 @@
+component->skipRender();
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Checksum.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Checksum.php
new file mode 100644
index 00000000..86ae42e4
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Checksum.php
@@ -0,0 +1,36 @@
+getKey();
+
+ // Remove the children from the memo in the snapshot, as it is actually Ok
+ // if the "children" tracking is tampered with. This way JavaScript can
+ // modify children as it needs to for dom-diffing purposes...
+ unset($snapshot['memo']['children']);
+
+ $checksum = hash_hmac('sha256', json_encode($snapshot), $hashKey);
+
+ trigger('checksum.generate', $checksum, $snapshot);
+
+ return $checksum;
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/ComponentContext.php b/portman/lib/Livewire/Mechanisms/HandleComponents/ComponentContext.php
new file mode 100644
index 00000000..4be9d69f
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/ComponentContext.php
@@ -0,0 +1,60 @@
+mounting;
+ }
+
+ public function addEffect($key, $value)
+ {
+ if (is_array($key)) {
+ foreach ($key as $iKey => $iValue) $this->addEffect($iKey, $iValue);
+
+ return;
+ }
+
+ $this->effects[$key] = $value;
+ }
+
+ public function pushEffect($key, $value, $iKey = null)
+ {
+ if (! isset($this->effects[$key])) $this->effects[$key] = [];
+
+ if ($iKey) {
+ $this->effects[$key][$iKey] = $value;
+ } else {
+ $this->effects[$key][] = $value;
+ }
+ }
+
+ public function addMemo($key, $value)
+ {
+ $this->memo[$key] = $value;
+ }
+
+ public function pushMemo($key, $value, $iKey = null)
+ {
+ if (! isset($this->memo[$key])) $this->memo[$key] = [];
+
+ if ($iKey) {
+ $this->memo[$key][$iKey] = $value;
+ } else {
+ $this->memo[$key][] = $value;
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/CorruptComponentPayloadException.php b/portman/lib/Livewire/Mechanisms/HandleComponents/CorruptComponentPayloadException.php
new file mode 100644
index 00000000..bd495049
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/CorruptComponentPayloadException.php
@@ -0,0 +1,18 @@
+propertySynthesizers, $class);
+ }
+ }
+
+ public function mount($name, $params = [], $key = null)
+ {
+ $parent = app('livewire')->current();
+
+ if ($html = $this->shortCircuitMount($name, $params, $key, $parent)) return $html;
+
+ $component = app('livewire')->new($name);
+
+ $this->pushOntoComponentStack($component);
+
+ $context = new ComponentContext($component, mounting: true);
+
+ if (config('app.debug')) $start = microtime(true);
+ $finish = trigger('mount', $component, $params, $key, $parent);
+ if (config('app.debug')) trigger('profile', 'mount', $component->getId(), [$start, microtime(true)]);
+
+ if (config('app.debug')) $start = microtime(true);
+ $html = $this->render($component, '
');
+ if (config('app.debug')) trigger('profile', 'render', $component->getId(), [$start, microtime(true)]);
+
+ if (config('app.debug')) $start = microtime(true);
+ trigger('dehydrate', $component, $context);
+
+ $snapshot = $this->snapshot($component, $context);
+ if (config('app.debug')) trigger('profile', 'dehydrate', $component->getId(), [$start, microtime(true)]);
+
+ trigger('destroy', $component, $context);
+
+ $html = Utils::insertAttributesIntoHtmlRoot($html, [
+ 'wire:snapshot' => $snapshot,
+ 'wire:effects' => $context->effects,
+ ]);
+
+ $this->popOffComponentStack();
+
+ return $finish($html, $snapshot);
+ }
+
+ protected function shortCircuitMount($name, $params, $key, $parent)
+ {
+ $newHtml = null;
+
+ trigger('pre-mount', $name, $params, $key, $parent, function ($html) use (&$newHtml) {
+ $newHtml = $html;
+ });
+
+ return $newHtml;
+ }
+
+ public function update($snapshot, $updates, $calls)
+ {
+ $data = $snapshot['data'];
+ $memo = $snapshot['memo'];
+
+ if (config('app.debug')) $start = microtime(true);
+ [ $component, $context ] = $this->fromSnapshot($snapshot);
+
+ $this->pushOntoComponentStack($component);
+
+ trigger('hydrate', $component, $memo, $context);
+
+ $this->updateProperties($component, $updates, $data, $context);
+ if (config('app.debug')) trigger('profile', 'hydrate', $component->getId(), [$start, microtime(true)]);
+
+ $this->callMethods($component, $calls, $context);
+
+ if (config('app.debug')) $start = microtime(true);
+ if ($html = $this->render($component)) {
+ $context->addEffect('html', $html);
+ if (config('app.debug')) trigger('profile', 'render', $component->getId(), [$start, microtime(true)]);
+ }
+
+ if (config('app.debug')) $start = microtime(true);
+ trigger('dehydrate', $component, $context);
+
+ $snapshot = $this->snapshot($component, $context);
+ if (config('app.debug')) trigger('profile', 'dehydrate', $component->getId(), [$start, microtime(true)]);
+
+ trigger('destroy', $component, $context);
+
+ $this->popOffComponentStack();
+
+ return [ $snapshot, $context->effects ];
+ }
+
+ public function fromSnapshot($snapshot)
+ {
+ Checksum::verify($snapshot);
+
+ trigger('snapshot-verified', $snapshot);
+
+ $data = $snapshot['data'];
+ $name = $snapshot['memo']['name'];
+ $id = $snapshot['memo']['id'];
+
+ $component = app('livewire')->new($name, id: $id);
+
+ $context = new ComponentContext($component);
+
+ $this->hydrateProperties($component, $data, $context);
+
+ return [ $component, $context ];
+ }
+
+ public function snapshot($component, $context = null)
+ {
+ $context ??= new ComponentContext($component);
+
+ $data = $this->dehydrateProperties($component, $context);
+
+ $snapshot = [
+ 'data' => $data,
+ 'memo' => [
+ 'id' => $component->getId(),
+ 'name' => $component->getName(),
+ ...$context->memo,
+ ],
+ ];
+
+ $snapshot['checksum'] = Checksum::generate($snapshot);
+
+ return $snapshot;
+ }
+
+ protected function dehydrateProperties($component, $context)
+ {
+ $data = Utils::getPublicPropertiesDefinedOnSubclass($component);
+
+ foreach ($data as $key => $value) {
+ $data[$key] = $this->dehydrate($value, $context, $key);
+ }
+
+ return $data;
+ }
+
+ protected function dehydrate($target, $context, $path)
+ {
+ if (Utils::isAPrimitive($target)) return $target;
+
+ $synth = $this->propertySynth($target, $context, $path);
+
+ [ $data, $meta ] = $synth->dehydrate($target, function ($name, $child) use ($context, $path) {
+ return $this->dehydrate($child, $context, "{$path}.{$name}");
+ });
+
+ $meta['s'] = $synth::getKey();
+
+ return [ $data, $meta ];
+ }
+
+ protected function hydrateProperties($component, $data, $context)
+ {
+ foreach ($data as $key => $value) {
+ if (! property_exists($component, $key)) continue;
+
+ $child = $this->hydrate($value, $context, $key);
+
+ // Typed properties shouldn't be set back to "null". It will throw an error...
+ if ((new \ReflectionProperty($component, $key))->getType() && is_null($child)) continue;
+
+ $component->$key = $child;
+ }
+ }
+
+ protected function hydrate($valueOrTuple, $context, $path)
+ {
+ if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
+
+ [$value, $meta] = $tuple;
+
+ // Nested properties get set as `__rm__` when they are removed. We don't want to hydrate these.
+ if ($this->isRemoval($value) && str($path)->contains('.')) {
+ return $value;
+ }
+
+ $synth = $this->propertySynth($meta['s'], $context, $path);
+
+ return $synth->hydrate($value, $meta, function ($name, $child) use ($context, $path) {
+ return $this->hydrate($child, $context, "{$path}.{$name}");
+ });
+ }
+
+ protected function hydratePropertyUpdate($valueOrTuple, $context, $path)
+ {
+ if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
+
+ [$value, $meta] = $tuple;
+
+ // Nested properties get set as `__rm__` when they are removed. We don't want to hydrate these.
+ if ($this->isRemoval($value) && str($path)->contains('.')) {
+ return $value;
+ }
+
+ $synth = $this->propertySynth($meta['s'], $context, $path);
+
+ return $synth->hydrate($value, $meta, function ($name, $child) {
+ return $child;
+ });
+ }
+
+ protected function render($component, $default = null)
+ {
+ if ($html = store($component)->get('skipRender', false)) {
+ $html = value(is_string($html) ? $html : $default);
+
+ if (! $html) return;
+
+ return Utils::insertAttributesIntoHtmlRoot($html, [
+ 'wire:id' => $component->getId(),
+ ]);
+ }
+
+ [ $view, $properties ] = $this->getView($component);
+
+ return $this->trackInRenderStack($component, function () use ($component, $view, $properties) {
+ $finish = trigger('render', $component, $view, $properties);
+
+ $revertA = Utils::shareWithViews('__livewire', $component);
+ $revertB = Utils::shareWithViews('_instance', $component); // @deprecated
+
+ $viewContext = new ViewContext;
+
+ $html = $view->render(function ($view) use ($viewContext) {
+ // Extract leftover slots, sections, and pushes before they get flushed...
+ $viewContext->extractFromEnvironment($view->getFactory());
+ });
+
+ $revertA(); $revertB();
+
+ $html = Utils::insertAttributesIntoHtmlRoot($html, [
+ 'wire:id' => $component->getId(),
+ ]);
+
+ $replaceHtml = function ($newHtml) use (&$html) {
+ $html = $newHtml;
+ };
+
+ $html = $finish($html, $replaceHtml, $viewContext);
+
+ return $html;
+ });
+ }
+
+ protected function getView($component)
+ {
+ $viewPath = config('livewire.view_path', resource_path('views/livewire'));
+
+ $dotName = $component->getName();
+
+ $fileName = str($dotName)->replace('.', '/')->__toString();
+
+ $viewOrString = method_exists($component, 'render')
+ ? wrap($component)->render()
+ : View::file($viewPath . '/' . $fileName . '.blade.php');
+
+ $properties = Utils::getPublicPropertiesDefinedOnSubclass($component);
+
+ $view = Utils::generateBladeView($viewOrString, $properties);
+
+ return [ $view, $properties ];
+ }
+
+ protected function trackInRenderStack($component, $callback)
+ {
+ array_push(static::$renderStack, $component);
+
+ return tap($callback(), function () {
+ array_pop(static::$renderStack);
+ });
+ }
+
+ protected function updateProperties($component, $updates, $data, $context)
+ {
+ $finishes = [];
+
+ foreach ($updates as $path => $value) {
+ $value = $this->hydrateForUpdate($data, $path, $value, $context);
+
+ // We only want to run "updated" hooks after all properties have
+ // been updated so that each individual hook has the ability
+ // to overwrite the updated states of other properties...
+ $finishes[] = $this->updateProperty($component, $path, $value, $context);
+ }
+
+ foreach ($finishes as $finish) {
+ $finish();
+ }
+ }
+
+ public function updateProperty($component, $path, $value, $context)
+ {
+ $segments = explode('.', $path);
+
+ $property = array_shift($segments);
+
+ $finish = trigger('update', $component, $path, $value);
+
+ // Ensure that it's a public property, not on the base class first...
+ if (! in_array($property, array_keys(Utils::getPublicPropertiesDefinedOnSubclass($component)))) {
+ throw new PublicPropertyNotFoundException($property, $component->getName());
+ }
+
+ // If this isn't a "deep" set, set it directly, otherwise we have to
+ // recursively get up and set down the value through the synths...
+ if (empty($segments)) {
+ $this->setComponentPropertyAwareOfTypes($component, $property, $value);
+ } else {
+ $propertyValue = $component->$property;
+
+ $this->setComponentPropertyAwareOfTypes($component, $property,
+ $this->recursivelySetValue($property, $propertyValue, $value, $segments, 0, $context)
+ );
+ }
+
+ return $finish;
+ }
+
+ protected function hydrateForUpdate($raw, $path, $value, $context)
+ {
+ $meta = $this->getMetaForPath($raw, $path);
+
+ // If we have meta data already for this property, let's use that to get a synth...
+ if ($meta) {
+ return $this->hydratePropertyUpdate([$value, $meta], $context, $path);
+ }
+
+ // If we don't, let's check to see if it's a typed property and fetch the synth that way...
+ $parent = str($path)->contains('.')
+ ? data_get($context->component, str($path)->beforeLast('.')->toString())
+ : $context->component;
+
+ $childKey = str($path)->afterLast('.');
+
+ if ($parent && is_object($parent) && property_exists($parent, $childKey) && Utils::propertyIsTyped($parent, $childKey)) {
+ $type = Utils::getProperty($parent, $childKey)->getType();
+
+ $types = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type];
+
+ foreach ($types as $type) {
+ $synth = $this->getSynthesizerByType($type->getName(), $context, $path);
+
+ if ($synth) return $synth->hydrateFromType($type->getName(), $value);
+ }
+ }
+
+ return $value;
+ }
+
+ protected function getMetaForPath($raw, $path)
+ {
+ $segments = explode('.', $path);
+
+ $first = array_shift($segments);
+
+ [$data, $meta] = Utils::isSyntheticTuple($raw) ? $raw : [$raw, null];
+
+ if ($path !== '') {
+ $value = $data[$first] ?? null;
+
+ return $this->getMetaForPath($value, implode('.', $segments));
+ }
+
+ return $meta;
+ }
+
+ protected function recursivelySetValue($baseProperty, $target, $leafValue, $segments, $index = 0, $context = null)
+ {
+ $isLastSegment = count($segments) === $index + 1;
+
+ $property = $segments[$index];
+
+ $path = implode('.', array_slice($segments, 0, $index + 1));
+
+ $synth = $this->propertySynth($target, $context, $path);
+
+ if ($isLastSegment) {
+ $toSet = $leafValue;
+ } else {
+ $propertyTarget = $synth->get($target, $property);
+
+ // "$path" is a dot-notated key. This means we may need to drill
+ // down and set a value on a deeply nested object. That object
+ // may not exist, so let's find the first one that does...
+
+ // Here's we've determined we're trying to set a deeply nested
+ // value on an object/array that doesn't exist, so we need
+ // to build up that non-existant nesting structure first.
+ if ($propertyTarget === null) $propertyTarget = [];
+
+ $toSet = $this->recursivelySetValue($baseProperty, $propertyTarget, $leafValue, $segments, $index + 1, $context);
+ }
+
+ $method = ($this->isRemoval($leafValue) && $isLastSegment) ? 'unset' : 'set';
+
+ $pathThusFar = collect([$baseProperty, ...$segments])->slice(0, $index + 1)->join('.');
+ $fullPath = collect([$baseProperty, ...$segments])->join('.');
+
+ $synth->$method($target, $property, $toSet, $pathThusFar, $fullPath);
+
+ return $target;
+ }
+
+ protected function setComponentPropertyAwareOfTypes($component, $property, $value)
+ {
+ try {
+ $component->$property = $value;
+ } catch (\TypeError $e) {
+ // If an "int" is being set to empty string, unset the property (making it null).
+ // This is common in the case of `wire:model`ing an int to a text field...
+ // If a value is being set to "null", do the same...
+ if ($value === '' || $value === null) {
+ unset($component->$property);
+ } else {
+ throw $e;
+ }
+ }
+ }
+
+ protected function callMethods($root, $calls, $context)
+ {
+ $returns = [];
+
+ foreach ($calls as $idx => $call) {
+ $method = $call['method'];
+ $params = $call['params'];
+
+
+ $earlyReturnCalled = false;
+ $earlyReturn = null;
+ $returnEarly = function ($return = null) use (&$earlyReturnCalled, &$earlyReturn) {
+ $earlyReturnCalled = true;
+ $earlyReturn = $return;
+ };
+
+ $finish = trigger('call', $root, $method, $params, $context, $returnEarly);
+
+ if ($earlyReturnCalled) {
+ $returns[] = $finish($earlyReturn);
+
+ continue;
+ }
+
+ $methods = Utils::getPublicMethodsDefinedBySubClass($root);
+
+ // Also remove "render" from the list...
+ $methods = array_values(array_diff($methods, ['render']));
+
+ // @todo: put this in a better place:
+ $methods[] = '__dispatch';
+
+ if (! in_array($method, $methods)) {
+ throw new MethodNotFoundException($method);
+ }
+
+ if (config('app.debug')) $start = microtime(true);
+ $return = wrap($root)->{$method}(...$params);
+ if (config('app.debug')) trigger('profile', 'call'.$idx, $root->getId(), [$start, microtime(true)]);
+
+ $returns[] = $finish($return);
+ }
+
+ $context->addEffect('returns', $returns);
+ }
+
+ public function findSynth($keyOrTarget, $component): ?Synth
+ {
+ $context = new ComponentContext($component);
+ try {
+ return $this->propertySynth($keyOrTarget, $context, null);
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+
+ public function propertySynth($keyOrTarget, $context, $path): Synth
+ {
+ return is_string($keyOrTarget)
+ ? $this->getSynthesizerByKey($keyOrTarget, $context, $path)
+ : $this->getSynthesizerByTarget($keyOrTarget, $context, $path);
+ }
+
+ protected function getSynthesizerByKey($key, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::getKey() === $key) {
+ return new $synth($context, $path);
+ }
+ }
+
+ throw new \Exception('No synthesizer found for key: "'.$key.'"');
+ }
+
+ protected function getSynthesizerByTarget($target, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::match($target)) {
+ return new $synth($context, $path);
+ }
+ }
+
+ throw new \Exception('Property type not supported in Livewire for property: ['.json_encode($target).']');
+ }
+
+ protected function getSynthesizerByType($type, $context, $path)
+ {
+ foreach ($this->propertySynthesizers as $synth) {
+ if ($synth::matchByType($type)) {
+ return new $synth($context, $path);
+ }
+ }
+
+ return null;
+ }
+
+ protected function pushOntoComponentStack($component)
+ {
+ array_push($this::$componentStack, $component);
+ }
+
+ protected function popOffComponentStack()
+ {
+ array_pop($this::$componentStack);
+ }
+
+ protected function isRemoval($value) {
+ return $value === '__rm__';
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php
new file mode 100644
index 00000000..23ed08e2
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php
@@ -0,0 +1,43 @@
+ $child) {
+ $target[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [$target, []];
+ }
+
+ function hydrate($value, $meta, $hydrateChild) {
+ // If we are "hydrating" a value about to be used in an update,
+ // Let's make sure it's actually an array before try to set it.
+ // This is most common in the case of "__rm__" values, but also
+ // applies to any non-array value...
+ if (! is_array($value)) {
+ return $value;
+ }
+
+ foreach ($value as $key => $child) {
+ $value[$key] = $hydrateChild($key, $child);
+ }
+
+ return $value;
+ }
+
+ function set(&$target, $key, $value) {
+ $target[$key] = $value;
+ }
+
+ function unset(&$target, $key) {
+ unset($target[$key]);
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/CarbonSynth.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/CarbonSynth.php
new file mode 100644
index 00000000..321c9b60
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/CarbonSynth.php
@@ -0,0 +1,52 @@
+ DateTime::class,
+ 'nativeImmutable' => DateTimeImmutable::class,
+ 'carbon' => Carbon::class,
+ 'carbonImmutable' => CarbonImmutable::class,
+ 'illuminate' => \Illuminate\Support\Carbon::class,
+ ];
+
+ public static $key = 'cbn';
+
+ static function match($target) {
+ foreach (static::$types as $type => $class) {
+ if ($target instanceof $class) return true;
+ }
+
+ return false;
+ }
+
+ static function matchByType($type) {
+ return is_subclass_of($type, DateTimeInterface::class);
+ }
+
+ function dehydrate($target) {
+ return [
+ $target->format(DateTimeInterface::ATOM),
+ ['type' => array_search(get_class($target), static::$types)],
+ ];
+ }
+
+ static function hydrateFromType($type, $value) {
+ if ($value === '' || $value === null) return null;
+
+ return new $type($value);
+ }
+
+ function hydrate($value, $meta) {
+ if ($value === '' || $value === null) return null;
+
+ return new static::$types[$meta['type']]($value);
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/CollectionSynth.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/CollectionSynth.php
new file mode 100644
index 00000000..d39845fc
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/CollectionSynth.php
@@ -0,0 +1,51 @@
+all();
+
+ foreach ($data as $key => $child) {
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [
+ $data,
+ ['class' => get_class($target)]
+ ];
+ }
+
+ function hydrate($value, $meta, $hydrateChild) {
+ foreach ($value as $key => $child) {
+ $value[$key] = $hydrateChild($key, $child);
+ }
+
+ return new $meta['class']($value);
+ }
+
+ function &get(&$target, $key) {
+ // We need this "$reader" callback to get a reference to
+ // the items property inside collections. Otherwise,
+ // we'd receive a copy instead of the reference.
+ $reader = function & ($object, $property) {
+ $value = & \Closure::bind(function & () use ($property) {
+ return $this->$property;
+ }, $object, $object)->__invoke();
+
+ return $value;
+ };
+
+ $items =& $reader($target, 'items');
+
+ return $items[$key];
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php
new file mode 100644
index 00000000..84ca8b8c
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php
@@ -0,0 +1,36 @@
+value,
+ ['class' => get_class($target)]
+ ];
+ }
+
+ function hydrate($value, $meta) {
+ if ($value === null || $value === '') return null;
+
+ $class = $meta['class'];
+
+ return $class::from($value);
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/FloatSynth.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/FloatSynth.php
new file mode 100644
index 00000000..de9fe45f
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/FloatSynth.php
@@ -0,0 +1,25 @@
+ $child) {
+ $data[$key] = $dehydrateChild($key, $child);
+ }
+
+ return [$data, []];
+ }
+
+ function hydrate($value, $meta, $hydrateChild) {
+ $obj = new stdClass;
+
+ foreach ($value as $key => $child) {
+ $obj->$key = $hydrateChild($key, $child);
+ }
+
+ return $obj;
+ }
+
+ function set(&$target, $key, $value) {
+ $target->$key = $value;
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/StringableSynth.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/StringableSynth.php
new file mode 100644
index 00000000..2065f66b
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/StringableSynth.php
@@ -0,0 +1,21 @@
+__toString(), []];
+ }
+
+ function hydrate($value) {
+ return str($value);
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/Synth.php b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/Synth.php
new file mode 100644
index 00000000..1bfd4ce4
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/Synthesizers/Synth.php
@@ -0,0 +1,66 @@
+$key;
+ }
+
+ function __call($method, $params) {
+ if ($method === 'dehydrate') {
+ throw new \Exception('You must define a "dehydrate" method');
+ }
+
+ if ($method === 'hydrate') {
+ throw new \Exception('You must define a "hydrate" method');
+ }
+
+ if ($method === 'hydrateFromType') {
+ throw new \Exception('You must define a "hydrateFromType" method');
+ }
+
+ if ($method === 'get') {
+ throw new \Exception('This synth doesn\'t support getting properties: '.get_class($this));
+ }
+
+ if ($method === 'set') {
+ throw new \Exception('This synth doesn\'t support setting properties: '.get_class($this));
+ }
+
+ if ($method === 'unset') {
+ throw new \Exception('This synth doesn\'t support unsetting properties: '.get_class($this));
+ }
+
+ if ($method === 'call') {
+ throw new \Exception('This synth doesn\'t support calling methods: '.get_class($this));
+ }
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleComponents/ViewContext.php b/portman/lib/Livewire/Mechanisms/HandleComponents/ViewContext.php
new file mode 100644
index 00000000..c6464114
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleComponents/ViewContext.php
@@ -0,0 +1,35 @@
+slots = $factory->slots;
+ $this->pushes = $factory->pushes;
+ $this->prepends = $factory->prepends;
+ $this->sections = $factory->sections;
+ }
+
+ function mergeIntoNewEnvironment($__env)
+ {
+ $factory = invade($__env);
+
+ $factory->slots = $this->slots;
+ $factory->pushes = $this->pushes;
+ $factory->prepends = $this->prepends;
+ $factory->sections = $this->sections;
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/HandleRequests/HandleRequests.php b/portman/lib/Livewire/Mechanisms/HandleRequests/HandleRequests.php
new file mode 100644
index 00000000..c0a06af8
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/HandleRequests/HandleRequests.php
@@ -0,0 +1,148 @@
+updateRoute && ! $this->updateRouteExists()) {
+ app($this::class)->setUpdateRoute(function ($handle) {
+ return Route::post('/livewire/update', $handle)
+ ->middleware('web')
+ ->name('default.livewire.update');
+ });
+ }
+
+ $this->skipRequestPayloadTamperingMiddleware();
+ }
+
+ protected function updateRouteExists()
+ {
+ return $this->findUpdateRoute() !== null;
+ }
+
+ function getUpdateUri()
+ {
+ // When routes are cached, $this->updateRoute may be null because
+ // setUpdateRoute() was never called (the route already existed).
+ // In this case, find the route from the router.
+ $route = $this->updateRoute ?? $this->findUpdateRoute();
+
+ return (string) str(
+ route($route->getName(), [], false)
+ )->start('/');
+ }
+
+ protected function findUpdateRoute()
+ {
+ // Find the route with name ending in 'livewire.update'.
+ // Custom routes can have prefixes (e.g., 'tenant.livewire.update')
+ // so we check for routes ending with 'livewire.update', not just exact matches.
+ // Prioritise custom routes over the default route.
+ $defaultRoute = null;
+
+ foreach (Route::getRoutes()->getRoutes() as $route) {
+ if (str($route->getName())->endsWith('livewire.update')) {
+ // If it's the default route, save it but keep looking for a custom one
+ if ($route->getName() === 'default.livewire.update') {
+ $defaultRoute = $route;
+ continue;
+ }
+
+ // Found a custom route, return it immediately
+ return $route;
+ }
+ }
+
+ return $defaultRoute;
+ }
+
+ function skipRequestPayloadTamperingMiddleware()
+ {
+ \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::skipWhen(function () {
+ return $this->isLivewireRequest();
+ });
+
+ \Illuminate\Foundation\Http\Middleware\TrimStrings::skipWhen(function () {
+ return $this->isLivewireRequest();
+ });
+ }
+
+ function setUpdateRoute($callback)
+ {
+ $route = $callback([self::class, 'handleUpdate']);
+
+ // Append `livewire.update` to the existing name, if any.
+ if (! str($route->getName())->endsWith('livewire.update')) {
+ $route->name('livewire.update');
+ }
+
+ $this->updateRoute = $route;
+ }
+
+ function isLivewireRequest()
+ {
+ return request()->hasHeader('X-Livewire');
+ }
+
+ function isLivewireRoute()
+ {
+ // @todo: Rename this back to `isLivewireRequest` once the need for it in tests has been fixed.
+ $route = request()->route();
+
+ if (! $route) return false;
+
+ /*
+ * Check to see if route name ends with `livewire.update`, as if
+ * a custom update route is used and they add a name, then when
+ * we call `->name('livewire.update')` on the route it will
+ * suffix the existing name with `livewire.update`.
+ */
+ return $route->named('*livewire.update');
+ }
+
+ function handleUpdate()
+ {
+ $requestPayload = request(key: 'components', default: []);
+
+ $finish = trigger('request', $requestPayload);
+
+ $requestPayload = $finish($requestPayload);
+
+ $componentResponses = [];
+
+ foreach ($requestPayload as $componentPayload) {
+ $snapshot = json_decode($componentPayload['snapshot'], associative: true);
+ $updates = $componentPayload['updates'];
+ $calls = $componentPayload['calls'];
+
+ [ $snapshot, $effects ] = app('livewire')->update($snapshot, $updates, $calls);
+
+ $componentResponses[] = [
+ 'snapshot' => json_encode($snapshot),
+ 'effects' => $effects,
+ ];
+ }
+
+ $responsePayload = [
+ 'components' => $componentResponses,
+ 'assets' => SupportScriptsAndAssets::getAssets(),
+ ];
+
+ $finish = trigger('response', $responsePayload);
+
+ return $finish($responsePayload);
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/Mechanism.php b/portman/lib/Livewire/Mechanisms/Mechanism.php
new file mode 100644
index 00000000..30825111
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/Mechanism.php
@@ -0,0 +1,16 @@
+instance(static::class, $this);
+ }
+
+ function boot()
+ {
+ //
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/PersistentMiddleware/PersistentMiddleware.php b/portman/lib/Livewire/Mechanisms/PersistentMiddleware/PersistentMiddleware.php
new file mode 100644
index 00000000..e09af9fc
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/PersistentMiddleware/PersistentMiddleware.php
@@ -0,0 +1,179 @@
+extractPathAndMethodFromRequest();
+
+ $context->addMemo('path', $path);
+ $context->addMemo('method', $method);
+ });
+
+ on('snapshot-verified', function ($snapshot) {
+ // Only apply middleware to requests hitting the Livewire update endpoint, and not any fake requests such as a test.
+ if (! app(HandleRequests::class)->isLivewireRoute()) return;
+
+ $this->extractPathAndMethodFromSnapshot($snapshot);
+
+ $this->applyPersistentMiddleware();
+ });
+
+ on('flush-state', function() {
+ // Only flush these at the end of a full request, so that child components have access to this data.
+ $this->path = null;
+ $this->method = null;
+ });
+ }
+
+ function addPersistentMiddleware($middleware)
+ {
+ static::$persistentMiddleware = Router::uniqueMiddleware(array_merge(static::$persistentMiddleware, (array) $middleware));
+ }
+
+ function setPersistentMiddleware($middleware)
+ {
+ static::$persistentMiddleware = Router::uniqueMiddleware((array) $middleware);
+ }
+
+ function getPersistentMiddleware()
+ {
+ return static::$persistentMiddleware;
+ }
+
+ protected function extractPathAndMethodFromRequest()
+ {
+ if (app(HandleRequests::class)->isLivewireRoute()) {
+ return [$this->path, $this->method];
+ }
+
+ return [request()->path(), request()->method()];
+ }
+
+ protected function extractPathAndMethodFromSnapshot($snapshot)
+ {
+ if (
+ ! isset($snapshot['memo']['path'])
+ || ! isset($snapshot['memo']['method'])
+ ) return;
+
+ // Store these locally, so dynamically added child components can use this data.
+ $this->path = $snapshot['memo']['path'];
+ $this->method = $snapshot['memo']['method'];
+ }
+
+ protected function applyPersistentMiddleware()
+ {
+ $request = $this->makeFakeRequest();
+
+ $middleware = $this->getApplicablePersistentMiddleware($request);
+
+ // Only send through pipeline if there are middleware found
+ if (is_null($middleware)) return;
+
+ Utils::applyMiddleware($request, $middleware);
+ }
+
+ protected function makeFakeRequest()
+ {
+ $originalPath = $this->formatPath($this->path);
+ $originalMethod = $this->method;
+
+ $currentPath = $this->formatPath(request()->path());
+
+ // Clone server bag to ensure changes below don't overwrite the original.
+ $serverBag = clone request()->server;
+
+ // Replace the Livewire endpoint path with the path from the original request.
+ $serverBag->set(
+ 'REQUEST_URI',
+ str_replace($currentPath, $originalPath, $serverBag->get('REQUEST_URI'))
+ );
+
+ $serverBag->set('REQUEST_METHOD', $originalMethod);
+
+ /**
+ * Make the fake request from the current request with path and method changed so
+ * all other request data, such as headers, are available in the fake request,
+ * but merge in the new server bag with the updated `REQUEST_URI`.
+ */
+ $request = request()->duplicate(
+ server: $serverBag->all()
+ );
+
+ return $request;
+ }
+
+ protected function formatPath($path)
+ {
+ return '/' . ltrim($path, '/');
+ }
+
+ protected function getApplicablePersistentMiddleware($request)
+ {
+ $route = $this->getRouteFromRequest($request);
+
+ if (! $route) return [];
+
+ $middleware = app('router')->gatherRouteMiddleware($route);
+
+ return $this->filterMiddlewareByPersistentMiddleware($middleware);
+ }
+
+ protected function getRouteFromRequest($request)
+ {
+ try {
+ $route = app('router')->getRoutes()->match($request);
+ $request->setRouteResolver(fn() => $route);
+ } catch (NotFoundHttpException $e){
+ return null;
+ }
+
+ return $route;
+ }
+
+ protected function filterMiddlewareByPersistentMiddleware($middleware)
+ {
+ $middleware = collect($middleware);
+
+ $persistentMiddleware = collect(app(PersistentMiddleware::class)->getPersistentMiddleware());
+
+ return $middleware
+ ->filter(function ($value, $key) use ($persistentMiddleware) {
+ return $persistentMiddleware->contains(function($iValue, $iKey) use ($value) {
+ // Some middlewares can be closures.
+ if (! is_string($value)) return false;
+
+ // Ensure any middleware arguments aren't included in the comparison
+ return Str::before($value, ':') == $iValue;
+ });
+ })
+ ->values()
+ ->all();
+ }
+}
diff --git a/portman/lib/Livewire/Mechanisms/RenderComponent.php b/portman/lib/Livewire/Mechanisms/RenderComponent.php
new file mode 100644
index 00000000..e62d62ee
--- /dev/null
+++ b/portman/lib/Livewire/Mechanisms/RenderComponent.php
@@ -0,0 +1,56 @@
+generate();
+ $deterministicBladeKey = "'{$deterministicBladeKey}'";
+
+ return <<mount(\$__name, \$__params, \$__key);
+
+echo \$__html;
+
+unset(\$__html);
+unset(\$__key);
+unset(\$__name);
+unset(\$__params);
+unset(\$__split);
+if (isset(\$__slots)) unset(\$__slots);
+?>
+EOT;
+ }
+}
diff --git a/portman/lib/Livewire/Pipe.php b/portman/lib/Livewire/Pipe.php
new file mode 100644
index 00000000..4164962a
--- /dev/null
+++ b/portman/lib/Livewire/Pipe.php
@@ -0,0 +1,36 @@
+target = $target;
+ }
+
+ function __invoke(...$params) {
+ if (empty($params)) return $this->target;
+
+ [ $before, $through, $after ] = [ [], null, [] ];
+
+ foreach ($params as $key => $param) {
+ if (! $through) {
+ if (is_callable($param)) $through = $param;
+
+ else $before[$key] = $param;
+ } else {
+ $after[$key] = $param;
+ }
+ }
+
+ $params = [ ...$before, $this->target, ...$after ];
+
+ $this->target = $through(...$params);
+
+ return $this;
+ }
+}
+
diff --git a/portman/lib/Livewire/Transparency.php b/portman/lib/Livewire/Transparency.php
new file mode 100644
index 00000000..ddb77765
--- /dev/null
+++ b/portman/lib/Livewire/Transparency.php
@@ -0,0 +1,44 @@
+target;
+ }
+
+ function offsetExists(mixed $offset): bool
+ {
+ return isset($this->target[$offset]);
+ }
+
+ function offsetGet(mixed $offset): mixed
+ {
+ return $this->target[$offset];
+ }
+
+ function offsetSet(mixed $offset, mixed $value): void
+ {
+ $this->target[$offset] = $value;
+ }
+
+ function offsetUnset(mixed $offset): void
+ {
+ unset($this->target[$offset]);
+ }
+
+ function getIterator(): Traversable
+ {
+ return (function () {
+ foreach ($this->target as $key => $value) {
+ yield $key => $value;
+ }
+ })();
+ }
+}
diff --git a/portman/lib/Livewire/WireDirective.php b/portman/lib/Livewire/WireDirective.php
new file mode 100644
index 00000000..80e5918a
--- /dev/null
+++ b/portman/lib/Livewire/WireDirective.php
@@ -0,0 +1,59 @@
+name;
+ }
+
+ public function directive()
+ {
+ return $this->directive;
+ }
+
+ public function value()
+ {
+ return $this->value;
+ }
+
+ public function modifiers()
+ {
+ return str($this->directive)
+ ->replace("wire:{$this->name}", '')
+ ->explode('.')
+ ->filter()->values();
+ }
+
+ public function hasModifier($modifier)
+ {
+ return $this->modifiers()->contains($modifier);
+ }
+
+ public function toHtml()
+ {
+ return (new ComponentAttributeBag([$this->directive => $this->value]))->toHtml();
+ }
+
+ public function toString()
+ {
+ return (string) $this;
+ }
+
+ public function __toString()
+ {
+ return (string) $this->value;
+ }
+}
diff --git a/portman/lib/Livewire/Wireable.php b/portman/lib/Livewire/Wireable.php
new file mode 100644
index 00000000..ad4d21d4
--- /dev/null
+++ b/portman/lib/Livewire/Wireable.php
@@ -0,0 +1,10 @@
+fallback = $fallback;
+
+ return $this;
+ }
+
+ function __call($method, $params)
+ {
+ if (! method_exists($this->target, $method)) return value($this->fallback);
+
+ try {
+ return ImplicitlyBoundMethod::call(app(), [$this->target, $method], $params);
+ } catch (\Throwable $e) {
+ $shouldPropagate = true;
+
+ $stopPropagation = function () use (&$shouldPropagate) {
+ $shouldPropagate = false;
+ };
+
+ trigger('exception', $this->target, $e, $stopPropagation);
+
+ $shouldPropagate && throw $e;
+ }
+ }
+}
+
+
+
+
diff --git a/portman/lib/Livewire/helpers.php b/portman/lib/Livewire/helpers.php
new file mode 100644
index 00000000..3c8fab4c
--- /dev/null
+++ b/portman/lib/Livewire/helpers.php
@@ -0,0 +1,173 @@
+obj = $obj;
+ $this->reflected = new ReflectionClass($obj);
+ }
+
+ public function &__get($name)
+ {
+ $getProperty = function &() use ($name) {
+ return $this->{$name};
+ };
+
+ $getProperty = $getProperty->bindTo($this->obj, get_class($this->obj));
+
+ return $getProperty();
+ }
+
+ public function __set($name, $value)
+ {
+ $setProperty = function () use ($name, &$value) {
+ $this->{$name} = $value;
+ };
+
+ $setProperty = $setProperty->bindTo($this->obj, get_class($this->obj));
+
+ $setProperty();
+ }
+
+ public function __call($name, $params)
+ {
+ $method = $this->reflected->getMethod($name);
+
+ return $method->invoke($this->obj, ...$params);
+ }
+ };
+}
+
+function once($fn)
+{
+ $hasRun = false;
+
+ return function (...$params) use ($fn, &$hasRun) {
+ if ($hasRun) return;
+
+ $hasRun = true;
+
+ return $fn(...$params);
+ };
+}
+
+function of(...$params)
+{
+ return $params;
+}
+
+function revert(&$variable)
+{
+ $cache = $variable;
+
+ return function () use (&$variable, $cache) {
+ $variable = $cache;
+ };
+}
+
+function wrap($subject) {
+ return new Wrapped($subject);
+}
+
+function pipe($subject) {
+ return new Pipe($subject);
+}
+
+function trigger($name, ...$params) {
+ return app(\Livewire\EventBus::class)->trigger($name, ...$params);
+}
+
+function on($name, $callback) {
+ return app(\Livewire\EventBus::class)->on($name, $callback);
+}
+
+function after($name, $callback) {
+ return app(\Livewire\EventBus::class)->after($name, $callback);
+}
+
+function before($name, $callback) {
+ return app(\Livewire\EventBus::class)->before($name, $callback);
+}
+
+function off($name, $callback) {
+ app(\Livewire\EventBus::class)->off($name, $callback);
+}
+
+function memoize($target) {
+ static $memo = new \WeakMap;
+
+ return new class ($target, $memo) {
+ function __construct(
+ protected $target,
+ protected &$memo,
+ ) {}
+
+ function __call($method, $params)
+ {
+ $this->memo[$this->target] ??= [];
+
+ $signature = $method . crc32(json_encode($params));
+
+ return $this->memo[$this->target][$signature]
+ ??= $this->target->$method(...$params);
+ }
+ };
+}
+
+function store($instance = null)
+{
+ if (! $instance) $instance = app(\Livewire\Mechanisms\DataStore::class);
+
+ return new class ($instance) {
+ function __construct(protected $instance) {}
+
+ function get($key, $default = null) {
+ return app(\Livewire\Mechanisms\DataStore::class)->get($this->instance, $key, $default);
+ }
+
+ function set($key, $value) {
+ return app(\Livewire\Mechanisms\DataStore::class)->set($this->instance, $key, $value);
+ }
+
+ function push($key, $value, $iKey = null)
+ {
+ return app(\Livewire\Mechanisms\DataStore::class)->push($this->instance, $key, $value, $iKey);
+ }
+
+ function find($key, $iKey = null, $default = null)
+ {
+ return app(\Livewire\Mechanisms\DataStore::class)->find($this->instance, $key, $iKey, $default);
+ }
+
+ function has($key, $iKey = null)
+ {
+ return app(\Livewire\Mechanisms\DataStore::class)->has($this->instance, $key, $iKey);
+ }
+
+ function unset($key, $iKey = null)
+ {
+ return app(\Livewire\Mechanisms\DataStore::class)->unset($this->instance, $key, $iKey);
+ }
+ };
+}
diff --git a/rector.php b/rector.php
new file mode 100644
index 00000000..ed29c807
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,30 @@
+withPaths([
+ realpath('dist'),
+ ])
+ ->withRules([
+ ClassPropertyAssignToConstructorPromotionRector::class,
+ ArraySpreadInsteadOfArrayMergeRector::class
+ ])
+ ->withPhpSets(
+ php82: true
+ )
+ ->withPreparedSets(
+ deadCode: true,
+ codeQuality: true,
+ codingStyle: true,
+ typeDeclarations: true,
+ )
+ ->withNoDiffs()
+ //->withTypeCoverageLevel(Level::PHP_82)
+ ->withImportNames(
+ removeUnusedImports: true
+ );
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 00000000..1f94d5d2
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,11 @@
+{
+ "packages": {
+ ".": {
+ "changelog-path": "CHANGELOG.md",
+ "release-type": "php",
+ "draft": false,
+ "prerelease": false
+ }
+ },
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
+}
diff --git a/src/Component.php b/src/Component.php
index d964ca4e..62f2e354 100644
--- a/src/Component.php
+++ b/src/Component.php
@@ -1,4 +1,5 @@
-getId();
+ }
- /**
- * Protected methods.
- *
- * @see getUncallables()
- * @var string[]
- */
- protected $uncallables = [];
+ function setId($id)
+ {
+ // Support backwards compatibility.
+ $this->id = $id;
- /** @var ComponentContext $context */
- protected $context;
+ $this->__id = $id;
+ }
- /**
- * @param ComponentContext $context
- */
- public function __construct(ComponentContext $context)
+ function getId()
{
- $this->context = $context;
+ return $this->__id;
}
- /**
- * Assign/overwrite public class properties.
- *
- * @lifecyclehook updatingProperty
- * @lifecyclehook updating
- * @lifecyclehook defineProperty
- * @lifecyclehook updated
- * @lifecyclehook updatedProperty
- *
- * @param string $name
- * @param mixed $value
- * @param bool $skipLifecycle
- * @param string|null $method
- * @return $this
- */
- public function assign(
- string $name,
- $value,
- bool $skipLifecycle = false,
- string $method = null
- ): self
+ function setName($name)
{
- try {
- if (!array_key_exists($name, $this->getPublicProperties())) {
- throw new ComponentException(__('Public property %1 does\'nt exist', [$name]));
- }
- if ($skipLifecycle) {
- throw new LifecycleException(__('Skips lifecycle'));
- }
-
- // Process lifecycle from this point on.
- $before = 'updating' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $method ?? $name)));
- $current = str_replace('updating', 'define', $before);
- $after = str_replace('define', 'updated', $current);
+ $this->__name = $name;
+ }
- $methods = [$before, 'updating', $current, 'updated', $after];
- $clone = $value;
+ public function getName()
+ {
+ return $this->__name;
+ }
- foreach ($methods as $m) {
- if (method_exists($this, $m)) {
- $clone = $this->{$m}(...[$value, $name]);
- }
+ public function setAlias(string|null $alias): void
+ {
+ $this->__alias = $alias;
+ }
- $this->{$name} = $clone;
- }
- } catch (LifecycleException $exception) {
- // We skip the rest and just assign the property.
- } catch (Exception $exception) {
- return $this;
- }
+ public function getAlias(): string|null
+ {
+ return $this->__alias;
+ }
- $this->{$name} = $value;
- return $this;
+ public function hasAlias(): bool
+ {
+ return $this->__alias !== null;
}
- /**
- * Assign/overwrite multiple public class properties at once.
- *
- * @param array $assignees
- * @param bool $skipLifecycle
- * @return $this
- */
- public function fill(array $assignees, bool $skipLifecycle = false): self
+ public function skipRender($html = null): void
{
- foreach ($assignees as $assignee => $value) {
- $this->assign($assignee, $value, $skipLifecycle);
- }
+ store($this)->set('skipRender', $html ?: true);
+ }
- return $this;
+ public function skipMount(): void
+ {
+ store($this)->set('skipMount', true);
}
- /**
- * @return Template|null
- */
- public function getParent(): ?Template
+ public function skipHydrate(): void
{
- return $this->parent;
+ store($this)->set('skipHydrate', true);
}
- /**
- * @param Template $parent
- * @return $this
- */
- public function setParent(Template $parent): self
+ public function __isset($property)
{
- $this->parent = $parent;
- return $this;
+ try {
+ $value = $this->__get($property);
+
+ if (isset($value)) {
+ return true;
+ }
+ } catch (\Magewirephp\Magewire\Exceptions\PropertyNotFoundException $exception) {
+ }
+
+ return false;
}
/**
- * Get a (optional cached) array with all available
- * (non-static) public property objects.
- *
- * @param bool $refresh
- * @return array
+ * @throws PropertyNotFoundException
*/
- public function getPublicProperties(bool $refresh = false): array
+ public function __get($property)
{
- if (($refresh ? null : $this->publicProperties) === null) {
- $properties = array_filter((new ReflectionClass($this))->getProperties(), static function ($property) {
- return $property->isPublic() && !$property->isStatic();
- });
+ $value = 'noneset';
- $data = [];
+ $returnValue = static function ($newValue) use (&$value) {
+ $value = $newValue;
+ };
- foreach ($properties as $property) {
- $data[$property->getName()] = $property->getValue($this);
- }
+ $finish = trigger('__get', $this, $property, $returnValue);
+
+ $value = $finish($value);
- $this->publicProperties = array_diff_key($data, array_flip(self::RESERVED_PROPERTIES));
+ if ($value === 'noneset') {
+ throw new PropertyNotFoundException($property, $this->getName());
}
- return $this->publicProperties;
+ return $value;
}
- /**
- * Returns an optional array with uncallable method names
- * who can not be executed by a subsequent request.
- *
- * These methods are still callable inside the component's template file.
- *
- * @return string[]
- */
- public function getUncallables(): array
+ public function __unset($property)
{
- return $this->uncallables;
+ trigger('__unset', $this, $property);
}
- /**
- * @param string $method
- * @param array $args
- * @return array|bool|mixed|null|void
- * @throws LocalizedException
- */
- public function __call(string $method, array $args)
+ public function __call($method, $params)
{
- if($this->ignoreCall($method)) {
- return;
- }
+ $value = 'noneset';
+
+ $returnValue = static function ($newValue) use (&$value) {
+ $value = $newValue;
+ };
+
+ $finish = trigger('__call', $this, $method, $params, $returnValue);
+
+ $value = $finish($value);
- $key = lcfirst(substr($method, 3));
-
- switch (substr($method, 0, 3)) {
- case 'get':
- if (property_exists($this, $key)) {
- return $this->{$key};
- }
- break;
- case 'has':
- return property_exists($this, $key) && $this->{$key} !== null;
- default:
- throw new LocalizedException(__('Invalid method %1::%2', [get_class($this), $method]));
+ if ($value !== 'noneset') {
+ return $value;
}
+
+ throw new BadMethodCallException(sprintf('Method %s::%s does not exist.', static::class, $method));
}
- /**
- * @param string $method
- * @return bool
- */
- public function ignoreCall(string $method): bool
+ public function tap($callback): static
{
- if (in_array($method, ['boot', 'mount', 'hydrate', 'dehydrate', 'updating', 'updated'])) {
- return true;
- }
-
- foreach (['hydrate', 'dehydrate', 'updating', 'updated'] as $start) {
- if (strncmp($method, $start, strlen($start)) === 0) {
- return true;
- }
- }
+ $callback($this);
- return false;
+ return $this;
}
}
diff --git a/src/Component/Form.php b/src/Component/Form.php
new file mode 100644
index 00000000..381e0381
--- /dev/null
+++ b/src/Component/Form.php
@@ -0,0 +1,108 @@
+validator = $validator;
+ }
+
+ /**
+ * @throws AcceptableException
+ */
+ public function validate(
+ array $rules = [],
+ array $messages = [],
+ array|null $data = null,
+ array $aliases = [],
+ bool $mergeWithClassProperties = true
+ ): bool {
+ $rules = $mergeWithClassProperties ? array_merge($this->rules, $rules) : $rules;
+ $data ??= $this->getPublicProperties(true);
+
+ $messages = array_map(
+ static function ($message) {
+ return __($message);
+ },
+ $mergeWithClassProperties ? array_merge($this->messages, $messages) : $messages
+ );
+
+ try {
+ $validation = $this->validator->make($data, $rules, $messages);
+ $validation->setAliases($aliases);
+
+ $validation->setTranslations([
+ 'or' => __('or'),
+ 'and' => __('and')
+ ]);
+
+ foreach (array_keys($rules) as $attributeName) {
+ foreach ($validation->getAttribute($attributeName)->getRules() as $rule) {
+ $rule->setMessage((string) __($rule->getMessage()));
+ }
+ }
+
+ $validation->validate();
+
+ if ($validation->fails()) {
+ foreach ($validation->errors()->toArray() as $key => $error) {
+ $this->error($key, current($error));
+ }
+
+ throw new ValidationException(__('Something went wrong while validating the form input'));
+ }
+ } catch (Exception $exception) {
+ throw new AcceptableException(__($exception->getMessage()));
+ }
+
+ return true;
+ }
+
+ /**
+ * @throws AcceptableException
+ */
+ public function validateOnly(array $rules = [], array $messages = [], array|null $data = null): bool
+ {
+ return $this->validate($rules, $messages, $data, [], false);
+ }
+}
diff --git a/src/Component/Pagination.php b/src/Component/Pagination.php
deleted file mode 100644
index b9f04829..00000000
--- a/src/Component/Pagination.php
+++ /dev/null
@@ -1,64 +0,0 @@
-getParent()) === null) {
- throw new ComponentException(__('No block attached onto the current Magewire component'));
- }
- if (($block = $parent->getLayout()->getBlock('magewire.pagination.pager')) === null) {
- throw new ComponentException(__('Pagination block could not be found'));
- }
-
- $block->setData('component', $this);
- $block->setTemplate($template ?? $this->pagerTemplate);
-
- return $block->toHtml();
- } catch (LocalizedException $exception) {
- return __('Pager not available.')->render();
- }
- }
-}
diff --git a/src/Component/PaginationInterface.php b/src/Component/PaginationInterface.php
deleted file mode 100644
index 35522144..00000000
--- a/src/Component/PaginationInterface.php
+++ /dev/null
@@ -1,111 +0,0 @@
-magewireServiceProvider->getHandleRequestsMechanismFacade();
+
+ return $this->updateResultFactory->create($handleRequestsMechanismFacade->update());
+ } catch (Exception $exception) {
+ try {
+ $handler = $this->exceptionManager->handle($exception);
+
+ // Set a default exception render handler.
+ $handler ??= static function (HttpResponseInterface $response) use ($exception) {
+ $response->setBody('An unexpected error occurred while processing your request.');
+ $response->setHttpResponseCode(500);
+
+ return $response;
+ };
+
+ return $this->magewireUpdateResultFactory->create()->renderWith($handler);
+ } catch (Exception $exception) {
+ if ($this->applicationState->getMode() === ApplicationState::MODE_PRODUCTION) {
+ throw $exception;
+ }
+
+ return $this->magewireUpdateResultFactory
+ ->create()
+ ->renderWith(static function (HttpResponseInterface $response) use ($exception) {
+ $response->setBody($exception->getMessage());
+ $response->setHttpResponseCode(500);
+
+ return $response;
+ });
+ }
+ }
+ }
+
+ /**
+ * @throws LocalizedException
+ */
+ public function validateForCsrf(RequestInterface $request): bool|null
+ {
+ return Security::compareStrings($request->getParam('token'), $this->formKey->getFormKey());
+ }
+
+ public function createCsrfValidationException(RequestInterface $request): InvalidRequestException|null
+ {
+ return null;
+ }
+}
diff --git a/src/Controller/MagewireDeveloperAction.php b/src/Controller/MagewireDeveloperAction.php
new file mode 100644
index 00000000..7ad7afca
--- /dev/null
+++ b/src/Controller/MagewireDeveloperAction.php
@@ -0,0 +1,55 @@
+applicationState->getMode() === ApplicationState::MODE_PRODUCTION) {
+ return $this->forward(Base::NO_ROUTE);
+ }
+
+ return $this->page();
+ }
+
+ protected function page(): Page
+ {
+ $page = $this->page ?? $this->pageFactory->create();
+ $page->getConfig()->getTitle()->set($this->pageTitle);
+
+ return $page;
+ }
+
+ protected function forward(string $action): Forward
+ {
+ return $this->resultForwardFactory->create()->forward($action);
+ }
+}
diff --git a/src/Controller/MagewireRoute.php b/src/Controller/MagewireRoute.php
new file mode 100644
index 00000000..34c3ea9e
--- /dev/null
+++ b/src/Controller/MagewireRoute.php
@@ -0,0 +1,92 @@
+getMatchConditions();
+
+ /*
+ * Magewire routes must always start with the Magewire base route.
+ * Although this is already validated during the normal routing process,
+ * this object can be used independently, so the route must always be revalidated here before proceeding.
+ */
+ array_unshift($conditions, fn () => $this->magewireRouteValidator->validate($request));
+
+ foreach ($conditions as $name => $condition) {
+ try {
+ if (! $condition($request)) {
+ return null;
+ }
+ } catch (Exception $exception) {
+ $this->log()->debug(sprintf('Route match condition "%s" threw an exception', $name), ['exception' => $exception]);
+
+ return null;
+ }
+ }
+
+ return $this->createAction($request);
+ }
+
+ /**
+ * Returns an array of match conditions as closures to validate certain aspects of a request.
+ *
+ * Each condition is a closure that takes a `Request` object as a parameter and returns a boolean value
+ * indicating whether the request satisfies the specific condition. These conditions could be used to
+ * filter or validate incoming HTTP requests based on certain criteria like request method, URI, and content type.
+ *
+ * TODO: Currently, conditions are a flat array of callables, each of which must return true for the check to pass.
+ * If any callable returns false, the entire check fails. Future plans include supporting nested arrays,
+ * where each sub-array acts as an OR condition—if any callable in a sub-array returns true,
+ * that group is considered satisfied.
+ *
+ * @return array
+ */
+ abstract public function getMatchConditions(): array;
+
+ abstract public function createAction(RequestInterface $request): ActionInterface;
+
+ public function actionFactory(): ActionFactory
+ {
+ return $this->actionFactory;
+ }
+
+ public function log(): LoggerInterface
+ {
+ return $this->logger;
+ }
+}
diff --git a/src/Controller/MagewireUpdateRoute.php b/src/Controller/MagewireUpdateRoute.php
new file mode 100644
index 00000000..cf5ef009
--- /dev/null
+++ b/src/Controller/MagewireUpdateRoute.php
@@ -0,0 +1,154 @@
+actionFactory, $this->logger, $this->magewireRouteValidator);
+ }
+
+ /**
+ * Matches and processes Magewire update requests.
+ *
+ * Validates the request, boots Magewire in subsequent mode, and parses
+ * component data. Returns a forward action on failure or the matched
+ * action on success.
+ */
+ public function match(RequestInterface $request): ActionInterface|null
+ {
+ $match = parent::match($request);
+
+ if ($match === null) {
+ return null;
+ }
+
+ /**
+ * Boot Magewire and initialize the request context.
+ *
+ * Magewire has two trigger points for initialization:
+ * 1. During component updates (the only feasible location after confirming an update request)
+ * 2. During page load via the view block observer
+ *
+ * This call marks the request as "subsequent" to distinguish between initial page loads
+ * and subsequent update requests, allowing system-wide determination of Magewire's state.
+ *
+ * @see \Magewirephp\Magewire\Observer\ViewBlockAbstractToHtmlBefore
+ */
+ $this->magewireServiceProvider->boot(RequestMode::SUBSEQUENT);
+
+ try {
+ $request->setParams($this->parseRequest($request));
+ } catch (Exception $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+
+ return null;
+ }
+
+ return $match;
+ }
+
+ /**
+ * Parses and validates the component update request payload.
+ *
+ * Deserializes component data from the request body, verifies checksums,
+ * validates component prerequisites (resolver/handle), and converts
+ * component data to service contract objects.
+ *
+ * @throws LocalizedException
+ * @throws FileSystemException
+ * @throws \Magento\Framework\Exception\RuntimeException
+ * @throws CorruptComponentPayloadException
+ */
+ protected function parseRequest(RequestInterface $request): array
+ {
+ // @todo: Consider finding a better way for retrieving request parameters.
+ $input = $this->serializer->unserialize(file_get_contents('php://input'));
+
+ foreach ($input[self::PARAM_COMPONENTS] as $key => $component) {
+ $component['snapshot'] = $this->serializer->unserialize($component['snapshot']);
+
+ $this->checksum->verify($component['snapshot']);
+ trigger('snapshot-verified', $component['snapshot']);
+
+ $handle = $component['snapshot']['memo']['handle'] ?? null;
+ $resolver = $component['snapshot']['memo']['resolver'] ?? null;
+
+ /**
+ * Magewire requires at least a component resolver accessor and any necessary layout update handles.
+ * These layout handles must adhere to core Magento requirements for layout handles to proceed
+ * with handling the update.
+ *
+ * @see Magento_Framework::View/Layout/etc/elements.xsd
+ */
+ if (! $resolver && (! $handle || preg_match('/^[a-zA-Z0-9][a-zA-Z\d\-_\.]*$/', $handle) !== 1)) {
+ throw new RuntimeException('Base component prerequisites not satisfied.');
+ }
+
+ $snapshot = $this->snapshotFactory->create([
+ 'data' => $component['snapshot']['data'] ?? [],
+ 'memo' => $component['snapshot']['memo'] ?? [],
+ 'checksum' => $component['snapshot']['checksum'] ?? '',
+ ]);
+
+ $input[self::PARAM_COMPONENTS][$key] = $this->componentRequestContextFactory->create([
+ 'snapshot' => $snapshot,
+ 'calls' => $component['calls'] ?? [],
+ 'updates' => $component['updates'] ?? [],
+ ]);
+ }
+
+ /** @var Request $request */
+ $request->setParam('token', $input['_token'] ?? null);
+
+ unset($input['_token']);
+
+ return array_merge($request->getParams(), $input);
+ }
+}
diff --git a/src/Controller/MagewireUpdateRouteFrontend.php b/src/Controller/MagewireUpdateRouteFrontend.php
new file mode 100644
index 00000000..e1cba0f3
--- /dev/null
+++ b/src/Controller/MagewireUpdateRouteFrontend.php
@@ -0,0 +1,34 @@
+ static fn (Request $request): bool => $request->isPost(),
+ 'update_uri' => static fn (Request $request): bool => str_starts_with($request->getPathInfo(), '/magewire/update'),
+ 'content_type' => static fn (Request $request): bool => $request->getHeader('Content-Type') === 'application/json'
+ ];
+ }
+
+ public function createAction(RequestInterface $request): ActionInterface
+ {
+ return $this->actionFactory()->create(Update::class);
+ }
+}
diff --git a/src/Controller/Playwright/Directives.php b/src/Controller/Playwright/Directives.php
new file mode 100644
index 00000000..d25fde97
--- /dev/null
+++ b/src/Controller/Playwright/Directives.php
@@ -0,0 +1,20 @@
+formKey = $formKey;
- $this->componentHelper = $componentHelper;
- $this->resultPageFactory = $resultPageFactory;
- $this->serializer = $serializer;
- $this->httpFactory = $httpFactory;
- $this->resultJsonFactory = $resultJsonFactory;
- }
-
- /**
- * @return Json
- */
- public function execute(): Json
- {
- $result = $this->resultJsonFactory->create();
-
- try {
- $post = $this->serializer->unserialize(file_get_contents('php://input'));
- $block = $this->locateWireBlock($post);
-
- $component = $this->componentHelper->extractComponentFromBlock($block);
- $component->setRequest($this->httpFactory->createRequest($post)->isSubsequent(true));
-
- $html = $block->toHtml();
- $response = $component->getResponse();
-
- if ($response === null) {
- throw new MagewireException(__('Response object not found for component'));
- }
-
- // Set final HTML for response
- $response->effects['html'] = $html;
-
- return $result->setData([
- 'effects' => $response->getEffects(),
- 'serverMemo' => $response->getServerMemo()
- ]);
- } catch (SubsequentRequestException $exception) {
- $result->setStatusHeader(Response::STATUS_CODE_500, AbstractMessage::VERSION_11, 'Bad Request');
-
- return $result->setData([
- 'message' => 'Something went wrong during the subsequent request lifecycle: ' . $exception->getMessage(),
- 'code' => $exception->getCode()
- ]);
- } catch (Exception $exception) {
- $result->setStatusHeader(Response::STATUS_CODE_500, AbstractMessage::VERSION_11, 'Bad Request');
-
- return $result->setData([
- 'message' => 'Something went wrong outside the component: ' . $exception->getMessage(),
- 'code' => $exception->getCode()
- ]);
- }
- }
-
- /**
- * @param array $post
- * @return BlockInterface
- * @throws SubsequentRequestException
- */
- public function locateWireBlock(array $post): BlockInterface
- {
- $resultPage = $this->resultPageFactory->create();
- $resultPage->addHandle($post['fingerprint']['handle'])->initLayout();
-
- $block = $resultPage->getLayout()->getBlock($post['fingerprint']['name']);
-
- if ($block === false) {
- throw new SubsequentRequestException('Magewire component does not exist');
- }
-
- return $block;
- }
-
- /**
- * @inheritDoc
- */
- public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
- {
- return null;
- }
-
- /**
- * @inheritDoc
- * @throws LocalizedException
- */
- public function validateForCsrf(RequestInterface $request): ?bool
- {
- return $request->isPost() && Security::compareStrings($request->getHeader('X-CSRF-TOKEN'), $this->formKey->getFormKey());
- }
-}
diff --git a/src/Controller/Router.php b/src/Controller/Router.php
new file mode 100644
index 00000000..9858d6bb
--- /dev/null
+++ b/src/Controller/Router.php
@@ -0,0 +1,47 @@
+matchesMagewireSpecifics($request)) {
+ foreach (array_filter($this->routes) as $route) {
+ if ($result = $route->match($request)) {
+ return $result;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private function matchesMagewireSpecifics(RequestInterface $request): bool
+ {
+ return $request instanceof Request && $this->magewireRouteValidator->validate($request);
+ }
+}
diff --git a/src/Controller/Vintage/Livewire.php b/src/Controller/Vintage/Livewire.php
deleted file mode 100644
index 19b512ea..00000000
--- a/src/Controller/Vintage/Livewire.php
+++ /dev/null
@@ -1,58 +0,0 @@
-origin = $origin;
- }
-
- public function execute(): Json
- {
- return $this->origin->execute();
- }
-
- /**
- * @inheritDoc
- */
- public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
- {
- return $this->origin->createCsrfValidationException($request);
- }
-
- /**
- * @inheritDoc
- */
- public function validateForCsrf(RequestInterface $request): ?bool
- {
- return $this->origin->validateForCsrf($request);
- }
-}
diff --git a/src/Exception/ComponentActionException.php b/src/Exception/ComponentActionException.php
deleted file mode 100644
index 548b6e57..00000000
--- a/src/Exception/ComponentActionException.php
+++ /dev/null
@@ -1,18 +0,0 @@
-getMagewire();
-
- if ($magewire) {
- if (is_array($magewire)) {
- return $magewire['type'];
- }
- if (is_object($magewire)) {
- return $magewire;
- }
- }
-
- throw new MissingComponentException(__('Magewire component not found'));
- }
-
- /**
- * @param BlockInterface $block
- * @param array $addition
- * @return array
- */
- public function extractDataFromBlock(BlockInterface $block, array $addition = []): array
- {
- $magewire = $block->getMagewire();
-
- if ($magewire && is_array($magewire)) {
- unset($magewire['type']); return array_merge_recursive($magewire, $addition);
- }
-
- return $addition;
- }
-}
diff --git a/src/Helper/Functions.php b/src/Helper/Functions.php
deleted file mode 100644
index 789cc7d8..00000000
--- a/src/Helper/Functions.php
+++ /dev/null
@@ -1,77 +0,0 @@
-serializer = $serializer;
- }
-
- /**
- * @param callable $callback
- * @param array $data
- * @return array
- */
- public function map(callable $callback, array $data): array
- {
- $keys = array_keys($data);
- $items = array_map($callback, $data, $keys);
-
- return array_combine($keys, $items);
- }
-
- /**
- * @param callable $callback
- * @param array $data
- * @return array
- */
- public function mapWithKeys(callable $callback, array $data): array
- {
- $result = [];
-
- foreach ($data as $key => $value) {
- $assoc = $callback($value, $key);
-
- foreach ($assoc as $mapKey => $mapValue) {
- $result[$mapKey] = $mapValue;
- }
- }
-
- return $result;
- }
-
- /**
- * @param $subject
- * @return string
- */
- public function escapeStringForHtml($subject): string
- {
- if (is_string($subject) || is_numeric($subject)) {
- return htmlspecialchars($subject);
- }
-
- return htmlspecialchars($this->serializer->serialize($subject));
- }
-}
diff --git a/src/Helper/Property.php b/src/Helper/Property.php
deleted file mode 100644
index da2393a4..00000000
--- a/src/Helper/Property.php
+++ /dev/null
@@ -1,85 +0,0 @@
-arrayManager = $arrayManager;
- }
-
- /**
- * @param string $property
- * @return bool
- */
- public function containsDots(string $property): bool
- {
- return strpos($property, '.') !== false;
- }
-
- /**
- * @param string $path
- * @param $value
- * @param Component $component
- * @return array
- */
- public function transformDots(string $path, $value, Component $component): array
- {
- $pieces = explode('.', $path);
- $property = $pieces[0];
-
- array_shift($pieces);
- $value = $this->arrayManager->set(implode('/', $pieces), $component->{$property}, $value);
-
- return compact('property', 'value');
- }
-
- /**
- * Use a callback function to assign component property
- * values except default reserved properties.
- *
- * NOTE: Will skip the lifecycle.
- *
- * @param callable $callback
- * @param RequestInterface|ResponseInterface $subject (waiting for PHP 8.x support)
- * @param Component $component
- * @return void
- */
- public function assign(callable $callback, $subject, Component $component): void
- {
- $publicProperties = $component->getPublicProperties();
-
- foreach ($subject->memo['data'] as $property => $value) {
- if (in_array($property, Component::RESERVED_PROPERTIES, true)) {
- continue;
- }
- if (array_key_exists($property, $publicProperties) && ($component->{$property} !== $value)) {
- $callback($component, $subject, $property, $value);
- }
- }
- }
-}
diff --git a/src/Helper/Security.php b/src/Helper/Security.php
deleted file mode 100644
index 0e3b8a00..00000000
--- a/src/Helper/Security.php
+++ /dev/null
@@ -1,68 +0,0 @@
-deployConfig = $deployConfig;
- $this->serializer = $serializer;
- }
-
- /**
- * @param array $data1
- * @param array $data2
- * @return string
- * @throws FileSystemException
- * @throws RuntimeException
- */
- public function generateChecksum(array $data1, array $data2): string
- {
- $key = $this->deployConfig->get('crypt/key');
-
- $hash = '' . $this->serializer->serialize($data1) . $this->serializer->serialize($data2);
- return hash_hmac('sha256', $hash, $key);
- }
-
- /**
- * @param string $checksum
- * @param array $data1
- * @param array $data2
- * @return bool
- * @throws FileSystemException
- * @throws RuntimeException
- */
- public function validateChecksum(string $checksum, array $data1, array $data2): bool
- {
- return hash_equals($this->generateChecksum($data1, $data2), $checksum);
- }
-}
diff --git a/src/Magewire/Playwright/Directives/Base.php b/src/Magewire/Playwright/Directives/Base.php
new file mode 100644
index 00000000..5280b1da
--- /dev/null
+++ b/src/Magewire/Playwright/Directives/Base.php
@@ -0,0 +1,18 @@
+runtime()->state()->isMinimally(RuntimeState::SETUP)) {
+ return;
+ }
+
+ try {
+ // Containers are always fully booted during setup.
+ $this->containers->boot();
+
+ // Before setup, but after containers service type booted.
+ trigger('magewire:setup', $this->runtime());
+
+ // Boot only persistent and above during setup.
+ $this->mechanisms->boot(ServiceTypeItemBootMode::PERSISTENT);
+ $this->features->boot(ServiceTypeItemBootMode::PERSISTENT);
+
+ $this->runtime()->state(RuntimeState::SETUP);
+ } catch (Exception $exception) {
+ $this->runtime()->state(RuntimeState::FAILED);
+
+ throw $exception;
+ }
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function boot(RequestMode $mode, bool $force = false): void
+ {
+ if (! $force && $this->runtime()->state()->is(RuntimeState::UNINITIALIZED)) {
+ $this->setup();
+ }
+ if (! $force && $this->runtime()->state()->isMinimally(RuntimeState::BOOTED)) {
+ return;
+ }
+
+ try {
+ // Define runtime boot mode.
+ $this->runtime()->mode($mode);
+ // Define runtime booting state right before service items boot attempt.
+ $this->runtime()->state(RuntimeState::BOOTING);
+
+ // Before boot.
+ $finish = trigger('magewire:boot', $this->runtime());
+
+ // Boot all remaining service types not yet booted during setup.
+ $boot['containers'] = $this->mechanisms->boot();
+ $boot['mechanisms'] = $this->mechanisms->boot();
+ $boot['features'] = $this->features->boot();
+
+ if (in_array(false, $boot, true)) {
+ throw new RuntimeException('One or more service types were unable to boot completely.');
+ }
+
+ // After boot without any exceptions.
+ $finish();
+
+ // Define runtime boot state as ready when all succeeded.
+ $this->runtime()->state(RuntimeState::BOOTED);
+ } catch (Exception $exception) {
+ $this->runtime()->state(RuntimeState::FAILED);
+
+ throw $exception;
+ }
+ }
+
+ public function runtime(): Runtime
+ {
+ return $this->runtime ??= $this->runtimeFactory->create();
+ }
+
+ public function __call($name, $arguments)
+ {
+ $matches = preg_match_all('/[A-Z][a-z]*/', $name, $matches) ? $matches[0] : [];
+
+ $operation = $this->getServiceType($matches[count($matches) - 1]);
+ $operation ??= $this->getServiceType($matches[count($matches) - 2]);
+
+ /*
+ * Enables the possibility to get either a mechanism- or a feature operation type or its
+ * belonging facade if it has any attached.
+ *
+ * @todo This lookup can be heavily optimized caching those that are found
+ * by there $name and when exist as key in a global class array,
+ * return that type instead of looking it up once again.
+ */
+ if ($operation) {
+ $item = strtolower($matches[count($matches) - 1]);
+
+ $argument = strtolower(implode('_', array_slice($matches, 0, count($matches) - 1)));
+ $argument = trim(preg_replace('/(?:container|mechanism|feature)\s*$/', '', $argument), '_');
+
+ try {
+ return match ($item) {
+ 'container', 'feature', 'mechanism' => $operation->item($argument),
+ 'facade' => $operation->facade($argument)
+ };
+ } catch (Exception $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+ }
+ }
+
+ throw new BadMethodCallException();
+ }
+
+ private function getServiceType(string $operation): ServiceType|null
+ {
+ $operation = strtolower(! str_ends_with($operation, 's') ? $operation . 's' : $operation);
+
+ if (property_exists(self::class, $operation)) {
+ return $this->{$operation};
+ }
+
+ return null;
+ }
+}
diff --git a/src/Model/Action/CallMethod.php b/src/Model/Action/CallMethod.php
deleted file mode 100644
index e21a5a66..00000000
--- a/src/Model/Action/CallMethod.php
+++ /dev/null
@@ -1,100 +0,0 @@
-typeFactory = $typeFactory;
- $this->uncallableMethods = $uncallableMethods;
- }
-
- /**
- * @inheritdoc
- * @throws ComponentActionException
- * @throws LocalizedException
- */
- public function handle(Component $component, array $payload)
- {
- $method = ltrim($payload['method'], '$');
- $params = $payload['params'];
-
- if ($this->isCallable($method, $component)) {
- return $component->{$method}(...$params);
- }
-
- // Determine the required type class by method in specific order
- $type = $this->determineType($method);
- // Add the component as a dynamic last method param
- $params[] = $component;
-
- if ($this->isCallable($method, $type)) {
- return $type->{$method}(...$params);
- }
-
- throw new ComponentActionException(__('Method %1 does not exist or can not be called', [$method]));
- }
-
- /**
- * @param $method
- * @param object $class
- * @return bool
- */
- public function isCallable($method, object $class): bool
- {
- $uncallables = $this->uncallableMethods;
-
- if ($class instanceof Component) {
- $uncallables = array_merge($class->getUncallables(), $this->uncallableMethods);
- }
-
- return in_array($method, array_diff(get_class_methods($class), $uncallables), true);
- }
-
- /**
- * @param string $method
- * @return mixed|string
- * @throws LocalizedException
- */
- public function determineType(string $method)
- {
- foreach ([Magic::class] as $type) {
- if (method_exists($type, $method)) {
- return $this->typeFactory->create($type);
- }
- }
-
- throw new LocalizedException(__('Method %1 does not exist', [$method]));
- }
-}
diff --git a/src/Model/Action/FireEvent.php b/src/Model/Action/FireEvent.php
deleted file mode 100644
index 344ef28a..00000000
--- a/src/Model/Action/FireEvent.php
+++ /dev/null
@@ -1,69 +0,0 @@
-functionsHelper = $functionsHelper;
- $this->callMethodHandler = $callMethodHandler;
- $this->listenerHydrator = $listenerHydrator;
- }
-
- /**
- * @inheritdoc
- *
- * @throws ComponentActionException|LocalizedException
- */
- public function handle(Component $component, array $payload)
- {
- $listeners = $this->listenerHydrator->assimilateListeners($component);
- $method = $listeners[$payload['event']] ?? false;
- $parameters = $payload['params'][0] ?? [];
-
- if ($method === false) {
- throw new ComponentActionException(__('Method %1 does not exist or can not be called', [$method]));
- }
-
- $this->callMethodHandler->handle($component, [
- 'method' => $method,
- 'params' => array_values($parameters)
- ]);
- }
-}
diff --git a/src/Model/Action/SyncInput.php b/src/Model/Action/SyncInput.php
deleted file mode 100644
index 560d3337..00000000
--- a/src/Model/Action/SyncInput.php
+++ /dev/null
@@ -1,61 +0,0 @@
-propertyHelper = $propertyHelper;
- }
-
- /**
- * @inheritdoc
- *
- * @throws ComponentActionException
- */
- public function handle(Component $component, array $payload)
- {
- if (!isset($payload['name'], $payload['value'])) {
- throw new ComponentActionException(__('Invalid update payload'));
- }
-
- if ($this->propertyHelper->containsDots($payload['name'])) {
- $transform = $this->propertyHelper->transformDots($payload['name'], $payload['value'], $component);
-
- // Define a new method since it's a nested property
- $method = preg_replace_callback('/[-_.](.?)/', static function ($matches) {
- return ucfirst($matches[1]);
- }, $payload['name']);
-
- // Re-assign original method properties
- $payload['name'] = $transform['property'];
- $payload['value'] = $transform['value'];
- }
-
- $component->assign($payload['name'], $payload['value'], false, $method ?? null);
- }
-}
diff --git a/src/Model/Action/Type/Factory.php b/src/Model/Action/Type/Factory.php
deleted file mode 100644
index 06417a1e..00000000
--- a/src/Model/Action/Type/Factory.php
+++ /dev/null
@@ -1,40 +0,0 @@
-objectManager = $objectManager;
- }
-
- /**
- * @param string $type
- * @return mixed|string
- */
- public function create(string $type)
- {
- return $this->objectManager->create($type);
- }
-}
diff --git a/src/Model/Action/Type/Magic.php b/src/Model/Action/Type/Magic.php
deleted file mode 100644
index 2169bb7d..00000000
--- a/src/Model/Action/Type/Magic.php
+++ /dev/null
@@ -1,73 +0,0 @@
-propertyHelper = $propertyHelper;
- }
-
- /**
- * Magic method ($toggle) to toggle boolean properties.
- *
- * Example: Toggle
- *
- * @param string $property
- * @param $component
- * @return void
- */
- public function toggle(string $property, $component): void
- {
- $this->set($property, !$component->{$property}, $component);
- }
-
- /**
- * Magic method ($set) to update the value of a property.
- *
- * Example: Set
- *
- * @param string $property
- * @param $value
- * @param $component
- * @return void
- */
- public function set(string $property, $value, $component): void
- {
- if ($this->propertyHelper->containsDots($property)) {
- $transform = $this->propertyHelper->transformDots($property, $value, $component);
-
- // Re-assign original method properties
- $property = $transform['property'];
- $value = $transform['value'];
- }
-
- $component->assign($property, $value);
- }
-
- public function refresh(): void
- {
- return;
- }
-}
diff --git a/src/Model/ActionInterface.php b/src/Model/ActionInterface.php
deleted file mode 100644
index c5b0c7dd..00000000
--- a/src/Model/ActionInterface.php
+++ /dev/null
@@ -1,30 +0,0 @@
-getMessage(), $exception->getCode(), $exception->getPrevious());
+ }
+
+ return $exception;
+ }
+
+ function handleWithBlock(AbstractBlock $block, Exception $exception, bool $subsequent = false): AbstractBlock
+ {
+ if ($block instanceof Template) {
+ $block->setTemplate('Magewirephp_Magewire::magewire/exception.phtml');
+ }
+
+ return $block;
+ }
+}
diff --git a/src/Model/App/ExceptionHandler.php b/src/Model/App/ExceptionHandler.php
new file mode 100644
index 00000000..1b591710
--- /dev/null
+++ b/src/Model/App/ExceptionHandler.php
@@ -0,0 +1,30 @@
+setData('application_state', $this->state);
+
+ return $block;
+ }
+}
diff --git a/src/Model/App/ExceptionManager.php b/src/Model/App/ExceptionManager.php
new file mode 100644
index 00000000..92b7838d
--- /dev/null
+++ b/src/Model/App/ExceptionManager.php
@@ -0,0 +1,137 @@
+> $specificExceptionHandlerPool
+ */
+ function __construct(
+ private readonly LoggerInterface $logger,
+ private readonly AbstractExceptionHandler $precedingHandler,
+ private readonly AbstractExceptionHandler $subsequentHandler,
+ private readonly MagewireServiceProvider $magewireServiceProvider,
+ private readonly array $specificExceptionHandlerPool = []
+ ) {
+ }
+
+ /**
+ * @throws Exception
+ */
+ function handle(Exception $exception, bool $log = true): callable|null
+ {
+ $subsequent = $this->magewireServiceProvider
+ ->runtime()
+ ->mode()
+ ->isSubsequent();
+
+ try {
+ $exception = $this->resolveExceptionHandler($exception, $subsequent)->handle($exception, $subsequent);
+ } catch (Exception $parent) {
+ $exception = $parent;
+ }
+
+ if ($exception instanceof SilentException) {
+ return null;
+ } elseif (is_callable($exception)) {
+ return $exception;
+ }
+
+ if ($log) {
+ $this->logger->info(sprintf('Magewire: %s', $exception->getMessage()), ['exception' => $exception]);
+ }
+
+ throw $exception;
+ }
+
+ /**
+ * @throws Exception
+ */
+ function handleWithBlock(AbstractBlock $block, Exception $exception, bool $log = true): AbstractBlock
+ {
+ $subsequent = $this->magewireServiceProvider
+ ->runtime()
+ ->mode()
+ ->isSubsequent();
+
+ if ($subsequent) {
+ $this->handle($exception, $log);
+ }
+
+ // Prevent cyclic loops from re-triggering the entire Magewire lifecycle.
+ $block->unsetData('magewire');
+ // Making sure the exception is always present on the block.
+ $block->setData('exception', $exception);
+
+ try {
+ $block = $this->resolveExceptionHandler($exception, $subsequent)->handleWithBlock($block, $exception, $subsequent);
+ } catch (Exception $parent) {
+ $this->handle(new SilentException($parent->getMessage(), $parent->getCode(), $parent->getPrevious()));
+ }
+
+ if ($log) {
+ $this->logger->info($exception->getMessage(), ['exception' => $exception]);
+ }
+
+ return $block;
+ }
+
+ private function resolveExceptionHandler(Exception $exception, bool $subsequent = false): AbstractExceptionHandler
+ {
+ $exceptionGroup = $subsequent ? 'subsequent' : 'preceding';
+ $exceptionClass = $exception::class;
+ $exceptionHandlerPool = $this->specificExceptionHandlerPool;
+
+ // Check for a general subsequent tor preceding handler.
+ if (is_object($exceptionHandlerPool[$exceptionGroup] ?? null)) {
+ $handler = $this->specificExceptionHandlerPool[$exceptionClass] ?? $exceptionHandlerPool[$exceptionGroup];
+
+ if ($handler instanceof AbstractExceptionHandler) {
+ return $handler;
+ }
+
+ // Enable to continue the search by switching the group into an array.
+ $exceptionHandlerPool[$exceptionGroup] = [];
+ }
+
+ $fallback = $subsequent ? $this->subsequentHandler : $this->precedingHandler;
+
+ // Check for a grouped handler for the specific exception class.
+ $handler = $exceptionHandlerPool[$exceptionGroup][$exceptionClass] ?? null
+ ? $exceptionHandlerPool[$exceptionGroup][$exceptionClass]
+ // Check for a handler for the specific exception class.
+ : (
+ $exceptionHandlerPool[$exceptionClass] ?? null
+ ? $exceptionHandlerPool[$exceptionClass]
+ // Continue with the fallback if nothing specific was found.
+ : $fallback
+ );
+
+ if (! $handler instanceof AbstractExceptionHandler) {
+ return $fallback;
+ }
+
+ return $handler;
+ }
+}
diff --git a/src/Model/ComponentManager.php b/src/Model/ComponentManager.php
deleted file mode 100644
index 0ce6f601..00000000
--- a/src/Model/ComponentManager.php
+++ /dev/null
@@ -1,214 +0,0 @@
-localeResolver = $localeResolver;
- $this->updateActionsPool = $updateActionsPool;
- $this->httpFactory = $httpFactory;
-
- /**
- * Important: specific order, don't change this spontaneously!
- */
- $this->hydrationPool = $this->sortHydrators($hydrationPool, [
- $hydratorContext->getSecurityHydrator(),
- $hydratorContext->getBrowserEventHydrator(),
- $hydratorContext->getFlashMessageHydrator(),
- $hydratorContext->getErrorHydrator(),
- $hydratorContext->getHashHydrator(),
- $hydratorContext->getComponentHydrator(),
- $hydratorContext->getQueryStringHydrator(),
- $hydratorContext->getPropertyHydrator(),
- $hydratorContext->getListenerHydrator(),
- $hydratorContext->getLoaderHydrator(),
- $hydratorContext->getEmitHydrator(),
- $hydratorContext->getRedirectHydrator()
- ]);
- }
-
- /**
- * @param Component $component
- * @param array $updates
- * @throws LocalizedException
- * @throws ComponentActionException
- */
- public function processUpdates(Component $component, array $updates): void
- {
- if ($component->hasRequest() === false) {
- throw new LocalizedException(__('No request object found'));
- }
-
- foreach ($updates as $update) {
- try {
- $this->updateActionsPool[$update['type']]->handle($component, $update['payload']);
- } catch (ValidationException $exception) {
- continue;
- } catch (Exception $exception) {
- throw new LocalizedException(__($exception->getMessage()));
- }
- }
- }
-
- /**
- * Runs on every request, after the component is hydrated,
- * but before an action is performed, or the layout block
- * has been rendered.
- *
- * @param Component $component
- * @return void
- * @throws LifecycleException
- */
- public function hydrate(Component $component): void
- {
- foreach ($this->hydrationPool as $hydrator) {
- try {
- $hydrator->hydrate($component, $component->getRequest());
- } catch (Exception $exception) {
- throw new LifecycleException(
- __('An error occurred while hydrating %1: %2', [get_class($hydrator), $exception->getMessage()])
- );
- }
- }
- }
-
- /**
- * Runs on every request, before the component is dehydrated,
- * right before the layout block gets rendered.
- *
- * @param Component $component
- * @throws LifecycleException
- */
- public function dehydrate(Component $component): void
- {
- foreach (array_reverse($this->hydrationPool) as $dehydrator) {
- try {
- $dehydrator->dehydrate($component, $component->getResponse());
- } catch (Exception $exception) {
- throw new LifecycleException(
- __('An error occurred while dehydrating %1: %2', [get_class($dehydrator), $exception->getMessage()])
- );
- }
- }
- }
-
- /**
- * @param Template $block
- * @param Component $component
- * @param array $arguments
- * @param string|null $handle
- * @return Request
- * @throws LocalizedException
- */
- public function createInitialRequest(
- Template $block,
- Component $component,
- array $arguments,
- string $handle = null
- ): Request {
- $properties = $component->getPublicProperties();
- $request = $block->getRequest();
- $data = array_intersect_key(array_replace($properties, $arguments), $properties);
-
- /**
- * SHA1 hashing the wire:id value is an idea which can change in the future. I'm still tumbling around the
- * acceptance of just using the block name which has to be unique which is the most important part. I need to
- * look into the security aspect when switching to an un-hashed version of the wire:id attribute.
- */
- $id = $component->id ?? sha1($block->getNameInLayout());
-
- $name = $block->getNameInLayout();
- $handle = $handle ?? $request->getFullActionName();
- $locale = $this->localeResolver->getLocale();
-
- return $this->httpFactory->createRequest([
- 'fingerprint' => [
- 'id' => $id,
- 'name' => $name,
- 'locale' => $locale,
- 'path' => '/',
- 'method' => 'GET',
-
- // Custom relative to Livewire's core.
- 'handle' => $handle,
- 'type' => $component::COMPONENT_TYPE
- ],
- 'serverMemo' => [
- 'data' => $data,
- 'errors' => [],
-
- // Custom relative to Livewire's core.
- 'custom' => [],
- ]
- ]);
- }
-
- /**
- * [
- * "class" => object,
- * "order" => int
- * ]
- *
- * @param array $hydrators
- * @param $systemHydrators
- * @return array
- *
- * @see ComponentManager::dehydrate()
- * @see ComponentManager::hydrate()
- */
- protected function sortHydrators(array $hydrators, $systemHydrators): array
- {
- usort($hydrators, static function ($x, $y) {
- return $x['order'] - $y['order'];
- });
-
- return array_merge($systemHydrators, array_column($hydrators, 'class'));
- }
-}
diff --git a/src/Model/Concern/BrowserEvent.php b/src/Model/Concern/BrowserEvent.php
deleted file mode 100644
index 14ba3628..00000000
--- a/src/Model/Concern/BrowserEvent.php
+++ /dev/null
@@ -1,36 +0,0 @@
-dispatchQueue;
- }
-
- /**
- * @param $event
- * @param null $data
- */
- public function dispatchBrowserEvent($event, $data = null): void
- {
- $this->dispatchQueue[] = compact('event', 'data');
- }
-}
diff --git a/src/Model/Concern/Conversation.php b/src/Model/Concern/Conversation.php
deleted file mode 100644
index f924cf62..00000000
--- a/src/Model/Concern/Conversation.php
+++ /dev/null
@@ -1,102 +0,0 @@
-hasRequest() && is_array($this->request->getSectionByName($section));
- }
-
- return $this->request !== null;
- }
-
- /**
- * @param string|null $section
- * @return RequestInterface|array|null
- */
- public function getRequest($section = null)
- {
- if (is_string($section)) {
- return $this->request->getSectionByName($section);
- }
-
- return $this->request;
- }
-
- /**
- * @param RequestInterface $request
- * @return $this
- */
- public function setRequest(RequestInterface $request): self
- {
- $this->request = $request;
- return $this;
- }
-
- /**
- * @param string|null $section
- * @return bool
- */
- public function hasResponse($section = null): bool
- {
- if (is_string($section)) {
- try {
- return $this->hasResponse() && is_array($this->response->getSectionByName($section));
- } catch (LocalizedException $exception) {
- return false;
- }
- }
-
- return $this->response !== null;
- }
-
- /**
- * @param string|null $section
- * @return ResponseInterface|array
- */
- public function getResponse($section = null)
- {
- if (is_string($section)) {
- return $this->response->getSectionByName($section);
- }
-
- return $this->response;
- }
-
- /**
- * @param ResponseInterface $response
- * @return $this
- */
- public function setResponse(ResponseInterface $response): self
- {
- $this->response = $response;
- return $this;
- }
-}
diff --git a/src/Model/Concern/Emit.php b/src/Model/Concern/Emit.php
deleted file mode 100644
index a27c26af..00000000
--- a/src/Model/Concern/Emit.php
+++ /dev/null
@@ -1,85 +0,0 @@
-eventQueue;
- }
-
- /**
- * @param string $event
- * @param ...$params
- * @return Event
- */
- public function emit(string $event, ...$params): Event
- {
- return $this->eventQueue[] = new Event($event, $params);
- }
-
-// /**
-// * @param string $event
-// * @param ...$params
-// * @return Event
-// */
-// public function emitUp(string $event, ...$params): Event
-// {
-// return $this->emit($event, ...$params)->up();
-// }
-
- /**
- * Only emit an event on the component that fired the event.
- *
- * @param string $event
- * @param ...$params
- * @return Event
- */
- public function emitSelf(string $event, ...$params): Event
- {
- return $this->emit($event, ...$params)->self();
- }
-
- /**
- * Only emit an event to other components of the same type.
- *
- * @param string $name
- * @param string $event
- * @param ...$params
- * @return Event
- */
- public function emitTo(string $name, string $event, ...$params): Event
- {
- return $this->emit($event, ...$params)->component($name);
- }
-
- /**
- * Only emit a "refresh" event to other components of the same type.
- *
- * @param string $name
- * @return Event
- */
- public function emitToRefresh(string $name): Event
- {
- return $this->emitTo($name, 'refresh');
- }
-}
diff --git a/src/Model/Concern/Event.php b/src/Model/Concern/Event.php
deleted file mode 100644
index 95aa2771..00000000
--- a/src/Model/Concern/Event.php
+++ /dev/null
@@ -1,27 +0,0 @@
-listeners;
- }
-}
diff --git a/src/Model/Concern/FlashMessage.php b/src/Model/Concern/FlashMessage.php
deleted file mode 100644
index 707d77fb..00000000
--- a/src/Model/Concern/FlashMessage.php
+++ /dev/null
@@ -1,98 +0,0 @@
-dispatchMessage(FlashMessageElement::ERROR, $message);
- }
-
- /**
- * @param Phrase|string $message
- * @return FlashMessageElement
- */
- public function dispatchWarningMessage($message): FlashMessageElement
- {
- return $this->dispatchMessage(FlashMessageElement::WARNING, $message);
- }
-
- /**
- * @param Phrase|string $message
- * @return FlashMessageElement
- */
- public function dispatchNoticeMessage($message): FlashMessageElement
- {
- return $this->dispatchMessage(FlashMessageElement::NOTICE, $message);
- }
-
- /**
- * @param Phrase|string $message
- * @return FlashMessageElement
- */
- public function dispatchSuccessMessage($message): FlashMessageElement
- {
- return $this->dispatchMessage(FlashMessageElement::SUCCESS, $message);
- }
-
- /**
- * @param string $type
- * @param Phrase|string $message
- * @return FlashMessageElement
- */
- public function dispatchMessage(string $type, $message): FlashMessageElement
- {
- return $this->messages[] = new FlashMessageElement($message, $type);
- }
-
- /**
- * Check if any flash messages have been dispatched.
- *
- * @return bool
- */
- public function hasFlashMessages(): bool
- {
- return count($this->messages) !== 0;
- }
-
- /**
- * Get all flash messages.
- *
- * @return FlashMessageElement[]
- */
- public function getFlashMessages(): array
- {
- return $this->messages;
- }
-
- /**
- * Reset all flash messages.
- *
- * @return void
- */
- public function clearFlashMessages(): void
- {
- $this->messages = [];
- }
-}
diff --git a/src/Model/Concern/QueryString.php b/src/Model/Concern/QueryString.php
deleted file mode 100644
index 631449bd..00000000
--- a/src/Model/Concern/QueryString.php
+++ /dev/null
@@ -1,34 +0,0 @@
-queryString;
- }
-
- /**
- * @return bool
- */
- public function hasQueryString(): bool
- {
- return !empty($this->getQueryString());
- }
-}
diff --git a/src/Model/Concern/Redirect.php b/src/Model/Concern/Redirect.php
deleted file mode 100644
index c3087804..00000000
--- a/src/Model/Concern/Redirect.php
+++ /dev/null
@@ -1,41 +0,0 @@
-redirect = new RedirectElement($path, $params);
- }
-
- /**
- * @return RedirectElement|null
- */
- public function getRedirect(): ?RedirectElement
- {
- return $this->redirect;
- }
-}
diff --git a/src/Model/Concern/View.php b/src/Model/Concern/View.php
deleted file mode 100644
index d352eae7..00000000
--- a/src/Model/Concern/View.php
+++ /dev/null
@@ -1,63 +0,0 @@
-skipRender = true;
- return $this;
- }
-
- /**
- * Check if the component can be rendered.
- *
- * @return bool
- */
- public function canRender(): bool
- {
- return !$this->skipRender;
- }
-
- /**
- * Switch template of the parent block.
- *
- * @param string $template
- */
- public function switchTemplate(string $template): void
- {
- if ($parent = $this->getParent()) {
- $parent->setTemplate($template);
- }
- }
-
- /**
- * @return bool|array
- */
- public function getLoader()
- {
- return $this->loader;
- }
-}
diff --git a/src/Model/Context/Hydrator.php b/src/Model/Context/Hydrator.php
deleted file mode 100644
index 20618f6c..00000000
--- a/src/Model/Context/Hydrator.php
+++ /dev/null
@@ -1,203 +0,0 @@
-hashHydrator = $hashHydrator;
- $this->listenerHydrator = $listenerHydrator;
- $this->emit = $emit;
- $this->componentHydrator = $componentHydrator;
- $this->propertyHydrator = $propertyHydrator;
- $this->queryStringHydrator = $queryStringHydrator;
- $this->errorHydrator = $errorHydrator;
- $this->redirectHydrator = $redirectHydrator;
- $this->flashMessageHydrator = $flashMessageHydrator;
- $this->browserEventHydrator = $browserEventHydrator;
- $this->securityHydrator = $securityHydrator;
- $this->loaderHydrator = $loaderHydrator;
- }
-
- /**
- * @return Hash
- */
- public function getHashHydrator(): Hash
- {
- return $this->hashHydrator;
- }
-
- /**
- * @return Listener
- */
- public function getListenerHydrator(): Listener
- {
- return $this->listenerHydrator;
- }
-
- /**
- * @return Emit
- */
- public function getEmitHydrator(): Emit
- {
- return $this->emit;
- }
-
- /**
- * @return BrowserEvent
- */
- public function getBrowserEventHydrator(): BrowserEvent
- {
- return $this->browserEventHydrator;
- }
-
- /**
- * @return Component
- */
- public function getComponentHydrator(): Component
- {
- return $this->componentHydrator;
- }
-
- /**
- * @return QueryString
- */
- public function getQueryStringHydrator(): QueryString
- {
- return $this->queryStringHydrator;
- }
-
- /**
- * @return Property
- */
- public function getPropertyHydrator(): Property
- {
- return $this->propertyHydrator;
- }
-
- /**
- * @return Error
- */
- public function getErrorHydrator(): Error
- {
- return $this->errorHydrator;
- }
-
- /**
- * @return Redirect
- */
- public function getRedirectHydrator(): Redirect
- {
- return $this->redirectHydrator;
- }
-
- /**
- * @return FlashMessage
- */
- public function getFlashMessageHydrator(): FlashMessage
- {
- return $this->flashMessageHydrator;
- }
-
- /**
- * @return Security
- */
- public function getSecurityHydrator(): Security
- {
- return $this->securityHydrator;
- }
-
- /**
- * @return Loader
- */
- public function getLoaderHydrator(): Loader
- {
- return $this->loaderHydrator;
- }
-}
diff --git a/src/Model/Csp.php b/src/Model/Csp.php
new file mode 100644
index 00000000..235c7f0d
--- /dev/null
+++ b/src/Model/Csp.php
@@ -0,0 +1,53 @@
+nonceProvider === null) {
+ $this->nonceProvider = $this->isCspAvailable() ? ObjectManager::getInstance()->get('Magento\Csp\Helper\CspNonceProvider') : false;
+ }
+
+ return $this->nonceProvider;
+ }
+
+ public function isCspAvailable(): bool
+ {
+ return $this->nonceProvider !== false && class_exists('Magento\Csp\Helper\CspNonceProvider') || is_object($this->nonceProvider) && method_exists($this->nonceProvider, 'generateNonce');
+ }
+
+ public function generateHash(string $content): string
+ {
+ return base64_encode(hash(self::HASH_ALGORITHM, $content, true));
+ }
+
+ public function getHashAlgorithm(): string
+ {
+ return self::HASH_ALGORITHM;
+ }
+}
diff --git a/src/Model/Element/Event.php b/src/Model/Element/Event.php
deleted file mode 100644
index ce095826..00000000
--- a/src/Model/Element/Event.php
+++ /dev/null
@@ -1,92 +0,0 @@
-name = $name;
- $this->params = $params;
- }
-
- /**
- * @return $this
- */
- public function up(): Event
- {
- $this->up = true;
- return $this;
- }
-
- /**
- * @return $this
- */
- public function self(): Event
- {
- $this->self = true;
- return $this;
- }
-
- /**
- * @param string $name
- * @return $this
- */
- public function component(string $name): Event
- {
- $this->component = $name;
- return $this;
- }
-
- /**
- * @return $this
- */
- public function to(): Event
- {
- return $this;
- }
-
- /**
- * @return array
- */
- public function serialize(): array
- {
- $output = [
- 'event' => $this->name,
- 'params' => $this->params,
- ];
-
- if ($this->up) {
- $output['ancestorsOnly'] = true;
- }
- if ($this->self) {
- $output['selfOnly'] = true;
- }
- if ($this->component) {
- $output['to'] = $this->component;
- }
-
- return $output;
- }
-}
diff --git a/src/Model/Element/FlashMessage.php b/src/Model/Element/FlashMessage.php
deleted file mode 100644
index 687a977f..00000000
--- a/src/Model/Element/FlashMessage.php
+++ /dev/null
@@ -1,62 +0,0 @@
-message = is_string($message) ? __($message) : $message;
- $this->type = $type;
- }
-
- /**
- * @return Phrase
- */
- public function getMessage(): Phrase
- {
- return $this->message;
- }
-
- /**
- * @return string
- */
- public function getType(): string
- {
- return $this->type;
- }
-}
diff --git a/src/Model/Element/Redirect.php b/src/Model/Element/Redirect.php
deleted file mode 100644
index 2979b685..00000000
--- a/src/Model/Element/Redirect.php
+++ /dev/null
@@ -1,56 +0,0 @@
-url = $path;
- $this->params = $params;
- }
-
- /**
- * @return string
- */
- public function getUrl(): string
- {
- return $this->url;
- }
-
- /**
- * @return array
- */
- public function getParams(): array
- {
- return $this->params;
- }
-
- /**
- * @return bool
- */
- public function hasParams(): bool
- {
- return !empty($this->params);
- }
-}
diff --git a/src/Model/HttpFactory.php b/src/Model/HttpFactory.php
deleted file mode 100644
index eacdc6af..00000000
--- a/src/Model/HttpFactory.php
+++ /dev/null
@@ -1,58 +0,0 @@
-serviceInputProcessor = $serviceInputProcessor;
- }
-
- /**
- * @param array $data
- * @return RequestInterface
- * @throws LocalizedException
- */
- public function createRequest(array $data): RequestInterface
- {
- return $this->serviceInputProcessor->convertValue($data, RequestInterface::class);
- }
-
- /**
- * @param RequestInterface $request
- * @param array $data
- * @return ResponseInterface
- * @throws LocalizedException
- */
- public function createResponse(RequestInterface $request, array $data = []): ResponseInterface
- {
- return $this->serviceInputProcessor->convertValue(array_merge([
- 'request' => $request,
- 'fingerprint' => $request->getFingerprint(),
- 'serverMemo' => $request->getServerMemo(),
- 'effects' => []
- ], $data), ResponseInterface::class);
- }
-}
diff --git a/src/Model/Hydrator/BrowserEvent.php b/src/Model/Hydrator/BrowserEvent.php
deleted file mode 100644
index 2fd49c46..00000000
--- a/src/Model/Hydrator/BrowserEvent.php
+++ /dev/null
@@ -1,41 +0,0 @@
-getBrowserEvents();
-
- if (!empty($browserEvents)) {
- $response->effects['dispatches'] = $browserEvents;
- }
- }
-}
diff --git a/src/Model/Hydrator/Component.php b/src/Model/Hydrator/Component.php
deleted file mode 100644
index 7561ecf5..00000000
--- a/src/Model/Hydrator/Component.php
+++ /dev/null
@@ -1,78 +0,0 @@
-componentHelper = $componentHelper;
- }
-
- /**
- * @lifecyclehook boot
- * @lifecyclehook hydrate
- * @lifecyclehook mount
- *
- * @inheritdoc
- */
- public function hydrate(MagewireComponent $component, RequestInterface $request): void
- {
- $data = $this->componentHelper->extractDataFromBlock($component->getParent(), ['request' => $request]);
- $component->boot(...array_values($data));
-
- if ($request->isSubsequent()) {
- $this->executePropertyLifecycleHook($component, 'hydrate', $request);
- $component->hydrate($request);
- } else {
- $component->mount(...array_values($data));
- }
- }
-
- /**
- * @lifecyclehook dehydrate
- *
- * @inheritdoc
- */
- public function dehydrate(MagewireComponent $component, ResponseInterface $response): void
- {
- $component->dehydrate($response);
- $this->executePropertyLifecycleHook($component, 'dehydrate', $response);
- }
-
- /**
- * @param MagewireComponent $component
- * @param string $type
- * @param object $object
- */
- public function executePropertyLifecycleHook(MagewireComponent $component, string $type, object $object): void
- {
- foreach ($component->getPublicProperties() as $property => $value) {
- $component->{strtolower($type) . ucfirst($property)}($value, $object);
- }
- }
-}
diff --git a/src/Model/Hydrator/Emit.php b/src/Model/Hydrator/Emit.php
deleted file mode 100644
index bc635fa4..00000000
--- a/src/Model/Hydrator/Emit.php
+++ /dev/null
@@ -1,39 +0,0 @@
-getEventQueue() as $event) {
- $response->effects['emits'][] = $event->serialize();
- }
- }
-}
diff --git a/src/Model/Hydrator/Error.php b/src/Model/Hydrator/Error.php
deleted file mode 100644
index 4917397f..00000000
--- a/src/Model/Hydrator/Error.php
+++ /dev/null
@@ -1,37 +0,0 @@
-memo['errors'] = $component->getErrors();
- }
-}
diff --git a/src/Model/Hydrator/FlashMessage.php b/src/Model/Hydrator/FlashMessage.php
deleted file mode 100644
index 70e7e017..00000000
--- a/src/Model/Hydrator/FlashMessage.php
+++ /dev/null
@@ -1,65 +0,0 @@
-messageManager = $messageManager;
- }
-
- /**
- * @inheritdoc
- */
- public function hydrate(Component $component, RequestInterface $request): void
- {
- //
- }
-
- /**
- * @inheritdoc
- */
- public function dehydrate(Component $component, ResponseInterface $response): void
- {
- if ($component->hasFlashMessages()) {
- $messages = array_map(static function($message) {
- return ['text' => $message->getMessage()->render(), 'type' => $message->getType()];
- }, $component->getFlashMessages());
-
- if (isset($response->effects['redirect']) || $response->getRequest()->isPreceding()) {
- $this->messageManager->addMessages(array_map(function($message) {
- return $this->messageManager->createMessage($message['type'])->setText($message['text']);
- }, $messages));
-
- return;
- }
-
- $component->dispatchBrowserEvent('messages-loaded', ['messages' => $messages]);
- }
- }
-}
diff --git a/src/Model/Hydrator/Hash.php b/src/Model/Hydrator/Hash.php
deleted file mode 100644
index d037c816..00000000
--- a/src/Model/Hydrator/Hash.php
+++ /dev/null
@@ -1,56 +0,0 @@
-fingerprint['id'], $request->fingerprint['name'])) {
- throw new ComponentHydrationException(__('Request fingerprint doesn\'t have all data available'));
- }
-
- $component->id = $request->fingerprint['id'];
- $component->name = $request->fingerprint['name'];
-
- if (isset($request->memo['htmlHash'])) {
- $this->domHashes[$component->id] = $request->memo['htmlHash'];
- }
- }
-
- /**
- * @inheritdoc
- */
- public function dehydrate(Component $component, ResponseInterface $response): void
- {
- $hash = $this->domHashes[$component->id] ?? null;
- $response->memo['htmlHash'] = hash('crc32b', $response->effects['html']);
-
- if (empty($response->effects['html']) || $hash === $response->memo['htmlHash']) {
- $response->effects['html'] = null;
- }
- }
-}
diff --git a/src/Model/Hydrator/Listener.php b/src/Model/Hydrator/Listener.php
deleted file mode 100644
index 1a68c739..00000000
--- a/src/Model/Hydrator/Listener.php
+++ /dev/null
@@ -1,79 +0,0 @@
-functionsHelper = $functionsHelper;
- $this->listeners = $listeners;
- }
-
- /**
- * @inheritdoc
- */
- public function hydrate(Component $component, RequestInterface $request): void
- {
- //
- }
-
- /**
- * @inheritdoc
- */
- public function dehydrate(Component $component, ResponseInterface $response): void
- {
- if ($response->getRequest()->isPreceding()) {
- $response->effects['listeners'] = array_keys($this->assimilateListeners($component));
- }
- }
-
- /**
- * Merges optional injected event listeners for the given Component.
- *
- * @param Component $component
- * @return array
- */
- public function assimilateListeners(Component $component): array
- {
- $listeners = $component->getListeners();
-
- if (isset($this->listeners[$component->name])) {
- $listeners = array_merge($this->listeners[$component->name], $listeners);
- }
-
- // Global event's for each component
- $listeners['refresh'] = '$refresh';
-
- return $this->functionsHelper->mapWithKeys(function ($value, $key) {
- return [is_numeric($key) ? $value : $key => $value];
- }, $listeners);
- }
-}
diff --git a/src/Model/Hydrator/Loader.php b/src/Model/Hydrator/Loader.php
deleted file mode 100644
index c2e9fdd0..00000000
--- a/src/Model/Hydrator/Loader.php
+++ /dev/null
@@ -1,39 +0,0 @@
-getLoader()) && $loader && $response->getRequest()->isPreceding()) {
- $response->effects['loader'] = $loader;
- }
- }
-}
diff --git a/src/Model/Hydrator/Property.php b/src/Model/Hydrator/Property.php
deleted file mode 100644
index 5cdb4e06..00000000
--- a/src/Model/Hydrator/Property.php
+++ /dev/null
@@ -1,70 +0,0 @@
-propertyHelper = $propertyHelper;
- }
-
- /**
- * @inheritdoc
- */
- public function hydrate(Component $component, RequestInterface $request): void
- {
- if ($request->isPreceding()) {
- $request->memo['data'] = array_merge($request->memo['data'], $component->getPublicProperties(true));
- }
-
- // Bind regular properties
- $this->propertyHelper->assign(function(Component $component, $request, $property, $value) {
- $component->assign($property, $value, true);
- }, $request, $component);
-
- // Flush properties cache
- $component->getPublicProperties(true);
- }
-
- /**
- * @inheritdoc
- */
- public function dehydrate(Component $component, ResponseInterface $response): void
- {
- if ($response->getRequest()->isSubsequent()) {
- $response->effects['dirty'] = [];
-
- $this->propertyHelper->assign(function (Component $component, $response, $property) {
- // A lot have could happen to the property, so lets set it one more time
- $response->memo['data'][$property] = $component->{$property};
- // The property can be seen as changed and dirty data, who needs a refresh
- $response->effects['dirty'][] = $property;
- }, $response, $component);
- }
- }
-}
diff --git a/src/Model/Hydrator/QueryString.php b/src/Model/Hydrator/QueryString.php
deleted file mode 100644
index ffb6867f..00000000
--- a/src/Model/Hydrator/QueryString.php
+++ /dev/null
@@ -1,97 +0,0 @@
-request = $request;
- $this->functionsHelper = $functionsHelper;
- }
-
- /**
- * @inheritdoc
- */
- public function hydrate(Component $component, RequestInterface $request): void
- {
- if ($request->isPreceding() && $component->hasQueryString()) {
- $properties = $this->getQueryParamsFromComponentProperties($component);
- $aliases = $this->filterQueryStringSetting($component, 'alias');
-
- foreach (array_keys($properties) as $property) {
- if ($value = $this->request->getParam($aliases[$property] ?? $property)) {
- $component->assign($property, $value);
- }
- }
- }
- }
-
- /**
- * @inheritDoc
- */
- public function dehydrate(Component $component, ResponseInterface $response): void
- {
- //
- }
-
- /**
- * Try to filter out any excepts "property:except-value" pairs.
- *
- * @param Component $component
- * @param string $setting
- * @return array|false
- */
- public function filterQueryStringSetting(Component $component, string $setting = 'except')
- {
- $excepts = $this->functionsHelper->mapWithKeys(static function ($value, $key) use ($setting) {
- return [$key => $value[$setting]];
- }, array_filter($component->getQueryString(), static function ($value) use ($setting) {
- return isset($value[$setting]);
- }, ARRAY_FILTER_USE_BOTH));
-
- return empty($excepts) ? false : $excepts;
- }
-
- /**
- * @param Component $component
- * @return array
- */
- public function getQueryParamsFromComponentProperties(Component $component): array
- {
- return $this->functionsHelper->mapWithKeys(function($value, $key) use ($component) {
- $key = is_string($key) ? $key : $value;
- return [$key => $component->{$key}];
- }, $component->getQueryString());
- }
-}
diff --git a/src/Model/Hydrator/Redirect.php b/src/Model/Hydrator/Redirect.php
deleted file mode 100644
index cd665d7f..00000000
--- a/src/Model/Hydrator/Redirect.php
+++ /dev/null
@@ -1,61 +0,0 @@
-builder = $builder;
- }
-
- /**
- * @inheritdoc
- */
- public function hydrate(Component $component, RequestInterface $request): void
- {
- //
- }
-
- /**
- * A redirect can both be set on the initial and/or subsequent request.
- *
- * @inheritdoc
- */
- public function dehydrate(Component $component, ResponseInterface $response): void
- {
- if ($redirect = $component->getRedirect()) {
- $url = $redirect->getUrl();
-
- if ($redirect->hasParams()) {
- $url = $this->builder->getUrl($url, $redirect->getParams());
- }
-
- $response->effects['redirect'] = $url;
- }
- }
-}
diff --git a/src/Model/Hydrator/Security.php b/src/Model/Hydrator/Security.php
deleted file mode 100644
index b775ebd1..00000000
--- a/src/Model/Hydrator/Security.php
+++ /dev/null
@@ -1,74 +0,0 @@
-securityHelper = $securityHelper;
- }
-
- /**
- * @inheritdoc
- *
- * @param Component $component
- * @param RequestInterface $request
- * @throws CorruptPayloadException
- * @throws FileSystemException
- * @throws RuntimeException
- */
- public function hydrate(Component $component, RequestInterface $request): void
- {
- if (!isset($request->memo['checksum'])) {
- return;
- }
-
- $checksum = $request->memo['checksum'];
- unset($request->memo['checksum']);
-
- $memo = array_diff_key($request->getServerMemo(), array_flip(['children']));
-
- if (!$this->securityHelper->validateChecksum($checksum, $request->getFingerprint(), $memo)) {
- throw new CorruptPayloadException('Temporary Component Name (todo)');
- }
- }
-
- /**
- * @inheritdoc
- *
- * @throws FileSystemException
- * @throws RuntimeException
- */
- public function dehydrate(Component $component, ResponseInterface $response): void
- {
- $response->memo['checksum'] = $this->securityHelper->generateChecksum($response->getFingerprint(), $response->getServerMemo());
- }
-}
diff --git a/src/Model/Magento/System/ConfigMagewire.php b/src/Model/Magento/System/ConfigMagewire.php
new file mode 100644
index 00000000..22abf87a
--- /dev/null
+++ b/src/Model/Magento/System/ConfigMagewire.php
@@ -0,0 +1,110 @@
+scopeConfig->getValue($this->createPath($path, $group), $scopeType, $scopeCode);
+ }
+
+ /**
+ * Retrieve a grouped config flag by path and scope.
+ */
+ public function isGroupFlag(
+ string $path,
+ string|null $group = null,
+ string $scopeType = ScopeInterface::SCOPE_STORE,
+ $scopeCode = null
+ ): bool {
+ return $this->scopeConfig->isSetFlag($this->createPath($path, $group), $scopeType, $scopeCode);
+ }
+
+ /**
+ * Retrieve features group config value by path and scope.
+ */
+ public function getFeaturesGroupValue(
+ string $path,
+ string $scopeType = ScopeInterface::SCOPE_STORE,
+ $scopeCode = null
+ ): mixed {
+ return $this->getGroupValue($path, self::GROUP_FEATURES, $scopeType, $scopeCode);
+ }
+
+ /**
+ * Retrieve a features group config flag by path and scope.
+ */
+ public function isFeatureGroupFlag(
+ string $path,
+ string $scopeType = ScopeInterface::SCOPE_STORE,
+ $scopeCode = null
+ ): bool {
+ return $this->scopeConfig->isSetFlag($this->createPath($path, self::GROUP_FEATURES), $scopeType, $scopeCode);
+ }
+
+ /**
+ * Retrieve debug group config value by path and scope.
+ */
+ public function getDebugGroupValue(
+ string $path,
+ string $scopeType = ScopeInterface::SCOPE_STORE,
+ $scopeCode = null
+ ): mixed {
+ return $this->getGroupValue($path, self::GROUP_DEBUG, $scopeType, $scopeCode);
+ }
+
+ /**
+ * Retrieve a debug group config flag by path and scope.
+ */
+ public function isDebugGroupFlag(
+ string $path,
+ string $scopeType = ScopeInterface::SCOPE_STORE,
+ $scopeCode = null
+ ): bool {
+ return $this->scopeConfig->isSetFlag($this->createPath($path, self::GROUP_DEBUG), $scopeType, $scopeCode);
+ }
+
+ /**
+ * Retrieve if Magewire is in debug mode.
+ */
+ public function isInDebugMode(): bool
+ {
+ return $this->isDebugGroupFlag('enable');
+ }
+
+ /**
+ * Returns a formatted path based on the provided path and optional group.
+ */
+ private function createPath(string $path, string|null $group = null): string
+ {
+ return $group ? sprintf('magewire/%s/%s', $group, trim($path)) : sprintf('magewire/%s', trim($path));
+ }
+}
diff --git a/src/Model/Magento/System/ConfigMagewireGroup.php b/src/Model/Magento/System/ConfigMagewireGroup.php
new file mode 100644
index 00000000..f9e30d75
--- /dev/null
+++ b/src/Model/Magento/System/ConfigMagewireGroup.php
@@ -0,0 +1,25 @@
+config;
+ }
+}
diff --git a/src/Model/Magewire/Notifier/NotificationStateEnum.php b/src/Model/Magewire/Notifier/NotificationStateEnum.php
new file mode 100644
index 00000000..d9c86328
--- /dev/null
+++ b/src/Model/Magewire/Notifier/NotificationStateEnum.php
@@ -0,0 +1,40 @@
+value;
+ }
+
+ public function getCssClass(): string
+ {
+ return 'state-' . $this->value;
+ }
+
+ public function getLevel(): int
+ {
+ $level = array_search($this, self::cases(), true);
+ return $level !== false ? $level : 0;
+ }
+}
diff --git a/src/Model/Magewire/Notifier/NotificationTypeEnum.php b/src/Model/Magewire/Notifier/NotificationTypeEnum.php
new file mode 100644
index 00000000..b36cf363
--- /dev/null
+++ b/src/Model/Magewire/Notifier/NotificationTypeEnum.php
@@ -0,0 +1,30 @@
+value;
+ }
+
+ public function getCssClass(): string
+ {
+ return 'notification-' . $this->value;
+ }
+}
diff --git a/src/Model/Request.php b/src/Model/Request.php
deleted file mode 100644
index 946ccd7b..00000000
--- a/src/Model/Request.php
+++ /dev/null
@@ -1,132 +0,0 @@
-fingerprint)) {
- return $this->fingerprint[$index] ?? null;
- }
-
- return $this->fingerprint;
- }
-
- /**
- * @inheritdoc
- */
- public function setFingerprint($fingerprint): RequestInterface
- {
- $this->fingerprint = $fingerprint;
- return $this;
- }
-
- /**
- * @inheritdoc
- */
- public function getServerMemo(string $index = null)
- {
- if ($index !== null && is_array($this->memo)) {
- return $this->memo[$index] ?? null;
- }
-
- return $this->memo;
- }
-
- /**
- * @inheritdoc
- */
- public function setServerMemo($memo): RequestInterface
- {
- $this->memo = $memo;
- return $this;
- }
-
- /**
- * @inheritdoc
- */
- public function getUpdates(string $index = null)
- {
- if ($index !== null && is_array($this->updates)) {
- return $this->updates[$index] ?? null;
- }
-
- return $this->updates;
- }
-
- /**
- * @inheritdoc
- */
- public function setUpdates($updates): RequestInterface
- {
- $this->updates = $updates;
- return $this;
- }
-
- /**
- * @inheritdoc
- *
- * @throws LocalizedException
- */
- public function getSectionByName(string $section): ?array
- {
- if (in_array($section, ['fingerprint', 'serverMemo', 'updates'])) {
- return $this->{$section};
- }
-
- throw new LocalizedException(__('Request section %s does not exist', $section));
- }
-
- /**
- * @inheritdoc
- */
- public function isSubsequent(bool $flag = null)
- {
- // Just return the update status
- if ($flag === null) {
- return $this->isSubsequent ?? false;
- }
-
- // Lock this property so it can't be changed later on
- if ($this->isSubsequent === null) {
- $this->isSubsequent = $flag;
- }
-
- return $this;
- }
-
- /**
- * @inheritdoc
- */
- public function isPreceding(): bool
- {
- return !$this->isSubsequent();
- }
-}
diff --git a/src/Model/Response.php b/src/Model/Response.php
deleted file mode 100644
index 38c69478..00000000
--- a/src/Model/Response.php
+++ /dev/null
@@ -1,149 +0,0 @@
-functionsHelper = $functionsHelper;
- }
-
- /**
- * @inheritdoc
- */
- public function getRequest()
- {
- return $this->request;
- }
-
- /**
- * @inheritdoc
- */
- public function setRequest($request): ResponseInterface
- {
- $this->request = $request;
- return $this;
- }
-
- /**
- * @inheritdoc
- */
- public function getFingerprint()
- {
- return $this->fingerprint;
- }
-
- /**
- * @inheritdoc
- */
- public function setFingerprint($fingerprint): ResponseInterface
- {
- $this->fingerprint = $fingerprint;
- return $this;
- }
-
- /**
- * @inheritdoc
- */
- public function getServerMemo()
- {
- return $this->memo;
- }
-
- /**
- * @inheritdoc
- */
- public function setServerMemo($memo): ResponseInterface
- {
- $this->memo = $memo;
- return $this;
- }
-
- /**
- * @inheritdoc
- */
- public function getEffects()
- {
- return $this->effects;
- }
-
- /**
- * @inheritdoc
- */
- public function setEffects($effects): ResponseInterface
- {
- $this->effects = $effects;
- return $this;
- }
-
- /**
- * @return array
- */
- public function toArrayWithoutHtml(): array
- {
- return [
- 'fingerprint' => $this->getFingerprint(),
- 'effects' => array_diff_key($this->getEffects(), ['html' => null]),
- 'serverMemo' => $this->getServerMemo(),
- ];
- }
-
- /**
- * @inheritdoc
- *
- * @throws LocalizedException
- * @throws RootTagMissingFromViewException
- */
- public function renderWithRootAttribute(array $data, bool $secure = false): string
- {
- $effects = $this->getEffects();
- $html = $effects['html'] ?? false;
-
- if ($html === false) {
- throw new LocalizedException(__('No response HTML found'));
- }
-
- preg_match('/<([a-zA-Z0-9\-]*)/', $html, $matches, PREG_OFFSET_CAPTURE);
-
- if (count($matches) === 0) {
- throw new RootTagMissingFromViewException();
- }
-
- $attributes = implode(' ', $this->functionsHelper->map(function($value, $key) {
- return sprintf('%s="%s"', $key, $value);
- }, $this->functionsHelper->mapWithKeys(function ($value, $key) {
- return ["wire:{$key}" => $this->functionsHelper->escapeStringForHtml($value)];
- }, $data)));
-
- return substr_replace($html, ' ' . $attributes, $matches[1][1] + strlen($matches[1][0]), 0);
- }
-}
diff --git a/src/Model/View/Element/Attributes.php b/src/Model/View/Element/Attributes.php
new file mode 100644
index 00000000..95d1082e
--- /dev/null
+++ b/src/Model/View/Element/Attributes.php
@@ -0,0 +1,63 @@
+all();
+
+ if (empty($attributes)) {
+ return '';
+ }
+
+ $parts = [];
+
+ foreach ($attributes as $name => $value) {
+ if (is_bool($value)) {
+ if ($value) {
+ $parts[] = $name;
+ }
+
+ continue;
+ }
+
+ if ($value === null) {
+ continue;
+ }
+ if (is_array($value)) {
+ $value = implode(' ', $value);
+ }
+
+ $parts[] = sprintf('%s="%s"', $name, $this->escaper->escapeHtmlAttr($value));
+ }
+
+ return empty($parts) ? '' : ' ' . implode(' ', $parts);
+ }
+}
diff --git a/src/Model/View/Element/Properties.php b/src/Model/View/Element/Properties.php
new file mode 100644
index 00000000..684c4cf3
--- /dev/null
+++ b/src/Model/View/Element/Properties.php
@@ -0,0 +1,18 @@
+ $validators */
+ private array $validators = [];
+
+ /**
+ * @param array $modifiers
+ */
+ public function __construct(
+ protected LoggerInterface $logger,
+ protected Escaper $escaper,
+ private array $modifiers = []
+ ) {
+ $this->id = Random::alphabetical(10);
+ }
+
+ public function id(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * Begins output buffering for the fragment.
+ *
+ * Prepares the fragment by calling setup and initiates output buffering
+ * so that any output can later be captured and processed.
+ */
+ public function start(): static
+ {
+ if ($this->buffering) {
+ return $this;
+ }
+
+ ob_start();
+
+ $this->buffering = true;
+ $this->level ??= ob_get_level();
+
+ return $this;
+ }
+
+ /**
+ * Ends output buffering, processes and echos the captured content.
+ *
+ * Retrieves the buffered output as raw content, applies an optional transformation
+ * to produce the final content, and triggers the inspection process.
+ */
+ public function end(): static
+ {
+ if (! $this->buffering || ob_get_level() !== $this->level) {
+ return $this;
+ }
+
+ // Stores the output to be able to flag buffering as false.
+ $this->raw = trim(ob_get_clean());
+ $this->buffering = false;
+
+ try {
+ $this->output = $this->render();
+ } catch (EmptyFragmentException $exception) {
+ // Unreachable: fragment buffering state verified in method preconditions.
+ }
+
+ // Echo result (hide content when not allowed to render).
+ echo $this->echo ? $this->output : '';
+
+ return $this;
+ }
+
+ /**
+ * Locks the fragment so that it cannot be modified.
+ */
+ public function lock(): static
+ {
+ $this->mutable = false;
+ return $this;
+ }
+
+ /**
+ * Avoid output echoing.
+ */
+ public function mute(): static
+ {
+ $this->echo = false;
+ return $this;
+ }
+
+ public function withTag(string $tag): static
+ {
+ if ($this->canTagFragment($tag)) {
+ return $this->withTagOrigin($tag);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Retrieve the raw output content.
+ */
+ protected function getRawOutput(): string
+ {
+ return $this->raw === false ? '' : $this->raw;
+ }
+
+ /**
+ * @throws FragmentValidationException
+ */
+ protected function validate(): bool
+ {
+ if ($this->buffering) {
+ return false;
+ }
+
+ foreach ($this->validators as $validator) {
+ if ($validator($this->raw)) {
+ continue;
+ }
+
+ throw new FragmentValidationException('Fragment did not pass validation.');
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets a validation callback who can only return true or false.
+ */
+ protected function withValidator(callable $callback): static
+ {
+ $this->validators[] = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Set a fragment modifier.
+ */
+ protected function withModifier(FragmentModifier|callable $modifier): static
+ {
+ $this->modifiers[] = $modifier;
+
+ return $this;
+ }
+
+ protected function handleModifierException(Throwable $exception): static
+ {
+ $message = 'An unexpected exception occurred while modifying a fragment.';
+ $this->logger->critical($message, ['exception' => $exception]);
+
+ return $this;
+ }
+
+ protected function handleValidationException(Throwable $exception): string
+ {
+ if ($exception instanceof EmptyFragmentException) {
+ return '';
+ }
+
+ $message = 'A validation exception occurred while processing the fragment.';
+ $this->logger->critical($message, ['exception' => $exception]);
+
+ return $this->raw ?: '';
+ }
+
+ protected function handleRenderException(Throwable $exception): string
+ {
+ $message = 'A render exception occurred while processing the fragment.';
+ $this->logger->critical($message, ['exception' => $exception]);
+
+ return '';
+ }
+
+ /**
+ * Returns the final fragment output.
+ */
+ protected function render(): string
+ {
+ try {
+ $output = $this->raw;
+
+ if ($output) {
+ try {
+ if ($this->validate()) {
+ $this->modify();
+ }
+ } catch (Throwable $exception) {
+ $output = $this->handleValidationException($exception);
+ }
+
+ return $output;
+ }
+
+ if ($this->buffering) {
+ throw new EmptyFragmentException('Unclosed output buffer detected. Fragment buffering must be properly terminated.');
+ }
+ } catch (Throwable $exception) {
+ return $this->handleRenderException($exception);
+ }
+
+ return '';
+ }
+
+ /**
+ * Runs all injected fragment modifiers.
+ */
+ private function modify(): static
+ {
+ $output = $this->raw;
+
+ if ($this->mutable && $output) {
+ foreach ($this->modifiers as $modifier) {
+ try {
+ if (is_callable($modifier)) {
+ $modifier($this);
+ } elseif ($modifier instanceof FragmentModifier) {
+ $modifier->modify($this);
+ }
+ } catch (Throwable $exception) {
+ $this->handleModifierException($exception);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ private function canTagFragment(string|null $tag = null): bool
+ {
+ if (is_string($tag) && $this->hasTags([$tag])) {
+ return false;
+ }
+ if (! $this->mutable || $this->buffering || is_string($this->raw)) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Model/View/Fragment/Component.php b/src/Model/View/Fragment/Component.php
new file mode 100644
index 00000000..44ecb0d3
--- /dev/null
+++ b/src/Model/View/Fragment/Component.php
@@ -0,0 +1,34 @@
+block;
+ }
+}
diff --git a/src/Model/View/Fragment/Element.php b/src/Model/View/Fragment/Element.php
new file mode 100644
index 00000000..8e5154da
--- /dev/null
+++ b/src/Model/View/Fragment/Element.php
@@ -0,0 +1,93 @@
+dictionary ??= Factory::create(DataArray::class);
+ }
+
+ /**
+ * @return DataArray
+ */
+ public function attributes(): DataArray
+ {
+ return $this->data()->attributes();
+ }
+
+ /**
+ * Returns the element properties/settings.
+ */
+ public function properties(): DataArray
+ {
+ return $this->data()->properties();
+ }
+
+ public function prop(string $name, mixed $default = null)
+ {
+ return $this->properties()->get($name, $default);
+ }
+
+ public function data(): DomElementData
+ {
+ return $this->data ??= Factory::create(DomElementData::class);
+ }
+
+ /**
+ * Returns the elements parent block.
+ */
+ public function parent(): AbstractBlock
+ {
+ return $this->block;
+ }
+
+ public function track(): static
+ {
+ $this->slotsRegistry->track($this);
+ return $this;
+ }
+
+ public function untrack(): static
+ {
+ $this->slotsRegistry->untrack();
+ return $this;
+ }
+}
diff --git a/src/Model/View/Fragment/Element/Slot.php b/src/Model/View/Fragment/Element/Slot.php
new file mode 100644
index 00000000..61d8d184
--- /dev/null
+++ b/src/Model/View/Fragment/Element/Slot.php
@@ -0,0 +1,92 @@
+slotsRegistry->register($this->variant, $this);
+
+ return parent::start();
+ }
+
+ /**
+ * Complete slot content capture and register the output.
+ *
+ * Finalizes the fragment rendering to ensure all buffered output is captured,
+ * then registers the captured content with the SlotsRegistry under this slot's
+ * variant name, making it available for retrieval in parent templates.
+ */
+ public function end(): static
+ {
+ // First, complete the fragment rendering to ensure output is fully buffered.
+ parent::end();
+
+ // Register the captured output as the content for this named slot (identified by variant).
+ // This makes it available in the component view as a slot variable (e.g., $header).
+ $this->slotsRegistry->update($this->variant, $this->output);
+
+ return $this;
+ }
+
+ /**
+ * Override to prevent slot area tracking.
+ *
+ * Slots do not create nested tracking contexts in the registry since they
+ * simply register content as named slots rather than managing their own
+ * slot collections. This no-op implementation ensures track() calls are
+ * safely ignored.
+ */
+ public function track(): static
+ {
+ return $this;
+ }
+
+ /**
+ * Override to prevent slot area untracking.
+ *
+ * Slots do not manage tracking contexts. This no-op implementation ensures
+ * untrack() calls are safely ignored, maintaining the current area's tracking
+ * state without interfering with the parent fragment's lifecycle.
+ */
+ public function untrack(): static
+ {
+ return $this;
+ }
+}
diff --git a/src/Model/View/Fragment/Element/Unknown.php b/src/Model/View/Fragment/Element/Unknown.php
new file mode 100644
index 00000000..2f9271de
--- /dev/null
+++ b/src/Model/View/Fragment/Element/Unknown.php
@@ -0,0 +1,48 @@
+raw = sprintf('', $this->variant);
+
+ if ($this->applicationState->getMode() === ApplicationState::MODE_PRODUCTION) {
+ $this->raw = '';
+ }
+
+ return parent::render();
+ }
+}
diff --git a/src/Model/View/Fragment/Exceptions/EmptyFragmentException.php b/src/Model/View/Fragment/Exceptions/EmptyFragmentException.php
new file mode 100644
index 00000000..cfe14f88
--- /dev/null
+++ b/src/Model/View/Fragment/Exceptions/EmptyFragmentException.php
@@ -0,0 +1,18 @@
+buffering = true;
+ $this->raw = $input;
+
+ try {
+ $this->start();
+ $output = $this->render();
+ $this->end();
+
+ return $output;
+ } catch (Throwable $exception) {
+ return $this->handleRenderException($exception);
+ }
+ }
+
+ public function withAttribute(string $name, string|float|int|null $value = null, string $area = 'root'): static
+ {
+ if ($value === null) {
+ $this->attributes[$area][] = $name;
+ } else {
+ $this->attributes[$area][$name] = $value;
+ }
+
+ return $this;
+ }
+
+ public function withAttributes(array $attributes, string $area = 'root'): static
+ {
+ foreach ($attributes as $name => $value) {
+ $this->withAttribute($name, $value, $area);
+ }
+
+ return $this;
+ }
+
+ protected function render(): string
+ {
+ $render = parent::render();
+ $attributes = $this->getAreaAttributes('root');
+
+ if (! empty($attributes)) {
+ $attributeStrings = [];
+
+ foreach ($attributes as $attribute => $value) {
+ $attributeStrings[] = is_numeric($attribute) ? $value : $attribute . '="' . $this->escaper->escapeHtmlAttr($value) . '"';
+ }
+
+ if (! empty($attributeStrings)) {
+ $attributeString = ' ' . implode(' ', $attributeStrings);
+ $render = preg_replace('/^(<[^>\s]+)/', '$1' . $attributeString, $render, 1);
+ }
+ }
+
+ return trim($render);
+ }
+
+ protected function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+ protected function getAreaAttributes(string $area): array
+ {
+ if ($area === 'root') {
+ // Filter those who are not an 'area' (an array value).
+ $attributes = array_filter($this->attributes, static fn ($value) => ! is_array($value));
+
+ return array_merge($this->attributes[$area] ?? [], $attributes);
+ }
+
+ return $this->attributes[$area] ?? [];
+ }
+}
diff --git a/src/Model/View/Fragment/Javascript.php b/src/Model/View/Fragment/Javascript.php
new file mode 100644
index 00000000..5be5b097
--- /dev/null
+++ b/src/Model/View/Fragment/Javascript.php
@@ -0,0 +1,18 @@
+csp->isCspAvailable()) {
+ return $fragment;
+ }
+
+ if ($this->useCspHeader()) {
+ $this->includeCspHeader($fragment);
+ } else {
+ $fragment->withAttribute('nonce', $this->csp->getMagentoCspNonceProvider()->generateNonce());
+ }
+
+ return $fragment;
+ }
+
+ private function includeCspHeader(Fragment\Script $fragment): void
+ {
+ // Generate a hash based on the DOM node content.
+ $hash = $this->csp->generateHash($fragment->getScriptCode());
+ $algorithm = $this->csp->getHashAlgorithm();
+
+ $this->dynamicCspCollector->add(new FetchPolicy('script-src', false, [], [], false, false, false, [], [$hash => $algorithm]));
+ }
+
+ private function useCspHeader(): bool
+ {
+ return $this->cacheState->isEnabled(FPC::TYPE_IDENTIFIER) && $this->layout->isCacheable();
+ }
+}
diff --git a/src/Model/View/Fragment/Modifier/Developer.php b/src/Model/View/Fragment/Modifier/Developer.php
new file mode 100644
index 00000000..56491c59
--- /dev/null
+++ b/src/Model/View/Fragment/Modifier/Developer.php
@@ -0,0 +1,33 @@
+applicationState->getMode() === ApplicationState::MODE_DEVELOPER) {
+ $fragment->withAttribute('magewire-fragment');
+ }
+
+ return $fragment;
+ }
+}
diff --git a/src/Model/View/Fragment/Script.php b/src/Model/View/Fragment/Script.php
new file mode 100644
index 00000000..cf6d37bb
--- /dev/null
+++ b/src/Model/View/Fragment/Script.php
@@ -0,0 +1,43 @@
+withValidator(static fn ($script) => str_starts_with($script, ''));
+ }
+
+ /**
+ * Returns the content between the script tags.
+ */
+ public function getScriptCode(): string
+ {
+ if (is_string($this->code)) {
+ return $this->code;
+ }
+
+ $start = strpos($this->raw, '>');
+ $end = strrpos($this->raw, '<');
+
+ if ($start !== false && $end !== false && $start < $end) {
+ $this->code = trim(substr($this->raw, $start + 1, $end - $start - 1));
+ }
+
+ return $this->code ?? '';
+ }
+}
diff --git a/src/Model/View/Fragment/Style.php b/src/Model/View/Fragment/Style.php
new file mode 100644
index 00000000..e2a9eba9
--- /dev/null
+++ b/src/Model/View/Fragment/Style.php
@@ -0,0 +1,16 @@
+element('slot', $block, $target);
+ }
+
+ /**
+ * @template T of Element
+ * @param class-string $type
+ * @return T
+ * @throws LogicException
+ */
+ public function element(string $type, AbstractBlock $block, string $variant = 'default'): Element
+ {
+ $type = $this->elements[$type] ?? Element\Unknown::class;
+
+ return $this->create($type, ['variant' => $variant, 'block' => $block]);
+ }
+
+ /**
+ * @template T of Fragment
+ * @param class-string $type
+ * @return T
+ * @throws LogicException
+ */
+ private function create(string $type, array $arguments = []): Fragment
+ {
+ $fragment = Factory::create($type, $arguments);
+
+ if ($fragment instanceof Fragment) {
+ return $fragment;
+ }
+
+ throw new LogicException(sprintf('Class "%s" does not implement Fragment interface. Expected Fragment, got %s.', $type, get_debug_type($fragment)));
+ }
+}
diff --git a/src/Model/View/FragmentFactory.php b/src/Model/View/FragmentFactory.php
new file mode 100644
index 00000000..0e7bc1c2
--- /dev/null
+++ b/src/Model/View/FragmentFactory.php
@@ -0,0 +1,100 @@
+ $types
+ */
+ public function __construct(
+ private FragmentElementFactory $elementFactory,
+ private array $types = []
+ ) {
+ }
+
+ public function html(): Html
+ {
+ return $this->create(Html::class);
+ }
+
+ public function javascript(): Script
+ {
+ return $this->create(Javascript::class);
+ }
+
+ public function script(): Script
+ {
+ return $this->create(Script::class);
+ }
+
+ public function style(): Style
+ {
+ return $this->create(Style::class);
+ }
+
+ public function elements(): FragmentElementFactory
+ {
+ return $this->elementFactory;
+ }
+
+ public function component(AbstractBlock $block): Component
+ {
+ return $this->create(Component::class, ['block' => $block]);
+ }
+
+ /**
+ * @template T of Fragment
+ * @param string $name
+ * @return T
+ * @throws InvalidArgumentException
+ * @phpstan-return T
+ */
+ public function custom(string $name, array $arguments = []): Fragment
+ {
+ if (array_key_exists($name, $this->types)) {
+ return $this->create($this->types[$name], $arguments);
+ }
+ if (class_exists($name)) {
+ return $this->create($name, $arguments);
+ }
+
+ throw new InvalidArgumentException(sprintf('Unknown fragment type "%s".', $name));
+ }
+
+ /**
+ * @template T of Fragment
+ * @param class-string $type
+ * @return T
+ * @throws LogicException
+ */
+ private function create(string $type, array $arguments = []): Fragment
+ {
+ $fragment = Factory::create($type, $arguments);
+
+ if ($fragment instanceof Fragment) {
+ return $fragment;
+ }
+
+ throw new LogicException(sprintf('Class "%s" does not implement Fragment interface. Expected Fragment, got %s.', $type, get_debug_type($fragment)));
+ }
+}
diff --git a/src/Model/View/FragmentModifier.php b/src/Model/View/FragmentModifier.php
new file mode 100644
index 00000000..a4d82d5b
--- /dev/null
+++ b/src/Model/View/FragmentModifier.php
@@ -0,0 +1,32 @@
+name;
+ }
+
+ /**
+ * Update the slot's content.
+ *
+ * Sets the rendered content for this slot. Once set, this content will be
+ * returned when the slot is cast to a string.
+ */
+ public function update(string $content): static
+ {
+ $this->content = $content;
+
+ return $this;
+ }
+
+ /**
+ * Retrieve a property value from the slot's element.
+ *
+ * Properties are custom data attributes associated with the element
+ * that owns this slot.
+ */
+ public function prop(string $name, mixed $default = null)
+ {
+ return $this->element
+ ->data()
+ ->properties()
+ ->get($name, $default);
+ }
+
+ /**
+ * Retrieve an HTML attribute value from the slot's element.
+ *
+ * Attributes are standard HTML attributes (class, id, data-*, etc.)
+ * associated with the element that owns this slot.
+ */
+ public function attr(string $name, mixed $default = '')
+ {
+ return $this->element
+ ->data()
+ ->attributes()
+ ->get($name, $default);
+ }
+
+ /**
+ * Get the element that owns this slot.
+ *
+ * Provides access to the parent fragment element for retrieving additional
+ * context, data, or performing element-level operations.
+ */
+ public function element(): Element
+ {
+ return $this->element;
+ }
+
+ /**
+ * Convert the slot to its string representation.
+ */
+ public function __toString(): string
+ {
+ return $this->content ??= sprintf('', $this->name);
+ }
+}
diff --git a/src/Model/View/SlotSnapshot.php b/src/Model/View/SlotSnapshot.php
new file mode 100644
index 00000000..69b92d8d
--- /dev/null
+++ b/src/Model/View/SlotSnapshot.php
@@ -0,0 +1,59 @@
+ $slots
+ */
+ public function __construct(
+ private readonly array $slots = []
+ ) {
+ }
+
+ /**
+ * Retrieve a slot by name.
+ *
+ * Allows the snapshot to be called as a function to access slots.
+ * Returns null if the requested slot doesn't exist.
+ */
+ public function __invoke(string $name = 'default'): Slot|string|null
+ {
+ return $this->slots[$name] ?? null;
+ }
+
+ /**
+ * Convert the snapshot to its string representation.
+ *
+ * Returns the content of the default slot when the snapshot is used
+ * in a string context (e.g., echo, concatenation).
+ */
+ public function __toString()
+ {
+ return $this->slots['default']->__toString();
+ }
+}
diff --git a/src/Model/View/SlotsRegistry.php b/src/Model/View/SlotsRegistry.php
new file mode 100644
index 00000000..28852f2f
--- /dev/null
+++ b/src/Model/View/SlotsRegistry.php
@@ -0,0 +1,199 @@
+> $slots */
+ private array $slots = [];
+ /** @var array $counters */
+ private array $counters = [];
+ /** @var array */
+ private array $elements = [];
+
+ private string $area = 'root-0';
+
+ /**
+ * Start tracking slots for a new area.
+ *
+ * Pushes a new slot collection onto the stack, initializes counters for the area,
+ * and automatically creates a 'default' slot to capture the area's overall content.
+ * The area identifier is generated by combining the element ID with an incremental counter.
+ */
+ public function track(Element $element): void
+ {
+ $area ??= $element->id();
+
+ // Initialize counter for this base key if it doesn't exist.
+ if (! isset($this->counters[$area])) {
+ $this->counters[$area] = -1;
+ }
+
+ $this->counters[$area]++;
+ // Register the latest area.
+ $this->area = $area . '-' . $this->counters[$area];
+ // Register the slot bag.
+ $this->slots[$this->area] = [];
+ // Register the current element.
+ $this->elements[$this->area] = $element;
+
+ // Register the default slot, meant for the overall content.
+ $this->register('default', $element);
+ }
+
+ /**
+ * Stop tracking slots for the current area and return its slots.
+ *
+ * Pops the current area from the stack, returning all slots that were
+ * registered within it. Automatically switches context back to the
+ * previous area in the stack.
+ *
+ * @return array
+ */
+ public function untrack(): array
+ {
+ $area ??= $this->area;
+
+ // Get the slots for this area (not array_pop)
+ $slots = $this->slots[$area] ?? [];
+ // Remove this area from the slots tracking
+ unset($this->slots[$area]);
+ // Set the current area to the last remaining area
+ $this->area = array_key_last($this->slots) ?? 'root-0';
+
+ return $slots;
+ }
+
+ /**
+ * Retrieve a slot by name from the current area.
+ *
+ * Returns null if the slot doesn't exist. Does not create the slot
+ * if missing - use register() for slot creation.
+ */
+ public function get(string $name): Slot|null
+ {
+ $current = $this->slots();
+
+ return $current[$name] ?? null;
+ }
+
+ /**
+ * Create and register a new slot in the current area.
+ *
+ * Instantiates a new Slot object and adds it to the current area's
+ * collection. The slot is immediately available for content updates.
+ */
+ public function register(string $name, Element $element): Slot
+ {
+ $slot = Factory::create(Slot::class, ['name' => $name, 'element' => $element]);
+ $this->slots[$this->area][$slot->name()] = $slot;
+
+ return $slot;
+ }
+
+ /**
+ * Update the content of an existing slot.
+ *
+ * Retrieves the slot by name and updates its content. The slot must
+ * already exist in the current area.
+ */
+ public function update(string $name, string $content): static
+ {
+ $this->get($name)->update($content);
+
+ return $this;
+ }
+
+ /**
+ * Render a slot's content as a string.
+ *
+ * Convenience method that retrieves a slot and returns its string
+ * representation, useful for direct output in templates.
+ */
+ public function print(string $name): string
+ {
+ return $this->get($name)->__toString();
+ }
+
+ /**
+ * Check if a slot exists in the current area.
+ */
+ public function exists(string $name): bool
+ {
+ return isset($this->slots[$this->area][$name]);
+ }
+
+ /**
+ * Check if any areas are currently being tracked.
+ *
+ * Useful for determining if the registry has active slot contexts
+ * beyond the root level.
+ */
+ public function hasAreas(): bool
+ {
+ return count($this->slots) !== 0;
+ }
+
+ /**
+ * Create an immutable snapshot of the current area's slots.
+ *
+ * Generates a SlotSnapshot containing all slots from the current area.
+ * The snapshot can be passed between template layers or stored for
+ * deferred rendering.
+ */
+ public function snapshot(): SlotSnapshot
+ {
+ return Factory::create(SlotSnapshot::class, [
+ 'slots' => $this->slots()
+ ]);
+ }
+
+ /**
+ * Get the element that owns the current area.
+ *
+ * Returns the parent element for the currently active area, providing
+ * access to element data, attributes, and properties.
+ */
+ public function element(): Element|null
+ {
+ return $this->elements[$this->area] ?? null;
+ }
+
+ /**
+ * Get all slots registered in the current area.
+ *
+ * Returns the complete slot collection for the currently active area.
+ * Used internally by other methods to access the current context.
+ *
+ * @return array
+ */
+ private function slots(): array
+ {
+ return $this->slots[$this->area] ?? [];
+ }
+}
diff --git a/src/Model/View/Utils.php b/src/Model/View/Utils.php
new file mode 100644
index 00000000..906135e3
--- /dev/null
+++ b/src/Model/View/Utils.php
@@ -0,0 +1,124 @@
+ $utilities
+ */
+ public function __construct(
+ private AlpineViewUtil $alpine,
+ private ApplicationViewUtil $application,
+ private EnvironmentViewUtil $environment,
+ private FragmentViewUtil $fragment,
+ private LayoutViewUtil $layout,
+ private MagewireViewUtil $magewire,
+ private SecurityViewUtil $security,
+ private TailwindViewUtil $tailwind,
+ private TemplateViewUtil $template,
+ private CspViewUtil $csp,
+ private array $utilities = []
+ ) {
+ }
+
+ public function alpinejs(): AlpineViewUtil
+ {
+ return $this->alpine;
+ }
+
+ public function application(): ApplicationViewUtil
+ {
+ return $this->application;
+ }
+
+ public function env(): EnvironmentViewUtil
+ {
+ return $this->environment;
+ }
+
+ public function layout(): LayoutViewUtil
+ {
+ return $this->layout;
+ }
+
+ public function magewire(): MagewireViewUtil
+ {
+ return $this->magewire;
+ }
+
+ public function security(): SecurityViewUtil
+ {
+ return $this->security;
+ }
+
+ public function tailwind(): TailwindViewUtil
+ {
+ return $this->tailwind;
+ }
+
+ public function template(): TemplateViewUtil
+ {
+ return $this->template;
+ }
+
+ public function csp(): CspViewUtil
+ {
+ return $this->csp;
+ }
+
+ public function fragment(): FragmentViewUtil
+ {
+ return $this->fragment;
+ }
+
+ /**
+ * @throws BadMethodCallException
+ */
+ public function __call(string $utility, array $arguments = []): UtilsInterface
+ {
+ if (array_key_exists($utility, $this->utilities)) {
+ $subject = $this->utilities[$utility];
+
+ if ($subject instanceof UtilsInterface) {
+ if (count($arguments) === 0) {
+ return $subject;
+ }
+
+ if (empty($arguments)) {
+ return $this->utilities[$utility] = Factory::get($subject::class);
+ }
+
+ return Factory::create($subject::class, $arguments);
+ }
+ }
+
+ throw new BadMethodCallException(sprintf('Utility "%s" was called but does not exist.', $utility));
+ }
+}
diff --git a/src/Model/View/Utils/Alpine.php b/src/Model/View/Utils/Alpine.php
new file mode 100644
index 00000000..8755c55e
--- /dev/null
+++ b/src/Model/View/Utils/Alpine.php
@@ -0,0 +1,18 @@
+csp->isCspAvailable() ? $this->csp->getMagentoCspNonceProvider()->generateNonce() : '';
+ }
+
+ public function generateNonceAttribute(string $format = ' %s'): string
+ {
+ return $this->csp->isCspAvailable() ? sprintf(sprintf($format, 'nonce="%s"'), $this->escaper->escapeHtmlAttr($this->generateNonce())) : '';
+ }
+}
diff --git a/src/Model/View/Utils/Environment.php b/src/Model/View/Utils/Environment.php
new file mode 100644
index 00000000..52635f1b
--- /dev/null
+++ b/src/Model/View/Utils/Environment.php
@@ -0,0 +1,33 @@
+applicationState->getMode() === ApplicationState::MODE_DEVELOPER;
+ }
+
+ public function isProductionMode(): bool
+ {
+ return $this->applicationState->getMode() === ApplicationState::MODE_PRODUCTION;
+ }
+}
diff --git a/src/Model/View/Utils/Fragment.php b/src/Model/View/Utils/Fragment.php
new file mode 100644
index 00000000..e78ff5e9
--- /dev/null
+++ b/src/Model/View/Utils/Fragment.php
@@ -0,0 +1,46 @@
+types (e.g., 'header', 'footer')
+ * 2. Direct class names
+ *
+ * @param string|null $name
+ * @return \Magewirephp\Magewire\Model\View\Fragment|FragmentFactory
+ * @throws InvalidArgumentException
+ *
+ * @see FragmentFactory::$types For registering custom fragment types via DI
+ */
+ public function make(string|null $name = null): FragmentFactory|\Magewirephp\Magewire\Model\View\Fragment
+ {
+ if ($name) {
+ return $this->fragmentFactory->custom($name);
+ }
+
+ return $this->fragmentFactory;
+ }
+}
diff --git a/src/Model/View/Utils/Layout.php b/src/Model/View/Utils/Layout.php
new file mode 100644
index 00000000..388994ae
--- /dev/null
+++ b/src/Model/View/Utils/Layout.php
@@ -0,0 +1,97 @@
+
+ */
+ public function containerizeBlock(AbstractBlock|false $block): array
+ {
+ if ($block) {
+ return array_filter(array_map(function (string $child) use ($block) {
+ try {
+ return $block->getChildBlock($block->getLayout()->getElementAlias($child) ?? $child);
+ } catch (LocalizedException $exception) {
+ $this->logger->critical(sprintf('Can not retrieve child block: %s', $child), ['exception' => $exception]);
+ }
+
+ return false;
+ }, $block->getChildNames()));
+ }
+
+ return [];
+ }
+
+ /**
+ * Determines whether the current block can be containerized.
+ */
+ public function canContainerizeBlock(AbstractBlock|false $block): bool
+ {
+ if ($block) {
+ return count($this->containerizeBlock($block)) !== 0;
+ }
+
+ return false;
+ }
+
+ /**
+ * Renders all child blocks of the given block.
+ */
+ public function renderBlockAsContainer(AbstractBlock|false $block): string
+ {
+ if ($block) {
+ return implode('', array_map(static function (object $child) {
+ return ( $child instanceof AbstractBlock ? $child->toHtml() : '' ) . PHP_EOL;
+ }, $this->containerizeBlock($block)));
+ }
+
+ return '';
+ }
+
+ public function getChild(AbstractBlock|false $block, string $alias, array $data = []): AbstractBlock|null
+ {
+ if ($block) {
+ $child = $block->getChildBlock($alias);
+
+ if ($child instanceof AbstractBlock) {
+ if (! empty($data)) {
+ $child->addData($data);
+ }
+
+ return $child;
+ }
+ }
+
+ return null;
+ }
+
+ public function getChildHtml(AbstractBlock|false $block, string $alias, array $data = []): string
+ {
+ $child = $this->getChild($block, $alias, $data);
+
+ return $child ? $child->toHtml() : '';
+ }
+}
diff --git a/src/Model/View/Utils/Magewire.php b/src/Model/View/Utils/Magewire.php
new file mode 100644
index 00000000..bb9aba9b
--- /dev/null
+++ b/src/Model/View/Utils/Magewire.php
@@ -0,0 +1,76 @@
+features;
+ }
+
+ public function mechanisms(): MechanismsViewUtil
+ {
+ return $this->mechanisms;
+ }
+
+ public function config(): MagewireSystemConfig
+ {
+ return $this->config;
+ }
+
+ public function getUpdateUri(): string
+ {
+ return '/magewire/update';
+ }
+
+ public function logger(): LoggerInterface
+ {
+ return $this->logger;
+ }
+
+ public function canRequireMagewireJsLibrary(): bool
+ {
+ try {
+ return $this->mechanisms()->resolveComponents()->doesPageHaveComponents();
+ } catch (BadMethodCallException $exception) {
+ $this->logger()->critical($exception->getMessage(), ['exception' => $exception]);
+ }
+
+ return false;
+ }
+
+ /**
+ * @deprecated Work in Progress.
+ */
+ public function build(): Builder
+ {
+ return $this->builder;
+ }
+}
diff --git a/src/Model/View/Utils/Magewire/Builder.php b/src/Model/View/Utils/Magewire/Builder.php
new file mode 100644
index 00000000..a80362cf
--- /dev/null
+++ b/src/Model/View/Utils/Magewire/Builder.php
@@ -0,0 +1,37 @@
+featuresServiceType->viewModel($utility);
+ } catch (NotFoundException $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+
+ throw new BadMethodCallException(sprintf('Feature view model "%1" does not exist.', $utility));
+ }
+ }
+}
diff --git a/src/Model/View/Utils/Magewire/Mechanisms.php b/src/Model/View/Utils/Magewire/Mechanisms.php
new file mode 100644
index 00000000..b13befb2
--- /dev/null
+++ b/src/Model/View/Utils/Magewire/Mechanisms.php
@@ -0,0 +1,49 @@
+mechanismsServiceType->viewModel($utility);
+ } catch (NotFoundException $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+
+ throw new BadMethodCallException(sprintf('Mechanism view model "%s" does not exist.', $utility));
+ }
+ }
+}
diff --git a/src/Model/View/Utils/Security.php b/src/Model/View/Utils/Security.php
new file mode 100644
index 00000000..285e5cf1
--- /dev/null
+++ b/src/Model/View/Utils/Security.php
@@ -0,0 +1,37 @@
+formKey->getFormKey();
+ } catch (LocalizedException $exception) {
+ $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
+ }
+
+ return 'unknown-csrf-token';
+ }
+}
diff --git a/src/Model/View/Utils/Tailwind.php b/src/Model/View/Utils/Tailwind.php
new file mode 100644
index 00000000..2613acab
--- /dev/null
+++ b/src/Model/View/Utils/Tailwind.php
@@ -0,0 +1,18 @@
+escaper->escapeHtml($text) . '. -->' . PHP_EOL;
+ }
+
+ return '';
+ }
+}
diff --git a/src/Model/View/UtilsInterface.php b/src/Model/View/UtilsInterface.php
new file mode 100644
index 00000000..10bf7dc3
--- /dev/null
+++ b/src/Model/View/UtilsInterface.php
@@ -0,0 +1,16 @@
+magewireServiceProvider->setup();
+ }
+}
diff --git a/src/Observer/Frontend/HyvaConfigGenerateBefore.php b/src/Observer/Frontend/HyvaConfigGenerateBefore.php
new file mode 100644
index 00000000..8d512a25
--- /dev/null
+++ b/src/Observer/Frontend/HyvaConfigGenerateBefore.php
@@ -0,0 +1,48 @@
+componentRegistrar = $componentRegistrar;
+ }
+
+ function execute(Observer $event)
+ {
+ $config = $event->getData('config');
+ $extensions = $config->hasData('extensions') ? $config->getData('extensions') : [];
+
+ $path = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, 'Magewirephp_Magewire');
+
+ // Since Hyva is the first-party, it needs to also register Magewire itself.
+ $extensions[] = ['src' => substr($path, strlen(BP) + 1)];
+
+ $moduleName = implode('_', array_slice(explode('\\', __CLASS__), 0, 2));
+ $path = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName);
+
+ // Only use the path relative to the Magento base dir.
+ $extensions[] = ['src' => substr($path, strlen(BP) + 1)];
+
+ $config->setData('extensions', $extensions);
+ }
+}
diff --git a/src/Observer/Frontend/ViewBlockAbstract.php b/src/Observer/Frontend/ViewBlockAbstract.php
deleted file mode 100644
index 9a83981b..00000000
--- a/src/Observer/Frontend/ViewBlockAbstract.php
+++ /dev/null
@@ -1,100 +0,0 @@
-applicationState = $applicationState;
- $this->componentHelper = $componentHelper;
- $this->componentManager = $componentManager;
- $this->httpFactory = $httpFactory;
- }
-
- /**
- * @return ComponentHelper
- */
- public function getComponentHelper(): ComponentHelper
- {
- return $this->componentHelper;
- }
-
- /**
- * @return ComponentManager
- */
- public function getComponentManager(): ComponentManager
- {
- return $this->componentManager;
- }
-
- /**
- * @return HttpFactory
- */
- public function getHttpFactory(): HttpFactory
- {
- return $this->httpFactory;
- }
-
- /**
- * @param Template $block
- * @param Exception $exception
- * @throws SubsequentRequestException
- */
- public function throwException(Template $block, Exception $exception): void
- {
- $magewire = $block->getMagewire();
-
- if ($magewire->getRequest() && $magewire->getRequest()->isSubsequent()) {
- throw new SubsequentRequestException($exception->getMessage());
- }
-
- // Detach the component who's given the problems
- $block->unsetData('magewire');
-
- $block->setTemplate('Magewirephp_Magewire::component/exception.phtml');
- $block->setException($exception);
- $block->setApplicationState($this->applicationState);
- }
-}
diff --git a/src/Observer/Frontend/ViewBlockAbstractToHtmlAfter.php b/src/Observer/Frontend/ViewBlockAbstractToHtmlAfter.php
deleted file mode 100644
index 55815892..00000000
--- a/src/Observer/Frontend/ViewBlockAbstractToHtmlAfter.php
+++ /dev/null
@@ -1,79 +0,0 @@
-getBlock();
-
- if ($block->hasMagewire()) {
- try {
- $component = $this->getComponentHelper()->extractComponentFromBlock($block);
- $response = $component->getResponse();
-
- if ($response === null) {
- throw new LifecycleException(__('Component response object not found'));
- }
-
- $observer->getTransport()->setHtml(
- $this->renderToView($response, $component, $observer->getTransport()->getHtml())
- );
- } catch (Exception $exception) {
- $this->throwException($block, $exception);
- }
- }
- }
-
- /**
- * @param ResponseInterface $response
- * @param Component $component
- * @param string $html
- * @return string|null
- * @throws LifecycleException
- */
- public function renderToView(ResponseInterface $response, Component $component, string $html): ?string
- {
- // Bind intended HTML onto the Response
- $response->effects['html'] = $html;
- // Dehydration lifecycle step
- $this->getComponentManager()->dehydrate($component);
-
- $data = ['id' => $response->fingerprint['id']];
-
- if ($response->getRequest()->isPreceding()) {
- $data['initial-data'] = $response->toArrayWithoutHtml();
- }
-
- if ($component->canRender() === false) {
- $response->effects['html'] = null;
- } else if (is_string($response->effects['html'])) {
- $response->effects['html'] = $response->renderWithRootAttribute($data);
- }
-
- return $response->effects['html'];
- }
-}
diff --git a/src/Observer/Frontend/ViewBlockAbstractToHtmlBefore.php b/src/Observer/Frontend/ViewBlockAbstractToHtmlBefore.php
deleted file mode 100644
index d5a20427..00000000
--- a/src/Observer/Frontend/ViewBlockAbstractToHtmlBefore.php
+++ /dev/null
@@ -1,112 +0,0 @@
-getBlock();
-
- if ($block->hasMagewire()) {
- try {
- $component = $this->getComponentHelper()->extractComponentFromBlock($block);
- $component->setParent($this->determineTemplate($block));
-
- $request = $component->getRequest();
- $data = $this->getComponentHelper()->extractDataFromBlock($block);
-
- // Fix for subsequent rendered wired children via e.g. a getChildHtml()
- if ($request !== null && $request->isSubsequent()) {
- $this->overwriteUpdateHandle($request->getFingerprint('handle'));
- }
- if (($request === null) || ($request->isPreceding())) {
- $request = $this->getComponentManager()->createInitialRequest(
- $block, $component, $data, $this->getUpdateHandle()
- );
- }
-
- // Hydration lifecycle step
- $this->getComponentManager()->hydrate($component->setRequest($request));
-
- if ($component->hasRequest('updates')) {
- $this->getComponentManager()->processUpdates($component, $request->getUpdates());
- }
-
- // Finalize component with its Response object
- $component->setResponse($this->getHttpFactory()->createResponse($component->getRequest()));
- // Re-attach the component onto the block
- $block->setData('magewire', $component);
- } catch (Exception $exception) {
- $this->throwException($block, $exception);
- }
- }
- }
-
- /**
- * Determines the template by a default template path
- * when the path is not defined within the layout.
- *
- * Results in: {Module_Name::magewire/dashed-class-name.phtml}
- *
- * @param Template $block
- * @return Template
- * @throws MissingComponentException
- */
- public function determineTemplate(Template $block): Template
- {
- if ($block->getTemplate() === null) {
- $magewire = $this->getComponentHelper()->extractComponentFromBlock($block);
- $module = explode('\\', get_class($magewire));
-
- $prefix = $module[0] . '_' . $module[1];
- $affix = strtolower(preg_replace('/(?setTemplate($prefix . '::magewire/' . $affix . '.phtml');
- }
-
- return $block;
- }
-
- /**
- * @param string $handle
- * @return string
- */
- public function overwriteUpdateHandle(string $handle): string
- {
- return $this->updateHandle = $handle;
- }
-
- /**
- * @return string|null
- */
- public function getUpdateHandle(): ?string
- {
- return $this->updateHandle;
- }
-}
diff --git a/src/Observer/ViewBlockAbstractToHtmlAfter.php b/src/Observer/ViewBlockAbstractToHtmlAfter.php
new file mode 100644
index 00000000..1932ce33
--- /dev/null
+++ b/src/Observer/ViewBlockAbstractToHtmlAfter.php
@@ -0,0 +1,75 @@
+getData('block');
+ /** @var Component|mixed $magewire */
+ $magewire = $block->getData('magewire');
+ /** @var DataObject $transport */
+ $transport = $observer->getData('transport');
+
+ if ($magewire) {
+ $html = $transport->getHtml();
+
+ try {
+ if (! $magewire instanceof Component) {
+ throw new ComponentNotFoundException('Something went wrong.');
+ }
+
+ if (! store($magewire)->get('magewire:update', false)) {
+ [$block, $html] = $this->magewireManager->render($block, $html);
+ $observer->setData('block', $block);
+ }
+ } catch (Exception $exception) {
+ $block = $this->exceptionManager->handleWithBlock($block, $exception);
+ // Making sure we do not end up in a cyclic event loop.
+ $block->unsetData('magewire');
+
+ $html = $block->toHtml();
+ }
+
+ $transport->setHtml($html);
+ }
+
+ $lifecycle = $this->componentRenderLifecycleManager->target('magewire')->pop();
+ trigger('magento:block:rendered', $lifecycle, $block);
+ }
+}
diff --git a/src/Observer/ViewBlockAbstractToHtmlBefore.php b/src/Observer/ViewBlockAbstractToHtmlBefore.php
new file mode 100644
index 00000000..888fda0c
--- /dev/null
+++ b/src/Observer/ViewBlockAbstractToHtmlBefore.php
@@ -0,0 +1,120 @@
+getData('block');
+ /** @var mixed $magewire */
+ $magewire = $block->getData('magewire');
+
+ $lifecycle = $this->componentRenderLifecycleManager->target('magewire')->push($block);
+ trigger('magento:block:render', $lifecycle, $block);
+
+ if ($magewire) {
+ try {
+ if ($magewire instanceof Component) {
+ // Update flag for use during subsequent updates to maintain synchronization and data consistency.
+ $update = store($magewire)->get('magewire:update', false);
+
+ if ($update) {
+ $this->handleUpdate($update, $block);
+
+ return;
+ }
+ }
+
+ /**
+ * Magewire has two trigger points for booting itself. One occurs during the rendering of a block.
+ * This is the only feasible location, after confirming we are not on an update request,
+ * where we should attempt to boot. A reboot is unlikely as the boot method includes a
+ * safety trigger to prevent such an occurrence. [1/2]
+ *
+ * @see \Magewirephp\Magewire\Controller\Router
+ */
+ $this->magewireServiceProvider->boot(RequestMode::PRECEDING);
+
+ $construct = trigger('magewire:component:construct', $block);
+ $block = $construct();
+
+ $this->handleMount($block);
+ } catch (Exception $exception) {
+ $this->exceptionManager->handleWithBlock($block, $exception);
+ }
+ }
+ }
+
+ /**
+ * @throws FileSystemException
+ * @throws RuntimeException
+ * @throws NotFoundException
+ */
+ private function handleUpdate(ComponentRequestContext $update, AbstractBlock $block): void
+ {
+ $this->magewireManager->update(
+ $update->getSnapshot(),
+ $update->getUpdates(),
+ $update->getCalls(),
+ $block
+ );
+ }
+
+ /**
+ * @throws NotFoundException
+ * @throws RuntimeException
+ */
+ private function handleMount(AbstractBlock $block): void
+ {
+ /** @var Component $component */
+ $component = $block->getData('magewire');
+
+ $this->magewireManager->mount(
+ $component->getName(),
+ $component->magewireResolver()->arguments()->forMount(),
+ $block->getCacheKey(),
+ $block,
+ $component
+ );
+ }
+}
diff --git a/src/Plugin/Magento/Framework/View/TemplateEngine/Php.php b/src/Plugin/Magento/Framework/View/TemplateEngine/Php.php
new file mode 100644
index 00000000..f6db19c5
--- /dev/null
+++ b/src/Plugin/Magento/Framework/View/TemplateEngine/Php.php
@@ -0,0 +1,47 @@
+ $block,
+ 'filename' => $filename,
+ 'dictionary' => $dictionary,
+ ]);
+
+ $this->renderers[] = trigger('magento:template:render', $dto, $subject);
+
+ return [$dto->block(), $dto->filename(), $dto->dictionary()];
+ }
+
+ public function afterRender(Subject $subject, string $html): string
+ {
+ $finish = array_pop($this->renderers);
+
+ return $finish ? $finish($html, $subject) : $html;
+ }
+}
diff --git a/src/Setup/Patch/Data/EnableCache.php b/src/Setup/Patch/Data/EnableCache.php
new file mode 100644
index 00000000..bf2e7c31
--- /dev/null
+++ b/src/Setup/Patch/Data/EnableCache.php
@@ -0,0 +1,45 @@
+cacheManager->setEnabled(array_intersect($this->cacheManager->getAvailableTypes(), [MagewireCacheType::TYPE_IDENTIFIER]), true);
+
+ $this->cacheManager->clean($enableMagewireCache);
+ return $this;
+ }
+}
diff --git a/src/ViewModel/Magewire.php b/src/ViewModel/Magewire.php
index f78cf56f..66481adc 100644
--- a/src/ViewModel/Magewire.php
+++ b/src/ViewModel/Magewire.php
@@ -1,4 +1,5 @@
-environment() should be used instead.
+ * @see ViewUtils\Environment
+ */
+ function isDeveloperMode(): bool
+ {
+ return $this->utils()->env()->isDeveloperMode();
+ }
- /** @var ProductMetadataInterface $productMetaData */
- protected $productMetaData;
+ /**
+ * @deprecated To make backwards compatible, utils()->environment() should be used instead.
+ * @see ViewUtils\Environment
+ */
+ function isProductionMode(): bool
+ {
+ return $this->utils()->env()->isProductionMode();
+ }
/**
- * @param FormKey $formKey
- * @param ApplicationState $applicationState
- * @param ProductMetadataInterface $productMetadata
+ * @deprecated To make backwards compatible, utils()->magewire() should be used instead.
+ * @see ViewUtils\Magewire
*/
- public function __construct(
- FormKey $formKey,
- ApplicationState $applicationState,
- ProductMetadataInterface $productMetadata
- ) {
- $this->formKey = $formKey;
- $this->applicationState = $applicationState;
- $this->productMetaData = $productMetadata;
+ function getSystemConfig(): MagewireSystemConfig
+ {
+ return $this->utils()->magewire()->config();
}
/**
- * @return bool
+ * @deprecated To make backwards compatible, utils()->magewire() method should be used instead.
+ * @see ViewUtils\Magewire
*/
- public function isDeveloperMode(): bool
+ function pageRequiresMagewire(): bool
{
- return $this->applicationState->getMode() === ApplicationState::MODE_DEVELOPER;
+ return $this->utils()
+ ->magewire()
+ ->mechanisms()
+ ->resolveComponents()
+ ->doesPageHaveComponents();
}
/**
- * @return bool
+ * @deprecated To make backwards compatible, utils()->magewire() method should be used instead.
+ * @see ViewUtils\Magewire
*/
- public function isBeforeTwoFourZero(): bool
+ function getUpdateUri(): string
{
- return version_compare($this->productMetaData->getVersion(), '2.4.0', '<');
+ return $this->utils()->magewire()->getUpdateUri();
}
/**
- * @return string
+ * @deprecated To make backwards compatible, utils()->magewire() method should be used instead.
+ * @see ViewUtils\Magewire
*/
- public function getPostRoute(): string
+ function getScriptPath(): string
{
- return $this->isBeforeTwoFourZero() ? '/magewire/vintage' : '/magewire/post';
+ return $this->utils()
+ ->magewire()
+ ->mechanisms()
+ ->frontendAssets()
+ ->getScriptPath();
}
}
diff --git a/src/etc/acl.xml b/src/etc/acl.xml
new file mode 100644
index 00000000..9c07ceef
--- /dev/null
+++ b/src/etc/acl.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/etc/adminhtml/di.xml b/src/etc/adminhtml/di.xml
new file mode 100644
index 00000000..f7cf8d43
--- /dev/null
+++ b/src/etc/adminhtml/di.xml
@@ -0,0 +1,553 @@
+
+
+
+
+
+
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\ResolveComponents\ResolveComponents
+ - 1000
+ - Magewirephp\Magewire\Mechanisms\ResolveComponents\ResolveComponentsViewModel
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\PersistentMiddleware\PersistentMiddleware
+ - 1050
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\HandleComponents\HandleComponents
+ - Magewirephp\Magewire\Mechanisms\HandleComponents\HandleComponentsFacade
+ - 1100
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\HandleRequests\HandleRequests
+ - Magewirephp\Magewire\Mechanisms\HandleRequests\HandleRequestFacade
+ - 1200
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\FrontendAssets\FrontendAssets
+ - Magewirephp\Magewire\Mechanisms\FrontendAssets\FrontendAssetsFacade
+ - Magewirephp\Magewire\Mechanisms\FrontendAssets\FrontendAssetsViewModel
+ - 1400
+
+ -
+
-
+
-
+ Magewirephp_Magewire::js/magewire.min.js
+
+
+ -
+
+
- df3a17f2
+
+
+ -
+
+
- true
+
+
+
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\DataStore
+ - 1250
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireViewModel\SupportMagewireViewModel
+ - 700
+ - 30
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireExceptionHandling\SupportMagewireExceptionHandling
+ - 800
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireRateLimiting\SupportMagewireRateLimiting
+ - 900
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportNestingComponents\SupportNestingComponents
+ - 1000
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireNestingComponents\SupportMagewireNestingComponents
+ - 1100
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportAttributes\SupportAttributes
+ - 1200
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportRedirects\SupportRedirects
+ - 1300
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportStreaming\SupportStreaming
+ - 1400
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportLocales\SupportLocales
+ - 1500
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportEvents\SupportEvents
+ - 1600
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagentoLayouts\SupportMagentoLayouts
+ - 5000
+
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagentoFlashMessages\SupportMagentoFlashMessages
+ - 5100
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireLoaders\SupportMagewireLoaders
+ - 5200
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireNotifications\SupportMagewireNotifications
+ - Magewirephp\Magewire\Features\SupportMagewireNotifications\MagewireNotificationsFacade
+ - 5200
+
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportLifecycleHooks\SupportLifecycleHooks
+ - 99000
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireCompiling\SupportMagewireCompiling
+ - 99300
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagentoObserverEvents\SupportMagentoObserverEvents
+ - 99400
+
+
+
+
+
+
+
+
+
+ -
+
-
+ Magewirephp\Magewire\Features\SupportMagewireRateLimiting\RateLimiterExceptionHandler
+
+
+
+
+
+
+
+
+
+
+ Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Compiler\MagentoTemplateCompiler
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Json
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Scope
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Scope
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Script
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Fragment
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Base\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Template\Proxy
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Base\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Base\Proxy
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Auth\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Auth\Proxy
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Block\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Block\Proxy
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Magewirephp\Magewire\Features\SupportMagewireCompiling\View\BaseDirectiveArea
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\MagentoBlockDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\MagentoEscapeDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\MagewireDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\AlpinejsDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\TailwindcssDirectiveArea
+
+
+
+
+
+
+
+
+
+ Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Action\Magewire\Proxy
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Action\Magento\Auth\Proxy
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Containers\Livewire
+ - Magewirephp\Magewire\Containers\Redirect
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Model\View\Fragment\Modifier\Developer
+
+
+
+ - Magewirephp\Magewire\Model\View\Fragment\Modifier\Csp
+
+
+
+
+
+
+
+
+
+ -
+ Magewirephp\Magewire\Mechanisms\ResolveComponents\ComponentResolver\LayoutResolver
+
+
+
+
+
+
+
+
+
+ - true
+
+
+
+
+
+
+
+
+
+ - dev/magewire/debug
+
+
+
+
+
+
+
+
+ Magewirephp\Magento\Framework\Reflection\TypeProcessor
+
+
+
+
+
+
+
+ Magewirephp\Magewire\Virtual\Magento\Framework\Reflection\MethodsMap
+
+
+
+
+
+
+
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\DataObjectSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\ArraySynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\EnumSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\FloatSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\IntSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\StdClassSynth
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ Magewirephp\Magewire\Features\SupportMagewireBackwardsCompatibility\Resolver\UpdatingUpdatedArgumentSwapResolver
+
+
+
+
+
diff --git a/src/etc/adminhtml/system.xml b/src/etc/adminhtml/system.xml
new file mode 100644
index 00000000..56624dc8
--- /dev/null
+++ b/src/etc/adminhtml/system.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/etc/adminhtml/system/magewire.xml b/src/etc/adminhtml/system/magewire.xml
new file mode 100644
index 00000000..dc00d77e
--- /dev/null
+++ b/src/etc/adminhtml/system/magewire.xml
@@ -0,0 +1,19 @@
+
+
+
+ Magewire
+ advanced
+ Magewirephp_Magewire::magewire
+
+
+
+
+
diff --git a/src/etc/adminhtml/system/magewire/debug.xml b/src/etc/adminhtml/system/magewire/debug.xml
new file mode 100644
index 00000000..dbb0942c
--- /dev/null
+++ b/src/etc/adminhtml/system/magewire/debug.xml
@@ -0,0 +1,25 @@
+
+
+
+ Debug
+
+
+ Enable
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
diff --git a/src/etc/adminhtml/system/magewire/features.xml b/src/etc/adminhtml/system/magewire/features.xml
new file mode 100644
index 00000000..c62b5fa3
--- /dev/null
+++ b/src/etc/adminhtml/system/magewire/features.xml
@@ -0,0 +1,16 @@
+
+
+
+ Features
+
+
+
+
diff --git a/src/etc/adminhtml/system/magewire/features/support-rate-limiting/support-rate-limiting.xml b/src/etc/adminhtml/system/magewire/features/support-rate-limiting/support-rate-limiting.xml
new file mode 100644
index 00000000..82cb498d
--- /dev/null
+++ b/src/etc/adminhtml/system/magewire/features/support-rate-limiting/support-rate-limiting.xml
@@ -0,0 +1,79 @@
+
+
+
+ Rate Limiting
+ Rate limiting can be applied globally to all Magewire requests, and/or individually per component. Global settings take precedence over component-specific limits when both are configured.
+
+
+ Variant
+ Magewirephp\Magewire\Features\SupportMagewireRateLimiting\Config\Source\RateLimitingVariant
+
+
+
+ Requests
+ Note: These settings only take effect when "requests" rate limiting is enabled.
+
+
+ Scope
+ Determine whether rate limiting applies across all components (shared) or individually per component (isolated).
+
+ Magewirephp\Magewire\Features\SupportMagewireRateLimiting\Config\Source\RequestsScope
+
+
+
+ Max Attempts
+ Maximum number of requests allowed globally within the decay seconds period configured below.
+
+ required-entry integer
+
+
+
+ Decay Seconds
+ Time window in seconds for the max attempts limit above. After this period, the attempt counter resets.
+
+ required-entry integer
+
+
+
+
diff --git a/src/etc/cache.xml b/src/etc/cache.xml
new file mode 100644
index 00000000..2903554f
--- /dev/null
+++ b/src/etc/cache.xml
@@ -0,0 +1,9 @@
+
+
+
+ Magewire Data
+ Magewire cache
+
+
diff --git a/src/etc/config.xml b/src/etc/config.xml
new file mode 100644
index 00000000..c1592170
--- /dev/null
+++ b/src/etc/config.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ 0
+
+
+ 0
+ 4
+ 5
+
+
+
+
+
+
diff --git a/src/etc/di.xml b/src/etc/di.xml
index 2d40a4df..9fbfccf2 100644
--- a/src/etc/di.xml
+++ b/src/etc/di.xml
@@ -2,8 +2,54 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/etc/events.xml b/src/etc/events.xml
new file mode 100644
index 00000000..6e32d42e
--- /dev/null
+++ b/src/etc/events.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/etc/frontend/di.xml b/src/etc/frontend/di.xml
index 4eb521be..4567c3a3 100644
--- a/src/etc/frontend/di.xml
+++ b/src/etc/frontend/di.xml
@@ -2,45 +2,592 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\ResolveComponents\ResolveComponents
+ - 1000
+ - Magewirephp\Magewire\Mechanisms\ResolveComponents\ResolveComponentsViewModel
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\PersistentMiddleware\PersistentMiddleware
+ - 1050
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\HandleComponents\HandleComponents
+ - Magewirephp\Magewire\Mechanisms\HandleComponents\HandleComponentsFacade
+ - 1100
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\HandleRequests\HandleRequests
+ - Magewirephp\Magewire\Mechanisms\HandleRequests\HandleRequestFacade
+ - 1200
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\FrontendAssets\FrontendAssets
+ - Magewirephp\Magewire\Mechanisms\FrontendAssets\FrontendAssetsFacade
+ - Magewirephp\Magewire\Mechanisms\FrontendAssets\FrontendAssetsViewModel
+ - 1400
+ - 30
+
+ -
+
-
+
-
+ Magewirephp_Magewire::js/magewire.csp.min.js
+
+
+ -
+
+
- 69cd902e
+
+
+ -
+
+
- true
+
+
+
+
+
+ -
+
- Magewirephp\Magewire\Mechanisms\DataStore
+ - 1250
+
+
+
+
+
+
+
+
+
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireViewModel\SupportMagewireViewModel
+ - 700
+ - 30
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireExceptionHandling\SupportMagewireExceptionHandling
+ - 800
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireRateLimiting\SupportMagewireRateLimiting
+ - 900
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportNestingComponents\SupportNestingComponents
+ - 1000
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireNestingComponents\SupportMagewireNestingComponents
+ - 1100
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportAttributes\SupportAttributes
+ - 1200
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportRedirects\SupportRedirects
+ - 1300
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportStreaming\SupportStreaming
+ - 1400
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportLocales\SupportLocales
+ - 1500
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportEvents\SupportEvents
+ - 1600
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagentoLayouts\SupportMagentoLayouts
+ - 5000
+
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagentoFlashMessages\SupportMagentoFlashMessages
+ - 5100
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireLoaders\SupportMagewireLoaders
+ - 5200
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireNotifications\SupportMagewireNotifications
+ - Magewirephp\Magewire\Features\SupportMagewireNotifications\MagewireNotificationsFacade
+ - 5200
+
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportLifecycleHooks\SupportLifecycleHooks
+ - 99000
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireBackwardsCompatibility\SupportMagewireBackwardsCompatibility
+ - 99100
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireFlakes\SupportMagewireFlakes
+ - 99200
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagewireCompiling\SupportMagewireCompiling
+ - 99300
+
+
+ -
+
- Magewirephp\Magewire\Features\SupportMagentoObserverEvents\SupportMagentoObserverEvents
+ - 99400
+
+
+
+
+
+
+
+
+
+ -
+
-
+ Magewirephp\Magewire\Features\SupportMagewireRateLimiting\RateLimiterExceptionHandler
+
+
+
+
+
+
+
+
+
+ Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Compiler\MagentoTemplateCompiler
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Json
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Scope
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Scope
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Script
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Fragment
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Base\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Template\Proxy
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Base\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Base\Proxy
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Auth\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Auth\Proxy
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Block\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Block\Proxy
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magento\Escape\Proxy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magewire\Slot\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Directive\Magewire\Component\Proxy
+
+
+
+
+
+
+
+
+
+
+
+
+ Magewirephp\Magewire\Features\SupportMagewireCompiling\View\BaseDirectiveArea
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\MagentoBlockDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\MagentoEscapeDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\MagentoCustomerDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\MagewireDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\AlpinejsDirectiveArea
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\TailwindcssDirectiveArea
+
+
+
+
+
+
+
+
+ Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Action\Magewire\Proxy
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Action\Magento\Auth\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireCompiling\View\Action\Magewire\Proxy
+ - Magewirephp\Magewire\Features\SupportMagewireFlakes\View\Action\Magewire\FlakeViewAction\Proxy
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Containers\Livewire
+ - Magewirephp\Magewire\Containers\Redirect
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Model\View\Fragment\Modifier\Developer
+
+
+
+ - Magewirephp\Magewire\Model\View\Fragment\Modifier\Csp
+
+
+
+
+
+
+
+
+ -
+ Magewirephp\Magewire\Features\SupportMagewireFlakes\View\Fragment\FlakeFragment
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Model\View\Fragment\Element\Slot
+
+ - Magewirephp\Magewire\Features\SupportMagewireFlakes\View\Fragment\Element\Flake
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Features\SupportMagewireFlakes\Mechanisms\ResolveComponent\ComponentResolver\FlakeResolver
+ - Magewirephp\Magewire\Mechanisms\ResolveComponents\ComponentResolver\LayoutResolver
+
+
+
+
+
-
- - Magewirephp\Magewire\Model\Action\CallMethod
- - Magewirephp\Magewire\Model\Action\FireEvent
- - Magewirephp\Magewire\Model\Action\SyncInput
+
+
+ - true
-
+
+
-
-
+
+
+ - dev/magewire/debug
+
+
+
-
- - mount
- - hydrate
- - dehydrate
- - updating
- - updated
+
- - getParent
- - setParent
- - getPublicProperties
- - getUncallables
- - __call
+ The MethodsMap in the Magento framework is a useful class for extracting information about a class's methods.
+ However, it relies on a TypeProcessor that reflects the method's docblock to determine type details, which can
+ break methods lacking a docblock or having incomplete information. Since the Magewire Wrapped class heavily
+ uses this functionality, a custom MethodsMap `virtual type` with a modified TypeProcessor resolves the issue
+ by returning an empty array when needed, ensuring compatibility.
+ -->
+
+
+ Magewirephp\Magento\Framework\Reflection\TypeProcessor
+
+
-
- - setRequest
- - getRequest
- - setResponse
- - getResponse
- - getEventQueue
+
+
+ Magewirephp\Magewire\Virtual\Magento\Framework\Reflection\MethodsMap
+
+
-
- - renderPagination
+
+
+
+
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\DataObjectSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\ArraySynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\EnumSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\FloatSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\IntSynth
+
+ -
+ Magewirephp\Magewire\Mechanisms\HandleComponents\Synthesizers\StdClassSynth
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ Magewirephp\Magewire\Features\SupportMagewireBackwardsCompatibility\Resolver\UpdatingUpdatedArgumentSwapResolver
+
+
+
+
+
+
+
+
+
+ - Magewirephp\Magewire\Controller\MagewireUpdateRouteFrontend
+
+
+
+
+
+
+
+ -
+
- Magewirephp\Magewire\Controller\Router
+ - false
+ - 5
+
diff --git a/src/etc/frontend/events.xml b/src/etc/frontend/events.xml
index 99cf3aca..b8551b2f 100644
--- a/src/etc/frontend/events.xml
+++ b/src/etc/frontend/events.xml
@@ -2,17 +2,10 @@
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/src/etc/module.xml b/src/etc/module.xml
index 792397e1..26720488 100644
--- a/src/etc/module.xml
+++ b/src/etc/module.xml
@@ -2,9 +2,5 @@
-
-
-
-
-
+
diff --git a/src/i18n/de_DE.csv b/src/i18n/de_DE.csv
deleted file mode 100644
index db14e976..00000000
--- a/src/i18n/de_DE.csv
+++ /dev/null
@@ -1 +0,0 @@
-"Pager not available.","Paginierung nicht verfügbar."
diff --git a/src/i18n/en_US.csv b/src/i18n/en_US.csv
index 2a531fc2..3c1055e6 100644
--- a/src/i18n/en_US.csv
+++ b/src/i18n/en_US.csv
@@ -1 +1,2 @@
-"Pager not available.","Pager not available."
+"An exception occurred while parsing and/or rendering the Magewire component.","An exception occurred while parsing and/or rendering the Magewire component."
+"An exception occurred while parsing and/or rendering the Magewire component associated with a block named: %1s.","An exception occurred while parsing and/or rendering the Magewire component associated with a block named: %1s."
diff --git a/src/i18n/fr_FR.csv b/src/i18n/fr_FR.csv
deleted file mode 100644
index 0fe08bb3..00000000
--- a/src/i18n/fr_FR.csv
+++ /dev/null
@@ -1 +0,0 @@
-"Pager not available.","Pagination non disponible."
diff --git a/src/i18n/nl_NL.csv b/src/i18n/nl_NL.csv
deleted file mode 100644
index b631fb67..00000000
--- a/src/i18n/nl_NL.csv
+++ /dev/null
@@ -1 +0,0 @@
-"Pager not available.","Paginering niet beschikbaar."
diff --git a/src/registration.php b/src/registration.php
index b11e52fe..29925854 100644
--- a/src/registration.php
+++ b/src/registration.php
@@ -1,15 +1,29 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/view/base/templates/js/alpinejs/components/magewire-notifier.phtml b/src/view/base/templates/js/alpinejs/components/magewire-notifier.phtml
new file mode 100644
index 00000000..97f472d9
--- /dev/null
+++ b/src/view/base/templates/js/alpinejs/components/magewire-notifier.phtml
@@ -0,0 +1,140 @@
+getData('view_model');
+$magewireTemplate = $magewireViewModel->utils()->template();
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+= $magewireTemplate->echoCodeComment('magewire notifier', false, 'Alpine JS') ?>
+make()->script()->start() ?>
+
+end() ?>
+
+= $block->getChildBlock('ac') ?>
diff --git a/src/view/base/templates/js/alpinejs/directives/.gitkeep b/src/view/base/templates/js/alpinejs/directives/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/base/templates/js/alpinejs/magewire.phtml b/src/view/base/templates/js/alpinejs/magewire.phtml
new file mode 100644
index 00000000..22200233
--- /dev/null
+++ b/src/view/base/templates/js/alpinejs/magewire.phtml
@@ -0,0 +1,32 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/**
+ * Alpine JS - Global
+ *
+ * @internal Do not modify to ensure Magewire continues to function correctly.
+ */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/addons.phtml b/src/view/base/templates/js/magewire/addons.phtml
new file mode 100644
index 00000000..2c441f41
--- /dev/null
+++ b/src/view/base/templates/js/magewire/addons.phtml
@@ -0,0 +1,11 @@
+
+= $block->getChildHtml() ?>
diff --git a/src/view/base/templates/js/magewire/addons/notifier.phtml b/src/view/base/templates/js/magewire/addons/notifier.phtml
new file mode 100644
index 00000000..61d85703
--- /dev/null
+++ b/src/view/base/templates/js/magewire/addons/notifier.phtml
@@ -0,0 +1,225 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/directives.phtml b/src/view/base/templates/js/magewire/directives.phtml
new file mode 100644
index 00000000..2c441f41
--- /dev/null
+++ b/src/view/base/templates/js/magewire/directives.phtml
@@ -0,0 +1,11 @@
+
+= $block->getChildHtml() ?>
diff --git a/src/view/base/templates/js/magewire/directives/mage-notify.phtml b/src/view/base/templates/js/magewire/directives/mage-notify.phtml
new file mode 100644
index 00000000..100a1783
--- /dev/null
+++ b/src/view/base/templates/js/magewire/directives/mage-notify.phtml
@@ -0,0 +1,115 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/directives/mage-throttle.phtml b/src/view/base/templates/js/magewire/directives/mage-throttle.phtml
new file mode 100644
index 00000000..53099b37
--- /dev/null
+++ b/src/view/base/templates/js/magewire/directives/mage-throttle.phtml
@@ -0,0 +1,81 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/directives/select.phtml b/src/view/base/templates/js/magewire/directives/select.phtml
new file mode 100644
index 00000000..af7d8358
--- /dev/null
+++ b/src/view/base/templates/js/magewire/directives/select.phtml
@@ -0,0 +1,86 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/features.phtml b/src/view/base/templates/js/magewire/features.phtml
new file mode 100644
index 00000000..2c441f41
--- /dev/null
+++ b/src/view/base/templates/js/magewire/features.phtml
@@ -0,0 +1,11 @@
+
+= $block->getChildHtml() ?>
diff --git a/src/view/base/templates/js/magewire/global.phtml b/src/view/base/templates/js/magewire/global.phtml
new file mode 100644
index 00000000..99a9899b
--- /dev/null
+++ b/src/view/base/templates/js/magewire/global.phtml
@@ -0,0 +1,139 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+$templateUtil = $magewireViewModel->utils()->template();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+= $block->getChildHtml('before') ?>
+
+make()->script()->start() ?>
+
+end() ?>
+
+= $templateUtil->echoCodeComment('utilities', false, 'container') ?>
+= $block->getChildHtml('utilities') ?>
+= $templateUtil->echoCodeComment('addons', false, 'container') ?>
+= $block->getChildHtml('addons') ?>
+= $block->getChildHtml('after') ?>
diff --git a/src/view/base/templates/js/magewire/internal.phtml b/src/view/base/templates/js/magewire/internal.phtml
new file mode 100644
index 00000000..7c21e0a0
--- /dev/null
+++ b/src/view/base/templates/js/magewire/internal.phtml
@@ -0,0 +1,31 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
+
+= $block->getChildHtml('internal.backwards-compatibility') ?>
diff --git a/src/view/base/templates/js/magewire/internal/.gitkeep b/src/view/base/templates/js/magewire/internal/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/base/templates/js/magewire/state.phtml b/src/view/base/templates/js/magewire/state.phtml
new file mode 100644
index 00000000..bbe29d93
--- /dev/null
+++ b/src/view/base/templates/js/magewire/state.phtml
@@ -0,0 +1,28 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+$state = $block->getData('state') ? 'active' : 'inactive';
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/utilities.phtml b/src/view/base/templates/js/magewire/utilities.phtml
new file mode 100644
index 00000000..2c441f41
--- /dev/null
+++ b/src/view/base/templates/js/magewire/utilities.phtml
@@ -0,0 +1,11 @@
+
+= $block->getChildHtml() ?>
diff --git a/src/view/base/templates/js/magewire/utilities/cookie.phtml b/src/view/base/templates/js/magewire/utilities/cookie.phtml
new file mode 100644
index 00000000..56df7e04
--- /dev/null
+++ b/src/view/base/templates/js/magewire/utilities/cookie.phtml
@@ -0,0 +1,72 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/utilities/dom.phtml b/src/view/base/templates/js/magewire/utilities/dom.phtml
new file mode 100644
index 00000000..0e5f0958
--- /dev/null
+++ b/src/view/base/templates/js/magewire/utilities/dom.phtml
@@ -0,0 +1,57 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/utilities/loader.phtml b/src/view/base/templates/js/magewire/utilities/loader.phtml
new file mode 100644
index 00000000..0c996187
--- /dev/null
+++ b/src/view/base/templates/js/magewire/utilities/loader.phtml
@@ -0,0 +1,84 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/js/magewire/utilities/str.phtml b/src/view/base/templates/js/magewire/utilities/str.phtml
new file mode 100644
index 00000000..080c5574
--- /dev/null
+++ b/src/view/base/templates/js/magewire/utilities/str.phtml
@@ -0,0 +1,48 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/magewire-features/support-magewire-loaders/support-magewire-loaders.phtml b/src/view/base/templates/magewire-features/support-magewire-loaders/support-magewire-loaders.phtml
new file mode 100644
index 00000000..8b9de18d
--- /dev/null
+++ b/src/view/base/templates/magewire-features/support-magewire-loaders/support-magewire-loaders.phtml
@@ -0,0 +1,351 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/magewire-features/support-magewire-rate-limiting/support-magewire-rate-limiting.phtml b/src/view/base/templates/magewire-features/support-magewire-rate-limiting/support-magewire-rate-limiting.phtml
new file mode 100644
index 00000000..28066406
--- /dev/null
+++ b/src/view/base/templates/magewire-features/support-magewire-rate-limiting/support-magewire-rate-limiting.phtml
@@ -0,0 +1,41 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/base/templates/magewire/exception.phtml b/src/view/base/templates/magewire/exception.phtml
new file mode 100644
index 00000000..d5b3075c
--- /dev/null
+++ b/src/view/base/templates/magewire/exception.phtml
@@ -0,0 +1,44 @@
+getNameInLayout() ?? '';
+
+$state = $block->getData('application_state');
+$exception = $block->getData('exception');
+?>
+getMode() === ApplicationState::MODE_DEVELOPER): ?>
+
+
+ Exception: = $escaper->escapeHtml($exception->getMessage()) ?>
+
+
+
+
+ = $escaper->escapeHtml(__('An exception occurred while parsing and/or rendering the Magewire component.')) ?>
+
+ = $escaper->escapeHtml(__('An exception occurred while parsing and/or rendering the Magewire component associated with a block named: %1.', $id)) ?>
+
+
+
+
+
+
+
+
+
+
diff --git a/src/view/base/templates/magewire/flakes/.gitkeep b/src/view/base/templates/magewire/flakes/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/base/templates/magewire/ui-components/notifier.phtml b/src/view/base/templates/magewire/ui-components/notifier.phtml
new file mode 100644
index 00000000..f4cce165
--- /dev/null
+++ b/src/view/base/templates/magewire/ui-components/notifier.phtml
@@ -0,0 +1,63 @@
+getChildBlock('notification.before');
+$notificationsAfterBlock = $block->getChildBlock('notification.after');
+
+$magewireViewModel = $block->getData('view_model');
+$templateUtil = $magewireViewModel->utils()->template();
+$layoutUtil = $magewireViewModel->utils()->layout();
+?>
+= $templateUtil->echoCodeComment('notifier', false, 'UI Component') ?>
+
+
+
+ canContainerizeBlock($notificationsBeforeBlock)): ?>
+
+ = $layoutUtil->renderBlockAsContainer($notificationsBeforeBlock) ?>
+
+
+
+
+
+ canContainerizeBlock($notificationsAfterBlock)): ?>
+
+ = $layoutUtil->renderBlockAsContainer($notificationsAfterBlock) ?>
+
+
+
+
+
diff --git a/src/view/base/templates/magewire/ui-components/notifier/activity-state.phtml b/src/view/base/templates/magewire/ui-components/notifier/activity-state.phtml
new file mode 100644
index 00000000..2e376e9d
--- /dev/null
+++ b/src/view/base/templates/magewire/ui-components/notifier/activity-state.phtml
@@ -0,0 +1,19 @@
+getChildBlock('loader') ?>
+
+
+
+
+ = $blockLoaderIcon->toHtml() ?>
+
+
+
diff --git a/src/view/base/templates/magewire/utils/icons/loading.phtml b/src/view/base/templates/magewire/utils/icons/loading.phtml
new file mode 100644
index 00000000..e8ef0cb9
--- /dev/null
+++ b/src/view/base/templates/magewire/utils/icons/loading.phtml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/src/view/base/templates/root.phtml b/src/view/base/templates/root.phtml
new file mode 100644
index 00000000..7e708d48
--- /dev/null
+++ b/src/view/base/templates/root.phtml
@@ -0,0 +1,49 @@
+getData('view_model');
+
+$templateUtil = $magewireViewModel->utils()->template();
+$layoutUtil = $magewireViewModel->utils()->layout();
+$magewireUtil = $magewireViewModel->utils()->magewire();
+
+$canRequireMagewire = $magewireUtil->canRequireMagewireJsLibrary();
+?>
+
+= $templateUtil->echoCodeComment('start', true) ?>
+= $templateUtil->echoCodeComment('global', false, 'container') ?>
+= $layoutUtil->getChildHtml($block, 'global', ['require_magewire' => $canRequireMagewire]) ?>
+
+= $templateUtil->echoCodeComment('before', false, 'container') ?>
+= $block->getChildHtml('before') ?>
+
+
+ = $templateUtil->echoCodeComment('internal before', false, 'container') ?>
+ = $block->getChildHtml('before.internal') ?>
+ = $templateUtil->echoCodeComment('script') ?>
+ = $block->getChildHtml('script') ?>
+ = $templateUtil->echoCodeComment('internal') ?>
+ = $block->getChildHtml('internal') ?>
+ = $templateUtil->echoCodeComment('directives', false, 'container') ?>
+ = $block->getChildHtml('directives') ?>
+ = $templateUtil->echoCodeComment('features', false, 'container') ?>
+ = $block->getChildHtml('features') ?>
+ = $templateUtil->echoCodeComment('internal after', false, 'container') ?>
+ = $block->getChildHtml('after.internal') ?>
+
+ = $templateUtil->echoCodeComment('disabled', false, 'container') ?>
+ = $block->getChildHtml('disabled') ?>
+
+
+= $templateUtil->echoCodeComment('after', false, 'container') ?>
+= $block->getChildHtml('after') ?>
+= $templateUtil->echoCodeComment('end', true) ?>
diff --git a/src/view/base/web/css/magewire.css b/src/view/base/web/css/magewire.css
new file mode 100644
index 00000000..aae43e2d
--- /dev/null
+++ b/src/view/base/web/css/magewire.css
@@ -0,0 +1,1031 @@
+/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
+@layer properties {
+ @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
+ *, :before, :after, ::backdrop {
+ --tw-rotate-x: initial;
+ --tw-rotate-y: initial;
+ --tw-rotate-z: initial;
+ --tw-skew-x: initial;
+ --tw-skew-y: initial;
+ --tw-border-style: solid;
+ --tw-font-weight: initial;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-color: initial;
+ --tw-shadow-alpha: 100%;
+ --tw-inset-shadow: 0 0 #0000;
+ --tw-inset-shadow-color: initial;
+ --tw-inset-shadow-alpha: 100%;
+ --tw-ring-color: initial;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-inset-ring-color: initial;
+ --tw-inset-ring-shadow: 0 0 #0000;
+ --tw-ring-inset: initial;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-blur: initial;
+ --tw-brightness: initial;
+ --tw-contrast: initial;
+ --tw-grayscale: initial;
+ --tw-hue-rotate: initial;
+ --tw-invert: initial;
+ --tw-opacity: initial;
+ --tw-saturate: initial;
+ --tw-sepia: initial;
+ --tw-drop-shadow: initial;
+ --tw-drop-shadow-color: initial;
+ --tw-drop-shadow-alpha: 100%;
+ --tw-drop-shadow-size: initial;
+ --tw-space-y-reverse: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-translate-z: 0;
+ --tw-outline-style: solid;
+ --tw-duration: initial;
+ --tw-ease: initial;
+ --tw-leading: initial;
+ }
+ }
+}
+
+@layer utilities {
+ @layer theme {
+ :root, :host {
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ --color-red-50: oklch(97.1% .013 17.38);
+ --color-red-600: oklch(57.7% .245 27.325);
+ --color-red-700: oklch(50.5% .213 27.518);
+ --color-yellow-50: oklch(98.7% .026 102.212);
+ --color-yellow-600: oklch(68.1% .162 75.834);
+ --color-yellow-700: oklch(55.4% .135 66.442);
+ --color-green-50: oklch(98.2% .018 155.826);
+ --color-green-600: oklch(62.7% .194 149.214);
+ --color-green-700: oklch(52.7% .154 150.069);
+ --color-blue-50: oklch(97% .014 254.604);
+ --color-blue-600: oklch(54.6% .245 262.881);
+ --color-blue-700: oklch(48.8% .243 264.376);
+ --color-gray-400: oklch(70.7% .022 261.325);
+ --color-gray-600: oklch(44.6% .03 256.802);
+ --color-gray-700: oklch(37.3% .034 259.733);
+ --color-black: #000;
+ --color-white: #fff;
+ --spacing: .25rem;
+ --container-xs: 20rem;
+ --text-xs: .75rem;
+ --text-xs--line-height: calc(1 / .75);
+ --text-sm: .875rem;
+ --text-sm--line-height: calc(1.25 / .875);
+ --text-lg: 1.125rem;
+ --text-lg--line-height: calc(1.75 / 1.125);
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ --radius-md: .375rem;
+ --ease-out: cubic-bezier(0, 0, .2, 1);
+ --animate-spin: spin 1s linear infinite;
+ --default-transition-duration: .15s;
+ --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1);
+ --default-font-family: var(--font-sans);
+ --default-mono-font-family: var(--font-mono);
+ }
+ }
+
+ @layer base {
+ *, :after, :before, ::backdrop {
+ box-sizing: border-box;
+ border: 0 solid;
+ margin: 0;
+ padding: 0;
+ }
+
+ ::file-selector-button {
+ box-sizing: border-box;
+ border: 0 solid;
+ margin: 0;
+ padding: 0;
+ }
+
+ html, :host {
+ -webkit-text-size-adjust: 100%;
+ tab-size: 4;
+ line-height: 1.5;
+ font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
+ font-feature-settings: var(--default-font-feature-settings, normal);
+ font-variation-settings: var(--default-font-variation-settings, normal);
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ hr {
+ height: 0;
+ color: inherit;
+ border-top-width: 1px;
+ }
+
+ abbr:where([title]) {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ font-size: inherit;
+ font-weight: inherit;
+ }
+
+ a {
+ color: inherit;
+ -webkit-text-decoration: inherit;
+ -webkit-text-decoration: inherit;
+ -webkit-text-decoration: inherit;
+ text-decoration: inherit;
+ }
+
+ b, strong {
+ font-weight: bolder;
+ }
+
+ code, kbd, samp, pre {
+ font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
+ font-feature-settings: var(--default-mono-font-feature-settings, normal);
+ font-variation-settings: var(--default-mono-font-variation-settings, normal);
+ font-size: 1em;
+ }
+
+ small {
+ font-size: 80%;
+ }
+
+ sub, sup {
+ vertical-align: baseline;
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ }
+
+ sub {
+ bottom: -.25em;
+ }
+
+ sup {
+ top: -.5em;
+ }
+
+ table {
+ text-indent: 0;
+ border-color: inherit;
+ border-collapse: collapse;
+ }
+
+ :-moz-focusring {
+ outline: auto;
+ }
+
+ progress {
+ vertical-align: baseline;
+ }
+
+ summary {
+ display: list-item;
+ }
+
+ ol, ul, menu {
+ list-style: none;
+ }
+
+ img, svg, video, canvas, audio, iframe, embed, object {
+ vertical-align: middle;
+ display: block;
+ }
+
+ img, video {
+ max-width: 100%;
+ height: auto;
+ }
+
+ button, input, select, optgroup, textarea {
+ font: inherit;
+ font-feature-settings: inherit;
+ font-variation-settings: inherit;
+ letter-spacing: inherit;
+ color: inherit;
+ opacity: 1;
+ background-color: #0000;
+ border-radius: 0;
+ }
+
+ ::file-selector-button {
+ font: inherit;
+ font-feature-settings: inherit;
+ font-variation-settings: inherit;
+ letter-spacing: inherit;
+ color: inherit;
+ opacity: 1;
+ background-color: #0000;
+ border-radius: 0;
+ }
+
+ :where(select:is([multiple], [size])) optgroup {
+ font-weight: bolder;
+ }
+
+ :where(select:is([multiple], [size])) optgroup option {
+ padding-inline-start: 20px;
+ }
+
+ ::file-selector-button {
+ margin-inline-end: 4px;
+ }
+
+ ::placeholder {
+ opacity: 1;
+ }
+
+ @supports (not ((-webkit-appearance: -apple-pay-button))) or (contain-intrinsic-size: 1px) {
+ ::placeholder {
+ color: currentColor;
+ }
+
+ @supports (color: color-mix(in lab, red, red)) {
+ ::placeholder {
+ color: color-mix(in oklab, currentcolor 50%, transparent);
+ }
+ }
+ }
+
+ textarea {
+ resize: vertical;
+ }
+
+ ::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+
+ ::-webkit-date-and-time-value {
+ min-height: 1lh;
+ text-align: inherit;
+ }
+
+ ::-webkit-datetime-edit {
+ display: inline-flex;
+ }
+
+ ::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+ }
+
+ ::-webkit-datetime-edit {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-year-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-month-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-day-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-hour-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-minute-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-second-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-millisecond-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-datetime-edit-meridiem-field {
+ padding-block: 0;
+ }
+
+ ::-webkit-calendar-picker-indicator {
+ line-height: 1;
+ }
+
+ :-moz-ui-invalid {
+ box-shadow: none;
+ }
+
+ button, input:where([type="button"], [type="reset"], [type="submit"]) {
+ appearance: button;
+ }
+
+ ::file-selector-button {
+ appearance: button;
+ }
+
+ ::-webkit-inner-spin-button {
+ height: auto;
+ }
+
+ ::-webkit-outer-spin-button {
+ height: auto;
+ }
+
+ [hidden]:where(:not([hidden="until-found"])) {
+ display: none !important;
+ }
+ }
+
+ @layer components;
+
+ @layer utilities {
+ .collapse {
+ visibility: collapse;
+ }
+
+ .invisible {
+ visibility: hidden;
+ }
+
+ .visible {
+ visibility: visible;
+ }
+
+ .absolute {
+ position: absolute;
+ }
+
+ .fixed {
+ position: fixed;
+ }
+
+ .relative {
+ position: relative;
+ }
+
+ .static {
+ position: static;
+ }
+
+ .isolate {
+ isolation: isolate;
+ }
+
+ .container {
+ width: 100%;
+ }
+
+ @media (min-width: 40rem) {
+ .container {
+ max-width: 40rem;
+ }
+ }
+
+ @media (min-width: 48rem) {
+ .container {
+ max-width: 48rem;
+ }
+ }
+
+ @media (min-width: 64rem) {
+ .container {
+ max-width: 64rem;
+ }
+ }
+
+ @media (min-width: 80rem) {
+ .container {
+ max-width: 80rem;
+ }
+ }
+
+ @media (min-width: 96rem) {
+ .container {
+ max-width: 96rem;
+ }
+ }
+
+ .mt-2 {
+ margin-top: calc(var(--spacing) * 2);
+ }
+
+ .mt-8 {
+ margin-top: calc(var(--spacing) * 8);
+ }
+
+ .mb-6 {
+ margin-bottom: calc(var(--spacing) * 6);
+ }
+
+ .block {
+ display: block;
+ }
+
+ .contents {
+ display: contents;
+ }
+
+ .flex {
+ display: flex;
+ }
+
+ .grid {
+ display: grid;
+ }
+
+ .hidden {
+ display: none;
+ }
+
+ .inline {
+ display: inline;
+ }
+
+ .inline-block {
+ display: inline-block;
+ }
+
+ .inline-flex {
+ display: inline-flex;
+ }
+
+ .table {
+ display: table;
+ }
+
+ .size-5 {
+ width: calc(var(--spacing) * 5);
+ height: calc(var(--spacing) * 5);
+ }
+
+ .h-full {
+ height: 100%;
+ }
+
+ .w-full {
+ width: 100%;
+ }
+
+ .flex-shrink-0 {
+ flex-shrink: 0;
+ }
+
+ .transform {
+ transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
+ }
+
+ .transform\! {
+ transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, ) !important;
+ }
+
+ .animate-spin {
+ animation: var(--animate-spin);
+ }
+
+ .resize {
+ resize: both;
+ }
+
+ .grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+ }
+
+ .grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .justify-center {
+ justify-content: center;
+ }
+
+ .gap-x-8 {
+ column-gap: calc(var(--spacing) * 8);
+ }
+
+ .gap-y-96 {
+ row-gap: calc(var(--spacing) * 96);
+ }
+
+ .overflow-auto {
+ overflow: auto;
+ }
+
+ .rounded {
+ border-radius: .25rem;
+ }
+
+ .border {
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ }
+
+ .border-gray-600 {
+ border-color: var(--color-gray-600);
+ }
+
+ .p-4 {
+ padding: calc(var(--spacing) * 4);
+ }
+
+ .px-6 {
+ padding-inline: calc(var(--spacing) * 6);
+ }
+
+ .py-3 {
+ padding-block: calc(var(--spacing) * 3);
+ }
+
+ .text-left {
+ text-align: left;
+ }
+
+ .text-lg {
+ font-size: var(--text-lg);
+ line-height: var(--tw-leading, var(--text-lg--line-height));
+ }
+
+ .text-sm {
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ }
+
+ .text-xs {
+ font-size: var(--text-xs);
+ line-height: var(--tw-leading, var(--text-xs--line-height));
+ }
+
+ .font-bold {
+ --tw-font-weight: var(--font-weight-bold);
+ font-weight: var(--font-weight-bold);
+ }
+
+ .whitespace-pre-line {
+ white-space: pre-line;
+ }
+
+ .text-gray-600 {
+ color: var(--color-gray-600);
+ }
+
+ .text-red-600 {
+ color: var(--color-red-600);
+ }
+
+ .capitalize {
+ text-transform: capitalize;
+ }
+
+ .lowercase {
+ text-transform: lowercase;
+ }
+
+ .uppercase {
+ text-transform: uppercase;
+ }
+
+ .italic {
+ font-style: italic;
+ }
+
+ .opacity-25 {
+ opacity: .25;
+ }
+
+ .shadow {
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a);
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+
+ .shadow-md {
+ --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, #0000001a), 0 2px 4px -2px var(--tw-shadow-color, #0000001a);
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+
+ .blur {
+ --tw-blur: blur(8px);
+ filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
+ }
+
+ .filter {
+ filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
+ }
+
+ .filter\! {
+ filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ) !important;
+ }
+
+ .transition {
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+
+ .\[wire\:model\=\"\'\+i\+\'\"\] {
+ wire: model= "'+i+'";
+ }
+
+ .\[wire\:model\=\\\"\'\+expression\+\'\\\"\] {
+ wire: model= \""+expression+"\";
+ }
+
+ .\[wire\:model\] {
+ wire: model;
+ }
+ }
+}
+
+.magewire-notifier {
+ right: calc(var(--spacing) * 3);
+ bottom: calc(var(--spacing) * 3);
+ left: calc(var(--spacing) * 3);
+ z-index: 50;
+ flex-direction: column;
+ display: flex;
+ position: fixed;
+}
+
+:where(.magewire-notifier > :not(:last-child)) {
+ --tw-space-y-reverse: 0;
+ margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
+ margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
+}
+
+@media (min-width: 40rem) {
+ .magewire-notifier {
+ right: auto;
+ bottom: calc(var(--spacing) * 4);
+ left: calc(var(--spacing) * 4);
+ width: 100%;
+ max-width: var(--container-xs);
+ }
+}
+
+.magewire-notifier__notification {
+ --tw-translate-y: calc(var(--spacing) * 0);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ border-radius: var(--radius-md);
+ border-left-style: var(--tw-border-style);
+ opacity: 1;
+ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, #0000001a), 0 4px 6px -4px var(--tw-shadow-color, #0000001a);
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ outline-style: var(--tw-outline-style);
+ border-left-width: 6px;
+ outline-width: 1px;
+ outline-color: #0000000d;
+}
+
+@supports (color: color-mix(in lab, red, red)) {
+ .magewire-notifier__notification {
+ outline-color: color-mix(in oklab, var(--color-black) 5%, transparent);
+ }
+}
+
+.magewire-notifier__notification {
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ --tw-duration: .3s;
+ --tw-ease: var(--ease-out);
+ transition-duration: .3s;
+ transition-timing-function: var(--ease-out);
+}
+
+@media (min-width: 40rem) {
+ .magewire-notifier__notification {
+ --tw-translate-x: calc(var(--spacing) * 0);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+}
+
+@starting-style {
+ .magewire-notifier__notification {
+ --tw-translate-y: calc(var(--spacing) * 2);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+}
+
+@starting-style {
+ .magewire-notifier__notification {
+ opacity: 0;
+ }
+}
+
+@media (min-width: 40rem) {
+ @starting-style {
+ .magewire-notifier__notification {
+ --tw-translate-x: calc(var(--spacing) * -2);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ }
+
+ @starting-style {
+ .magewire-notifier__notification {
+ --tw-translate-y: calc(var(--spacing) * 0);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ }
+}
+
+.magewire-notifier__notification {
+ background-color: var(--notifier-bg, var(--color-white));
+ border-left-color: var(--notifier-border, var(--color-gray-400));
+}
+
+.magewire-notifier__notification-content {
+ align-items: flex-start;
+ gap: calc(var(--spacing) * 3);
+ padding: calc(var(--spacing) * 4);
+ display: flex;
+}
+
+.magewire-notifier__notification-before {
+ flex-shrink: 0;
+ align-items: center;
+ display: flex;
+}
+
+.magewire-notifier__notification-body {
+ min-width: calc(var(--spacing) * 0);
+ flex: 1;
+}
+
+.magewire-notifier__notification-title {
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ --tw-leading: calc(var(--spacing) * 5);
+ line-height: calc(var(--spacing) * 5);
+ --tw-font-weight: var(--font-weight-semibold);
+ font-weight: var(--font-weight-semibold);
+ color: var(--notifier-title, var(--color-gray-700));
+}
+
+.magewire-notifier__notification-message {
+ margin-top: calc(var(--spacing) * .5);
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ color: var(--color-gray-600);
+}
+
+.magewire-notifier__notification-after {
+ align-items: center;
+ gap: calc(var(--spacing) * 2);
+ flex-shrink: 0;
+ display: flex;
+}
+
+.magewire-notifier__notification--success {
+ --notifier-bg: var(--color-green-50);
+ --notifier-border: var(--color-green-600);
+ --notifier-title: var(--color-green-700);
+}
+
+.magewire-notifier__notification--error {
+ --notifier-bg: var(--color-red-50);
+ --notifier-border: var(--color-red-600);
+ --notifier-title: var(--color-red-700);
+}
+
+.magewire-notifier__notification--warning {
+ --notifier-bg: var(--color-yellow-50);
+ --notifier-border: var(--color-yellow-600);
+ --notifier-title: var(--color-yellow-700);
+}
+
+.magewire-notifier__notification--info {
+ --notifier-bg: var(--color-blue-50);
+ --notifier-border: var(--color-blue-600);
+ --notifier-title: var(--color-blue-700);
+}
+
+@property --tw-rotate-x {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-rotate-y {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-rotate-z {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-skew-x {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-skew-y {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-border-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
+
+@property --tw-font-weight {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+
+@property --tw-shadow-color {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
+
+@property --tw-inset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+
+@property --tw-inset-shadow-color {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-inset-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
+
+@property --tw-ring-color {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+
+@property --tw-inset-ring-color {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-inset-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+
+@property --tw-ring-inset {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-ring-offset-width {
+ syntax: "";
+ inherits: false;
+ initial-value: 0;
+}
+
+@property --tw-ring-offset-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: #fff;
+}
+
+@property --tw-ring-offset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+
+@property --tw-blur {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-brightness {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-contrast {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-grayscale {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-hue-rotate {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-invert {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-opacity {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-saturate {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-sepia {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-drop-shadow {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-drop-shadow-color {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-drop-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
+
+@property --tw-drop-shadow-size {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-space-y-reverse {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+
+@property --tw-translate-x {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+
+@property --tw-translate-y {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+
+@property --tw-translate-z {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+
+@property --tw-outline-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
+
+@property --tw-duration {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-ease {
+ syntax: "*";
+ inherits: false
+}
+
+@property --tw-leading {
+ syntax: "*";
+ inherits: false
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/view/base/web/js/magewire.csp.esm.js b/src/view/base/web/js/magewire.csp.esm.js
new file mode 100644
index 00000000..cd309988
--- /dev/null
+++ b/src/view/base/web/js/magewire.csp.esm.js
@@ -0,0 +1,11181 @@
+var __create = Object.create;
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getProtoOf = Object.getPrototypeOf;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __commonJS = (cb, mod) => function __require() {
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+var __copyProps = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames(from))
+ if (!__hasOwnProp.call(to, key) && key !== except)
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+ }
+ return to;
+};
+var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod));
+
+// ../alpine/packages/csp/dist/module.cjs.js
+var require_module_cjs = __commonJS({
+ "../alpine/packages/csp/dist/module.cjs.js"(exports, module) {
+ var __create2 = Object.create;
+ var __defProp2 = Object.defineProperty;
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
+ var __getProtoOf2 = Object.getPrototypeOf;
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
+ var __commonJS2 = (cb, mod) => function __require() {
+ return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+ };
+ var __export = (target, all2) => {
+ for (var name in all2)
+ __defProp2(target, name, { get: all2[name], enumerable: true });
+ };
+ var __copyProps2 = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames2(from))
+ if (!__hasOwnProp2.call(to, key) && key !== except)
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
+ }
+ return to;
+ };
+ var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__getProtoOf2(mod)) : {}, __copyProps2(isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target, mod));
+ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
+ var require_shared_cjs = __commonJS2({
+ "node_modules/@vue/shared/dist/shared.cjs.js"(exports2) {
+ "use strict";
+ Object.defineProperty(exports2, "__esModule", { value: true });
+ function makeMap(str, expectsLowerCase) {
+ const map = /* @__PURE__ */ Object.create(null);
+ const list = str.split(",");
+ for (let i = 0; i < list.length; i++) {
+ map[list[i]] = true;
+ }
+ return expectsLowerCase ? (val) => !!map[val.toLowerCase()] : (val) => !!map[val];
+ }
+ var PatchFlagNames = {
+ [1]: `TEXT`,
+ [2]: `CLASS`,
+ [4]: `STYLE`,
+ [8]: `PROPS`,
+ [16]: `FULL_PROPS`,
+ [32]: `HYDRATE_EVENTS`,
+ [64]: `STABLE_FRAGMENT`,
+ [128]: `KEYED_FRAGMENT`,
+ [256]: `UNKEYED_FRAGMENT`,
+ [512]: `NEED_PATCH`,
+ [1024]: `DYNAMIC_SLOTS`,
+ [2048]: `DEV_ROOT_FRAGMENT`,
+ [-1]: `HOISTED`,
+ [-2]: `BAIL`
+ };
+ var slotFlagsText = {
+ [1]: "STABLE",
+ [2]: "DYNAMIC",
+ [3]: "FORWARDED"
+ };
+ var GLOBALS_WHITE_LISTED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt";
+ var isGloballyWhitelisted = /* @__PURE__ */ makeMap(GLOBALS_WHITE_LISTED);
+ var range = 2;
+ function generateCodeFrame(source, start22 = 0, end = source.length) {
+ let lines = source.split(/(\r?\n)/);
+ const newlineSequences = lines.filter((_, idx) => idx % 2 === 1);
+ lines = lines.filter((_, idx) => idx % 2 === 0);
+ let count = 0;
+ const res = [];
+ for (let i = 0; i < lines.length; i++) {
+ count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0);
+ if (count >= start22) {
+ for (let j = i - range; j <= i + range || end > count; j++) {
+ if (j < 0 || j >= lines.length)
+ continue;
+ const line = j + 1;
+ res.push(`${line}${" ".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`);
+ const lineLength = lines[j].length;
+ const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0;
+ if (j === i) {
+ const pad = start22 - (count - (lineLength + newLineSeqLength));
+ const length = Math.max(1, end > count ? lineLength - pad : end - start22);
+ res.push(` | ` + " ".repeat(pad) + "^".repeat(length));
+ } else if (j > i) {
+ if (end > count) {
+ const length = Math.max(Math.min(end - count, lineLength), 1);
+ res.push(` | ` + "^".repeat(length));
+ }
+ count += lineLength + newLineSeqLength;
+ }
+ }
+ break;
+ }
+ }
+ return res.join("\n");
+ }
+ var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;
+ var isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs);
+ var isBooleanAttr2 = /* @__PURE__ */ makeMap(specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected`);
+ var unsafeAttrCharRE = /[>/="'\u0009\u000a\u000c\u0020]/;
+ var attrValidationCache = {};
+ function isSSRSafeAttrName(name) {
+ if (attrValidationCache.hasOwnProperty(name)) {
+ return attrValidationCache[name];
+ }
+ const isUnsafe = unsafeAttrCharRE.test(name);
+ if (isUnsafe) {
+ console.error(`unsafe attribute name: ${name}`);
+ }
+ return attrValidationCache[name] = !isUnsafe;
+ }
+ var propsToAttrMap = {
+ acceptCharset: "accept-charset",
+ className: "class",
+ htmlFor: "for",
+ httpEquiv: "http-equiv"
+ };
+ var isNoUnitNumericStyleProp = /* @__PURE__ */ makeMap(`animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width`);
+ var isKnownAttr = /* @__PURE__ */ makeMap(`accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap`);
+ function normalizeStyle(value) {
+ if (isArray2(value)) {
+ const res = {};
+ for (let i = 0; i < value.length; i++) {
+ const item = value[i];
+ const normalized = normalizeStyle(isString(item) ? parseStringStyle(item) : item);
+ if (normalized) {
+ for (const key in normalized) {
+ res[key] = normalized[key];
+ }
+ }
+ }
+ return res;
+ } else if (isObject2(value)) {
+ return value;
+ }
+ }
+ var listDelimiterRE = /;(?![^(]*\))/g;
+ var propertyDelimiterRE = /:(.+)/;
+ function parseStringStyle(cssText) {
+ const ret = {};
+ cssText.split(listDelimiterRE).forEach((item) => {
+ if (item) {
+ const tmp = item.split(propertyDelimiterRE);
+ tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());
+ }
+ });
+ return ret;
+ }
+ function stringifyStyle(styles) {
+ let ret = "";
+ if (!styles) {
+ return ret;
+ }
+ for (const key in styles) {
+ const value = styles[key];
+ const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key);
+ if (isString(value) || typeof value === "number" && isNoUnitNumericStyleProp(normalizedKey)) {
+ ret += `${normalizedKey}:${value};`;
+ }
+ }
+ return ret;
+ }
+ function normalizeClass(value) {
+ let res = "";
+ if (isString(value)) {
+ res = value;
+ } else if (isArray2(value)) {
+ for (let i = 0; i < value.length; i++) {
+ const normalized = normalizeClass(value[i]);
+ if (normalized) {
+ res += normalized + " ";
+ }
+ }
+ } else if (isObject2(value)) {
+ for (const name in value) {
+ if (value[name]) {
+ res += name + " ";
+ }
+ }
+ }
+ return res.trim();
+ }
+ var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot";
+ var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view";
+ var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr";
+ var isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS);
+ var isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS);
+ var isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS);
+ var escapeRE = /["'&<>]/;
+ function escapeHtml(string) {
+ const str = "" + string;
+ const match = escapeRE.exec(str);
+ if (!match) {
+ return str;
+ }
+ let html = "";
+ let escaped;
+ let index;
+ let lastIndex = 0;
+ for (index = match.index; index < str.length; index++) {
+ switch (str.charCodeAt(index)) {
+ case 34:
+ escaped = """;
+ break;
+ case 38:
+ escaped = "&";
+ break;
+ case 39:
+ escaped = "'";
+ break;
+ case 60:
+ escaped = "<";
+ break;
+ case 62:
+ escaped = ">";
+ break;
+ default:
+ continue;
+ }
+ if (lastIndex !== index) {
+ html += str.substring(lastIndex, index);
+ }
+ lastIndex = index + 1;
+ html += escaped;
+ }
+ return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
+ }
+ var commentStripRE = /^-?>||--!>| looseEqual(item, val));
+ }
+ var toDisplayString = (val) => {
+ return val == null ? "" : isObject2(val) ? JSON.stringify(val, replacer, 2) : String(val);
+ };
+ var replacer = (_key, val) => {
+ if (isMap(val)) {
+ return {
+ [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val2]) => {
+ entries[`${key} =>`] = val2;
+ return entries;
+ }, {})
+ };
+ } else if (isSet(val)) {
+ return {
+ [`Set(${val.size})`]: [...val.values()]
+ };
+ } else if (isObject2(val) && !isArray2(val) && !isPlainObject(val)) {
+ return String(val);
+ }
+ return val;
+ };
+ var babelParserDefaultPlugins = [
+ "bigInt",
+ "optionalChaining",
+ "nullishCoalescingOperator"
+ ];
+ var EMPTY_OBJ = Object.freeze({});
+ var EMPTY_ARR = Object.freeze([]);
+ var NOOP = () => {
+ };
+ var NO = () => false;
+ var onRE = /^on[^a-z]/;
+ var isOn = (key) => onRE.test(key);
+ var isModelListener = (key) => key.startsWith("onUpdate:");
+ var extend = Object.assign;
+ var remove = (arr, el) => {
+ const i = arr.indexOf(el);
+ if (i > -1) {
+ arr.splice(i, 1);
+ }
+ };
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ var hasOwn = (val, key) => hasOwnProperty.call(val, key);
+ var isArray2 = Array.isArray;
+ var isMap = (val) => toTypeString(val) === "[object Map]";
+ var isSet = (val) => toTypeString(val) === "[object Set]";
+ var isDate = (val) => val instanceof Date;
+ var isFunction2 = (val) => typeof val === "function";
+ var isString = (val) => typeof val === "string";
+ var isSymbol = (val) => typeof val === "symbol";
+ var isObject2 = (val) => val !== null && typeof val === "object";
+ var isPromise = (val) => {
+ return isObject2(val) && isFunction2(val.then) && isFunction2(val.catch);
+ };
+ var objectToString = Object.prototype.toString;
+ var toTypeString = (value) => objectToString.call(value);
+ var toRawType = (value) => {
+ return toTypeString(value).slice(8, -1);
+ };
+ var isPlainObject = (val) => toTypeString(val) === "[object Object]";
+ var isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key;
+ var isReservedProp = /* @__PURE__ */ makeMap(",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");
+ var cacheStringFunction = (fn) => {
+ const cache = /* @__PURE__ */ Object.create(null);
+ return (str) => {
+ const hit = cache[str];
+ return hit || (cache[str] = fn(str));
+ };
+ };
+ var camelizeRE = /-(\w)/g;
+ var camelize = cacheStringFunction((str) => {
+ return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : "");
+ });
+ var hyphenateRE = /\B([A-Z])/g;
+ var hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, "-$1").toLowerCase());
+ var capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));
+ var toHandlerKey = cacheStringFunction((str) => str ? `on${capitalize(str)}` : ``);
+ var hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);
+ var invokeArrayFns = (fns, arg) => {
+ for (let i = 0; i < fns.length; i++) {
+ fns[i](arg);
+ }
+ };
+ var def = (obj, key, value) => {
+ Object.defineProperty(obj, key, {
+ configurable: true,
+ enumerable: false,
+ value
+ });
+ };
+ var toNumber = (val) => {
+ const n = parseFloat(val);
+ return isNaN(n) ? val : n;
+ };
+ var _globalThis;
+ var getGlobalThis = () => {
+ return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {});
+ };
+ exports2.EMPTY_ARR = EMPTY_ARR;
+ exports2.EMPTY_OBJ = EMPTY_OBJ;
+ exports2.NO = NO;
+ exports2.NOOP = NOOP;
+ exports2.PatchFlagNames = PatchFlagNames;
+ exports2.babelParserDefaultPlugins = babelParserDefaultPlugins;
+ exports2.camelize = camelize;
+ exports2.capitalize = capitalize;
+ exports2.def = def;
+ exports2.escapeHtml = escapeHtml;
+ exports2.escapeHtmlComment = escapeHtmlComment;
+ exports2.extend = extend;
+ exports2.generateCodeFrame = generateCodeFrame;
+ exports2.getGlobalThis = getGlobalThis;
+ exports2.hasChanged = hasChanged;
+ exports2.hasOwn = hasOwn;
+ exports2.hyphenate = hyphenate;
+ exports2.invokeArrayFns = invokeArrayFns;
+ exports2.isArray = isArray2;
+ exports2.isBooleanAttr = isBooleanAttr2;
+ exports2.isDate = isDate;
+ exports2.isFunction = isFunction2;
+ exports2.isGloballyWhitelisted = isGloballyWhitelisted;
+ exports2.isHTMLTag = isHTMLTag;
+ exports2.isIntegerKey = isIntegerKey;
+ exports2.isKnownAttr = isKnownAttr;
+ exports2.isMap = isMap;
+ exports2.isModelListener = isModelListener;
+ exports2.isNoUnitNumericStyleProp = isNoUnitNumericStyleProp;
+ exports2.isObject = isObject2;
+ exports2.isOn = isOn;
+ exports2.isPlainObject = isPlainObject;
+ exports2.isPromise = isPromise;
+ exports2.isReservedProp = isReservedProp;
+ exports2.isSSRSafeAttrName = isSSRSafeAttrName;
+ exports2.isSVGTag = isSVGTag;
+ exports2.isSet = isSet;
+ exports2.isSpecialBooleanAttr = isSpecialBooleanAttr;
+ exports2.isString = isString;
+ exports2.isSymbol = isSymbol;
+ exports2.isVoidTag = isVoidTag;
+ exports2.looseEqual = looseEqual;
+ exports2.looseIndexOf = looseIndexOf;
+ exports2.makeMap = makeMap;
+ exports2.normalizeClass = normalizeClass;
+ exports2.normalizeStyle = normalizeStyle;
+ exports2.objectToString = objectToString;
+ exports2.parseStringStyle = parseStringStyle;
+ exports2.propsToAttrMap = propsToAttrMap;
+ exports2.remove = remove;
+ exports2.slotFlagsText = slotFlagsText;
+ exports2.stringifyStyle = stringifyStyle;
+ exports2.toDisplayString = toDisplayString;
+ exports2.toHandlerKey = toHandlerKey;
+ exports2.toNumber = toNumber;
+ exports2.toRawType = toRawType;
+ exports2.toTypeString = toTypeString;
+ }
+ });
+ var require_shared = __commonJS2({
+ "node_modules/@vue/shared/index.js"(exports2, module2) {
+ "use strict";
+ if (false) {
+ module2.exports = null;
+ } else {
+ module2.exports = require_shared_cjs();
+ }
+ }
+ });
+ var require_reactivity_cjs = __commonJS2({
+ "node_modules/@vue/reactivity/dist/reactivity.cjs.js"(exports2) {
+ "use strict";
+ Object.defineProperty(exports2, "__esModule", { value: true });
+ var shared = require_shared();
+ var targetMap = /* @__PURE__ */ new WeakMap();
+ var effectStack = [];
+ var activeEffect;
+ var ITERATE_KEY = Symbol("iterate");
+ var MAP_KEY_ITERATE_KEY = Symbol("Map key iterate");
+ function isEffect(fn) {
+ return fn && fn._isEffect === true;
+ }
+ function effect3(fn, options = shared.EMPTY_OBJ) {
+ if (isEffect(fn)) {
+ fn = fn.raw;
+ }
+ const effect4 = createReactiveEffect(fn, options);
+ if (!options.lazy) {
+ effect4();
+ }
+ return effect4;
+ }
+ function stop2(effect4) {
+ if (effect4.active) {
+ cleanup(effect4);
+ if (effect4.options.onStop) {
+ effect4.options.onStop();
+ }
+ effect4.active = false;
+ }
+ }
+ var uid = 0;
+ function createReactiveEffect(fn, options) {
+ const effect4 = function reactiveEffect() {
+ if (!effect4.active) {
+ return fn();
+ }
+ if (!effectStack.includes(effect4)) {
+ cleanup(effect4);
+ try {
+ enableTracking();
+ effectStack.push(effect4);
+ activeEffect = effect4;
+ return fn();
+ } finally {
+ effectStack.pop();
+ resetTracking();
+ activeEffect = effectStack[effectStack.length - 1];
+ }
+ }
+ };
+ effect4.id = uid++;
+ effect4.allowRecurse = !!options.allowRecurse;
+ effect4._isEffect = true;
+ effect4.active = true;
+ effect4.raw = fn;
+ effect4.deps = [];
+ effect4.options = options;
+ return effect4;
+ }
+ function cleanup(effect4) {
+ const { deps } = effect4;
+ if (deps.length) {
+ for (let i = 0; i < deps.length; i++) {
+ deps[i].delete(effect4);
+ }
+ deps.length = 0;
+ }
+ }
+ var shouldTrack = true;
+ var trackStack = [];
+ function pauseTracking() {
+ trackStack.push(shouldTrack);
+ shouldTrack = false;
+ }
+ function enableTracking() {
+ trackStack.push(shouldTrack);
+ shouldTrack = true;
+ }
+ function resetTracking() {
+ const last = trackStack.pop();
+ shouldTrack = last === void 0 ? true : last;
+ }
+ function track2(target, type, key) {
+ if (!shouldTrack || activeEffect === void 0) {
+ return;
+ }
+ let depsMap = targetMap.get(target);
+ if (!depsMap) {
+ targetMap.set(target, depsMap = /* @__PURE__ */ new Map());
+ }
+ let dep = depsMap.get(key);
+ if (!dep) {
+ depsMap.set(key, dep = /* @__PURE__ */ new Set());
+ }
+ if (!dep.has(activeEffect)) {
+ dep.add(activeEffect);
+ activeEffect.deps.push(dep);
+ if (activeEffect.options.onTrack) {
+ activeEffect.options.onTrack({
+ effect: activeEffect,
+ target,
+ type,
+ key
+ });
+ }
+ }
+ }
+ function trigger2(target, type, key, newValue, oldValue, oldTarget) {
+ const depsMap = targetMap.get(target);
+ if (!depsMap) {
+ return;
+ }
+ const effects = /* @__PURE__ */ new Set();
+ const add2 = (effectsToAdd) => {
+ if (effectsToAdd) {
+ effectsToAdd.forEach((effect4) => {
+ if (effect4 !== activeEffect || effect4.allowRecurse) {
+ effects.add(effect4);
+ }
+ });
+ }
+ };
+ if (type === "clear") {
+ depsMap.forEach(add2);
+ } else if (key === "length" && shared.isArray(target)) {
+ depsMap.forEach((dep, key2) => {
+ if (key2 === "length" || key2 >= newValue) {
+ add2(dep);
+ }
+ });
+ } else {
+ if (key !== void 0) {
+ add2(depsMap.get(key));
+ }
+ switch (type) {
+ case "add":
+ if (!shared.isArray(target)) {
+ add2(depsMap.get(ITERATE_KEY));
+ if (shared.isMap(target)) {
+ add2(depsMap.get(MAP_KEY_ITERATE_KEY));
+ }
+ } else if (shared.isIntegerKey(key)) {
+ add2(depsMap.get("length"));
+ }
+ break;
+ case "delete":
+ if (!shared.isArray(target)) {
+ add2(depsMap.get(ITERATE_KEY));
+ if (shared.isMap(target)) {
+ add2(depsMap.get(MAP_KEY_ITERATE_KEY));
+ }
+ }
+ break;
+ case "set":
+ if (shared.isMap(target)) {
+ add2(depsMap.get(ITERATE_KEY));
+ }
+ break;
+ }
+ }
+ const run = (effect4) => {
+ if (effect4.options.onTrigger) {
+ effect4.options.onTrigger({
+ effect: effect4,
+ target,
+ key,
+ type,
+ newValue,
+ oldValue,
+ oldTarget
+ });
+ }
+ if (effect4.options.scheduler) {
+ effect4.options.scheduler(effect4);
+ } else {
+ effect4();
+ }
+ };
+ effects.forEach(run);
+ }
+ var isNonTrackableKeys = /* @__PURE__ */ shared.makeMap(`__proto__,__v_isRef,__isVue`);
+ var builtInSymbols = new Set(Object.getOwnPropertyNames(Symbol).map((key) => Symbol[key]).filter(shared.isSymbol));
+ var get2 = /* @__PURE__ */ createGetter();
+ var shallowGet = /* @__PURE__ */ createGetter(false, true);
+ var readonlyGet = /* @__PURE__ */ createGetter(true);
+ var shallowReadonlyGet = /* @__PURE__ */ createGetter(true, true);
+ var arrayInstrumentations = /* @__PURE__ */ createArrayInstrumentations();
+ function createArrayInstrumentations() {
+ const instrumentations = {};
+ ["includes", "indexOf", "lastIndexOf"].forEach((key) => {
+ instrumentations[key] = function(...args) {
+ const arr = toRaw2(this);
+ for (let i = 0, l = this.length; i < l; i++) {
+ track2(arr, "get", i + "");
+ }
+ const res = arr[key](...args);
+ if (res === -1 || res === false) {
+ return arr[key](...args.map(toRaw2));
+ } else {
+ return res;
+ }
+ };
+ });
+ ["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
+ instrumentations[key] = function(...args) {
+ pauseTracking();
+ const res = toRaw2(this)[key].apply(this, args);
+ resetTracking();
+ return res;
+ };
+ });
+ return instrumentations;
+ }
+ function createGetter(isReadonly2 = false, shallow = false) {
+ return function get3(target, key, receiver) {
+ if (key === "__v_isReactive") {
+ return !isReadonly2;
+ } else if (key === "__v_isReadonly") {
+ return isReadonly2;
+ } else if (key === "__v_raw" && receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) {
+ return target;
+ }
+ const targetIsArray = shared.isArray(target);
+ if (!isReadonly2 && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) {
+ return Reflect.get(arrayInstrumentations, key, receiver);
+ }
+ const res = Reflect.get(target, key, receiver);
+ if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
+ return res;
+ }
+ if (!isReadonly2) {
+ track2(target, "get", key);
+ }
+ if (shallow) {
+ return res;
+ }
+ if (isRef(res)) {
+ const shouldUnwrap = !targetIsArray || !shared.isIntegerKey(key);
+ return shouldUnwrap ? res.value : res;
+ }
+ if (shared.isObject(res)) {
+ return isReadonly2 ? readonly(res) : reactive3(res);
+ }
+ return res;
+ };
+ }
+ var set2 = /* @__PURE__ */ createSetter();
+ var shallowSet = /* @__PURE__ */ createSetter(true);
+ function createSetter(shallow = false) {
+ return function set3(target, key, value, receiver) {
+ let oldValue = target[key];
+ if (!shallow) {
+ value = toRaw2(value);
+ oldValue = toRaw2(oldValue);
+ if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) {
+ oldValue.value = value;
+ return true;
+ }
+ }
+ const hadKey = shared.isArray(target) && shared.isIntegerKey(key) ? Number(key) < target.length : shared.hasOwn(target, key);
+ const result = Reflect.set(target, key, value, receiver);
+ if (target === toRaw2(receiver)) {
+ if (!hadKey) {
+ trigger2(target, "add", key, value);
+ } else if (shared.hasChanged(value, oldValue)) {
+ trigger2(target, "set", key, value, oldValue);
+ }
+ }
+ return result;
+ };
+ }
+ function deleteProperty(target, key) {
+ const hadKey = shared.hasOwn(target, key);
+ const oldValue = target[key];
+ const result = Reflect.deleteProperty(target, key);
+ if (result && hadKey) {
+ trigger2(target, "delete", key, void 0, oldValue);
+ }
+ return result;
+ }
+ function has(target, key) {
+ const result = Reflect.has(target, key);
+ if (!shared.isSymbol(key) || !builtInSymbols.has(key)) {
+ track2(target, "has", key);
+ }
+ return result;
+ }
+ function ownKeys(target) {
+ track2(target, "iterate", shared.isArray(target) ? "length" : ITERATE_KEY);
+ return Reflect.ownKeys(target);
+ }
+ var mutableHandlers = {
+ get: get2,
+ set: set2,
+ deleteProperty,
+ has,
+ ownKeys
+ };
+ var readonlyHandlers = {
+ get: readonlyGet,
+ set(target, key) {
+ {
+ console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
+ }
+ return true;
+ },
+ deleteProperty(target, key) {
+ {
+ console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target);
+ }
+ return true;
+ }
+ };
+ var shallowReactiveHandlers = /* @__PURE__ */ shared.extend({}, mutableHandlers, {
+ get: shallowGet,
+ set: shallowSet
+ });
+ var shallowReadonlyHandlers = /* @__PURE__ */ shared.extend({}, readonlyHandlers, {
+ get: shallowReadonlyGet
+ });
+ var toReactive = (value) => shared.isObject(value) ? reactive3(value) : value;
+ var toReadonly = (value) => shared.isObject(value) ? readonly(value) : value;
+ var toShallow = (value) => value;
+ var getProto = (v) => Reflect.getPrototypeOf(v);
+ function get$1(target, key, isReadonly2 = false, isShallow = false) {
+ target = target["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const rawKey = toRaw2(key);
+ if (key !== rawKey) {
+ !isReadonly2 && track2(rawTarget, "get", key);
+ }
+ !isReadonly2 && track2(rawTarget, "get", rawKey);
+ const { has: has2 } = getProto(rawTarget);
+ const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;
+ if (has2.call(rawTarget, key)) {
+ return wrap(target.get(key));
+ } else if (has2.call(rawTarget, rawKey)) {
+ return wrap(target.get(rawKey));
+ } else if (target !== rawTarget) {
+ target.get(key);
+ }
+ }
+ function has$1(key, isReadonly2 = false) {
+ const target = this["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const rawKey = toRaw2(key);
+ if (key !== rawKey) {
+ !isReadonly2 && track2(rawTarget, "has", key);
+ }
+ !isReadonly2 && track2(rawTarget, "has", rawKey);
+ return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey);
+ }
+ function size(target, isReadonly2 = false) {
+ target = target["__v_raw"];
+ !isReadonly2 && track2(toRaw2(target), "iterate", ITERATE_KEY);
+ return Reflect.get(target, "size", target);
+ }
+ function add(value) {
+ value = toRaw2(value);
+ const target = toRaw2(this);
+ const proto = getProto(target);
+ const hadKey = proto.has.call(target, value);
+ if (!hadKey) {
+ target.add(value);
+ trigger2(target, "add", value, value);
+ }
+ return this;
+ }
+ function set$1(key, value) {
+ value = toRaw2(value);
+ const target = toRaw2(this);
+ const { has: has2, get: get3 } = getProto(target);
+ let hadKey = has2.call(target, key);
+ if (!hadKey) {
+ key = toRaw2(key);
+ hadKey = has2.call(target, key);
+ } else {
+ checkIdentityKeys(target, has2, key);
+ }
+ const oldValue = get3.call(target, key);
+ target.set(key, value);
+ if (!hadKey) {
+ trigger2(target, "add", key, value);
+ } else if (shared.hasChanged(value, oldValue)) {
+ trigger2(target, "set", key, value, oldValue);
+ }
+ return this;
+ }
+ function deleteEntry(key) {
+ const target = toRaw2(this);
+ const { has: has2, get: get3 } = getProto(target);
+ let hadKey = has2.call(target, key);
+ if (!hadKey) {
+ key = toRaw2(key);
+ hadKey = has2.call(target, key);
+ } else {
+ checkIdentityKeys(target, has2, key);
+ }
+ const oldValue = get3 ? get3.call(target, key) : void 0;
+ const result = target.delete(key);
+ if (hadKey) {
+ trigger2(target, "delete", key, void 0, oldValue);
+ }
+ return result;
+ }
+ function clear() {
+ const target = toRaw2(this);
+ const hadItems = target.size !== 0;
+ const oldTarget = shared.isMap(target) ? new Map(target) : new Set(target);
+ const result = target.clear();
+ if (hadItems) {
+ trigger2(target, "clear", void 0, void 0, oldTarget);
+ }
+ return result;
+ }
+ function createForEach(isReadonly2, isShallow) {
+ return function forEach(callback, thisArg) {
+ const observed = this;
+ const target = observed["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;
+ !isReadonly2 && track2(rawTarget, "iterate", ITERATE_KEY);
+ return target.forEach((value, key) => {
+ return callback.call(thisArg, wrap(value), wrap(key), observed);
+ });
+ };
+ }
+ function createIterableMethod(method, isReadonly2, isShallow) {
+ return function(...args) {
+ const target = this["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const targetIsMap = shared.isMap(rawTarget);
+ const isPair = method === "entries" || method === Symbol.iterator && targetIsMap;
+ const isKeyOnly = method === "keys" && targetIsMap;
+ const innerIterator = target[method](...args);
+ const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;
+ !isReadonly2 && track2(rawTarget, "iterate", isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY);
+ return {
+ next() {
+ const { value, done } = innerIterator.next();
+ return done ? { value, done } : {
+ value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
+ done
+ };
+ },
+ [Symbol.iterator]() {
+ return this;
+ }
+ };
+ };
+ }
+ function createReadonlyMethod(type) {
+ return function(...args) {
+ {
+ const key = args[0] ? `on key "${args[0]}" ` : ``;
+ console.warn(`${shared.capitalize(type)} operation ${key}failed: target is readonly.`, toRaw2(this));
+ }
+ return type === "delete" ? false : this;
+ };
+ }
+ function createInstrumentations() {
+ const mutableInstrumentations2 = {
+ get(key) {
+ return get$1(this, key);
+ },
+ get size() {
+ return size(this);
+ },
+ has: has$1,
+ add,
+ set: set$1,
+ delete: deleteEntry,
+ clear,
+ forEach: createForEach(false, false)
+ };
+ const shallowInstrumentations2 = {
+ get(key) {
+ return get$1(this, key, false, true);
+ },
+ get size() {
+ return size(this);
+ },
+ has: has$1,
+ add,
+ set: set$1,
+ delete: deleteEntry,
+ clear,
+ forEach: createForEach(false, true)
+ };
+ const readonlyInstrumentations2 = {
+ get(key) {
+ return get$1(this, key, true);
+ },
+ get size() {
+ return size(this, true);
+ },
+ has(key) {
+ return has$1.call(this, key, true);
+ },
+ add: createReadonlyMethod("add"),
+ set: createReadonlyMethod("set"),
+ delete: createReadonlyMethod("delete"),
+ clear: createReadonlyMethod("clear"),
+ forEach: createForEach(true, false)
+ };
+ const shallowReadonlyInstrumentations2 = {
+ get(key) {
+ return get$1(this, key, true, true);
+ },
+ get size() {
+ return size(this, true);
+ },
+ has(key) {
+ return has$1.call(this, key, true);
+ },
+ add: createReadonlyMethod("add"),
+ set: createReadonlyMethod("set"),
+ delete: createReadonlyMethod("delete"),
+ clear: createReadonlyMethod("clear"),
+ forEach: createForEach(true, true)
+ };
+ const iteratorMethods = ["keys", "values", "entries", Symbol.iterator];
+ iteratorMethods.forEach((method) => {
+ mutableInstrumentations2[method] = createIterableMethod(method, false, false);
+ readonlyInstrumentations2[method] = createIterableMethod(method, true, false);
+ shallowInstrumentations2[method] = createIterableMethod(method, false, true);
+ shallowReadonlyInstrumentations2[method] = createIterableMethod(method, true, true);
+ });
+ return [
+ mutableInstrumentations2,
+ readonlyInstrumentations2,
+ shallowInstrumentations2,
+ shallowReadonlyInstrumentations2
+ ];
+ }
+ var [mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations] = /* @__PURE__ */ createInstrumentations();
+ function createInstrumentationGetter(isReadonly2, shallow) {
+ const instrumentations = shallow ? isReadonly2 ? shallowReadonlyInstrumentations : shallowInstrumentations : isReadonly2 ? readonlyInstrumentations : mutableInstrumentations;
+ return (target, key, receiver) => {
+ if (key === "__v_isReactive") {
+ return !isReadonly2;
+ } else if (key === "__v_isReadonly") {
+ return isReadonly2;
+ } else if (key === "__v_raw") {
+ return target;
+ }
+ return Reflect.get(shared.hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver);
+ };
+ }
+ var mutableCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(false, false)
+ };
+ var shallowCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(false, true)
+ };
+ var readonlyCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(true, false)
+ };
+ var shallowReadonlyCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(true, true)
+ };
+ function checkIdentityKeys(target, has2, key) {
+ const rawKey = toRaw2(key);
+ if (rawKey !== key && has2.call(target, rawKey)) {
+ const type = shared.toRawType(target);
+ console.warn(`Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`);
+ }
+ }
+ var reactiveMap = /* @__PURE__ */ new WeakMap();
+ var shallowReactiveMap = /* @__PURE__ */ new WeakMap();
+ var readonlyMap = /* @__PURE__ */ new WeakMap();
+ var shallowReadonlyMap = /* @__PURE__ */ new WeakMap();
+ function targetTypeMap(rawType) {
+ switch (rawType) {
+ case "Object":
+ case "Array":
+ return 1;
+ case "Map":
+ case "Set":
+ case "WeakMap":
+ case "WeakSet":
+ return 2;
+ default:
+ return 0;
+ }
+ }
+ function getTargetType(value) {
+ return value["__v_skip"] || !Object.isExtensible(value) ? 0 : targetTypeMap(shared.toRawType(value));
+ }
+ function reactive3(target) {
+ if (target && target["__v_isReadonly"]) {
+ return target;
+ }
+ return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
+ }
+ function shallowReactive(target) {
+ return createReactiveObject(target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap);
+ }
+ function readonly(target) {
+ return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap);
+ }
+ function shallowReadonly(target) {
+ return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap);
+ }
+ function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {
+ if (!shared.isObject(target)) {
+ {
+ console.warn(`value cannot be made reactive: ${String(target)}`);
+ }
+ return target;
+ }
+ if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) {
+ return target;
+ }
+ const existingProxy = proxyMap.get(target);
+ if (existingProxy) {
+ return existingProxy;
+ }
+ const targetType = getTargetType(target);
+ if (targetType === 0) {
+ return target;
+ }
+ const proxy = new Proxy(target, targetType === 2 ? collectionHandlers : baseHandlers);
+ proxyMap.set(target, proxy);
+ return proxy;
+ }
+ function isReactive2(value) {
+ if (isReadonly(value)) {
+ return isReactive2(value["__v_raw"]);
+ }
+ return !!(value && value["__v_isReactive"]);
+ }
+ function isReadonly(value) {
+ return !!(value && value["__v_isReadonly"]);
+ }
+ function isProxy(value) {
+ return isReactive2(value) || isReadonly(value);
+ }
+ function toRaw2(observed) {
+ return observed && toRaw2(observed["__v_raw"]) || observed;
+ }
+ function markRaw(value) {
+ shared.def(value, "__v_skip", true);
+ return value;
+ }
+ var convert = (val) => shared.isObject(val) ? reactive3(val) : val;
+ function isRef(r) {
+ return Boolean(r && r.__v_isRef === true);
+ }
+ function ref(value) {
+ return createRef(value);
+ }
+ function shallowRef(value) {
+ return createRef(value, true);
+ }
+ var RefImpl = class {
+ constructor(value, _shallow = false) {
+ this._shallow = _shallow;
+ this.__v_isRef = true;
+ this._rawValue = _shallow ? value : toRaw2(value);
+ this._value = _shallow ? value : convert(value);
+ }
+ get value() {
+ track2(toRaw2(this), "get", "value");
+ return this._value;
+ }
+ set value(newVal) {
+ newVal = this._shallow ? newVal : toRaw2(newVal);
+ if (shared.hasChanged(newVal, this._rawValue)) {
+ this._rawValue = newVal;
+ this._value = this._shallow ? newVal : convert(newVal);
+ trigger2(toRaw2(this), "set", "value", newVal);
+ }
+ }
+ };
+ function createRef(rawValue, shallow = false) {
+ if (isRef(rawValue)) {
+ return rawValue;
+ }
+ return new RefImpl(rawValue, shallow);
+ }
+ function triggerRef(ref2) {
+ trigger2(toRaw2(ref2), "set", "value", ref2.value);
+ }
+ function unref(ref2) {
+ return isRef(ref2) ? ref2.value : ref2;
+ }
+ var shallowUnwrapHandlers = {
+ get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
+ set: (target, key, value, receiver) => {
+ const oldValue = target[key];
+ if (isRef(oldValue) && !isRef(value)) {
+ oldValue.value = value;
+ return true;
+ } else {
+ return Reflect.set(target, key, value, receiver);
+ }
+ }
+ };
+ function proxyRefs(objectWithRefs) {
+ return isReactive2(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers);
+ }
+ var CustomRefImpl = class {
+ constructor(factory) {
+ this.__v_isRef = true;
+ const { get: get3, set: set3 } = factory(() => track2(this, "get", "value"), () => trigger2(this, "set", "value"));
+ this._get = get3;
+ this._set = set3;
+ }
+ get value() {
+ return this._get();
+ }
+ set value(newVal) {
+ this._set(newVal);
+ }
+ };
+ function customRef(factory) {
+ return new CustomRefImpl(factory);
+ }
+ function toRefs(object) {
+ if (!isProxy(object)) {
+ console.warn(`toRefs() expects a reactive object but received a plain one.`);
+ }
+ const ret = shared.isArray(object) ? new Array(object.length) : {};
+ for (const key in object) {
+ ret[key] = toRef(object, key);
+ }
+ return ret;
+ }
+ var ObjectRefImpl = class {
+ constructor(_object, _key) {
+ this._object = _object;
+ this._key = _key;
+ this.__v_isRef = true;
+ }
+ get value() {
+ return this._object[this._key];
+ }
+ set value(newVal) {
+ this._object[this._key] = newVal;
+ }
+ };
+ function toRef(object, key) {
+ return isRef(object[key]) ? object[key] : new ObjectRefImpl(object, key);
+ }
+ var ComputedRefImpl = class {
+ constructor(getter, _setter, isReadonly2) {
+ this._setter = _setter;
+ this._dirty = true;
+ this.__v_isRef = true;
+ this.effect = effect3(getter, {
+ lazy: true,
+ scheduler: () => {
+ if (!this._dirty) {
+ this._dirty = true;
+ trigger2(toRaw2(this), "set", "value");
+ }
+ }
+ });
+ this["__v_isReadonly"] = isReadonly2;
+ }
+ get value() {
+ const self2 = toRaw2(this);
+ if (self2._dirty) {
+ self2._value = this.effect();
+ self2._dirty = false;
+ }
+ track2(self2, "get", "value");
+ return self2._value;
+ }
+ set value(newValue) {
+ this._setter(newValue);
+ }
+ };
+ function computed(getterOrOptions) {
+ let getter;
+ let setter;
+ if (shared.isFunction(getterOrOptions)) {
+ getter = getterOrOptions;
+ setter = () => {
+ console.warn("Write operation failed: computed value is readonly");
+ };
+ } else {
+ getter = getterOrOptions.get;
+ setter = getterOrOptions.set;
+ }
+ return new ComputedRefImpl(getter, setter, shared.isFunction(getterOrOptions) || !getterOrOptions.set);
+ }
+ exports2.ITERATE_KEY = ITERATE_KEY;
+ exports2.computed = computed;
+ exports2.customRef = customRef;
+ exports2.effect = effect3;
+ exports2.enableTracking = enableTracking;
+ exports2.isProxy = isProxy;
+ exports2.isReactive = isReactive2;
+ exports2.isReadonly = isReadonly;
+ exports2.isRef = isRef;
+ exports2.markRaw = markRaw;
+ exports2.pauseTracking = pauseTracking;
+ exports2.proxyRefs = proxyRefs;
+ exports2.reactive = reactive3;
+ exports2.readonly = readonly;
+ exports2.ref = ref;
+ exports2.resetTracking = resetTracking;
+ exports2.shallowReactive = shallowReactive;
+ exports2.shallowReadonly = shallowReadonly;
+ exports2.shallowRef = shallowRef;
+ exports2.stop = stop2;
+ exports2.toRaw = toRaw2;
+ exports2.toRef = toRef;
+ exports2.toRefs = toRefs;
+ exports2.track = track2;
+ exports2.trigger = trigger2;
+ exports2.triggerRef = triggerRef;
+ exports2.unref = unref;
+ }
+ });
+ var require_reactivity = __commonJS2({
+ "node_modules/@vue/reactivity/index.js"(exports2, module2) {
+ "use strict";
+ if (false) {
+ module2.exports = null;
+ } else {
+ module2.exports = require_reactivity_cjs();
+ }
+ }
+ });
+ var module_exports = {};
+ __export(module_exports, {
+ Alpine: () => src_default,
+ default: () => module_default
+ });
+ module.exports = __toCommonJS(module_exports);
+ var flushPending = false;
+ var flushing = false;
+ var queue = [];
+ var lastFlushedIndex = -1;
+ function scheduler(callback) {
+ queueJob(callback);
+ }
+ function queueJob(job) {
+ if (!queue.includes(job))
+ queue.push(job);
+ queueFlush();
+ }
+ function dequeueJob(job) {
+ let index = queue.indexOf(job);
+ if (index !== -1 && index > lastFlushedIndex)
+ queue.splice(index, 1);
+ }
+ function queueFlush() {
+ if (!flushing && !flushPending) {
+ flushPending = true;
+ queueMicrotask(flushJobs);
+ }
+ }
+ function flushJobs() {
+ flushPending = false;
+ flushing = true;
+ for (let i = 0; i < queue.length; i++) {
+ queue[i]();
+ lastFlushedIndex = i;
+ }
+ queue.length = 0;
+ lastFlushedIndex = -1;
+ flushing = false;
+ }
+ var reactive;
+ var effect;
+ var release;
+ var raw;
+ var shouldSchedule = true;
+ function disableEffectScheduling(callback) {
+ shouldSchedule = false;
+ callback();
+ shouldSchedule = true;
+ }
+ function setReactivityEngine(engine) {
+ reactive = engine.reactive;
+ release = engine.release;
+ effect = (callback) => engine.effect(callback, { scheduler: (task) => {
+ if (shouldSchedule) {
+ scheduler(task);
+ } else {
+ task();
+ }
+ } });
+ raw = engine.raw;
+ }
+ function overrideEffect(override) {
+ effect = override;
+ }
+ function elementBoundEffect(el) {
+ let cleanup = () => {
+ };
+ let wrappedEffect = (callback) => {
+ let effectReference = effect(callback);
+ if (!el._x_effects) {
+ el._x_effects = /* @__PURE__ */ new Set();
+ el._x_runEffects = () => {
+ el._x_effects.forEach((i) => i());
+ };
+ }
+ el._x_effects.add(effectReference);
+ cleanup = () => {
+ if (effectReference === void 0)
+ return;
+ el._x_effects.delete(effectReference);
+ release(effectReference);
+ };
+ return effectReference;
+ };
+ return [wrappedEffect, () => {
+ cleanup();
+ }];
+ }
+ function watch(getter, callback) {
+ let firstTime = true;
+ let oldValue;
+ let effectReference = effect(() => {
+ let value = getter();
+ JSON.stringify(value);
+ if (!firstTime) {
+ queueMicrotask(() => {
+ callback(value, oldValue);
+ oldValue = value;
+ });
+ } else {
+ oldValue = value;
+ }
+ firstTime = false;
+ });
+ return () => release(effectReference);
+ }
+ var onAttributeAddeds = [];
+ var onElRemoveds = [];
+ var onElAddeds = [];
+ function onElAdded(callback) {
+ onElAddeds.push(callback);
+ }
+ function onElRemoved(el, callback) {
+ if (typeof callback === "function") {
+ if (!el._x_cleanups)
+ el._x_cleanups = [];
+ el._x_cleanups.push(callback);
+ } else {
+ callback = el;
+ onElRemoveds.push(callback);
+ }
+ }
+ function onAttributesAdded(callback) {
+ onAttributeAddeds.push(callback);
+ }
+ function onAttributeRemoved(el, name, callback) {
+ if (!el._x_attributeCleanups)
+ el._x_attributeCleanups = {};
+ if (!el._x_attributeCleanups[name])
+ el._x_attributeCleanups[name] = [];
+ el._x_attributeCleanups[name].push(callback);
+ }
+ function cleanupAttributes(el, names) {
+ if (!el._x_attributeCleanups)
+ return;
+ Object.entries(el._x_attributeCleanups).forEach(([name, value]) => {
+ if (names === void 0 || names.includes(name)) {
+ value.forEach((i) => i());
+ delete el._x_attributeCleanups[name];
+ }
+ });
+ }
+ function cleanupElement(el) {
+ var _a, _b;
+ (_a = el._x_effects) == null ? void 0 : _a.forEach(dequeueJob);
+ while ((_b = el._x_cleanups) == null ? void 0 : _b.length)
+ el._x_cleanups.pop()();
+ }
+ var observer = new MutationObserver(onMutate);
+ var currentlyObserving = false;
+ function startObservingMutations() {
+ observer.observe(document, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
+ currentlyObserving = true;
+ }
+ function stopObservingMutations() {
+ flushObserver();
+ observer.disconnect();
+ currentlyObserving = false;
+ }
+ var queuedMutations = [];
+ function flushObserver() {
+ let records = observer.takeRecords();
+ queuedMutations.push(() => records.length > 0 && onMutate(records));
+ let queueLengthWhenTriggered = queuedMutations.length;
+ queueMicrotask(() => {
+ if (queuedMutations.length === queueLengthWhenTriggered) {
+ while (queuedMutations.length > 0)
+ queuedMutations.shift()();
+ }
+ });
+ }
+ function mutateDom(callback) {
+ if (!currentlyObserving)
+ return callback();
+ stopObservingMutations();
+ let result = callback();
+ startObservingMutations();
+ return result;
+ }
+ var isCollecting = false;
+ var deferredMutations = [];
+ function deferMutations() {
+ isCollecting = true;
+ }
+ function flushAndStopDeferringMutations() {
+ isCollecting = false;
+ onMutate(deferredMutations);
+ deferredMutations = [];
+ }
+ function onMutate(mutations) {
+ if (isCollecting) {
+ deferredMutations = deferredMutations.concat(mutations);
+ return;
+ }
+ let addedNodes = [];
+ let removedNodes = /* @__PURE__ */ new Set();
+ let addedAttributes = /* @__PURE__ */ new Map();
+ let removedAttributes = /* @__PURE__ */ new Map();
+ for (let i = 0; i < mutations.length; i++) {
+ if (mutations[i].target._x_ignoreMutationObserver)
+ continue;
+ if (mutations[i].type === "childList") {
+ mutations[i].removedNodes.forEach((node) => {
+ if (node.nodeType !== 1)
+ return;
+ if (!node._x_marker)
+ return;
+ removedNodes.add(node);
+ });
+ mutations[i].addedNodes.forEach((node) => {
+ if (node.nodeType !== 1)
+ return;
+ if (removedNodes.has(node)) {
+ removedNodes.delete(node);
+ return;
+ }
+ if (node._x_marker)
+ return;
+ addedNodes.push(node);
+ });
+ }
+ if (mutations[i].type === "attributes") {
+ let el = mutations[i].target;
+ let name = mutations[i].attributeName;
+ let oldValue = mutations[i].oldValue;
+ let add = () => {
+ if (!addedAttributes.has(el))
+ addedAttributes.set(el, []);
+ addedAttributes.get(el).push({ name, value: el.getAttribute(name) });
+ };
+ let remove = () => {
+ if (!removedAttributes.has(el))
+ removedAttributes.set(el, []);
+ removedAttributes.get(el).push(name);
+ };
+ if (el.hasAttribute(name) && oldValue === null) {
+ add();
+ } else if (el.hasAttribute(name)) {
+ remove();
+ add();
+ } else {
+ remove();
+ }
+ }
+ }
+ removedAttributes.forEach((attrs, el) => {
+ cleanupAttributes(el, attrs);
+ });
+ addedAttributes.forEach((attrs, el) => {
+ onAttributeAddeds.forEach((i) => i(el, attrs));
+ });
+ for (let node of removedNodes) {
+ if (addedNodes.some((i) => i.contains(node)))
+ continue;
+ onElRemoveds.forEach((i) => i(node));
+ }
+ for (let node of addedNodes) {
+ if (!node.isConnected)
+ continue;
+ onElAddeds.forEach((i) => i(node));
+ }
+ addedNodes = null;
+ removedNodes = null;
+ addedAttributes = null;
+ removedAttributes = null;
+ }
+ function scope(node) {
+ return mergeProxies(closestDataStack(node));
+ }
+ function addScopeToNode(node, data2, referenceNode) {
+ node._x_dataStack = [data2, ...closestDataStack(referenceNode || node)];
+ return () => {
+ node._x_dataStack = node._x_dataStack.filter((i) => i !== data2);
+ };
+ }
+ function closestDataStack(node) {
+ if (node._x_dataStack)
+ return node._x_dataStack;
+ if (typeof ShadowRoot === "function" && node instanceof ShadowRoot) {
+ return closestDataStack(node.host);
+ }
+ if (!node.parentNode) {
+ return [];
+ }
+ return closestDataStack(node.parentNode);
+ }
+ function mergeProxies(objects) {
+ return new Proxy({ objects }, mergeProxyTrap);
+ }
+ var mergeProxyTrap = {
+ ownKeys({ objects }) {
+ return Array.from(new Set(objects.flatMap((i) => Object.keys(i))));
+ },
+ has({ objects }, name) {
+ if (name == Symbol.unscopables)
+ return false;
+ return objects.some((obj) => Object.prototype.hasOwnProperty.call(obj, name) || Reflect.has(obj, name));
+ },
+ get({ objects }, name, thisProxy) {
+ if (name == "toJSON")
+ return collapseProxies;
+ return Reflect.get(objects.find((obj) => Reflect.has(obj, name)) || {}, name, thisProxy);
+ },
+ set({ objects }, name, value, thisProxy) {
+ const target = objects.find((obj) => Object.prototype.hasOwnProperty.call(obj, name)) || objects[objects.length - 1];
+ const descriptor = Object.getOwnPropertyDescriptor(target, name);
+ if ((descriptor == null ? void 0 : descriptor.set) && (descriptor == null ? void 0 : descriptor.get))
+ return descriptor.set.call(thisProxy, value) || true;
+ return Reflect.set(target, name, value);
+ }
+ };
+ function collapseProxies() {
+ let keys = Reflect.ownKeys(this);
+ return keys.reduce((acc, key) => {
+ acc[key] = Reflect.get(this, key);
+ return acc;
+ }, {});
+ }
+ function initInterceptors(data2) {
+ let isObject2 = (val) => typeof val === "object" && !Array.isArray(val) && val !== null;
+ let recurse = (obj, basePath = "") => {
+ Object.entries(Object.getOwnPropertyDescriptors(obj)).forEach(([key, { value, enumerable }]) => {
+ if (enumerable === false || value === void 0)
+ return;
+ if (typeof value === "object" && value !== null && value.__v_skip)
+ return;
+ let path = basePath === "" ? key : `${basePath}.${key}`;
+ if (typeof value === "object" && value !== null && value._x_interceptor) {
+ obj[key] = value.initialize(data2, path, key);
+ } else {
+ if (isObject2(value) && value !== obj && !(value instanceof Element)) {
+ recurse(value, path);
+ }
+ }
+ });
+ };
+ return recurse(data2);
+ }
+ function interceptor(callback, mutateObj = () => {
+ }) {
+ let obj = {
+ initialValue: void 0,
+ _x_interceptor: true,
+ initialize(data2, path, key) {
+ return callback(this.initialValue, () => get(data2, path), (value) => set(data2, path, value), path, key);
+ }
+ };
+ mutateObj(obj);
+ return (initialValue) => {
+ if (typeof initialValue === "object" && initialValue !== null && initialValue._x_interceptor) {
+ let initialize = obj.initialize.bind(obj);
+ obj.initialize = (data2, path, key) => {
+ let innerValue = initialValue.initialize(data2, path, key);
+ obj.initialValue = innerValue;
+ return initialize(data2, path, key);
+ };
+ } else {
+ obj.initialValue = initialValue;
+ }
+ return obj;
+ };
+ }
+ function get(obj, path) {
+ return path.split(".").reduce((carry, segment) => carry[segment], obj);
+ }
+ function set(obj, path, value) {
+ if (typeof path === "string")
+ path = path.split(".");
+ if (path.length === 1)
+ obj[path[0]] = value;
+ else if (path.length === 0)
+ throw error;
+ else {
+ if (obj[path[0]])
+ return set(obj[path[0]], path.slice(1), value);
+ else {
+ obj[path[0]] = {};
+ return set(obj[path[0]], path.slice(1), value);
+ }
+ }
+ }
+ var magics = {};
+ function magic(name, callback) {
+ magics[name] = callback;
+ }
+ function injectMagics(obj, el) {
+ let memoizedUtilities = getUtilities(el);
+ Object.entries(magics).forEach(([name, callback]) => {
+ Object.defineProperty(obj, `$${name}`, {
+ get() {
+ return callback(el, memoizedUtilities);
+ },
+ enumerable: false
+ });
+ });
+ return obj;
+ }
+ function getUtilities(el) {
+ let [utilities, cleanup] = getElementBoundUtilities(el);
+ let utils = { interceptor, ...utilities };
+ onElRemoved(el, cleanup);
+ return utils;
+ }
+ function tryCatch(el, expression, callback, ...args) {
+ try {
+ return callback(...args);
+ } catch (e) {
+ handleError(e, el, expression);
+ }
+ }
+ function handleError(error2, el, expression = void 0) {
+ error2 = Object.assign(error2 != null ? error2 : { message: "No error message given." }, { el, expression });
+ console.warn(`Alpine Expression Error: ${error2.message}
+
+${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
+ setTimeout(() => {
+ throw error2;
+ }, 0);
+ }
+ var shouldAutoEvaluateFunctions = true;
+ function dontAutoEvaluateFunctions(callback) {
+ let cache = shouldAutoEvaluateFunctions;
+ shouldAutoEvaluateFunctions = false;
+ let result = callback();
+ shouldAutoEvaluateFunctions = cache;
+ return result;
+ }
+ function evaluate(el, expression, extras = {}) {
+ let result;
+ evaluateLater(el, expression)((value) => result = value, extras);
+ return result;
+ }
+ function evaluateLater(...args) {
+ return theEvaluatorFunction(...args);
+ }
+ var theEvaluatorFunction = normalEvaluator;
+ function setEvaluator(newEvaluator) {
+ theEvaluatorFunction = newEvaluator;
+ }
+ function normalEvaluator(el, expression) {
+ let overriddenMagics = {};
+ injectMagics(overriddenMagics, el);
+ let dataStack = [overriddenMagics, ...closestDataStack(el)];
+ let evaluator = typeof expression === "function" ? generateEvaluatorFromFunction(dataStack, expression) : generateEvaluatorFromString(dataStack, expression, el);
+ return tryCatch.bind(null, el, expression, evaluator);
+ }
+ function generateEvaluatorFromFunction(dataStack, func) {
+ return (receiver = () => {
+ }, { scope: scope2 = {}, params = [] } = {}) => {
+ let result = func.apply(mergeProxies([scope2, ...dataStack]), params);
+ runIfTypeOfFunction(receiver, result);
+ };
+ }
+ var evaluatorMemo = {};
+ function generateFunctionFromString(expression, el) {
+ if (evaluatorMemo[expression]) {
+ return evaluatorMemo[expression];
+ }
+ let AsyncFunction = Object.getPrototypeOf(async function() {
+ }).constructor;
+ let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression.trim()) || /^(let|const)\s/.test(expression.trim()) ? `(async()=>{ ${expression} })()` : expression;
+ const safeAsyncFunction = () => {
+ try {
+ let func2 = new AsyncFunction(["__self", "scope"], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`);
+ Object.defineProperty(func2, "name", {
+ value: `[Alpine] ${expression}`
+ });
+ return func2;
+ } catch (error2) {
+ handleError(error2, el, expression);
+ return Promise.resolve();
+ }
+ };
+ let func = safeAsyncFunction();
+ evaluatorMemo[expression] = func;
+ return func;
+ }
+ function generateEvaluatorFromString(dataStack, expression, el) {
+ let func = generateFunctionFromString(expression, el);
+ return (receiver = () => {
+ }, { scope: scope2 = {}, params = [] } = {}) => {
+ func.result = void 0;
+ func.finished = false;
+ let completeScope = mergeProxies([scope2, ...dataStack]);
+ if (typeof func === "function") {
+ let promise = func(func, completeScope).catch((error2) => handleError(error2, el, expression));
+ if (func.finished) {
+ runIfTypeOfFunction(receiver, func.result, completeScope, params, el);
+ func.result = void 0;
+ } else {
+ promise.then((result) => {
+ runIfTypeOfFunction(receiver, result, completeScope, params, el);
+ }).catch((error2) => handleError(error2, el, expression)).finally(() => func.result = void 0);
+ }
+ }
+ };
+ }
+ function runIfTypeOfFunction(receiver, value, scope2, params, el) {
+ if (shouldAutoEvaluateFunctions && typeof value === "function") {
+ let result = value.apply(scope2, params);
+ if (result instanceof Promise) {
+ result.then((i) => runIfTypeOfFunction(receiver, i, scope2, params)).catch((error2) => handleError(error2, el, value));
+ } else {
+ receiver(result);
+ }
+ } else if (typeof value === "object" && value instanceof Promise) {
+ value.then((i) => receiver(i));
+ } else {
+ receiver(value);
+ }
+ }
+ var prefixAsString = "x-";
+ function prefix(subject = "") {
+ return prefixAsString + subject;
+ }
+ function setPrefix(newPrefix) {
+ prefixAsString = newPrefix;
+ }
+ var directiveHandlers = {};
+ function directive2(name, callback) {
+ directiveHandlers[name] = callback;
+ return {
+ before(directive22) {
+ if (!directiveHandlers[directive22]) {
+ console.warn(String.raw`Cannot find directive \`${directive22}\`. \`${name}\` will use the default order of execution`);
+ return;
+ }
+ const pos = directiveOrder.indexOf(directive22);
+ directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf("DEFAULT"), 0, name);
+ }
+ };
+ }
+ function directiveExists(name) {
+ return Object.keys(directiveHandlers).includes(name);
+ }
+ function directives(el, attributes, originalAttributeOverride) {
+ attributes = Array.from(attributes);
+ if (el._x_virtualDirectives) {
+ let vAttributes = Object.entries(el._x_virtualDirectives).map(([name, value]) => ({ name, value }));
+ let staticAttributes = attributesOnly(vAttributes);
+ vAttributes = vAttributes.map((attribute) => {
+ if (staticAttributes.find((attr) => attr.name === attribute.name)) {
+ return {
+ name: `x-bind:${attribute.name}`,
+ value: `"${attribute.value}"`
+ };
+ }
+ return attribute;
+ });
+ attributes = attributes.concat(vAttributes);
+ }
+ let transformedAttributeMap = {};
+ let directives2 = attributes.map(toTransformedAttributes((newName, oldName) => transformedAttributeMap[newName] = oldName)).filter(outNonAlpineAttributes).map(toParsedDirectives(transformedAttributeMap, originalAttributeOverride)).sort(byPriority);
+ return directives2.map((directive22) => {
+ return getDirectiveHandler(el, directive22);
+ });
+ }
+ function attributesOnly(attributes) {
+ return Array.from(attributes).map(toTransformedAttributes()).filter((attr) => !outNonAlpineAttributes(attr));
+ }
+ var isDeferringHandlers = false;
+ var directiveHandlerStacks = /* @__PURE__ */ new Map();
+ var currentHandlerStackKey = Symbol();
+ function deferHandlingDirectives(callback) {
+ isDeferringHandlers = true;
+ let key = Symbol();
+ currentHandlerStackKey = key;
+ directiveHandlerStacks.set(key, []);
+ let flushHandlers = () => {
+ while (directiveHandlerStacks.get(key).length)
+ directiveHandlerStacks.get(key).shift()();
+ directiveHandlerStacks.delete(key);
+ };
+ let stopDeferring = () => {
+ isDeferringHandlers = false;
+ flushHandlers();
+ };
+ callback(flushHandlers);
+ stopDeferring();
+ }
+ function getElementBoundUtilities(el) {
+ let cleanups2 = [];
+ let cleanup = (callback) => cleanups2.push(callback);
+ let [effect3, cleanupEffect] = elementBoundEffect(el);
+ cleanups2.push(cleanupEffect);
+ let utilities = {
+ Alpine: alpine_default,
+ effect: effect3,
+ cleanup,
+ evaluateLater: evaluateLater.bind(evaluateLater, el),
+ evaluate: evaluate.bind(evaluate, el)
+ };
+ let doCleanup = () => cleanups2.forEach((i) => i());
+ return [utilities, doCleanup];
+ }
+ function getDirectiveHandler(el, directive22) {
+ let noop = () => {
+ };
+ let handler4 = directiveHandlers[directive22.type] || noop;
+ let [utilities, cleanup] = getElementBoundUtilities(el);
+ onAttributeRemoved(el, directive22.original, cleanup);
+ let fullHandler = () => {
+ if (el._x_ignore || el._x_ignoreSelf)
+ return;
+ handler4.inline && handler4.inline(el, directive22, utilities);
+ handler4 = handler4.bind(handler4, el, directive22, utilities);
+ isDeferringHandlers ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler4) : handler4();
+ };
+ fullHandler.runCleanups = cleanup;
+ return fullHandler;
+ }
+ var startingWith = (subject, replacement) => ({ name, value }) => {
+ if (name.startsWith(subject))
+ name = name.replace(subject, replacement);
+ return { name, value };
+ };
+ var into = (i) => i;
+ function toTransformedAttributes(callback = () => {
+ }) {
+ return ({ name, value }) => {
+ let { name: newName, value: newValue } = attributeTransformers.reduce((carry, transform) => {
+ return transform(carry);
+ }, { name, value });
+ if (newName !== name)
+ callback(newName, name);
+ return { name: newName, value: newValue };
+ };
+ }
+ var attributeTransformers = [];
+ function mapAttributes(callback) {
+ attributeTransformers.push(callback);
+ }
+ function outNonAlpineAttributes({ name }) {
+ return alpineAttributeRegex().test(name);
+ }
+ var alpineAttributeRegex = () => new RegExp(`^${prefixAsString}([^:^.]+)\\b`);
+ function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) {
+ return ({ name, value }) => {
+ let typeMatch = name.match(alpineAttributeRegex());
+ let valueMatch = name.match(/:([a-zA-Z0-9\-_:]+)/);
+ let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
+ let original = originalAttributeOverride || transformedAttributeMap[name] || name;
+ return {
+ type: typeMatch ? typeMatch[1] : null,
+ value: valueMatch ? valueMatch[1] : null,
+ modifiers: modifiers.map((i) => i.replace(".", "")),
+ expression: value,
+ original
+ };
+ };
+ }
+ var DEFAULT = "DEFAULT";
+ var directiveOrder = [
+ "ignore",
+ "ref",
+ "data",
+ "id",
+ "anchor",
+ "bind",
+ "init",
+ "for",
+ "model",
+ "modelable",
+ "transition",
+ "show",
+ "if",
+ DEFAULT,
+ "teleport"
+ ];
+ function byPriority(a, b) {
+ let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type;
+ let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type;
+ return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB);
+ }
+ function dispatch3(el, name, detail = {}) {
+ el.dispatchEvent(new CustomEvent(name, {
+ detail,
+ bubbles: true,
+ composed: true,
+ cancelable: true
+ }));
+ }
+ function walk(el, callback) {
+ if (typeof ShadowRoot === "function" && el instanceof ShadowRoot) {
+ Array.from(el.children).forEach((el2) => walk(el2, callback));
+ return;
+ }
+ let skip = false;
+ callback(el, () => skip = true);
+ if (skip)
+ return;
+ let node = el.firstElementChild;
+ while (node) {
+ walk(node, callback, false);
+ node = node.nextElementSibling;
+ }
+ }
+ function warn(message, ...args) {
+ console.warn(`Alpine Warning: ${message}`, ...args);
+ }
+ var started = false;
+ function start2() {
+ if (started)
+ warn("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.");
+ started = true;
+ if (!document.body)
+ warn("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `
diff --git a/src/view/frontend/templates/html/loader.phtml b/src/view/frontend/templates/html/loader.phtml
deleted file mode 100644
index d5b2cc92..00000000
--- a/src/view/frontend/templates/html/loader.phtml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/src/view/frontend/templates/html/pagination/pager.phtml b/src/view/frontend/templates/html/pagination/pager.phtml
deleted file mode 100644
index c2f1cfbd..00000000
--- a/src/view/frontend/templates/html/pagination/pager.phtml
+++ /dev/null
@@ -1,37 +0,0 @@
-getComponent();
-?>
-
- hasPages()): ?>
-
- onFirstPage()): ?>
- «
-
-
- «
-
-
-
- hasMorePages()): ?>
- »
-
- »
-
-
-
-
diff --git a/src/view/frontend/templates/js/alpinejs/components/magewire-script.phtml b/src/view/frontend/templates/js/alpinejs/components/magewire-script.phtml
new file mode 100644
index 00000000..e3bff16e
--- /dev/null
+++ b/src/view/frontend/templates/js/alpinejs/components/magewire-script.phtml
@@ -0,0 +1,41 @@
+getData('view_model');
+$magewireUtility = $magewireViewModel->utils()->magewire();
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/frontend/templates/js/alpinejs/directives/.gitkeep b/src/view/frontend/templates/js/alpinejs/directives/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/frontend/templates/js/magewire/directives/.gitkeep b/src/view/frontend/templates/js/magewire/directives/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/frontend/templates/js/magewire/object-proxy.phtml b/src/view/frontend/templates/js/magewire/object-proxy.phtml
new file mode 100644
index 00000000..552ec7e0
--- /dev/null
+++ b/src/view/frontend/templates/js/magewire/object-proxy.phtml
@@ -0,0 +1,144 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/**
+ * @internal Do not modify to ensure Magewire continues to function correctly.
+ *
+ * This template creates a JavaScript Proxy that acts as a temporary stand-in for
+ * window.Magewire before the real Magewire (Livewire v3) runtime has loaded.
+ *
+ * Problem it solves:
+ * Third-party scripts and theme code may call Magewire APIs (e.g. Magewire.onError())
+ * in inline scripts that execute before the Magewire JS bundle has been parsed.
+ * Without this Proxy, those calls would throw "Magewire is not defined" or similar errors.
+ *
+ * How it works:
+ * 1. A JS Proxy is assigned to window.Magewire immediately (inline, blocking).
+ * 2. Any method call on the Proxy is captured and pushed onto a queue.
+ * 3. Once the real Magewire runtime fires the "magewire:init" event, the queued
+ * calls are replayed against known backwards-compatibility handlers.
+ * 4. After init, the real Magewire object replaces the Proxy — the queue is no
+ * longer needed and is discarded.
+ *
+ * This file also contains the V1 -> V2 onError bridge (magewireBcOnError), which
+ * translates the old Magewire.onError(callback) signature into a V2 request hook.
+ */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/src/view/frontend/templates/magewire-features/.gitkeep b/src/view/frontend/templates/magewire-features/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/frontend/templates/magewire/ui-components/.gitkeep b/src/view/frontend/templates/magewire/ui-components/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/frontend/templates/magewire/utils/.gitkeep b/src/view/frontend/templates/magewire/utils/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/view/frontend/templates/page/js/magewire-initialize.phtml b/src/view/frontend/templates/page/js/magewire-initialize.phtml
deleted file mode 100644
index 59402743..00000000
--- a/src/view/frontend/templates/page/js/magewire-initialize.phtml
+++ /dev/null
@@ -1,69 +0,0 @@
-require(Magewire::class);
-?>
-
diff --git a/src/view/frontend/templates/page/js/magewire.phtml b/src/view/frontend/templates/page/js/magewire.phtml
deleted file mode 100644
index eab4fd57..00000000
--- a/src/view/frontend/templates/page/js/magewire.phtml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
diff --git a/src/view/frontend/templates/page/js/magewire/plugin/intersect.phtml b/src/view/frontend/templates/page/js/magewire/plugin/intersect.phtml
deleted file mode 100644
index 82df3b5c..00000000
--- a/src/view/frontend/templates/page/js/magewire/plugin/intersect.phtml
+++ /dev/null
@@ -1,20 +0,0 @@
-
diff --git a/src/view/frontend/templates/tests/magewire/playwright/directives/area/escape.phtml b/src/view/frontend/templates/tests/magewire/playwright/directives/area/escape.phtml
new file mode 100644
index 00000000..3f2b053c
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/directives/area/escape.phtml
@@ -0,0 +1,12 @@
+
+
diff --git a/src/Model/WireableInterface.php b/src/view/frontend/templates/tests/magewire/playwright/directives/area/render.phtml
similarity index 68%
rename from src/Model/WireableInterface.php
rename to src/view/frontend/templates/tests/magewire/playwright/directives/area/render.phtml
index 4ebfd279..fa9e88ba 100644
--- a/src/Model/WireableInterface.php
+++ b/src/view/frontend/templates/tests/magewire/playwright/directives/area/render.phtml
@@ -1,4 +1,5 @@
+
+ Parent
-}
+ @renderChild(alias: 'child')
+
diff --git a/src/view/frontend/templates/tests/magewire/playwright/directives/area/render/child-child.phtml b/src/view/frontend/templates/tests/magewire/playwright/directives/area/render/child-child.phtml
new file mode 100644
index 00000000..c4f3262f
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/directives/area/render/child-child.phtml
@@ -0,0 +1,14 @@
+
+
+ Child's Child
+
diff --git a/src/view/frontend/templates/tests/magewire/playwright/directives/area/render/child.phtml b/src/view/frontend/templates/tests/magewire/playwright/directives/area/render/child.phtml
new file mode 100644
index 00000000..f63539d4
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/directives/area/render/child.phtml
@@ -0,0 +1,16 @@
+
+
+ Child
+
+ @renderChild(alias: 'child-child')
+
diff --git a/src/view/frontend/templates/tests/magewire/playwright/directives/base.phtml b/src/view/frontend/templates/tests/magewire/playwright/directives/base.phtml
new file mode 100644
index 00000000..2017abc4
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/directives/base.phtml
@@ -0,0 +1,43 @@
+
+
+
+ Basics
+
+
+
+
+
+ Directive
+ Input
+ Result
+ Expected
+
+
+
+
+
+ translate
+ foo
+ @translate(value: 'foo', escape: false)
+ foo
+
+
+
+ translate (escaped)
+ bar
+ @translate(value: 'bar')
+ bar
+
+
+
+
diff --git a/src/view/frontend/templates/tests/magewire/playwright/directives/scope.phtml b/src/view/frontend/templates/tests/magewire/playwright/directives/scope.phtml
new file mode 100644
index 00000000..536a7081
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/directives/scope.phtml
@@ -0,0 +1,34 @@
+
+
+
+ Scope
+
+
+
+
+
+ Key
+ Value
+
+
+
+
+ @foreach(['a', 'b', 'c'] as $key => $value)
+
+ = $key ?>
+ = $value ?>
+
+ @endforeach
+
+
+
diff --git a/src/view/frontend/templates/tests/magewire/playwright/resolvers/aliased.phtml b/src/view/frontend/templates/tests/magewire/playwright/resolvers/aliased.phtml
new file mode 100644
index 00000000..00449675
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/resolvers/aliased.phtml
@@ -0,0 +1,14 @@
+
+
+ Layout resolver (aliased)
+
\ No newline at end of file
diff --git a/src/view/frontend/templates/tests/magewire/playwright/resolvers/default.phtml b/src/view/frontend/templates/tests/magewire/playwright/resolvers/default.phtml
new file mode 100644
index 00000000..0d0561ae
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/resolvers/default.phtml
@@ -0,0 +1,14 @@
+
+
+ Layout resolver (default)
+
diff --git a/src/view/frontend/templates/tests/magewire/playwright/resolvers/nested.phtml b/src/view/frontend/templates/tests/magewire/playwright/resolvers/nested.phtml
new file mode 100644
index 00000000..a244c58e
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/resolvers/nested.phtml
@@ -0,0 +1,16 @@
+
+
+ Layout resolver (nested parent)
+
+ @renderChild(alias: 'child')
+
\ No newline at end of file
diff --git a/src/view/frontend/templates/tests/magewire/playwright/resolvers/nested/child.phtml b/src/view/frontend/templates/tests/magewire/playwright/resolvers/nested/child.phtml
new file mode 100644
index 00000000..e8edcafb
--- /dev/null
+++ b/src/view/frontend/templates/tests/magewire/playwright/resolvers/nested/child.phtml
@@ -0,0 +1,14 @@
+
+
+ Layout resolver (nested child)
+
\ No newline at end of file
diff --git a/src/view/frontend/web/js/livewire.js b/src/view/frontend/web/js/livewire.js
deleted file mode 100644
index 77cc6a14..00000000
--- a/src/view/frontend/web/js/livewire.js
+++ /dev/null
@@ -1,14 +0,0 @@
-!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):(global=global||self).Livewire=factory()}(this,(function(){"use strict";function _typeof(obj){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(obj){return typeof obj}:function(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj})(obj)}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _defineProperties(target,props){for(var i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0&&void 0!==arguments[0]?arguments[0]:"right";return this.modifiers.includes("up")?"up":this.modifiers.includes("down")?"down":this.modifiers.includes("left")?"left":this.modifiers.includes("right")?"right":fallback}},{key:"value",get:function(){return this.el.getAttribute(this.rawName)}},{key:"method",get:function(){return this.parseOutMethodAndParams(this.value).method}},{key:"params",get:function(){return this.parseOutMethodAndParams(this.value).params}}]),Directive}();function walk(root,callback){if(!1!==callback(root))for(var node=root.firstElementChild;node;)walk(node,callback),node=node.nextElementSibling}function dispatch(eventName){var event=document.createEvent("Events");return event.initEvent(eventName,!0,!0),document.dispatchEvent(event),event}function getCsrfToken(){var _window$livewire_toke,tokenTag=document.head.querySelector('meta[name="csrf-token"]');return tokenTag?tokenTag.content:null!==(_window$livewire_toke=window.livewire_token)&&void 0!==_window$livewire_toke?_window$livewire_toke:void 0}function kebabCase(subject){return subject.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}
- /*!
- * isobject
- *
- * Copyright (c) 2014-2017, Jon Schlinkert.
- * Released under the MIT License.
- */var isobject=function(val){return null!=val&&"object"==typeof val&&!1===Array.isArray(val)},getValue=function(target,path,options){if(isobject(options)||(options={default:options}),!isValidObject(target))return void 0!==options.default?options.default:target;"number"==typeof path&&(path=String(path));const isArray=Array.isArray(path),isString="string"==typeof path,splitChar=options.separator||".",joinChar=options.joinChar||("string"==typeof splitChar?splitChar:".");if(!isString&&!isArray)return target;if(isString&&path in target)return isValid(path,target,options)?target[path]:options.default;let segs=isArray?path:split(path,splitChar,options),len=segs.length,idx=0;do{let prop=segs[idx];for("number"==typeof prop&&(prop=String(prop));prop&&"\\"===prop.slice(-1);)prop=join([prop.slice(0,-1),segs[++idx]||""],joinChar,options);if(prop in target){if(!isValid(prop,target,options))return options.default;target=target[prop]}else{let hasProp=!1,n=idx+1;for(;n
- *
- * Copyright (c) 2014-2018, Jon Schlinkert.
- * Released under the MIT License.
- */function join(segs,joinChar,options){return"function"==typeof options.join?options.join(segs):segs[0]+joinChar+segs[1]}function split(path,splitChar,options){return"function"==typeof options.split?options.split(path):path.split(splitChar)}function isValid(key,target,options){return"function"!=typeof options.isValid||options.isValid(key,target)}function isValidObject(val){return isobject(val)||Array.isArray(val)||"function"==typeof val}var _default=function(){function _default(el){var skipWatcher=arguments.length>1&&void 0!==arguments[1]&&arguments[1];_classCallCheck(this,_default),this.el=el,this.skipWatcher=skipWatcher,this.resolveCallback=function(){},this.rejectCallback=function(){}}return _createClass(_default,[{key:"toId",value:function(){return btoa(encodeURIComponent(this.el.outerHTML))}},{key:"onResolve",value:function(callback){this.resolveCallback=callback}},{key:"onReject",value:function(callback){this.rejectCallback=callback}},{key:"resolve",value:function(thing){this.resolveCallback(thing)}},{key:"reject",value:function(thing){this.rejectCallback(thing)}}]),_default}(),_default$1=function(_Action){_inherits(_default,_Action);var _super=_createSuper(_default);function _default(event,params,el){var _this;return _classCallCheck(this,_default),(_this=_super.call(this,el)).type="fireEvent",_this.payload={event:event,params:params},_this}return _createClass(_default,[{key:"toId",value:function(){return btoa(encodeURIComponent(this.type,this.payload.event,JSON.stringify(this.payload.params)))}}]),_default}(_default),MessageBus=function(){function MessageBus(){_classCallCheck(this,MessageBus),this.listeners={}}return _createClass(MessageBus,[{key:"register",value:function(name,callback){this.listeners[name]||(this.listeners[name]=[]),this.listeners[name].push(callback)}},{key:"call",value:function(name){for(var _len=arguments.length,params=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++)params[_key-1]=arguments[_key];(this.listeners[name]||[]).forEach((function(callback){callback.apply(void 0,params)}))}},{key:"has",value:function(name){return Object.keys(this.listeners).includes(name)}}]),MessageBus}(),HookManager={availableHooks:["component.initialized","element.initialized","element.updating","element.updated","element.removed","message.sent","message.failed","message.received","message.processed","interceptWireModelSetValue","interceptWireModelAttachListener","beforeReplaceState","beforePushState"],bus:new MessageBus,register:function(name,callback){if(!this.availableHooks.includes(name))throw"Livewire: Referencing unknown hook: [".concat(name,"]");this.bus.register(name,callback)},call:function(name){for(var _this$bus,_len=arguments.length,params=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++)params[_key-1]=arguments[_key];(_this$bus=this.bus).call.apply(_this$bus,[name].concat(params))}},DirectiveManager$1={directives:new MessageBus,register:function(name,callback){if(this.has(name))throw"Livewire: Directive already registered: [".concat(name,"]");this.directives.register(name,callback)},call:function(name,el,directive,component){this.directives.call(name,el,directive,component)},has:function(name){return this.directives.has(name)}},store={componentsById:{},listeners:new MessageBus,initialRenderIsFinished:!1,livewireIsInBackground:!1,livewireIsOffline:!1,sessionHasExpired:!1,directives:DirectiveManager$1,hooks:HookManager,onErrorCallback:function(){},components:function(){var _this=this;return Object.keys(this.componentsById).map((function(key){return _this.componentsById[key]}))},addComponent:function(component){return this.componentsById[component.id]=component},findComponent:function(id){return this.componentsById[id]},getComponentsByName:function(name){return this.components().filter((function(component){return component.name===name}))},hasComponent:function(id){return!!this.componentsById[id]},tearDownComponents:function(){var _this2=this;this.components().forEach((function(component){_this2.removeComponent(component)}))},on:function(event,callback){this.listeners.register(event,callback)},emit:function(event){for(var _this$listeners,_len=arguments.length,params=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++)params[_key-1]=arguments[_key];(_this$listeners=this.listeners).call.apply(_this$listeners,[event].concat(params)),this.componentsListeningForEvent(event).forEach((function(component){return component.addAction(new _default$1(event,params))}))},emitUp:function(el,event){for(var _len2=arguments.length,params=new Array(_len2>2?_len2-2:0),_key2=2;_key2<_len2;_key2++)params[_key2-2]=arguments[_key2];this.componentsListeningForEventThatAreTreeAncestors(el,event).forEach((function(component){return component.addAction(new _default$1(event,params))}))},emitSelf:function(componentId,event){var component=this.findComponent(componentId);if(component.listeners.includes(event)){for(var _len3=arguments.length,params=new Array(_len3>2?_len3-2:0),_key3=2;_key3<_len3;_key3++)params[_key3-2]=arguments[_key3];component.addAction(new _default$1(event,params))}},emitTo:function(componentName,event){for(var _len4=arguments.length,params=new Array(_len4>2?_len4-2:0),_key4=2;_key4<_len4;_key4++)params[_key4-2]=arguments[_key4];var components=this.getComponentsByName(componentName);components.forEach((function(component){component.listeners.includes(event)&&component.addAction(new _default$1(event,params))}))},componentsListeningForEventThatAreTreeAncestors:function(el,event){for(var parentIds=[],parent=el.parentElement.closest("[wire\\:id]");parent;)parentIds.push(parent.getAttribute("wire:id")),parent=parent.parentElement.closest("[wire\\:id]");return this.components().filter((function(component){return component.listeners.includes(event)&&parentIds.includes(component.id)}))},componentsListeningForEvent:function(event){return this.components().filter((function(component){return component.listeners.includes(event)}))},registerDirective:function(name,callback){this.directives.register(name,callback)},registerHook:function(name,callback){this.hooks.register(name,callback)},callHook:function(name){for(var _this$hooks,_len5=arguments.length,params=new Array(_len5>1?_len5-1:0),_key5=1;_key5<_len5;_key5++)params[_key5-1]=arguments[_key5];(_this$hooks=this.hooks).call.apply(_this$hooks,[name].concat(params))},changeComponentId:function(component,newId){var oldId=component.id;component.id=newId,component.fingerprint.id=newId,this.componentsById[newId]=component,delete this.componentsById[oldId],this.components().forEach((function(component){var children=component.serverMemo.children||{};Object.entries(children).forEach((function(_ref){var _ref2=_slicedToArray(_ref,2),key=_ref2[0],_ref2$=_ref2[1],id=_ref2$.id;_ref2$.tagName;id===oldId&&(children[key].id=newId)}))}))},removeComponent:function(component){component.tearDown(),delete this.componentsById[component.id]},onError:function(callback){this.onErrorCallback=callback},getClosestParentId:function(childId,subsetOfParentIds){var _this3=this,distancesByParentId={};subsetOfParentIds.forEach((function(parentId){var distance=_this3.getDistanceToChild(parentId,childId);distance&&(distancesByParentId[parentId]=distance)}));var closestParentId,smallestDistance=Math.min.apply(Math,_toConsumableArray(Object.values(distancesByParentId)));return Object.entries(distancesByParentId).forEach((function(_ref3){var _ref4=_slicedToArray(_ref3,2),parentId=_ref4[0];_ref4[1]===smallestDistance&&(closestParentId=parentId)})),closestParentId},getDistanceToChild:function(parentId,childId){var distanceMemo=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,parentComponent=this.findComponent(parentId);if(parentComponent){var childIds=parentComponent.childIds;if(childIds.includes(childId))return distanceMemo;for(var i=0;i0&&void 0!==arguments[0]?arguments[0]:null;null===node&&(node=document);var allEls=Array.from(node.querySelectorAll("[wire\\:initial-data]")),onlyChildEls=Array.from(node.querySelectorAll("[wire\\:initial-data] [wire\\:initial-data]"));return allEls.filter((function(el){return!onlyChildEls.includes(el)}))},allModelElementsInside:function(root){return Array.from(root.querySelectorAll("[wire\\:model]"))},getByAttributeAndValue:function(attribute,value){return document.querySelector("[wire\\:".concat(attribute,'="').concat(value,'"]'))},nextFrame:function(fn){var _this=this;requestAnimationFrame((function(){requestAnimationFrame(fn.bind(_this))}))},closestRoot:function(el){return this.closestByAttribute(el,"id")},closestByAttribute:function(el,attribute){var closestEl=el.closest("[wire\\:".concat(attribute,"]"));if(!closestEl)throw"\nLivewire Error:\n\nCannot find parent element in DOM tree containing attribute: [wire:".concat(attribute,"].\n\nUsually this is caused by Livewire's DOM-differ not being able to properly track changes.\n\nReference the following guide for common causes: https://laravel-livewire.com/docs/troubleshooting \n\nReferenced element:\n\n").concat(el.outerHTML,"\n");return closestEl},isComponentRootEl:function(el){return this.hasAttribute(el,"id")},hasAttribute:function(el,attribute){return el.hasAttribute("wire:".concat(attribute))},getAttribute:function(el,attribute){return el.getAttribute("wire:".concat(attribute))},removeAttribute:function(el,attribute){return el.removeAttribute("wire:".concat(attribute))},setAttribute:function(el,attribute,value){return el.setAttribute("wire:".concat(attribute),value)},hasFocus:function(el){return el===document.activeElement},isInput:function(el){return["INPUT","TEXTAREA","SELECT"].includes(el.tagName.toUpperCase())},isTextInput:function(el){return["INPUT","TEXTAREA"].includes(el.tagName.toUpperCase())&&!["checkbox","radio"].includes(el.type)},valueFromInput:function(el,component){if("checkbox"===el.type){var modelName=wireDirectives(el).get("model").value,modelValue=component.deferredActions[modelName]?component.deferredActions[modelName].payload.value:getValue(component.data,modelName);return Array.isArray(modelValue)?this.mergeCheckboxValueIntoArray(el,modelValue):!!el.checked&&(el.getAttribute("value")||!0)}return"SELECT"===el.tagName&&el.multiple?this.getSelectValues(el):el.value},mergeCheckboxValueIntoArray:function(el,arrayValue){return el.checked?arrayValue.includes(el.value)?arrayValue:arrayValue.concat(el.value):arrayValue.filter((function(item){return item!==el.value}))},setInputValueFromModel:function(el,component){var modelString=wireDirectives(el).get("model").value,modelValue=getValue(component.data,modelString);"input"===el.tagName.toLowerCase()&&"file"===el.type||this.setInputValue(el,modelValue)},setInputValue:function(el,value){if(store.callHook("interceptWireModelSetValue",value,el),"radio"===el.type)el.checked=el.value==value;else if("checkbox"===el.type)if(Array.isArray(value)){var valueFound=!1;value.forEach((function(val){val==el.value&&(valueFound=!0)})),el.checked=valueFound}else el.checked=!!value;else"SELECT"===el.tagName?this.updateSelect(el,value):(value=void 0===value?"":value,el.value=value)},getSelectValues:function(el){return Array.from(el.options).filter((function(option){return option.selected})).map((function(option){return option.value||option.text}))},updateSelect:function(el,value){var arrayWrappedValue=[].concat(value).map((function(value){return value+""}));Array.from(el.options).forEach((function(option){option.selected=arrayWrappedValue.includes(option.value)}))}},ceil=Math.ceil,floor=Math.floor,toInteger=function(argument){return isNaN(argument=+argument)?0:(argument>0?floor:ceil)(argument)},requireObjectCoercible=function(it){if(null==it)throw TypeError("Can't call method on "+it);return it},createMethod=function(CONVERT_TO_STRING){return function($this,pos){var first,second,S=String(requireObjectCoercible($this)),position=toInteger(pos),size=S.length;return position<0||position>=size?CONVERT_TO_STRING?"":void 0:(first=S.charCodeAt(position))<55296||first>56319||position+1===size||(second=S.charCodeAt(position+1))<56320||second>57343?CONVERT_TO_STRING?S.charAt(position):first:CONVERT_TO_STRING?S.slice(position,position+2):second-56320+(first-55296<<10)+65536}},stringMultibyte={codeAt:createMethod(!1),charAt:createMethod(!0)},commonjsGlobal="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function createCommonjsModule(fn,basedir,module){return fn(module={path:basedir,exports:{},require:function(path,base){return commonjsRequire(path,null==base?module.path:base)}},module.exports),module.exports}function commonjsRequire(){throw new Error("Dynamic requires are not currently supported by @rollup/plugin-commonjs")}var check=function(it){return it&&it.Math==Math&&it},global_1=check("object"==typeof globalThis&&globalThis)||check("object"==typeof window&&window)||check("object"==typeof self&&self)||check("object"==typeof commonjsGlobal&&commonjsGlobal)||Function("return this")(),fails=function(exec){try{return!!exec()}catch(error){return!0}},descriptors=!fails((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),isObject=function(it){return"object"==typeof it?null!==it:"function"==typeof it},document$1=global_1.document,EXISTS=isObject(document$1)&&isObject(document$1.createElement),documentCreateElement=function(it){return EXISTS?document$1.createElement(it):{}},ie8DomDefine=!descriptors&&!fails((function(){return 7!=Object.defineProperty(documentCreateElement("div"),"a",{get:function(){return 7}}).a})),anObject=function(it){if(!isObject(it))throw TypeError(String(it)+" is not an object");return it},toPrimitive=function(input,PREFERRED_STRING){if(!isObject(input))return input;var fn,val;if(PREFERRED_STRING&&"function"==typeof(fn=input.toString)&&!isObject(val=fn.call(input)))return val;if("function"==typeof(fn=input.valueOf)&&!isObject(val=fn.call(input)))return val;if(!PREFERRED_STRING&&"function"==typeof(fn=input.toString)&&!isObject(val=fn.call(input)))return val;throw TypeError("Can't convert object to primitive value")},nativeDefineProperty=Object.defineProperty,f=descriptors?nativeDefineProperty:function(O,P,Attributes){if(anObject(O),P=toPrimitive(P,!0),anObject(Attributes),ie8DomDefine)try{return nativeDefineProperty(O,P,Attributes)}catch(error){}if("get"in Attributes||"set"in Attributes)throw TypeError("Accessors not supported");return"value"in Attributes&&(O[P]=Attributes.value),O},objectDefineProperty={f:f},createPropertyDescriptor=function(bitmap,value){return{enumerable:!(1&bitmap),configurable:!(2&bitmap),writable:!(4&bitmap),value:value}},createNonEnumerableProperty=descriptors?function(object,key,value){return objectDefineProperty.f(object,key,createPropertyDescriptor(1,value))}:function(object,key,value){return object[key]=value,object},setGlobal=function(key,value){try{createNonEnumerableProperty(global_1,key,value)}catch(error){global_1[key]=value}return value},SHARED="__core-js_shared__",store$1=global_1[SHARED]||setGlobal(SHARED,{}),sharedStore=store$1,functionToString=Function.toString;"function"!=typeof sharedStore.inspectSource&&(sharedStore.inspectSource=function(it){return functionToString.call(it)});var inspectSource=sharedStore.inspectSource,WeakMap=global_1.WeakMap,nativeWeakMap="function"==typeof WeakMap&&/native code/.test(inspectSource(WeakMap)),hasOwnProperty={}.hasOwnProperty,has=function(it,key){return hasOwnProperty.call(it,key)},shared=createCommonjsModule((function(module){(module.exports=function(key,value){return sharedStore[key]||(sharedStore[key]=void 0!==value?value:{})})("versions",[]).push({version:"3.6.5",mode:"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})})),id=0,postfix=Math.random(),uid=function(key){return"Symbol("+String(void 0===key?"":key)+")_"+(++id+postfix).toString(36)},keys=shared("keys"),sharedKey=function(key){return keys[key]||(keys[key]=uid(key))},hiddenKeys={},WeakMap$1=global_1.WeakMap,set,get,has$1,enforce=function(it){return has$1(it)?get(it):set(it,{})},getterFor=function(TYPE){return function(it){var state;if(!isObject(it)||(state=get(it)).type!==TYPE)throw TypeError("Incompatible receiver, "+TYPE+" required");return state}};if(nativeWeakMap){var store$2=new WeakMap$1,wmget=store$2.get,wmhas=store$2.has,wmset=store$2.set;set=function(it,metadata){return wmset.call(store$2,it,metadata),metadata},get=function(it){return wmget.call(store$2,it)||{}},has$1=function(it){return wmhas.call(store$2,it)}}else{var STATE=sharedKey("state");hiddenKeys[STATE]=!0,set=function(it,metadata){return createNonEnumerableProperty(it,STATE,metadata),metadata},get=function(it){return has(it,STATE)?it[STATE]:{}},has$1=function(it){return has(it,STATE)}}var internalState={set:set,get:get,has:has$1,enforce:enforce,getterFor:getterFor},nativePropertyIsEnumerable={}.propertyIsEnumerable,getOwnPropertyDescriptor=Object.getOwnPropertyDescriptor,NASHORN_BUG=getOwnPropertyDescriptor&&!nativePropertyIsEnumerable.call({1:2},1),f$1=NASHORN_BUG?function(V){var descriptor=getOwnPropertyDescriptor(this,V);return!!descriptor&&descriptor.enumerable}:nativePropertyIsEnumerable,objectPropertyIsEnumerable={f:f$1},toString={}.toString,classofRaw=function(it){return toString.call(it).slice(8,-1)},split$1="".split,indexedObject=fails((function(){return!Object("z").propertyIsEnumerable(0)}))?function(it){return"String"==classofRaw(it)?split$1.call(it,""):Object(it)}:Object,toIndexedObject=function(it){return indexedObject(requireObjectCoercible(it))},nativeGetOwnPropertyDescriptor=Object.getOwnPropertyDescriptor,f$2=descriptors?nativeGetOwnPropertyDescriptor:function(O,P){if(O=toIndexedObject(O),P=toPrimitive(P,!0),ie8DomDefine)try{return nativeGetOwnPropertyDescriptor(O,P)}catch(error){}if(has(O,P))return createPropertyDescriptor(!objectPropertyIsEnumerable.f.call(O,P),O[P])},objectGetOwnPropertyDescriptor={f:f$2},redefine=createCommonjsModule((function(module){var getInternalState=internalState.get,enforceInternalState=internalState.enforce,TEMPLATE=String(String).split("String");(module.exports=function(O,key,value,options){var unsafe=!!options&&!!options.unsafe,simple=!!options&&!!options.enumerable,noTargetGet=!!options&&!!options.noTargetGet;"function"==typeof value&&("string"!=typeof key||has(value,"name")||createNonEnumerableProperty(value,"name",key),enforceInternalState(value).source=TEMPLATE.join("string"==typeof key?key:"")),O!==global_1?(unsafe?!noTargetGet&&O[key]&&(simple=!0):delete O[key],simple?O[key]=value:createNonEnumerableProperty(O,key,value)):simple?O[key]=value:setGlobal(key,value)})(Function.prototype,"toString",(function(){return"function"==typeof this&&getInternalState(this).source||inspectSource(this)}))})),path=global_1,aFunction=function(variable){return"function"==typeof variable?variable:void 0},getBuiltIn=function(namespace,method){return arguments.length<2?aFunction(path[namespace])||aFunction(global_1[namespace]):path[namespace]&&path[namespace][method]||global_1[namespace]&&global_1[namespace][method]},min=Math.min,toLength=function(argument){return argument>0?min(toInteger(argument),9007199254740991):0},max=Math.max,min$1=Math.min,toAbsoluteIndex=function(index,length){var integer=toInteger(index);return integer<0?max(integer+length,0):min$1(integer,length)},createMethod$1=function(IS_INCLUDES){return function($this,el,fromIndex){var value,O=toIndexedObject($this),length=toLength(O.length),index=toAbsoluteIndex(fromIndex,length);if(IS_INCLUDES&&el!=el){for(;length>index;)if((value=O[index++])!=value)return!0}else for(;length>index;index++)if((IS_INCLUDES||index in O)&&O[index]===el)return IS_INCLUDES||index||0;return!IS_INCLUDES&&-1}},arrayIncludes={includes:createMethod$1(!0),indexOf:createMethod$1(!1)},indexOf=arrayIncludes.indexOf,objectKeysInternal=function(object,names){var key,O=toIndexedObject(object),i=0,result=[];for(key in O)!has(hiddenKeys,key)&&has(O,key)&&result.push(key);for(;names.length>i;)has(O,key=names[i++])&&(~indexOf(result,key)||result.push(key));return result},enumBugKeys=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],hiddenKeys$1=enumBugKeys.concat("length","prototype"),f$3=Object.getOwnPropertyNames||function(O){return objectKeysInternal(O,hiddenKeys$1)},objectGetOwnPropertyNames={f:f$3},f$4=Object.getOwnPropertySymbols,objectGetOwnPropertySymbols={f:f$4},ownKeys$1=getBuiltIn("Reflect","ownKeys")||function(it){var keys=objectGetOwnPropertyNames.f(anObject(it)),getOwnPropertySymbols=objectGetOwnPropertySymbols.f;return getOwnPropertySymbols?keys.concat(getOwnPropertySymbols(it)):keys},copyConstructorProperties=function(target,source){for(var keys=ownKeys$1(source),defineProperty=objectDefineProperty.f,getOwnPropertyDescriptor=objectGetOwnPropertyDescriptor.f,i=0;iindex;)objectDefineProperty.f(O,key=keys[index++],Properties[key]);return O},html=getBuiltIn("document","documentElement"),GT=">",LT="<",PROTOTYPE="prototype",SCRIPT="script",IE_PROTO$1=sharedKey("IE_PROTO"),EmptyConstructor=function(){},scriptTag=function(content){return LT+SCRIPT+GT+content+LT+"/"+SCRIPT+GT},NullProtoObjectViaActiveX=function(activeXDocument){activeXDocument.write(scriptTag("")),activeXDocument.close();var temp=activeXDocument.parentWindow.Object;return activeXDocument=null,temp},NullProtoObjectViaIFrame=function(){var iframeDocument,iframe=documentCreateElement("iframe"),JS="java"+SCRIPT+":";return iframe.style.display="none",html.appendChild(iframe),iframe.src=String(JS),(iframeDocument=iframe.contentWindow.document).open(),iframeDocument.write(scriptTag("document.F=Object")),iframeDocument.close(),iframeDocument.F},activeXDocument,NullProtoObject=function(){try{activeXDocument=document.domain&&new ActiveXObject("htmlfile")}catch(error){}NullProtoObject=activeXDocument?NullProtoObjectViaActiveX(activeXDocument):NullProtoObjectViaIFrame();for(var length=enumBugKeys.length;length--;)delete NullProtoObject[PROTOTYPE][enumBugKeys[length]];return NullProtoObject()};hiddenKeys[IE_PROTO$1]=!0;var objectCreate=Object.create||function(O,Properties){var result;return null!==O?(EmptyConstructor[PROTOTYPE]=anObject(O),result=new EmptyConstructor,EmptyConstructor[PROTOTYPE]=null,result[IE_PROTO$1]=O):result=NullProtoObject(),void 0===Properties?result:objectDefineProperties(result,Properties)},defineProperty=objectDefineProperty.f,TO_STRING_TAG=wellKnownSymbol("toStringTag"),setToStringTag=function(it,TAG,STATIC){it&&!has(it=STATIC?it:it.prototype,TO_STRING_TAG)&&defineProperty(it,TO_STRING_TAG,{configurable:!0,value:TAG})},iterators={},IteratorPrototype$1=iteratorsCore.IteratorPrototype,returnThis$1=function(){return this},createIteratorConstructor=function(IteratorConstructor,NAME,next){var TO_STRING_TAG=NAME+" Iterator";return IteratorConstructor.prototype=objectCreate(IteratorPrototype$1,{next:createPropertyDescriptor(1,next)}),setToStringTag(IteratorConstructor,TO_STRING_TAG,!1),iterators[TO_STRING_TAG]=returnThis$1,IteratorConstructor},aPossiblePrototype=function(it){if(!isObject(it)&&null!==it)throw TypeError("Can't set "+String(it)+" as a prototype");return it},objectSetPrototypeOf=Object.setPrototypeOf||("__proto__"in{}?function(){var setter,CORRECT_SETTER=!1,test={};try{(setter=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set).call(test,[]),CORRECT_SETTER=test instanceof Array}catch(error){}return function(O,proto){return anObject(O),aPossiblePrototype(proto),CORRECT_SETTER?setter.call(O,proto):O.__proto__=proto,O}}():void 0),IteratorPrototype$2=iteratorsCore.IteratorPrototype,BUGGY_SAFARI_ITERATORS$1=iteratorsCore.BUGGY_SAFARI_ITERATORS,ITERATOR$1=wellKnownSymbol("iterator"),KEYS="keys",VALUES="values",ENTRIES="entries",returnThis$2=function(){return this},defineIterator=function(Iterable,NAME,IteratorConstructor,next,DEFAULT,IS_SET,FORCED){createIteratorConstructor(IteratorConstructor,NAME,next);var CurrentIteratorPrototype,methods,KEY,getIterationMethod=function(KIND){if(KIND===DEFAULT&&defaultIterator)return defaultIterator;if(!BUGGY_SAFARI_ITERATORS$1&&KIND in IterablePrototype)return IterablePrototype[KIND];switch(KIND){case KEYS:case VALUES:case ENTRIES:return function(){return new IteratorConstructor(this,KIND)}}return function(){return new IteratorConstructor(this)}},TO_STRING_TAG=NAME+" Iterator",INCORRECT_VALUES_NAME=!1,IterablePrototype=Iterable.prototype,nativeIterator=IterablePrototype[ITERATOR$1]||IterablePrototype["@@iterator"]||DEFAULT&&IterablePrototype[DEFAULT],defaultIterator=!BUGGY_SAFARI_ITERATORS$1&&nativeIterator||getIterationMethod(DEFAULT),anyNativeIterator="Array"==NAME&&IterablePrototype.entries||nativeIterator;if(anyNativeIterator&&(CurrentIteratorPrototype=objectGetPrototypeOf(anyNativeIterator.call(new Iterable)),IteratorPrototype$2!==Object.prototype&&CurrentIteratorPrototype.next&&(objectGetPrototypeOf(CurrentIteratorPrototype)!==IteratorPrototype$2&&(objectSetPrototypeOf?objectSetPrototypeOf(CurrentIteratorPrototype,IteratorPrototype$2):"function"!=typeof CurrentIteratorPrototype[ITERATOR$1]&&createNonEnumerableProperty(CurrentIteratorPrototype,ITERATOR$1,returnThis$2)),setToStringTag(CurrentIteratorPrototype,TO_STRING_TAG,!0))),DEFAULT==VALUES&&nativeIterator&&nativeIterator.name!==VALUES&&(INCORRECT_VALUES_NAME=!0,defaultIterator=function(){return nativeIterator.call(this)}),IterablePrototype[ITERATOR$1]!==defaultIterator&&createNonEnumerableProperty(IterablePrototype,ITERATOR$1,defaultIterator),iterators[NAME]=defaultIterator,DEFAULT)if(methods={values:getIterationMethod(VALUES),keys:IS_SET?defaultIterator:getIterationMethod(KEYS),entries:getIterationMethod(ENTRIES)},FORCED)for(KEY in methods)(BUGGY_SAFARI_ITERATORS$1||INCORRECT_VALUES_NAME||!(KEY in IterablePrototype))&&redefine(IterablePrototype,KEY,methods[KEY]);else _export({target:NAME,proto:!0,forced:BUGGY_SAFARI_ITERATORS$1||INCORRECT_VALUES_NAME},methods);return methods},charAt=stringMultibyte.charAt,STRING_ITERATOR="String Iterator",setInternalState=internalState.set,getInternalState=internalState.getterFor(STRING_ITERATOR);defineIterator(String,"String",(function(iterated){setInternalState(this,{type:STRING_ITERATOR,string:String(iterated),index:0})}),(function(){var point,state=getInternalState(this),string=state.string,index=state.index;return index>=string.length?{value:void 0,done:!0}:(point=charAt(string,index),state.index+=point.length,{value:point,done:!1})}));var aFunction$1=function(it){if("function"!=typeof it)throw TypeError(String(it)+" is not a function");return it},functionBindContext=function(fn,that,length){if(aFunction$1(fn),void 0===that)return fn;switch(length){case 0:return function(){return fn.call(that)};case 1:return function(a){return fn.call(that,a)};case 2:return function(a,b){return fn.call(that,a,b)};case 3:return function(a,b,c){return fn.call(that,a,b,c)}}return function(){return fn.apply(that,arguments)}},callWithSafeIterationClosing=function(iterator,fn,value,ENTRIES){try{return ENTRIES?fn(anObject(value)[0],value[1]):fn(value)}catch(error){var returnMethod=iterator.return;throw void 0!==returnMethod&&anObject(returnMethod.call(iterator)),error}},ITERATOR$2=wellKnownSymbol("iterator"),ArrayPrototype=Array.prototype,isArrayIteratorMethod=function(it){return void 0!==it&&(iterators.Array===it||ArrayPrototype[ITERATOR$2]===it)},createProperty=function(object,key,value){var propertyKey=toPrimitive(key);propertyKey in object?objectDefineProperty.f(object,propertyKey,createPropertyDescriptor(0,value)):object[propertyKey]=value},TO_STRING_TAG$1=wellKnownSymbol("toStringTag"),test={};test[TO_STRING_TAG$1]="z";var toStringTagSupport="[object z]"===String(test),TO_STRING_TAG$2=wellKnownSymbol("toStringTag"),CORRECT_ARGUMENTS="Arguments"==classofRaw(function(){return arguments}()),tryGet=function(it,key){try{return it[key]}catch(error){}},classof=toStringTagSupport?classofRaw:function(it){var O,tag,result;return void 0===it?"Undefined":null===it?"Null":"string"==typeof(tag=tryGet(O=Object(it),TO_STRING_TAG$2))?tag:CORRECT_ARGUMENTS?classofRaw(O):"Object"==(result=classofRaw(O))&&"function"==typeof O.callee?"Arguments":result},ITERATOR$3=wellKnownSymbol("iterator"),getIteratorMethod=function(it){if(null!=it)return it[ITERATOR$3]||it["@@iterator"]||iterators[classof(it)]},arrayFrom=function(arrayLike){var length,result,step,iterator,next,value,O=toObject(arrayLike),C="function"==typeof this?this:Array,argumentsLength=arguments.length,mapfn=argumentsLength>1?arguments[1]:void 0,mapping=void 0!==mapfn,iteratorMethod=getIteratorMethod(O),index=0;if(mapping&&(mapfn=functionBindContext(mapfn,argumentsLength>2?arguments[2]:void 0,2)),null==iteratorMethod||C==Array&&isArrayIteratorMethod(iteratorMethod))for(result=new C(length=toLength(O.length));length>index;index++)value=mapping?mapfn(O[index],index):O[index],createProperty(result,index,value);else for(next=(iterator=iteratorMethod.call(O)).next,result=new C;!(step=next.call(iterator)).done;index++)value=mapping?callWithSafeIterationClosing(iterator,mapfn,[step.value,index],!0):step.value,createProperty(result,index,value);return result.length=index,result},ITERATOR$4=wellKnownSymbol("iterator"),SAFE_CLOSING=!1;try{var called=0,iteratorWithReturn={next:function(){return{done:!!called++}},return:function(){SAFE_CLOSING=!0}};iteratorWithReturn[ITERATOR$4]=function(){return this},Array.from(iteratorWithReturn,(function(){throw 2}))}catch(error){}var checkCorrectnessOfIteration=function(exec,SKIP_CLOSING){if(!SKIP_CLOSING&&!SAFE_CLOSING)return!1;var ITERATION_SUPPORT=!1;try{var object={};object[ITERATOR$4]=function(){return{next:function(){return{done:ITERATION_SUPPORT=!0}}}},exec(object)}catch(error){}return ITERATION_SUPPORT},INCORRECT_ITERATION=!checkCorrectnessOfIteration((function(iterable){Array.from(iterable)}));_export({target:"Array",stat:!0,forced:INCORRECT_ITERATION},{from:arrayFrom});var from_1=path.Array.from,UNSCOPABLES=wellKnownSymbol("unscopables"),ArrayPrototype$1=Array.prototype;null==ArrayPrototype$1[UNSCOPABLES]&&objectDefineProperty.f(ArrayPrototype$1,UNSCOPABLES,{configurable:!0,value:objectCreate(null)});var addToUnscopables=function(key){ArrayPrototype$1[UNSCOPABLES][key]=!0},defineProperty$1=Object.defineProperty,cache={},thrower=function(it){throw it},arrayMethodUsesToLength=function(METHOD_NAME,options){if(has(cache,METHOD_NAME))return cache[METHOD_NAME];options||(options={});var method=[][METHOD_NAME],ACCESSORS=!!has(options,"ACCESSORS")&&options.ACCESSORS,argument0=has(options,0)?options[0]:thrower,argument1=has(options,1)?options[1]:void 0;return cache[METHOD_NAME]=!!method&&!fails((function(){if(ACCESSORS&&!descriptors)return!0;var O={length:-1};ACCESSORS?defineProperty$1(O,1,{enumerable:!0,get:thrower}):O[1]=1,method.call(O,argument0,argument1)}))},$includes=arrayIncludes.includes,USES_TO_LENGTH=arrayMethodUsesToLength("indexOf",{ACCESSORS:!0,1:0});_export({target:"Array",proto:!0,forced:!USES_TO_LENGTH},{includes:function(el){return $includes(this,el,arguments.length>1?arguments[1]:void 0)}}),addToUnscopables("includes");var call=Function.call,entryUnbind=function(CONSTRUCTOR,METHOD,length){return functionBindContext(call,global_1[CONSTRUCTOR].prototype[METHOD],length)},includes=entryUnbind("Array","includes"),isArray=Array.isArray||function(arg){return"Array"==classofRaw(arg)},flattenIntoArray=function(target,original,source,sourceLen,start,depth,mapper,thisArg){for(var element,targetIndex=start,sourceIndex=0,mapFn=!!mapper&&functionBindContext(mapper,thisArg,3);sourceIndex0&&isArray(element))targetIndex=flattenIntoArray(target,original,element,toLength(element.length),targetIndex,depth-1)-1;else{if(targetIndex>=9007199254740991)throw TypeError("Exceed the acceptable array length");target[targetIndex]=element}targetIndex++}sourceIndex++}return targetIndex},flattenIntoArray_1=flattenIntoArray,SPECIES=wellKnownSymbol("species"),arraySpeciesCreate=function(originalArray,length){var C;return isArray(originalArray)&&("function"!=typeof(C=originalArray.constructor)||C!==Array&&!isArray(C.prototype)?isObject(C)&&null===(C=C[SPECIES])&&(C=void 0):C=void 0),new(void 0===C?Array:C)(0===length?0:length)};_export({target:"Array",proto:!0},{flat:function(){var depthArg=arguments.length?arguments[0]:void 0,O=toObject(this),sourceLen=toLength(O.length),A=arraySpeciesCreate(O,0);return A.length=flattenIntoArray_1(A,O,O,sourceLen,0,void 0===depthArg?1:toInteger(depthArg)),A}}),addToUnscopables("flat");var flat=entryUnbind("Array","flat"),push=[].push,createMethod$2=function(TYPE){var IS_MAP=1==TYPE,IS_FILTER=2==TYPE,IS_SOME=3==TYPE,IS_EVERY=4==TYPE,IS_FIND_INDEX=6==TYPE,NO_HOLES=5==TYPE||IS_FIND_INDEX;return function($this,callbackfn,that,specificCreate){for(var value,result,O=toObject($this),self=indexedObject(O),boundFunction=functionBindContext(callbackfn,that,3),length=toLength(self.length),index=0,create=specificCreate||arraySpeciesCreate,target=IS_MAP?create($this,length):IS_FILTER?create($this,0):void 0;length>index;index++)if((NO_HOLES||index in self)&&(result=boundFunction(value=self[index],index,O),TYPE))if(IS_MAP)target[index]=result;else if(result)switch(TYPE){case 3:return!0;case 5:return value;case 6:return index;case 2:push.call(target,value)}else if(IS_EVERY)return!1;return IS_FIND_INDEX?-1:IS_SOME||IS_EVERY?IS_EVERY:target}},arrayIteration={forEach:createMethod$2(0),map:createMethod$2(1),filter:createMethod$2(2),some:createMethod$2(3),every:createMethod$2(4),find:createMethod$2(5),findIndex:createMethod$2(6)},$find=arrayIteration.find,FIND="find",SKIPS_HOLES=!0,USES_TO_LENGTH$1=arrayMethodUsesToLength(FIND);FIND in[]&&Array(1)[FIND]((function(){SKIPS_HOLES=!1})),_export({target:"Array",proto:!0,forced:SKIPS_HOLES||!USES_TO_LENGTH$1},{find:function(callbackfn){return $find(this,callbackfn,arguments.length>1?arguments[1]:void 0)}}),addToUnscopables(FIND);var find=entryUnbind("Array","find"),nativeAssign=Object.assign,defineProperty$2=Object.defineProperty,objectAssign=!nativeAssign||fails((function(){if(descriptors&&1!==nativeAssign({b:1},nativeAssign(defineProperty$2({},"a",{enumerable:!0,get:function(){defineProperty$2(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var A={},B={},symbol=Symbol();return A[symbol]=7,"abcdefghijklmnopqrst".split("").forEach((function(chr){B[chr]=chr})),7!=nativeAssign({},A)[symbol]||"abcdefghijklmnopqrst"!=objectKeys(nativeAssign({},B)).join("")}))?function(target,source){for(var T=toObject(target),argumentsLength=arguments.length,index=1,getOwnPropertySymbols=objectGetOwnPropertySymbols.f,propertyIsEnumerable=objectPropertyIsEnumerable.f;argumentsLength>index;)for(var key,S=indexedObject(arguments[index++]),keys=getOwnPropertySymbols?objectKeys(S).concat(getOwnPropertySymbols(S)):objectKeys(S),length=keys.length,j=0;length>j;)key=keys[j++],descriptors&&!propertyIsEnumerable.call(S,key)||(T[key]=S[key]);return T}:nativeAssign;_export({target:"Object",stat:!0,forced:Object.assign!==objectAssign},{assign:objectAssign});var assign=path.Object.assign,propertyIsEnumerable=objectPropertyIsEnumerable.f,createMethod$3=function(TO_ENTRIES){return function(it){for(var key,O=toIndexedObject(it),keys=objectKeys(O),length=keys.length,i=0,result=[];length>i;)key=keys[i++],descriptors&&!propertyIsEnumerable.call(O,key)||result.push(TO_ENTRIES?[key,O[key]]:O[key]);return result}},objectToArray={entries:createMethod$3(!0),values:createMethod$3(!1)},$entries=objectToArray.entries;_export({target:"Object",stat:!0},{entries:function(O){return $entries(O)}});var entries=path.Object.entries,$values=objectToArray.values;_export({target:"Object",stat:!0},{values:function(O){return $values(O)}});var values=path.Object.values,objectToString=toStringTagSupport?{}.toString:function(){return"[object "+classof(this)+"]"};toStringTagSupport||redefine(Object.prototype,"toString",objectToString,{unsafe:!0});var domIterables={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0},ARRAY_ITERATOR="Array Iterator",setInternalState$1=internalState.set,getInternalState$1=internalState.getterFor(ARRAY_ITERATOR),es_array_iterator=defineIterator(Array,"Array",(function(iterated,kind){setInternalState$1(this,{type:ARRAY_ITERATOR,target:toIndexedObject(iterated),index:0,kind:kind})}),(function(){var state=getInternalState$1(this),target=state.target,kind=state.kind,index=state.index++;return!target||index>=target.length?(state.target=void 0,{value:void 0,done:!0}):"keys"==kind?{value:index,done:!1}:"values"==kind?{value:target[index],done:!1}:{value:[index,target[index]],done:!1}}),"values");iterators.Arguments=iterators.Array,addToUnscopables("keys"),addToUnscopables("values"),addToUnscopables("entries");var ITERATOR$5=wellKnownSymbol("iterator"),TO_STRING_TAG$3=wellKnownSymbol("toStringTag"),ArrayValues=es_array_iterator.values;for(var COLLECTION_NAME in domIterables){var Collection=global_1[COLLECTION_NAME],CollectionPrototype=Collection&&Collection.prototype;if(CollectionPrototype){if(CollectionPrototype[ITERATOR$5]!==ArrayValues)try{createNonEnumerableProperty(CollectionPrototype,ITERATOR$5,ArrayValues)}catch(error){CollectionPrototype[ITERATOR$5]=ArrayValues}if(CollectionPrototype[TO_STRING_TAG$3]||createNonEnumerableProperty(CollectionPrototype,TO_STRING_TAG$3,COLLECTION_NAME),domIterables[COLLECTION_NAME])for(var METHOD_NAME in es_array_iterator)if(CollectionPrototype[METHOD_NAME]!==es_array_iterator[METHOD_NAME])try{createNonEnumerableProperty(CollectionPrototype,METHOD_NAME,es_array_iterator[METHOD_NAME])}catch(error){CollectionPrototype[METHOD_NAME]=es_array_iterator[METHOD_NAME]}}}var nativePromiseConstructor=global_1.Promise,redefineAll=function(target,src,options){for(var key in src)redefine(target,key,src[key],options);return target},SPECIES$1=wellKnownSymbol("species"),setSpecies=function(CONSTRUCTOR_NAME){var Constructor=getBuiltIn(CONSTRUCTOR_NAME),defineProperty=objectDefineProperty.f;descriptors&&Constructor&&!Constructor[SPECIES$1]&&defineProperty(Constructor,SPECIES$1,{configurable:!0,get:function(){return this}})},anInstance=function(it,Constructor,name){if(!(it instanceof Constructor))throw TypeError("Incorrect "+(name?name+" ":"")+"invocation");return it},iterate_1=createCommonjsModule((function(module){var Result=function(stopped,result){this.stopped=stopped,this.result=result};(module.exports=function(iterable,fn,that,AS_ENTRIES,IS_ITERATOR){var iterator,iterFn,index,length,result,next,step,boundFunction=functionBindContext(fn,that,AS_ENTRIES?2:1);if(IS_ITERATOR)iterator=iterable;else{if("function"!=typeof(iterFn=getIteratorMethod(iterable)))throw TypeError("Target is not iterable");if(isArrayIteratorMethod(iterFn)){for(index=0,length=toLength(iterable.length);length>index;index++)if((result=AS_ENTRIES?boundFunction(anObject(step=iterable[index])[0],step[1]):boundFunction(iterable[index]))&&result instanceof Result)return result;return new Result(!1)}iterator=iterFn.call(iterable)}for(next=iterator.next;!(step=next.call(iterator)).done;)if("object"==typeof(result=callWithSafeIterationClosing(iterator,boundFunction,step.value,AS_ENTRIES))&&result&&result instanceof Result)return result;return new Result(!1)}).stop=function(result){return new Result(!0,result)}})),SPECIES$2=wellKnownSymbol("species"),speciesConstructor=function(O,defaultConstructor){var S,C=anObject(O).constructor;return void 0===C||null==(S=anObject(C)[SPECIES$2])?defaultConstructor:aFunction$1(S)},engineUserAgent=getBuiltIn("navigator","userAgent")||"",engineIsIos=/(iphone|ipod|ipad).*applewebkit/i.test(engineUserAgent),location=global_1.location,set$1=global_1.setImmediate,clear=global_1.clearImmediate,process=global_1.process,MessageChannel=global_1.MessageChannel,Dispatch=global_1.Dispatch,counter=0,queue={},ONREADYSTATECHANGE="onreadystatechange",defer,channel,port,run=function(id){if(queue.hasOwnProperty(id)){var fn=queue[id];delete queue[id],fn()}},runner=function(id){return function(){run(id)}},listener=function(event){run(event.data)},post=function(id){global_1.postMessage(id+"",location.protocol+"//"+location.host)};set$1&&clear||(set$1=function(fn){for(var args=[],i=1;arguments.length>i;)args.push(arguments[i++]);return queue[++counter]=function(){("function"==typeof fn?fn:Function(fn)).apply(void 0,args)},defer(counter),counter},clear=function(id){delete queue[id]},"process"==classofRaw(process)?defer=function(id){process.nextTick(runner(id))}:Dispatch&&Dispatch.now?defer=function(id){Dispatch.now(runner(id))}:MessageChannel&&!engineIsIos?(channel=new MessageChannel,port=channel.port2,channel.port1.onmessage=listener,defer=functionBindContext(port.postMessage,port,1)):!global_1.addEventListener||"function"!=typeof postMessage||global_1.importScripts||fails(post)||"file:"===location.protocol?defer=ONREADYSTATECHANGE in documentCreateElement("script")?function(id){html.appendChild(documentCreateElement("script"))[ONREADYSTATECHANGE]=function(){html.removeChild(this),run(id)}}:function(id){setTimeout(runner(id),0)}:(defer=post,global_1.addEventListener("message",listener,!1)));var task={set:set$1,clear:clear},getOwnPropertyDescriptor$2=objectGetOwnPropertyDescriptor.f,macrotask=task.set,MutationObserver=global_1.MutationObserver||global_1.WebKitMutationObserver,process$1=global_1.process,Promise$1=global_1.Promise,IS_NODE="process"==classofRaw(process$1),queueMicrotaskDescriptor=getOwnPropertyDescriptor$2(global_1,"queueMicrotask"),queueMicrotask=queueMicrotaskDescriptor&&queueMicrotaskDescriptor.value,flush,head,last,notify,toggle,node,promise,then;queueMicrotask||(flush=function(){var parent,fn;for(IS_NODE&&(parent=process$1.domain)&&parent.exit();head;){fn=head.fn,head=head.next;try{fn()}catch(error){throw head?notify():last=void 0,error}}last=void 0,parent&&parent.enter()},IS_NODE?notify=function(){process$1.nextTick(flush)}:MutationObserver&&!engineIsIos?(toggle=!0,node=document.createTextNode(""),new MutationObserver(flush).observe(node,{characterData:!0}),notify=function(){node.data=toggle=!toggle}):Promise$1&&Promise$1.resolve?(promise=Promise$1.resolve(void 0),then=promise.then,notify=function(){then.call(promise,flush)}):notify=function(){macrotask.call(global_1,flush)});var microtask=queueMicrotask||function(fn){var task={fn:fn,next:void 0};last&&(last.next=task),head||(head=task,notify()),last=task},PromiseCapability=function(C){var resolve,reject;this.promise=new C((function($$resolve,$$reject){if(void 0!==resolve||void 0!==reject)throw TypeError("Bad Promise constructor");resolve=$$resolve,reject=$$reject})),this.resolve=aFunction$1(resolve),this.reject=aFunction$1(reject)},f$5=function(C){return new PromiseCapability(C)},newPromiseCapability={f:f$5},promiseResolve=function(C,x){if(anObject(C),isObject(x)&&x.constructor===C)return x;var promiseCapability=newPromiseCapability.f(C);return(0,promiseCapability.resolve)(x),promiseCapability.promise},hostReportErrors=function(a,b){var console=global_1.console;console&&console.error&&(1===arguments.length?console.error(a):console.error(a,b))},perform=function(exec){try{return{error:!1,value:exec()}}catch(error){return{error:!0,value:error}}},process$2=global_1.process,versions=process$2&&process$2.versions,v8=versions&&versions.v8,match,version;v8?(match=v8.split("."),version=match[0]+match[1]):engineUserAgent&&(match=engineUserAgent.match(/Edge\/(\d+)/),(!match||match[1]>=74)&&(match=engineUserAgent.match(/Chrome\/(\d+)/),match&&(version=match[1])));var engineV8Version=version&&+version,task$1=task.set,SPECIES$3=wellKnownSymbol("species"),PROMISE="Promise",getInternalState$2=internalState.get,setInternalState$2=internalState.set,getInternalPromiseState=internalState.getterFor(PROMISE),PromiseConstructor=nativePromiseConstructor,TypeError$1=global_1.TypeError,document$2=global_1.document,process$3=global_1.process,$fetch=getBuiltIn("fetch"),newPromiseCapability$1=newPromiseCapability.f,newGenericPromiseCapability=newPromiseCapability$1,IS_NODE$1="process"==classofRaw(process$3),DISPATCH_EVENT=!!(document$2&&document$2.createEvent&&global_1.dispatchEvent),UNHANDLED_REJECTION="unhandledrejection",REJECTION_HANDLED="rejectionhandled",PENDING=0,FULFILLED=1,REJECTED=2,HANDLED=1,UNHANDLED=2,Internal,OwnPromiseCapability,PromiseWrapper,nativeThen,FORCED=isForced_1(PROMISE,(function(){if(!(inspectSource(PromiseConstructor)!==String(PromiseConstructor))){if(66===engineV8Version)return!0;if(!IS_NODE$1&&"function"!=typeof PromiseRejectionEvent)return!0}if(engineV8Version>=51&&/native code/.test(PromiseConstructor))return!1;var promise=PromiseConstructor.resolve(1),FakePromise=function(exec){exec((function(){}),(function(){}))};return(promise.constructor={})[SPECIES$3]=FakePromise,!(promise.then((function(){}))instanceof FakePromise)})),INCORRECT_ITERATION$1=FORCED||!checkCorrectnessOfIteration((function(iterable){PromiseConstructor.all(iterable).catch((function(){}))})),isThenable=function(it){var then;return!(!isObject(it)||"function"!=typeof(then=it.then))&&then},notify$1=function(promise,state,isReject){if(!state.notified){state.notified=!0;var chain=state.reactions;microtask((function(){for(var value=state.value,ok=state.state==FULFILLED,index=0;chain.length>index;){var result,then,exited,reaction=chain[index++],handler=ok?reaction.ok:reaction.fail,resolve=reaction.resolve,reject=reaction.reject,domain=reaction.domain;try{handler?(ok||(state.rejection===UNHANDLED&&onHandleUnhandled(promise,state),state.rejection=HANDLED),!0===handler?result=value:(domain&&domain.enter(),result=handler(value),domain&&(domain.exit(),exited=!0)),result===reaction.promise?reject(TypeError$1("Promise-chain cycle")):(then=isThenable(result))?then.call(result,resolve,reject):resolve(result)):reject(value)}catch(error){domain&&!exited&&domain.exit(),reject(error)}}state.reactions=[],state.notified=!1,isReject&&!state.rejection&&onUnhandled(promise,state)}))}},dispatchEvent=function(name,promise,reason){var event,handler;DISPATCH_EVENT?((event=document$2.createEvent("Event")).promise=promise,event.reason=reason,event.initEvent(name,!1,!0),global_1.dispatchEvent(event)):event={promise:promise,reason:reason},(handler=global_1["on"+name])?handler(event):name===UNHANDLED_REJECTION&&hostReportErrors("Unhandled promise rejection",reason)},onUnhandled=function(promise,state){task$1.call(global_1,(function(){var result,value=state.value;if(isUnhandled(state)&&(result=perform((function(){IS_NODE$1?process$3.emit("unhandledRejection",value,promise):dispatchEvent(UNHANDLED_REJECTION,promise,value)})),state.rejection=IS_NODE$1||isUnhandled(state)?UNHANDLED:HANDLED,result.error))throw result.value}))},isUnhandled=function(state){return state.rejection!==HANDLED&&!state.parent},onHandleUnhandled=function(promise,state){task$1.call(global_1,(function(){IS_NODE$1?process$3.emit("rejectionHandled",promise):dispatchEvent(REJECTION_HANDLED,promise,state.value)}))},bind=function(fn,promise,state,unwrap){return function(value){fn(promise,state,value,unwrap)}},internalReject=function(promise,state,value,unwrap){state.done||(state.done=!0,unwrap&&(state=unwrap),state.value=value,state.state=REJECTED,notify$1(promise,state,!0))},internalResolve=function(promise,state,value,unwrap){if(!state.done){state.done=!0,unwrap&&(state=unwrap);try{if(promise===value)throw TypeError$1("Promise can't be resolved itself");var then=isThenable(value);then?microtask((function(){var wrapper={done:!1};try{then.call(value,bind(internalResolve,promise,wrapper,state),bind(internalReject,promise,wrapper,state))}catch(error){internalReject(promise,wrapper,error,state)}})):(state.value=value,state.state=FULFILLED,notify$1(promise,state,!1))}catch(error){internalReject(promise,{done:!1},error,state)}}};FORCED&&(PromiseConstructor=function(executor){anInstance(this,PromiseConstructor,PROMISE),aFunction$1(executor),Internal.call(this);var state=getInternalState$2(this);try{executor(bind(internalResolve,this,state),bind(internalReject,this,state))}catch(error){internalReject(this,state,error)}},Internal=function(executor){setInternalState$2(this,{type:PROMISE,done:!1,notified:!1,parent:!1,reactions:[],rejection:!1,state:PENDING,value:void 0})},Internal.prototype=redefineAll(PromiseConstructor.prototype,{then:function(onFulfilled,onRejected){var state=getInternalPromiseState(this),reaction=newPromiseCapability$1(speciesConstructor(this,PromiseConstructor));return reaction.ok="function"!=typeof onFulfilled||onFulfilled,reaction.fail="function"==typeof onRejected&&onRejected,reaction.domain=IS_NODE$1?process$3.domain:void 0,state.parent=!0,state.reactions.push(reaction),state.state!=PENDING&¬ify$1(this,state,!1),reaction.promise},catch:function(onRejected){return this.then(void 0,onRejected)}}),OwnPromiseCapability=function(){var promise=new Internal,state=getInternalState$2(promise);this.promise=promise,this.resolve=bind(internalResolve,promise,state),this.reject=bind(internalReject,promise,state)},newPromiseCapability.f=newPromiseCapability$1=function(C){return C===PromiseConstructor||C===PromiseWrapper?new OwnPromiseCapability(C):newGenericPromiseCapability(C)},"function"==typeof nativePromiseConstructor&&(nativeThen=nativePromiseConstructor.prototype.then,redefine(nativePromiseConstructor.prototype,"then",(function(onFulfilled,onRejected){var that=this;return new PromiseConstructor((function(resolve,reject){nativeThen.call(that,resolve,reject)})).then(onFulfilled,onRejected)}),{unsafe:!0}),"function"==typeof $fetch&&_export({global:!0,enumerable:!0,forced:!0},{fetch:function(input){return promiseResolve(PromiseConstructor,$fetch.apply(global_1,arguments))}}))),_export({global:!0,wrap:!0,forced:FORCED},{Promise:PromiseConstructor}),setToStringTag(PromiseConstructor,PROMISE,!1),setSpecies(PROMISE),PromiseWrapper=getBuiltIn(PROMISE),_export({target:PROMISE,stat:!0,forced:FORCED},{reject:function(r){var capability=newPromiseCapability$1(this);return capability.reject.call(void 0,r),capability.promise}}),_export({target:PROMISE,stat:!0,forced:FORCED},{resolve:function(x){return promiseResolve(this,x)}}),_export({target:PROMISE,stat:!0,forced:INCORRECT_ITERATION$1},{all:function(iterable){var C=this,capability=newPromiseCapability$1(C),resolve=capability.resolve,reject=capability.reject,result=perform((function(){var $promiseResolve=aFunction$1(C.resolve),values=[],counter=0,remaining=1;iterate_1(iterable,(function(promise){var index=counter++,alreadyCalled=!1;values.push(void 0),remaining++,$promiseResolve.call(C,promise).then((function(value){alreadyCalled||(alreadyCalled=!0,values[index]=value,--remaining||resolve(values))}),reject)})),--remaining||resolve(values)}));return result.error&&reject(result.value),capability.promise},race:function(iterable){var C=this,capability=newPromiseCapability$1(C),reject=capability.reject,result=perform((function(){var $promiseResolve=aFunction$1(C.resolve);iterate_1(iterable,(function(promise){$promiseResolve.call(C,promise).then(capability.resolve,reject)}))}));return result.error&&reject(result.value),capability.promise}}),_export({target:"Promise",stat:!0},{allSettled:function(iterable){var C=this,capability=newPromiseCapability.f(C),resolve=capability.resolve,reject=capability.reject,result=perform((function(){var promiseResolve=aFunction$1(C.resolve),values=[],counter=0,remaining=1;iterate_1(iterable,(function(promise){var index=counter++,alreadyCalled=!1;values.push(void 0),remaining++,promiseResolve.call(C,promise).then((function(value){alreadyCalled||(alreadyCalled=!0,values[index]={status:"fulfilled",value:value},--remaining||resolve(values))}),(function(e){alreadyCalled||(alreadyCalled=!0,values[index]={status:"rejected",reason:e},--remaining||resolve(values))}))})),--remaining||resolve(values)}));return result.error&&reject(result.value),capability.promise}});var NON_GENERIC=!!nativePromiseConstructor&&fails((function(){nativePromiseConstructor.prototype.finally.call({then:function(){}},(function(){}))}));_export({target:"Promise",proto:!0,real:!0,forced:NON_GENERIC},{finally:function(onFinally){var C=speciesConstructor(this,getBuiltIn("Promise")),isFunction="function"==typeof onFinally;return this.then(isFunction?function(x){return promiseResolve(C,onFinally()).then((function(){return x}))}:onFinally,isFunction?function(e){return promiseResolve(C,onFinally()).then((function(){throw e}))}:onFinally)}}),"function"!=typeof nativePromiseConstructor||nativePromiseConstructor.prototype.finally||redefine(nativePromiseConstructor.prototype,"finally",getBuiltIn("Promise").prototype.finally);var promise$1=path.Promise,setInternalState$3=internalState.set,getInternalAggregateErrorState=internalState.getterFor("AggregateError"),$AggregateError=function(errors,message){var that=this;if(!(that instanceof $AggregateError))return new $AggregateError(errors,message);objectSetPrototypeOf&&(that=objectSetPrototypeOf(new Error(message),objectGetPrototypeOf(that)));var errorsArray=[];return iterate_1(errors,errorsArray.push,errorsArray),descriptors?setInternalState$3(that,{errors:errorsArray,type:"AggregateError"}):that.errors=errorsArray,void 0!==message&&createNonEnumerableProperty(that,"message",String(message)),that};$AggregateError.prototype=objectCreate(Error.prototype,{constructor:createPropertyDescriptor(5,$AggregateError),message:createPropertyDescriptor(5,""),name:createPropertyDescriptor(5,"AggregateError")}),descriptors&&objectDefineProperty.f($AggregateError.prototype,"errors",{get:function(){return getInternalAggregateErrorState(this).errors},configurable:!0}),_export({global:!0},{AggregateError:$AggregateError}),_export({target:"Promise",stat:!0},{try:function(callbackfn){var promiseCapability=newPromiseCapability.f(this),result=perform(callbackfn);return(result.error?promiseCapability.reject:promiseCapability.resolve)(result.value),promiseCapability.promise}});var PROMISE_ANY_ERROR="No one promise resolved";_export({target:"Promise",stat:!0},{any:function(iterable){var C=this,capability=newPromiseCapability.f(C),resolve=capability.resolve,reject=capability.reject,result=perform((function(){var promiseResolve=aFunction$1(C.resolve),errors=[],counter=0,remaining=1,alreadyResolved=!1;iterate_1(iterable,(function(promise){var index=counter++,alreadyRejected=!1;errors.push(void 0),remaining++,promiseResolve.call(C,promise).then((function(value){alreadyRejected||alreadyResolved||(alreadyResolved=!0,resolve(value))}),(function(e){alreadyRejected||alreadyResolved||(alreadyRejected=!0,errors[index]=e,--remaining||reject(new(getBuiltIn("AggregateError"))(errors,PROMISE_ANY_ERROR)))}))})),--remaining||reject(new(getBuiltIn("AggregateError"))(errors,PROMISE_ANY_ERROR))}));return result.error&&reject(result.value),capability.promise}});var MATCH=wellKnownSymbol("match"),isRegexp=function(it){var isRegExp;return isObject(it)&&(void 0!==(isRegExp=it[MATCH])?!!isRegExp:"RegExp"==classofRaw(it))},notARegexp=function(it){if(isRegexp(it))throw TypeError("The method doesn't accept regular expressions");return it},MATCH$1=wellKnownSymbol("match"),correctIsRegexpLogic=function(METHOD_NAME){var regexp=/./;try{"/./"[METHOD_NAME](regexp)}catch(e){try{return regexp[MATCH$1]=!1,"/./"[METHOD_NAME](regexp)}catch(f){}}return!1},getOwnPropertyDescriptor$3=objectGetOwnPropertyDescriptor.f,nativeStartsWith="".startsWith,min$2=Math.min,CORRECT_IS_REGEXP_LOGIC=correctIsRegexpLogic("startsWith"),MDN_POLYFILL_BUG=!(CORRECT_IS_REGEXP_LOGIC||(descriptor=getOwnPropertyDescriptor$3(String.prototype,"startsWith"),!descriptor||descriptor.writable)),descriptor;_export({target:"String",proto:!0,forced:!MDN_POLYFILL_BUG&&!CORRECT_IS_REGEXP_LOGIC},{startsWith:function(searchString){var that=String(requireObjectCoercible(this));notARegexp(searchString);var index=toLength(min$2(arguments.length>1?arguments[1]:void 0,that.length)),search=String(searchString);return nativeStartsWith?nativeStartsWith.call(that,search,index):that.slice(index,index+search.length)===search}});var startsWith=entryUnbind("String","startsWith"),global$1="undefined"!=typeof globalThis&&globalThis||"undefined"!=typeof self&&self||void 0!==global$1&&global$1,support={searchParams:"URLSearchParams"in global$1,iterable:"Symbol"in global$1&&"iterator"in Symbol,blob:"FileReader"in global$1&&"Blob"in global$1&&function(){try{return new Blob,!0}catch(e){return!1}}(),formData:"FormData"in global$1,arrayBuffer:"ArrayBuffer"in global$1};function isDataView(obj){return obj&&DataView.prototype.isPrototypeOf(obj)}if(support.arrayBuffer)var viewClasses=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],isArrayBufferView=ArrayBuffer.isView||function(obj){return obj&&viewClasses.indexOf(Object.prototype.toString.call(obj))>-1};function normalizeName(name){if("string"!=typeof name&&(name=String(name)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name)||""===name)throw new TypeError("Invalid character in header field name");return name.toLowerCase()}function normalizeValue(value){return"string"!=typeof value&&(value=String(value)),value}function iteratorFor(items){var iterator={next:function(){var value=items.shift();return{done:void 0===value,value:value}}};return support.iterable&&(iterator[Symbol.iterator]=function(){return iterator}),iterator}function Headers(headers){this.map={},headers instanceof Headers?headers.forEach((function(value,name){this.append(name,value)}),this):Array.isArray(headers)?headers.forEach((function(header){this.append(header[0],header[1])}),this):headers&&Object.getOwnPropertyNames(headers).forEach((function(name){this.append(name,headers[name])}),this)}function consumed(body){if(body.bodyUsed)return Promise.reject(new TypeError("Already read"));body.bodyUsed=!0}function fileReaderReady(reader){return new Promise((function(resolve,reject){reader.onload=function(){resolve(reader.result)},reader.onerror=function(){reject(reader.error)}}))}function readBlobAsArrayBuffer(blob){var reader=new FileReader,promise=fileReaderReady(reader);return reader.readAsArrayBuffer(blob),promise}function readBlobAsText(blob){var reader=new FileReader,promise=fileReaderReady(reader);return reader.readAsText(blob),promise}function readArrayBufferAsText(buf){for(var view=new Uint8Array(buf),chars=new Array(view.length),i=0;i-1?upcased:method}function Request(input,options){if(!(this instanceof Request))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');var body=(options=options||{}).body;if(input instanceof Request){if(input.bodyUsed)throw new TypeError("Already read");this.url=input.url,this.credentials=input.credentials,options.headers||(this.headers=new Headers(input.headers)),this.method=input.method,this.mode=input.mode,this.signal=input.signal,body||null==input._bodyInit||(body=input._bodyInit,input.bodyUsed=!0)}else this.url=String(input);if(this.credentials=options.credentials||this.credentials||"same-origin",!options.headers&&this.headers||(this.headers=new Headers(options.headers)),this.method=normalizeMethod(options.method||this.method||"GET"),this.mode=options.mode||this.mode||null,this.signal=options.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&body)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(body),!("GET"!==this.method&&"HEAD"!==this.method||"no-store"!==options.cache&&"no-cache"!==options.cache)){var reParamSearch=/([?&])_=[^&]*/;if(reParamSearch.test(this.url))this.url=this.url.replace(reParamSearch,"$1_="+(new Date).getTime());else{this.url+=(/\?/.test(this.url)?"&":"?")+"_="+(new Date).getTime()}}}function decode(body){var form=new FormData;return body.trim().split("&").forEach((function(bytes){if(bytes){var split=bytes.split("="),name=split.shift().replace(/\+/g," "),value=split.join("=").replace(/\+/g," ");form.append(decodeURIComponent(name),decodeURIComponent(value))}})),form}function parseHeaders(rawHeaders){var headers=new Headers;return rawHeaders.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach((function(line){var parts=line.split(":"),key=parts.shift().trim();if(key){var value=parts.join(":").trim();headers.append(key,value)}})),headers}function Response(bodyInit,options){if(!(this instanceof Response))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');options||(options={}),this.type="default",this.status=void 0===options.status?200:options.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in options?options.statusText:"",this.headers=new Headers(options.headers),this.url=options.url||"",this._initBody(bodyInit)}Request.prototype.clone=function(){return new Request(this,{body:this._bodyInit})},Body.call(Request.prototype),Body.call(Response.prototype),Response.prototype.clone=function(){return new Response(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new Headers(this.headers),url:this.url})},Response.error=function(){var response=new Response(null,{status:0,statusText:""});return response.type="error",response};var redirectStatuses=[301,302,303,307,308];Response.redirect=function(url,status){if(-1===redirectStatuses.indexOf(status))throw new RangeError("Invalid status code");return new Response(null,{status:status,headers:{location:url}})};var DOMException=global$1.DOMException;try{new DOMException}catch(err){DOMException=function(message,name){this.message=message,this.name=name;var error=Error(message);this.stack=error.stack},DOMException.prototype=Object.create(Error.prototype),DOMException.prototype.constructor=DOMException}function fetch$1(input,init){return new Promise((function(resolve,reject){var request=new Request(input,init);if(request.signal&&request.signal.aborted)return reject(new DOMException("Aborted","AbortError"));var xhr=new XMLHttpRequest;function abortXhr(){xhr.abort()}xhr.onload=function(){var options={status:xhr.status,statusText:xhr.statusText,headers:parseHeaders(xhr.getAllResponseHeaders()||"")};options.url="responseURL"in xhr?xhr.responseURL:options.headers.get("X-Request-URL");var body="response"in xhr?xhr.response:xhr.responseText;setTimeout((function(){resolve(new Response(body,options))}),0)},xhr.onerror=function(){setTimeout((function(){reject(new TypeError("Network request failed"))}),0)},xhr.ontimeout=function(){setTimeout((function(){reject(new TypeError("Network request failed"))}),0)},xhr.onabort=function(){setTimeout((function(){reject(new DOMException("Aborted","AbortError"))}),0)},xhr.open(request.method,function(url){try{return""===url&&global$1.location.href?global$1.location.href:url}catch(e){return url}}(request.url),!0),"include"===request.credentials?xhr.withCredentials=!0:"omit"===request.credentials&&(xhr.withCredentials=!1),"responseType"in xhr&&(support.blob?xhr.responseType="blob":support.arrayBuffer&&request.headers.get("Content-Type")&&-1!==request.headers.get("Content-Type").indexOf("application/octet-stream")&&(xhr.responseType="arraybuffer")),!init||"object"!=typeof init.headers||init.headers instanceof Headers?request.headers.forEach((function(value,name){xhr.setRequestHeader(name,value)})):Object.getOwnPropertyNames(init.headers).forEach((function(name){xhr.setRequestHeader(name,normalizeValue(init.headers[name]))})),request.signal&&(request.signal.addEventListener("abort",abortXhr),xhr.onreadystatechange=function(){4===xhr.readyState&&request.signal.removeEventListener("abort",abortXhr)}),xhr.send(void 0===request._bodyInit?null:request._bodyInit)}))}fetch$1.polyfill=!0,global$1.fetch||(global$1.fetch=fetch$1,global$1.Headers=Headers,global$1.Request=Request,global$1.Response=Response),null==Element.prototype.getAttributeNames&&(Element.prototype.getAttributeNames=function(){for(var attributes=this.attributes,length=attributes.length,result=new Array(length),i=0;i=0&&matches.item(i)!==this;);return i>-1}),Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),Element.prototype.closest||(Element.prototype.closest=function(s){var el=this;do{if(el.matches(s))return el;el=el.parentElement||el.parentNode}while(null!==el&&1===el.nodeType);return null});var Connection=function(){function Connection(){_classCallCheck(this,Connection)}return _createClass(Connection,[{key:"onMessage",value:function(message,payload){message.component.receiveMessage(message,payload)}},{key:"onError",value:function(message,status){return message.component.messageSendFailed(),store.onErrorCallback(status)}},{key:"showExpiredMessage",value:function(){confirm("This page has expired due to inactivity.\nWould you like to refresh the page?")&&window.location.reload()}},{key:"sendMessage",value:function(message){var _this=this,payload=message.payload(),csrfToken=getCsrfToken(),socketId=this.getSocketId();if(window.__testing_request_interceptor)return window.__testing_request_interceptor(payload,this);fetch("".concat(window.livewire_app_url,"/livewire/message/").concat(payload.fingerprint.name),{method:"POST",body:JSON.stringify(payload),credentials:"same-origin",headers:_objectSpread2(_objectSpread2({"Content-Type":"application/json",Accept:"text/html, application/xhtml+xml","X-Livewire":!0,Referer:window.location.href},csrfToken&&{"X-CSRF-TOKEN":csrfToken}),socketId&&{"X-Socket-ID":socketId})}).then((function(response){if(response.ok)response.text().then((function(response){_this.isOutputFromDump(response)?(_this.onError(message),_this.showHtmlModal(response)):_this.onMessage(message,JSON.parse(response))}));else{if(!1===_this.onError(message,response.status))return;if(419===response.status){if(store.sessionHasExpired)return;store.sessionHasExpired=!0,_this.showExpiredMessage()}else response.text().then((function(response){_this.showHtmlModal(response)}))}})).catch((function(){_this.onError(message)}))}},{key:"isOutputFromDump",value:function(output){return!!output.match(/
+end() ?>
diff --git a/themes/Hyva/view/frontend/layout/default_hyva.xml b/themes/Hyva/view/frontend/layout/default_hyva.xml
new file mode 100644
index 00000000..7f7c2783
--- /dev/null
+++ b/themes/Hyva/view/frontend/layout/default_hyva.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/themes/Hyva/view/frontend/layout/hyva_checkout_index_index.xml b/themes/Hyva/view/frontend/layout/hyva_checkout_index_index.xml
new file mode 100644
index 00000000..3d0f522a
--- /dev/null
+++ b/themes/Hyva/view/frontend/layout/hyva_checkout_index_index.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/themes/Hyva/view/frontend/tailwind/module.css b/themes/Hyva/view/frontend/tailwind/module.css
new file mode 100644
index 00000000..743a3abc
--- /dev/null
+++ b/themes/Hyva/view/frontend/tailwind/module.css
@@ -0,0 +1,3 @@
+@source "../../../../../src/view";
+
+@import "./ui-components/notifier.css";
diff --git a/themes/Hyva/view/frontend/tailwind/tailwind-source.css b/themes/Hyva/view/frontend/tailwind/tailwind-source.css
new file mode 100644
index 00000000..b62cb32c
--- /dev/null
+++ b/themes/Hyva/view/frontend/tailwind/tailwind-source.css
@@ -0,0 +1 @@
+@import "./ui-components/notifier.css";
diff --git a/themes/Hyva/view/frontend/tailwind/tailwind.config.js b/themes/Hyva/view/frontend/tailwind/tailwind.config.js
new file mode 100644
index 00000000..b9c194d0
--- /dev/null
+++ b/themes/Hyva/view/frontend/tailwind/tailwind.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ content: [
+ '../../../../themes/Hyva/view/frontend/templates/**/*.phtml'
+ ]
+};
diff --git a/themes/Hyva/view/frontend/tailwind/ui-components/notifier.css b/themes/Hyva/view/frontend/tailwind/ui-components/notifier.css
new file mode 100644
index 00000000..dea524f3
--- /dev/null
+++ b/themes/Hyva/view/frontend/tailwind/ui-components/notifier.css
@@ -0,0 +1,38 @@
+.magewire-notifier {
+ --offset: 0.5rem;
+ --index: 40;
+ --gap: 0.25rem;
+ z-index: var(--index);
+ position: fixed;
+ inset-inline: 0;
+ inset-block-end: 0;
+ max-width: 100%;
+ margin: var(--offset);
+ display: inline-flex;
+ flex-direction: column;
+ gap: var(--gap);
+
+ /* Desktop */
+ @media(min-width: 768px) {
+ --gap: 0.5rem;
+ --offset: 1.5rem;
+ inset-inline-end: auto;
+ }
+
+ & .message {
+ @apply shadow-xl;
+
+ margin-bottom: 0;
+ padding-inline: 1rem;
+ padding-block: 0.875rem;
+ max-width: 70ch;
+ justify-content: flex-start;
+ overflow-wrap: anywhere;
+ cursor: pointer;
+ gap: 0.5rem;
+ }
+}
+
+.magewire-notifier-after {
+ margin-inline-start: auto;
+}
diff --git a/themes/Hyva/view/frontend/templates/js/magewire/internal/backwards-compatibility-emits.phtml b/themes/Hyva/view/frontend/templates/js/magewire/internal/backwards-compatibility-emits.phtml
new file mode 100644
index 00000000..717f2f01
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/js/magewire/internal/backwards-compatibility-emits.phtml
@@ -0,0 +1,45 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-attributes.phtml b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-attributes.phtml
new file mode 100644
index 00000000..18d5727c
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-attributes.phtml
@@ -0,0 +1,66 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-components.phtml b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-components.phtml
new file mode 100644
index 00000000..e559a32e
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-components.phtml
@@ -0,0 +1,107 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-events.phtml b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-events.phtml
new file mode 100644
index 00000000..e7cddd20
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-events.phtml
@@ -0,0 +1,178 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-hooks.phtml b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-hooks.phtml
new file mode 100644
index 00000000..7f31118e
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/magewire-features/support-hyva-checkout-backwards-compatibility/magewire-hooks.phtml
@@ -0,0 +1,75 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+
+/** @internal Do not modify to ensure Magewire continues to function correctly. */
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/themes/Hyva/view/frontend/templates/magewire-features/support-magento-flash-messages/support-magento-flash-messages.phtml b/themes/Hyva/view/frontend/templates/magewire-features/support-magento-flash-messages/support-magento-flash-messages.phtml
new file mode 100644
index 00000000..be52f17b
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/magewire-features/support-magento-flash-messages/support-magento-flash-messages.phtml
@@ -0,0 +1,20 @@
+getData('view_model');
+$magewireFragment = $magewireViewModel->utils()->fragment();
+?>
+make()->script()->start() ?>
+
+end() ?>
diff --git a/themes/Hyva/view/frontend/templates/overwrite/Hyva_Theme/page/js/alpinejs.phtml b/themes/Hyva/view/frontend/templates/overwrite/Hyva_Theme/page/js/alpinejs.phtml
new file mode 100644
index 00000000..826ad8b0
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/overwrite/Hyva_Theme/page/js/alpinejs.phtml
@@ -0,0 +1,18 @@
+
+= $block->getChildHtml() ?>
diff --git a/themes/Hyva/view/frontend/templates/script.phtml b/themes/Hyva/view/frontend/templates/script.phtml
new file mode 100644
index 00000000..28d9f7f5
--- /dev/null
+++ b/themes/Hyva/view/frontend/templates/script.phtml
@@ -0,0 +1,48 @@
+getData('view_model');
+
+$magewireUtility = $magewireViewModel->utils()->magewire();
+
+// The Hyvä theme requires AlpineJS to function. Magewire ships with its own bundled version of AlpineJS,
+// so when Magewire is present and active on the page, it acts as the AlpineJS provider for Hyvä as well.
+//
+// The condition below determines which AlpineJS source should be used:
+//
+// - TRUE (canRequireMagewire): Magewire is responsible for bootstrapping Alpine. The Magewire script
+// tag is rendered directly, which includes Alpine out of the box. Hyva piggybacks on this.
+//
+// - FALSE (else): Magewire is not driving the page, so Alpine must come from elsewhere. The child
+// block output is rendered instead, which is expected to contain the standalone Alpine script
+// configured by the Hyvä theme or another module.
+//
+// @todo Make Alpine.js loading configurable with three options:
+// 1. Load the Magewire-bundled Alpine.js version.
+// 2. Load standalone Alpine.js only (coming with the Hyvä theme).
+// 3. Load the Alpine.js version that ships with Magewire, without loading Magewire itself.
+?>
+canRequireMagewireJsLibrary()): ?>
+
+
+ = $block->getChildHtml() ?>
+
diff --git a/themes/Luma/etc/module.xml b/themes/Luma/etc/module.xml
new file mode 100644
index 00000000..140bb374
--- /dev/null
+++ b/themes/Luma/etc/module.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/themes/Luma/registration.php b/themes/Luma/registration.php
new file mode 100644
index 00000000..bfc9bb1d
--- /dev/null
+++ b/themes/Luma/registration.php
@@ -0,0 +1,14 @@
+