├── dist └── manifest.json ├── src ├── Features │ ├── SupportConsoleCommands │ │ ├── Commands │ │ │ ├── livewire.view.stub │ │ │ ├── livewire.form.stub │ │ │ ├── livewire.attribute.stub │ │ │ ├── livewire.pest.stub │ │ │ ├── livewire.stub │ │ │ ├── MakeLivewireCommand.php │ │ │ ├── livewire.inline.stub │ │ │ ├── MvCommand.php │ │ │ ├── RmCommand.php │ │ │ ├── CpCommand.php │ │ │ ├── TouchCommand.php │ │ │ ├── livewire.layout.stub │ │ │ ├── Upgrade │ │ │ │ ├── ClearViewCache.php │ │ │ │ ├── RemoveDeferModifierFromWireModelDirectives.php │ │ │ │ ├── AddLiveModifierToWireModelDirectives.php │ │ │ │ ├── RemovePrefetchModifierFromWireClickDirective.php │ │ │ │ ├── ChangeLazyToBlurModifierOnWireModelDirectives.php │ │ │ │ ├── RemovePreventModifierFromWireSubmitDirective.php │ │ │ │ ├── ChangeWireLoadDirectiveToWireInit.php │ │ │ │ ├── RemoveDeferModifierFromEntangleDirectives.php │ │ │ │ ├── ThirdPartyUpgradeNotice.php │ │ │ │ ├── ReplaceTemporaryUploadedFileNamespace.php │ │ │ │ ├── RepublishNavigation.php │ │ │ │ ├── UpgradeConfigInstructions.php │ │ │ │ ├── UpgradeIntroduction.php │ │ │ │ ├── AddLiveModifierToEntangleDirectives.php │ │ │ │ ├── ChangeForgetComputedToUnset.php │ │ │ │ ├── UpgradeAlpineInstructions.php │ │ │ │ ├── ChangeDefaultLayoutView.php │ │ │ │ └── ChangeTestAssertionMethods.php │ │ │ ├── livewire.test.stub │ │ │ ├── the-tao.php │ │ │ ├── FormCommand.php │ │ │ ├── AttributeCommand.php │ │ │ ├── PublishCommand.php │ │ │ ├── FileManipulationCommand.php │ │ │ ├── StubsCommand.php │ │ │ ├── ComponentParserFromExistingComponent.php │ │ │ ├── LayoutCommand.php │ │ │ └── DeleteCommand.php │ │ └── SupportConsoleCommands.php │ ├── SupportScriptsAndAssets │ │ ├── test.js │ │ └── non-livewire-asset.js │ ├── SupportFileUploads │ │ ├── browser_test_image.png │ │ ├── browser_test_image2.png │ │ ├── browser_test_image_big.jpg │ │ ├── FileNotPreviewableException.php │ │ ├── MissingFileUploadsTraitException.php │ │ ├── S3DoesntSupportMultipleFileUploads.php │ │ ├── FilePreviewController.php │ │ ├── SupportFileUploads.php │ │ ├── FileUploadController.php │ │ ├── FileUploadSynth.php │ │ └── GenerateSignedUploadUrl.php │ ├── SupportWireLoading │ │ └── browser_test_image.png │ ├── SupportPagination │ │ ├── WithoutUrlPagination.php │ │ ├── PaginationUrl.php │ │ ├── HandlesPagination.php │ │ └── views │ │ │ └── simple-bootstrap.blade.php │ ├── SupportNavigate │ │ ├── test-views │ │ │ ├── test-navigate-asset.js │ │ │ ├── layout.blade.php │ │ │ ├── changed-layout.blade.php │ │ │ ├── tracked-layout.blade.php │ │ │ ├── changed-tracked-layout.blade.php │ │ │ ├── html-attributes1.blade.php │ │ │ ├── html-attributes2.blade.php │ │ │ ├── layout-with-noscript.blade.php │ │ │ └── layout-with-navigate-outside.blade.php │ │ └── SupportNavigate.php │ ├── SupportAttributes │ │ ├── AttributeLevel.php │ │ ├── HandlesAttributes.php │ │ ├── AttributeCollection.php │ │ └── Attribute.php │ ├── SupportIsolating │ │ ├── BaseIsolate.php │ │ └── SupportIsolating.php │ ├── SupportJsEvaluation │ │ ├── HandlesJsEvaluation.php │ │ ├── SupportJsEvaluation.php │ │ └── BaseJs.php │ ├── SupportValidation │ │ ├── BaseRule.php │ │ └── SupportValidation.php │ ├── SupportTesting │ │ ├── ShowDuskComponent.php │ │ ├── Render.php │ │ ├── RequestBroker.php │ │ ├── ComponentState.php │ │ ├── SubsequentRender.php │ │ └── InitialRender.php │ ├── SupportWireables │ │ ├── SupportWireables.php │ │ └── WireableSynth.php │ ├── SupportPageComponents │ │ ├── BaseTitle.php │ │ ├── BaseLayout.php │ │ ├── MissingLayoutException.php │ │ ├── HandlesPageComponents.php │ │ └── PageComponentConfig.php │ ├── SupportLazyLoading │ │ └── BaseLazy.php │ ├── SupportModels │ │ ├── SupportModels.php │ │ ├── ModelSynth.php │ │ └── EloquentCollectionSynth.php │ ├── SupportLockedProperties │ │ ├── BaseLocked.php │ │ └── CannotUpdateLockedPropertyException.php │ ├── SupportStreaming │ │ ├── HandlesStreaming.php │ │ └── SupportStreaming.php │ ├── SupportReactiveProps │ │ ├── CannotMutateReactivePropException.php │ │ ├── SupportReactiveProps.php │ │ └── BaseReactive.php │ ├── SupportLocales │ │ └── SupportLocales.php │ ├── SupportFormObjects │ │ ├── HandlesFormObjects.php │ │ ├── SupportFormObjects.php │ │ └── FormObjectSynth.php │ ├── SupportComputed │ │ ├── CannotCallComputedDirectlyException.php │ │ └── SupportLegacyComputedPropertySyntax.php │ ├── SupportDisablingBackButtonCache │ │ ├── HandlesDisablingBackButtonCache.php │ │ ├── SupportDisablingBackButtonCache.php │ │ └── DisableBackButtonCacheMiddleware.php │ ├── SupportEvents │ │ ├── HandlesEvents.php │ │ ├── BaseOn.php │ │ ├── Event.php │ │ ├── fake-echo.js │ │ └── TestsEvents.php │ ├── SupportLifecycleHooks │ │ └── DirectlyCallingLifecycleHooksNotAllowedException.php │ ├── SupportMultipleRootElementDetection │ │ ├── MultipleRootElementsDetectedException.php │ │ └── SupportMultipleRootElementDetection.php │ ├── SupportLegacyModels │ │ └── CannotBindToModelDataWithoutValidationRuleException.php │ ├── SupportTeleporting │ │ └── SupportTeleporting.php │ ├── SupportBladeAttributes │ │ └── SupportBladeAttributes.php │ ├── SupportEntangle │ │ └── SupportEntangle.php │ ├── SupportRedirects │ │ ├── HandlesRedirects.php │ │ ├── Redirector.php │ │ ├── TestsRedirects.php │ │ └── SupportRedirects.php │ ├── SupportFileDownloads │ │ ├── TestsFileDownloads.php │ │ └── SupportFileDownloads.php │ ├── SupportSession │ │ └── BaseSession.php │ ├── SupportWireModelingNestedComponents │ │ └── BaseModelable.php │ ├── SupportQueryString │ │ └── SupportQueryString.php │ ├── SupportNestedComponentListeners │ │ └── SupportNestedComponentListeners.php │ ├── SupportChecksumErrorDebugging │ │ └── SupportChecksumErrorDebugging.php │ └── SupportWireCurrent │ │ └── test-views │ │ └── navbar-sidebar.blade.php ├── Exceptions │ ├── BypassViewHandler.php │ ├── ComponentNotFoundException.php │ ├── EventHandlerDoesNotExist.php │ ├── NonPublicComponentMethodCall.php │ ├── ComponentAttributeMissingOnDynamicComponentException.php │ ├── MethodNotFoundException.php │ ├── PropertyNotFoundException.php │ ├── MissingRulesException.php │ ├── PublicPropertyNotFoundException.php │ ├── RootTagMissingFromViewException.php │ └── LivewirePageExpiredBecauseNewDeploymentHasSignificantEnoughChanges.php ├── Form.php ├── Wireable.php ├── WithPagination.php ├── Attributes │ ├── Js.php │ ├── Lazy.php │ ├── Url.php │ ├── Isolate.php │ ├── Title.php │ ├── Computed.php │ ├── Layout.php │ ├── Locked.php │ ├── Reactive.php │ ├── Renderless.php │ ├── Modelable.php │ ├── Session.php │ ├── Rule.php │ ├── On.php │ └── Validate.php ├── Attribute.php ├── WithFileUploads.php ├── WithoutUrlPagination.php ├── Mechanisms │ ├── Mechanism.php │ ├── CompileLivewireTags │ │ └── CompileLivewireTags.php │ ├── HandleComponents │ │ ├── BaseRenderless.php │ │ ├── Synthesizers │ │ │ ├── StringableSynth.php │ │ │ ├── IntSynth.php │ │ │ ├── FloatSynth.php │ │ │ ├── StdClassSynth.php │ │ │ ├── EnumSynth.php │ │ │ ├── ArraySynth.php │ │ │ ├── CollectionSynth.php │ │ │ ├── CarbonSynth.php │ │ │ └── Synth.php │ │ ├── CorruptComponentPayloadException.php │ │ ├── Checksum.php │ │ ├── ViewContext.php │ │ └── ComponentContext.php │ ├── RenderComponent.php │ ├── ExtendBlade │ │ ├── DeterministicBladeKeys.php │ │ └── ExtendedCompilerEngine.php │ └── DataStore.php ├── Pipe.php ├── Wrapped.php ├── Transparency.php ├── WireDirective.php ├── Livewire.php ├── EventBus.php └── ComponentHook.php ├── LICENSE.md ├── composer.json └── README.md /dist/manifest.json: -------------------------------------------------------------------------------- 1 | 2 | {"/livewire.js":"13b7c601"} 3 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.view.stub: -------------------------------------------------------------------------------- 1 |
2 | {{-- [quote] --}} 3 |
4 | -------------------------------------------------------------------------------- /src/Features/SupportScriptsAndAssets/test.js: -------------------------------------------------------------------------------- 1 | 2 | document.querySelector('[dusk="foo"]').textContent = 'evaluated' 3 | -------------------------------------------------------------------------------- /src/Exceptions/BypassViewHandler.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.stub: -------------------------------------------------------------------------------- 1 | instance(static::class, $this); 10 | } 11 | 12 | function boot() 13 | { 14 | // 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Attributes/Validate.php: -------------------------------------------------------------------------------- 1 | push('js', $expression); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/changed-layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Features/SupportValidation/BaseRule.php: -------------------------------------------------------------------------------- 1 | call(app('livewire')->new($class)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/tracked-layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Exceptions/EventHandlerDoesNotExist.php: -------------------------------------------------------------------------------- 1 | propertySynthesizer(WireableSynth::class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/changed-tracked-layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Exceptions/NonPublicComponentMethodCall.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/html-attributes2.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Mechanisms/CompileLivewireTags/CompileLivewireTags.php: -------------------------------------------------------------------------------- 1 | precompiler(new LivewireTagPrecompiler); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.inline.stub: -------------------------------------------------------------------------------- 1 | 13 | {{-- [quote] --}} 14 | 15 | HTML; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/BaseRenderless.php: -------------------------------------------------------------------------------- 1 | component->skipRender(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/MvCommand.php: -------------------------------------------------------------------------------- 1 | setHidden(true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/RmCommand.php: -------------------------------------------------------------------------------- 1 | setHidden(true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportLazyLoading/BaseLazy.php: -------------------------------------------------------------------------------- 1 | setHidden(true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportPageComponents/BaseLayout.php: -------------------------------------------------------------------------------- 1 | setHidden(true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportModels/SupportModels.php: -------------------------------------------------------------------------------- 1 | propertySynthesizer([ 12 | ModelSynth::class, 13 | EloquentCollectionSynth::class, 14 | ]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Features/SupportLockedProperties/BaseLocked.php: -------------------------------------------------------------------------------- 1 | getName()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Features/SupportLockedProperties/CannotUpdateLockedPropertyException.php: -------------------------------------------------------------------------------- 1 | property.']' 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportStreaming/HandlesStreaming.php: -------------------------------------------------------------------------------- 1 | stream($to, $content, $replace); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.layout.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $title ?? 'Page Title' }} 8 | 9 | 10 | {{ $slot }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Exceptions/MissingRulesException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ $slot }} 9 | 10 | @stack('scripts') 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Features/SupportReactiveProps/CannotMutateReactivePropException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Go to second page (outside) 8 | 9 | {{ $slot }} 10 | 11 | @stack('scripts') 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Exceptions/PublicPropertyNotFoundException.php: -------------------------------------------------------------------------------- 1 | setLocale($locale); 12 | } 13 | 14 | function dehydrate($context) 15 | { 16 | $context->addMemo('locale', app()->getLocale()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Features/SupportFormObjects/HandlesFormObjects.php: -------------------------------------------------------------------------------- 1 | all() as $key => $value) { 12 | if ($value instanceof Form) { 13 | $forms[] = $value; 14 | } 15 | } 16 | 17 | return $forms; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Features/SupportJsEvaluation/SupportJsEvaluation.php: -------------------------------------------------------------------------------- 1 | component)->has('js')) return; 14 | 15 | $context->addEffect('xjs', store($this->component)->get('js')); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Features/SupportComputed/CannotCallComputedDirectlyException.php: -------------------------------------------------------------------------------- 1 | call('view:clear'); 12 | 13 | return $next($console); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/RootTagMissingFromViewException.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Features/SupportJsEvaluation/BaseJs.php: -------------------------------------------------------------------------------- 1 | getName(); 13 | 14 | $stringifiedMethod = $this->component->$name(); 15 | 16 | $context->pushEffect('js', $stringifiedMethod, $name); 17 | } 18 | } 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/HandlesEvents.php: -------------------------------------------------------------------------------- 1 | listeners; 13 | } 14 | 15 | public function dispatch($event, ...$params) 16 | { 17 | $event = new Event($event, $params); 18 | 19 | store($this)->push('dispatched', $event); 20 | 21 | return $event; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exceptions/LivewirePageExpiredBecauseNewDeploymentHasSignificantEnoughChanges.php: -------------------------------------------------------------------------------- 1 | __toString(), []]; 16 | } 17 | 18 | function hydrate($value) { 19 | return str($value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/FileNotPreviewableException.php: -------------------------------------------------------------------------------- 1 | guessExtension()}\" is not previewable. See the livewire.temporary_file_upload.preview_mimes config." 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/MissingFileUploadsTraitException.php: -------------------------------------------------------------------------------- 1 | getName()}] component class." 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/S3DoesntSupportMultipleFileUploads.php: -------------------------------------------------------------------------------- 1 | getName() . ']'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Features/SupportLegacyModels/CannotBindToModelDataWithoutValidationRuleException.php: -------------------------------------------------------------------------------- 1 | ">'; 14 | }); 15 | 16 | Blade::directive('endteleport', function () { 17 | return ''; 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Features/SupportIsolating/SupportIsolating.php: -------------------------------------------------------------------------------- 1 | shouldIsolate()) { 12 | $context->addMemo('isolate', true); 13 | } 14 | } 15 | 16 | public function shouldIsolate() 17 | { 18 | return $this->component->getAttributes() 19 | ->filter(fn ($i) => is_subclass_of($i, BaseIsolate::class)) 20 | ->count() > 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportPagination/PaginationUrl.php: -------------------------------------------------------------------------------- 1 | pushQueryStringEffect($context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/CorruptComponentPayloadException.php: -------------------------------------------------------------------------------- 1 | whereStartsWith('wire:'.$name)); 15 | 16 | $directive = head(array_keys($entries)); 17 | $value = head(array_values($entries)); 18 | 19 | return new WireDirective($name, $directive, $value); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/FloatSynth.php: -------------------------------------------------------------------------------- 1 | new Middleware($middleware), static::$middleware); 16 | } 17 | 18 | public function handle($filename) 19 | { 20 | abort_unless(request()->hasValidSignature(), 401); 21 | 22 | return Utils::pretendPreviewResponseIsPreviewFile($filename); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Features/SupportAttributes/HandlesAttributes.php: -------------------------------------------------------------------------------- 1 | attributes ??= AttributeCollection::fromComponent($this); 12 | } 13 | 14 | function setPropertyAttribute($property, $attribute) 15 | { 16 | $attribute->__boot($this, AttributeLevel::PROPERTY, $property); 17 | 18 | $this->mergeOutsideAttributes(new AttributeCollection([$attribute])); 19 | } 20 | 21 | function mergeOutsideAttributes(AttributeCollection $attributes) 22 | { 23 | $this->attributes = $this->getAttributes()->concat($attributes); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Features/SupportDisablingBackButtonCache/SupportDisablingBackButtonCache.php: -------------------------------------------------------------------------------- 1 | make(\Illuminate\Contracts\Http\Kernel::class); 14 | 15 | if ($kernel->hasMiddleware(DisableBackButtonCacheMiddleware::class)) { 16 | return; 17 | } 18 | 19 | $kernel->pushMiddleware(DisableBackButtonCacheMiddleware::class); 20 | } 21 | 22 | public function boot() 23 | { 24 | static::$disableBackButtonCache = true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Features/SupportEntangle/SupportEntangle.php: -------------------------------------------------------------------------------- 1 | window.Livewire.find('{{ \$__livewire->getId() }}').entangle('{{ {$expression}->value() }}'){{ {$expression}->hasModifier('live') ? '.live' : '' }}window.Livewire.find('{{ \$__livewire->getId() }}').entangle('{{ {$expression} }}') 15 | EOT; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/BaseOn.php: -------------------------------------------------------------------------------- 1 | event) as $event) { 19 | store($this->component)->push( 20 | 'listenersFromAttributes', 21 | $this->getName() ?? '$refresh', 22 | $event, 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromWireModelDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:model directive is now deferred by default.', 14 | before: 'wire:model.defer', 15 | after: 'wire:model', 16 | pattern: '/wire:model\.defer/', 17 | replacement: 'wire:model', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToWireModelDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:model directive is now deferred by default.', 14 | before: 'wire:model', 15 | after: 'wire:model.live', 16 | pattern: '/wire:model(?!\.(?:defer|lazy|live))/', 17 | replacement: 'wire:model.live', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/RemovePrefetchModifierFromWireClickDirective.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:click.prefetch modifier has been removed.', 14 | before: 'wire:click.prefetch', 15 | after: 'wire:click', 16 | pattern: '/wire:click\.prefetch/', 17 | replacement: 'wire:click', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ChangeLazyToBlurModifierOnWireModelDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:model.lazy modifier is now wire:model.blur.', 14 | before: 'wire:model.lazy', 15 | after: 'wire:model.blur', 16 | pattern: '/wire:model.lazy/', 17 | replacement: 'wire:model.blur', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportTesting/Render.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:submit directive now prevents submission by default.', 14 | before: 'wire:submit.prevent', 15 | after: 'wire:submit', 16 | pattern: '/wire:submit\.prevent/', 17 | replacement: 'wire:submit', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ChangeWireLoadDirectiveToWireInit.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The livewire:load is now livewire:init.', 14 | before: 'livewire:load', 15 | after: 'livewire:init', 16 | pattern: '/livewire:load/', 17 | replacement: 'livewire:init', 18 | directories: ['resources/views', 'resources/js'] 19 | ); 20 | 21 | return $next($console); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromEntangleDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The @entangle(...) directive is now deferred by default.', 14 | before: '@entangle(...).defer', 15 | after: '@entangle(...)', 16 | pattern: '/@entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)\.defer/', 17 | replacement: '@entangle($1)', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportReactiveProps/SupportReactiveProps.php: -------------------------------------------------------------------------------- 1 | static::$pendingChildParams = []); 15 | 16 | on('mount.stub', function ($tag, $id, $params, $parent, $key) { 17 | static::$pendingChildParams[$id] = $params; 18 | }); 19 | } 20 | 21 | static function hasPassedInProps($id) { 22 | return isset(static::$pendingChildParams[$id]); 23 | } 24 | 25 | static function getPassedInProp($id, $name) { 26 | $params = static::$pendingChildParams[$id] ?? []; 27 | 28 | return $params[$name] ?? null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ThirdPartyUpgradeNotice.php: -------------------------------------------------------------------------------- 1 | line(' Third-party package upgrade 🚀 '); 12 | $console->newLine(); 13 | $console->comment('!! Please be aware that the following upgrade steps are registered by third-parties !!'); 14 | $console->newLine(); 15 | $console->newLine(); 16 | $console->line('You can abort this command at any time by pressing ctrl+c.'); 17 | 18 | if($console->confirm('Ready to continue?', true)) 19 | { 20 | return $next($console); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Pipe.php: -------------------------------------------------------------------------------- 1 | target = $target; 12 | } 13 | 14 | function __invoke(...$params) { 15 | if (empty($params)) return $this->target; 16 | 17 | [ $before, $through, $after ] = [ [], null, [] ]; 18 | 19 | foreach ($params as $key => $param) { 20 | if (! $through) { 21 | if (is_callable($param)) $through = $param; 22 | 23 | else $before[$key] = $param; 24 | } else { 25 | $after[$key] = $param; 26 | } 27 | } 28 | 29 | $params = [ ...$before, $this->target, ...$after ]; 30 | 31 | $this->target = $through(...$params); 32 | 33 | return $this; 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Checksum.php: -------------------------------------------------------------------------------- 1 | getKey(); 24 | 25 | $checksum = hash_hmac('sha256', json_encode($snapshot), $hashKey); 26 | 27 | trigger('checksum.generate', $checksum, $snapshot); 28 | 29 | return $checksum; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Wrapped.php: -------------------------------------------------------------------------------- 1 | fallback = $fallback; 14 | 15 | return $this; 16 | } 17 | 18 | function __call($method, $params) 19 | { 20 | if (! method_exists($this->target, $method)) return value($this->fallback); 21 | 22 | try { 23 | return ImplicitlyBoundMethod::call(app(), [$this->target, $method], $params); 24 | } catch (\Throwable $e) { 25 | $shouldPropagate = true; 26 | 27 | $stopPropagation = function () use (&$shouldPropagate) { 28 | $shouldPropagate = false; 29 | }; 30 | 31 | trigger('exception', $this->target, $e, $stopPropagation); 32 | 33 | $shouldPropagate && throw $e; 34 | } 35 | } 36 | } 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/ViewContext.php: -------------------------------------------------------------------------------- 1 | slots = $factory->slots; 21 | $this->pushes = $factory->pushes; 22 | $this->prepends = $factory->prepends; 23 | $this->sections = $factory->sections; 24 | } 25 | 26 | function mergeIntoNewEnvironment($__env) 27 | { 28 | $factory = invade($__env); 29 | 30 | $factory->slots = $this->slots; 31 | $factory->pushes = $this->pushes; 32 | $factory->prepends = $this->prepends; 33 | $factory->sections = $this->sections; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/StdClassSynth.php: -------------------------------------------------------------------------------- 1 | $child) { 18 | $data[$key] = $dehydrateChild($key, $child); 19 | } 20 | 21 | return [$data, []]; 22 | } 23 | 24 | function hydrate($value, $meta, $hydrateChild) { 25 | $obj = new stdClass; 26 | 27 | foreach ($value as $key => $child) { 28 | $obj->$key = $hydrateChild($key, $child); 29 | } 30 | 31 | return $obj; 32 | } 33 | 34 | function set(&$target, $key, $value) { 35 | $target->$key = $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Transparency.php: -------------------------------------------------------------------------------- 1 | target; 14 | } 15 | 16 | function offsetExists(mixed $offset): bool 17 | { 18 | return isset($this->target[$offset]); 19 | } 20 | 21 | function offsetGet(mixed $offset): mixed 22 | { 23 | return $this->target[$offset]; 24 | } 25 | 26 | function offsetSet(mixed $offset, mixed $value): void 27 | { 28 | $this->target[$offset] = $value; 29 | } 30 | 31 | function offsetUnset(mixed $offset): void 32 | { 33 | unset($this->target[$offset]); 34 | } 35 | 36 | function getIterator(): Traversable 37 | { 38 | return (function () { 39 | foreach ($this->target as $key => $value) { 40 | yield $key => $value; 41 | } 42 | })(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php: -------------------------------------------------------------------------------- 1 | value, 25 | ['class' => get_class($target)] 26 | ]; 27 | } 28 | 29 | function hydrate($value, $meta) { 30 | if ($value === null || $value === '') return null; 31 | 32 | $class = $meta['class']; 33 | 34 | return $class::from($value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php: -------------------------------------------------------------------------------- 1 | headers->add([ 23 | 'Pragma' => 'no-cache', 24 | 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT', 25 | 'Cache-Control' => 'no-cache, must-revalidate, no-store, max-age=0, private', 26 | ]); 27 | } 28 | 29 | return $response; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/SupportNavigate.php: -------------------------------------------------------------------------------- 1 | forceAssetInjection(); ?>
'; 15 | }); 16 | 17 | Blade::directive('endpersist', function ($expression) { 18 | return '
'; 19 | }); 20 | 21 | app('livewire')->useScriptTagAttributes([ 22 | 'data-navigate-once' => true, 23 | ]); 24 | 25 | Vite::useScriptTagAttributes([ 26 | 'data-navigate-track' => 'reload', 27 | ]); 28 | 29 | Vite::useStyleTagAttributes([ 30 | 'data-navigate-track' => 'reload', 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceTemporaryUploadedFileNamespace.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'Livewire\TemporaryUploadedFile is now Livewire\Features\SupportFileUploads\TemporaryUploadedFile.', 14 | before: 'Livewire\TemporaryUploadedFile', 15 | after: 'Livewire\Features\SupportFileUploads\TemporaryUploadedFile', 16 | pattern: '/Livewire\\\\TemporaryUploadedFile/', 17 | replacement: "Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile", 18 | directories: ['app', 'tests', 'resources/views'] 19 | ); 20 | if ($console->confirm('Continue?', true)) { 21 | return $next($console); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright © Caleb Porzio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/the-tao.php: -------------------------------------------------------------------------------- 1 | filesystem()->directoryExists('resources/views/vendor/livewire')) { 12 | $console->line(' The Livewire pagination views have changed. '); 13 | $console->newLine(); 14 | 15 | $console->line('Republishing of the pagination views is required.'); 16 | 17 | $confirm = $console->confirm('Do you want to republish the pagination views?', true); 18 | 19 | if($confirm) { 20 | $console->call('vendor:publish', [ 21 | '--tag' => 'livewire:pagination', 22 | '--force' => true, 23 | ]); 24 | } 25 | } 26 | 27 | return $next($console); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeConfigInstructions.php: -------------------------------------------------------------------------------- 1 | line(' Manual Upgrade: New configuration '); 12 | $console->newLine(); 13 | $console->line('Livewire V3 has both added and removed certain configuration items.'); 14 | $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.'); 15 | $console->line('Added options: legacy_model_binding, inject_assets, inject_morph_markers, and navigate'); 16 | $console->line('Removed options: app_url, middleware_group, manifest_path, back_button_cache'); 17 | $console->newLine(); 18 | 19 | if($console->confirm('Continue?', true)) 20 | { 21 | return $next($console); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Features/SupportWireables/WireableSynth.php: -------------------------------------------------------------------------------- 1 | toLivewire(); 20 | 21 | foreach ($data as $key => $child) { 22 | $data[$key] = $dehydrateChild($key, $child); 23 | } 24 | 25 | return [ 26 | $data, 27 | ['class' => get_class($target)], 28 | ]; 29 | } 30 | 31 | function hydrate($value, $meta, $hydrateChild) { 32 | foreach ($value as $key => $child) { 33 | $value[$key] = $hydrateChild($key, $child); 34 | } 35 | 36 | return $meta['class']::fromLivewire($value); 37 | } 38 | 39 | function set(&$target, $key, $value) { 40 | $target->{$key} = $value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/HandlesRedirects.php: -------------------------------------------------------------------------------- 1 | set('redirect', $url); 12 | 13 | if ($navigate) store($this)->set('redirectUsingNavigate', true); 14 | 15 | $shouldSkipRender = ! config('livewire.render_on_redirect', false); 16 | 17 | $shouldSkipRender && $this->skipRender(); 18 | } 19 | 20 | public function redirectRoute($name, $parameters = [], $absolute = true, $navigate = false) 21 | { 22 | $this->redirect(route($name, $parameters, $absolute), $navigate); 23 | } 24 | 25 | public function redirectIntended($default = '/', $navigate = false) 26 | { 27 | $url = session()->pull('url.intended', $default); 28 | 29 | $this->redirect($url, $navigate); 30 | } 31 | 32 | public function redirectAction($name, $parameters = [], $absolute = true, $navigate = false) 33 | { 34 | $this->redirect(action($name, $parameters, $absolute), $navigate); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/Event.php: -------------------------------------------------------------------------------- 1 | name = $name; 17 | $this->params = $params; 18 | } 19 | 20 | public function self() 21 | { 22 | $this->self = true; 23 | 24 | return $this; 25 | } 26 | 27 | public function component($name) 28 | { 29 | $this->component = $name; 30 | 31 | return $this; 32 | } 33 | 34 | public function to($name) 35 | { 36 | return $this->component($name); 37 | } 38 | 39 | public function serialize() 40 | { 41 | $output = [ 42 | 'name' => $this->name, 43 | 'params' => $this->params, 44 | ]; 45 | 46 | if ($this->self) $output['self'] = true; 47 | if ($this->component) $output['to'] = app(ComponentRegistry::class)->getName($this->component); 48 | 49 | return $output; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeIntroduction.php: -------------------------------------------------------------------------------- 1 | line(' LIVEWIRE v2 to v3 UPGRADE 🚀 '); 12 | $console->newLine(); 13 | $console->comment('!! Running this command multiple times may result in incorrect replacements !!'); 14 | $console->newLine(); 15 | $console->line('This command will help you upgrade from Livewire v2 to v3.'); 16 | $console->newLine(); 17 | $console->line('Files will be modified in-place, so make sure you have a backup of your project before continuing.'); 18 | $console->newLine(); 19 | $console->newLine(); 20 | $console->line('You can abort this command at any time by pressing ctrl+c.'); 21 | 22 | if($console->confirm('Ready to continue?', true)) 23 | { 24 | return $next($console); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/Redirector.php: -------------------------------------------------------------------------------- 1 | component->redirect($this->generator->to($path, [], $secure)); 15 | 16 | return $this; 17 | } 18 | 19 | public function away($path, $status = 302, $headers = []) 20 | { 21 | return $this->to($path, $status, $headers); 22 | } 23 | 24 | public function with($key, $value = null) 25 | { 26 | $key = is_array($key) ? $key : [$key => $value]; 27 | 28 | foreach ($key as $k => $v) { 29 | $this->session->flash($k, $v); 30 | } 31 | 32 | return $this; 33 | } 34 | 35 | public function component(Component $component) 36 | { 37 | $this->component = $component; 38 | 39 | return $this; 40 | } 41 | 42 | public function response($to) 43 | { 44 | return $this->createRedirect($to, 302, []); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Features/SupportStreaming/SupportStreaming.php: -------------------------------------------------------------------------------- 1 | $name, 'content' => $content, 'replace' => $replace]); 16 | } 17 | 18 | public static function ensureStreamResponseStarted() 19 | { 20 | if (static::$response) return; 21 | 22 | static::$response = response()->stream(null , 200, [ 23 | 'Cache-Control' => 'no-cache', 24 | 'Content-Type' => 'text/event-stream', 25 | 'X-Accel-Buffering' => 'no', 26 | 'X-Livewire-Stream' => true, 27 | ]); 28 | 29 | static::$response->sendHeaders(); 30 | } 31 | 32 | public static function streamContent($body) 33 | { 34 | echo json_encode(['stream' => true, 'body' => $body, 'endStream' => true]); 35 | 36 | if (ob_get_level() > 0) { 37 | ob_flush(); 38 | } 39 | 40 | flush(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Features/SupportPageComponents/HandlesPageComponents.php: -------------------------------------------------------------------------------- 1 | mount($this::class, $params); 18 | }); 19 | 20 | $layoutConfig = $layoutConfig ?: new PageComponentConfig; 21 | 22 | $layoutConfig->normalizeViewNameAndParamsForBladeComponents(); 23 | 24 | $response = response(SupportPageComponents::renderContentsIntoLayout($html, $layoutConfig)); 25 | 26 | if (is_callable($layoutConfig->response)) { 27 | call_user_func($layoutConfig->response, $response); 28 | } 29 | 30 | return $response; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php: -------------------------------------------------------------------------------- 1 | $child) { 14 | $target[$key] = $dehydrateChild($key, $child); 15 | } 16 | 17 | return [$target, []]; 18 | } 19 | 20 | function hydrate($value, $meta, $hydrateChild) { 21 | // If we are "hydrating" a value about to be used in an update, 22 | // Let's make sure it's actually an array before try to set it. 23 | // This is most common in the case of "__rm__" values, but also 24 | // applies to any non-array value... 25 | if (! is_array($value)) { 26 | return $value; 27 | } 28 | 29 | foreach ($value as $key => $child) { 30 | $value[$key] = $hydrateChild($key, $child); 31 | } 32 | 33 | return $value; 34 | } 35 | 36 | function set(&$target, $key, $value) { 37 | $target[$key] = $value; 38 | } 39 | 40 | function unset(&$target, $key) { 41 | unset($target[$key]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Mechanisms/RenderComponent.php: -------------------------------------------------------------------------------- 1 | generate(); 27 | $key = "'{$key}'"; 28 | } 29 | 30 | return <<mount(\$__name, \$__params, $key, \$__slots ?? [], get_defined_vars()); 38 | 39 | echo \$__html; 40 | 41 | unset(\$__html); 42 | unset(\$__name); 43 | unset(\$__params); 44 | unset(\$__split); 45 | if (isset(\$__slots)) unset(\$__slots); 46 | ?> 47 | EOT; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/WireDirective.php: -------------------------------------------------------------------------------- 1 | name; 20 | } 21 | 22 | public function directive() 23 | { 24 | return $this->directive; 25 | } 26 | 27 | public function value() 28 | { 29 | return $this->value; 30 | } 31 | 32 | public function modifiers() 33 | { 34 | return str($this->directive) 35 | ->replace("wire:{$this->name}", '') 36 | ->explode('.') 37 | ->filter()->values(); 38 | } 39 | 40 | public function hasModifier($modifier) 41 | { 42 | return $this->modifiers()->contains($modifier); 43 | } 44 | 45 | public function toHtml() 46 | { 47 | return (new ComponentAttributeBag([$this->directive => $this->value]))->toHtml(); 48 | } 49 | 50 | public function toString() 51 | { 52 | return (string) $this; 53 | } 54 | 55 | public function __toString() 56 | { 57 | return (string) $this->value; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Features/SupportFileDownloads/TestsFileDownloads.php: -------------------------------------------------------------------------------- 1 | effects, 'download'); 12 | 13 | if ($filename) { 14 | PHPUnit::assertEquals( 15 | $filename, 16 | data_get($downloadEffect, 'name') 17 | ); 18 | } else { 19 | PHPUnit::assertNotNull($downloadEffect); 20 | } 21 | 22 | if ($content) { 23 | $downloadedContent = data_get($this->effects, 'download.content'); 24 | 25 | PHPUnit::assertEquals( 26 | $content, 27 | base64_decode($downloadedContent) 28 | ); 29 | } 30 | 31 | if ($contentType) { 32 | PHPUnit::assertEquals( 33 | $contentType, 34 | data_get($this->effects, 'download.contentType') 35 | ); 36 | } 37 | 38 | return $this; 39 | } 40 | 41 | public function assertNoFileDownloaded() 42 | { 43 | $downloadEffect = data_get($this->effects, 'download'); 44 | 45 | PHPUnit::assertNull($downloadEffect); 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Features/SupportReactiveProps/BaseReactive.php: -------------------------------------------------------------------------------- 1 | getName(); 18 | 19 | store($this->component)->push('reactiveProps', $property); 20 | 21 | $this->originalValueHash = crc32(json_encode($this->getValue())); 22 | } 23 | 24 | public function hydrate() 25 | { 26 | if (SupportReactiveProps::hasPassedInProps($this->component->getId())) { 27 | $updatedValue = SupportReactiveProps::getPassedInProp( 28 | $this->component->getId(), $this->getName() 29 | ); 30 | 31 | $this->setValue($updatedValue); 32 | } 33 | 34 | $this->originalValueHash = crc32(json_encode($this->getValue())); 35 | } 36 | 37 | public function dehydrate($context) 38 | { 39 | if ($this->originalValueHash !== crc32(json_encode($this->getValue()))) { 40 | throw new CannotMutateReactivePropException($this->component->getName(), $this->getName()); 41 | } 42 | 43 | $context->pushMemo('props', $this->getName()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToEntangleDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The @entangle(...) directive is now deferred by default.', 14 | before: '@entangle(...)', 15 | after: '@entangle(...).live', 16 | pattern: '/@entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)(?!\.(?:defer|live))/', 17 | replacement: '@entangle($1).live', 18 | ); 19 | 20 | $this->interactiveReplacement( 21 | console: $console, 22 | title: 'The $wire.entangle function is now deferred by default and has been changed to $wire.$entangle.', 23 | before: '$wire.entangle(...)', 24 | after: '$wire.$entangle(..., true)', 25 | pattern: '/\$wire\.entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)(?!\.(?:defer))/', 26 | replacement: '$wire.$entangle($1, true)', 27 | directories: ['resources/views', 'resources/js'] 28 | ); 29 | 30 | return $next($console); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Features/SupportMultipleRootElementDetection/SupportMultipleRootElementDetection.php: -------------------------------------------------------------------------------- 1 | warnAgainstMoreThanOneRootElement($component, $html); 17 | 18 | }; 19 | }); 20 | } 21 | 22 | function warnAgainstMoreThanOneRootElement($component, $html) 23 | { 24 | $count = $this->getRootElementCount($html); 25 | 26 | if ($count > 1) { 27 | throw new MultipleRootElementsDetectedException($component); 28 | } 29 | } 30 | 31 | function getRootElementCount($html) 32 | { 33 | $dom = new \DOMDocument(); 34 | 35 | @$dom->loadHTML($html); 36 | 37 | $body = $dom->getElementsByTagName('body')->item(0); 38 | 39 | $count = 0; 40 | 41 | foreach ($body->childNodes as $child) { 42 | if ($child->nodeType == XML_ELEMENT_NODE) { 43 | if ($child->tagName === 'script') continue; 44 | 45 | $count++; 46 | } 47 | } 48 | 49 | return $count; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/SupportFileUploads.php: -------------------------------------------------------------------------------- 1 | runningUnitTests()) { 15 | // Don't actually generate S3 signedUrls during testing. 16 | GenerateSignedUploadUrlFacade::swap(new class extends GenerateSignedUploadUrl { 17 | public function forS3($file, $visibility = '') { return []; } 18 | }); 19 | } 20 | 21 | app('livewire')->propertySynthesizer([ 22 | FileUploadSynth::class, 23 | ]); 24 | 25 | on('call', function ($component, $method, $params, $addEffect, $earlyReturn) { 26 | if ($method === '_startUpload') { 27 | if (! method_exists($component, $method)) { 28 | throw new MissingFileUploadsTraitException($component); 29 | } 30 | } 31 | }); 32 | 33 | Route::post('/livewire/upload-file', [FileUploadController::class, 'handle']) 34 | ->name('livewire.upload-file'); 35 | 36 | Route::get('/livewire/preview-file/{filename}', [FilePreviewController::class, 'handle']) 37 | ->name('livewire.preview-file'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mechanisms/ExtendBlade/DeterministicBladeKeys.php: -------------------------------------------------------------------------------- 1 | currentPathHash) { 16 | throw new \Exception('Latest compiled component path not found.'); 17 | } 18 | 19 | $path = $this->currentPathHash; 20 | $count = $this->counter(); 21 | 22 | // $key = "lw-[hash of Blade view path]-[current @livewire directive count]" 23 | return 'lw-' . $this->currentPathHash . '-' . $count; 24 | } 25 | 26 | public function counter() 27 | { 28 | if (! isset($this->countersByPath[$this->currentPathHash])) { 29 | $this->countersByPath[$this->currentPathHash] = 0; 30 | } 31 | 32 | return $this->countersByPath[$this->currentPathHash]++; 33 | } 34 | 35 | public function hookIntoCompile(BladeCompiler $compiler, $viewContent) 36 | { 37 | $path = $compiler->getPath(); 38 | 39 | // If there is no path this means this Blade is being compiled 40 | // with ->compileString(...) directly instead of ->compile() 41 | // therefore we'll generate a hash of the contents instead 42 | if ($path === null) { 43 | $path = $viewContent; 44 | } 45 | 46 | $this->currentPathHash = crc32($path); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/ComponentContext.php: -------------------------------------------------------------------------------- 1 | mounting; 21 | } 22 | 23 | public function addEffect($key, $value) 24 | { 25 | if (is_array($key)) { 26 | foreach ($key as $iKey => $iValue) $this->addEffect($iKey, $iValue); 27 | 28 | return; 29 | } 30 | 31 | $this->effects[$key] = $value; 32 | } 33 | 34 | public function pushEffect($key, $value, $iKey = null) 35 | { 36 | if (! isset($this->effects[$key])) $this->effects[$key] = []; 37 | 38 | if ($iKey) { 39 | $this->effects[$key][$iKey] = $value; 40 | } else { 41 | $this->effects[$key][] = $value; 42 | } 43 | } 44 | 45 | public function addMemo($key, $value) 46 | { 47 | $this->memo[$key] = $value; 48 | } 49 | 50 | public function pushMemo($key, $value, $iKey = null) 51 | { 52 | if (! isset($this->memo[$key])) $this->memo[$key] = []; 53 | 54 | if ($iKey) { 55 | $this->memo[$key][$iKey] = $value; 56 | } else { 57 | $this->memo[$key][] = $value; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/FormCommand.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The forgetComputed component method is replaced by PHP\'s unset function', 14 | before: '$this->forgetComputed(\'title\')', 15 | after: 'unset($this->title)', 16 | pattern: '/\$this->forgetComputed\((.*?)\);/', 17 | replacement: function($matches) { 18 | $replacement = ''; 19 | 20 | if(isset($matches[1])) { 21 | preg_match_all('/(?:\'|")(.*?)(?:\'|")/', $matches[1], $keys); 22 | 23 | $replacement .= 'unset('; 24 | foreach($keys[1] ?? [] as $key) { 25 | $replacement .= '$this->' . $key . ', '; 26 | } 27 | $replacement = rtrim($replacement, ', '); 28 | $replacement .= ');'; 29 | } 30 | 31 | // Leave unchanged if no replacement was possible. 32 | return $replacement ?: $matches[0]; 33 | }, 34 | directories: 'app', 35 | ); 36 | 37 | return $next($console); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/CollectionSynth.php: -------------------------------------------------------------------------------- 1 | all(); 16 | 17 | foreach ($data as $key => $child) { 18 | $data[$key] = $dehydrateChild($key, $child); 19 | } 20 | 21 | return [ 22 | $data, 23 | ['class' => get_class($target)] 24 | ]; 25 | } 26 | 27 | function hydrate($value, $meta, $hydrateChild) { 28 | foreach ($value as $key => $child) { 29 | $value[$key] = $hydrateChild($key, $child); 30 | } 31 | 32 | return new $meta['class']($value); 33 | } 34 | 35 | function &get(&$target, $key) { 36 | // We need this "$reader" callback to get a reference to 37 | // the items property inside collections. Otherwise, 38 | // we'd receive a copy instead of the reference. 39 | $reader = function & ($object, $property) { 40 | $value = & \Closure::bind(function & () use ($property) { 41 | return $this->$property; 42 | }, $object, $object)->__invoke(); 43 | 44 | return $value; 45 | }; 46 | 47 | $items =& $reader($target, 'items'); 48 | 49 | return $items[$key]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/CarbonSynth.php: -------------------------------------------------------------------------------- 1 | DateTime::class, 14 | 'nativeImmutable' => DateTimeImmutable::class, 15 | 'carbon' => Carbon::class, 16 | 'carbonImmutable' => CarbonImmutable::class, 17 | 'illuminate' => \Illuminate\Support\Carbon::class, 18 | ]; 19 | 20 | public static $key = 'cbn'; 21 | 22 | static function match($target) { 23 | foreach (static::$types as $type => $class) { 24 | if ($target instanceof $class) return true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | static function matchByType($type) { 31 | return is_subclass_of($type, DateTimeInterface::class); 32 | } 33 | 34 | function dehydrate($target) { 35 | return [ 36 | $target->format(DateTimeInterface::ATOM), 37 | ['type' => array_search(get_class($target), static::$types)], 38 | ]; 39 | } 40 | 41 | static function hydrateFromType($type, $value) { 42 | if ($value === '' || $value === null) return null; 43 | 44 | return new $type($value); 45 | } 46 | 47 | function hydrate($value, $meta) { 48 | if ($value === '' || $value === null) return null; 49 | 50 | return new static::$types[$meta['type']]($value); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/AttributeCommand.php: -------------------------------------------------------------------------------- 1 | app = $app; 20 | } 21 | 22 | function temporarilyDisableExceptionHandlingAndMiddleware($callback) 23 | { 24 | $cachedHandler = app(ExceptionHandler::class); 25 | 26 | $cachedShouldSkipMiddleware = $this->app->shouldSkipMiddleware(); 27 | 28 | $this->withoutExceptionHandling([HttpException::class, AuthorizationException::class])->withoutMiddleware(); 29 | 30 | $result = $callback($this); 31 | 32 | $this->app->instance(ExceptionHandler::class, $cachedHandler); 33 | 34 | if (! $cachedShouldSkipMiddleware) { 35 | unset($this->app['middleware.disable']); 36 | } 37 | 38 | return $result; 39 | } 40 | 41 | function withoutHandling($except = []) 42 | { 43 | return $this->withoutExceptionHandling($except); 44 | } 45 | 46 | function addHeaders(array $headers) 47 | { 48 | $this->serverVariables = $this->transformHeadersToServerVars($headers); 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Features/SupportFormObjects/SupportFormObjects.php: -------------------------------------------------------------------------------- 1 | propertySynthesizer( 14 | FormObjectSynth::class 15 | ); 16 | } 17 | 18 | function boot() 19 | { 20 | $this->initializeFormObjects(); 21 | } 22 | 23 | protected function initializeFormObjects() 24 | { 25 | foreach ((new ReflectionClass($this->component))->getProperties() as $property) { 26 | // Public properties only... 27 | if ($property->isPublic() !== true) continue; 28 | // Uninitialized properties only... 29 | if ($property->isInitialized($this->component)) continue; 30 | 31 | $type = $property->getType(); 32 | 33 | if (! $type instanceof ReflectionNamedType) continue; 34 | 35 | $typeName = $type->getName(); 36 | 37 | // "Form" object property types only... 38 | if (! is_subclass_of($typeName, Form::class)) continue; 39 | 40 | $form = new $typeName( 41 | $this->component, 42 | $name = $property->getName() 43 | ); 44 | 45 | $callBootMethod = FormObjectSynth::bootFormObject($this->component, $form, $name); 46 | 47 | $property->setValue($this->component, $form); 48 | 49 | $callBootMethod(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livewire/livewire", 3 | "description": "A front-end framework for Laravel.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Caleb Porzio", 8 | "email": "calebporzio@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.1", 13 | "illuminate/database": "^10.0|^11.0", 14 | "illuminate/routing": "^10.0|^11.0", 15 | "illuminate/support": "^10.0|^11.0", 16 | "illuminate/validation": "^10.0|^11.0", 17 | "league/mime-type-detection": "^1.9", 18 | "symfony/console": "^6.0|^7.0", 19 | "symfony/http-kernel": "^6.2|^7.0", 20 | "laravel/prompts": "^0.1.24|^0.2|^0.3" 21 | }, 22 | "require-dev": { 23 | "psy/psysh": "^0.11.22|^0.12", 24 | "mockery/mockery": "^1.3.1", 25 | "phpunit/phpunit": "^10.4", 26 | "laravel/framework": "^10.15.0|^11.0", 27 | "orchestra/testbench": "^8.21.0|^9.0", 28 | "orchestra/testbench-dusk": "^8.24|^9.1", 29 | "calebporzio/sushi": "^2.1" 30 | }, 31 | "autoload": { 32 | "files": [ 33 | "src/helpers.php" 34 | ], 35 | "psr-4": { 36 | "Livewire\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "App\\": "vendor/orchestra/testbench-core/laravel/app", 42 | "Tests\\": "tests/", 43 | "LegacyTests\\": "legacy_tests/" 44 | } 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Livewire\\LivewireServiceProvider" 50 | ], 51 | "aliases": { 52 | "Livewire": "Livewire\\Livewire" 53 | } 54 | } 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/PublishCommand.php: -------------------------------------------------------------------------------- 1 | option('assets')) { 21 | $this->publishAssets(); 22 | } elseif ($this->option('config')) { 23 | $this->publishConfig(); 24 | } elseif ($this->option('pagination')) { 25 | $this->publishPagination(); 26 | } else { 27 | $this->publishConfig(); 28 | $this->publishPagination(); 29 | } 30 | } 31 | 32 | public function publishAssets() 33 | { 34 | $this->call('vendor:publish', ['--tag' => 'livewire:assets', '--force' => true]); 35 | } 36 | 37 | public function publishConfig() 38 | { 39 | $this->call('vendor:publish', ['--tag' => 'livewire:config', '--force' => true]); 40 | } 41 | 42 | public function publishPagination() 43 | { 44 | $this->call('vendor:publish', ['--tag' => 'livewire:pagination', '--force' => true]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/FileManipulationCommand.php: -------------------------------------------------------------------------------- 1 | replaceFirst(app()->getNamespace(), ''); 24 | 25 | $livewireFolder = app_path($namespace->explode('\\')->implode(DIRECTORY_SEPARATOR)); 26 | 27 | return ! File::isDirectory($livewireFolder); 28 | } 29 | 30 | public function writeWelcomeMessage() 31 | { 32 | $asciiLogo = << _._ 34 | / /o\ \ || () () __ 35 | |_\ /_| || || \\\// /_\ \\\ // || |~~ /_\ 36 | |`|`| || || \/ \\\_ \^/ || || \\\_ 37 | EOT; 38 | // _._ 39 | // / /o\ \ || () () __ 40 | // |_\ /_| || || \\\// /_\ \\\ // || |~~ /_\ 41 | // |`|`| || || \/ \\\_ \^/ || || \\\_ 42 | $this->line("\n".$asciiLogo."\n"); 43 | $this->line("\nCongratulations, you've created your first Livewire component! 🎉🎉🎉\n"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeAlpineInstructions.php: -------------------------------------------------------------------------------- 1 | line(' Manual Upgrade: Remove Alpine references '); 12 | $console->newLine(); 13 | $console->line('Livewire version 3 ships with AlpineJS by default.'); 14 | $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.'); 15 | $console->line('Livewire version 3 now also ships with the following Alpine plugins:'); 16 | $console->line('- Intersect'); 17 | $console->line('- Collapse'); 18 | $console->line('- Persist'); 19 | $console->line('- Morph'); 20 | $console->line('- Focus'); 21 | $console->line('- Mask'); 22 | $console->newLine(); 23 | $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:'); 24 | $console->newLine(); 25 | $console->line('import { Livewire, Alpine } from \'../../vendor/livewire/livewire/dist/livewire.esm\';'); 26 | $console->line('Alpine.plugin(yourCustomPlugin);'); 27 | $console->line('Livewire.start();'); 28 | 29 | if($console->confirm('Continue?', true)) 30 | { 31 | return $next($console); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Features/SupportValidation/SupportValidation.php: -------------------------------------------------------------------------------- 1 | component->setErrorBag( 15 | $memo['errors'] ?? [] 16 | ); 17 | } 18 | 19 | function render($view, $data) 20 | { 21 | $errors = (new ViewErrorBag)->put('default', $this->component->getErrorBag()); 22 | 23 | $revert = Utils::shareWithViews('errors', $errors); 24 | 25 | return function () use ($revert) { 26 | // After the component has rendered, let's revert our global 27 | // sharing of the "errors" variable with blade views... 28 | $revert(); 29 | }; 30 | } 31 | 32 | function dehydrate($context) 33 | { 34 | $errors = $this->component->getErrorBag()->toArray(); 35 | 36 | // Only persist errors that were born from properties on the component 37 | // and not from custom validators (Validator::make) that were run. 38 | $context->addMemo('errors', collect($errors) 39 | ->filter(function ($value, $key) { 40 | return Utils::hasProperty($this->component, $key); 41 | }) 42 | ->toArray() 43 | ); 44 | } 45 | 46 | function exception($e, $stopPropagation) 47 | { 48 | if (! $e instanceof ValidationException) return; 49 | 50 | $this->component->setErrorBag($e->validator->errors()); 51 | 52 | $stopPropagation(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Livewire Logo

2 | 3 |

4 | 5 | Total Downloads 6 | 7 | 8 | Latest Stable Version 9 | 10 | 11 | License 12 | 13 |

14 | 15 | ## Introduction 16 | 17 | Livewire is a full-stack framework for Laravel that allows you to build dynamic UI components without leaving PHP. 18 | 19 | ## Official Documentation 20 | 21 | You can read the official documentation on the [Livewire website](https://livewire.laravel.com/docs). 22 | 23 | ## Contributing 24 | 25 | 26 | Thank you for considering contributing to Livewire! You can read the contribution guide [here](.github/CONTRIBUTING.md). 27 | 28 | ## Code of Conduct 29 | 30 | 31 | In order to ensure that the Laravel community is welcoming to all, please review and abide by Laravel's [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 32 | 33 | ## Security Vulnerabilities 34 | 35 | 36 | Please review [our security policy](https://github.com/livewire/livewire/security/policy) on how to report security vulnerabilities. 37 | 38 | ## License 39 | 40 | 41 | Livewire is open-sourced software licensed under the [MIT license](LICENSE.md). 42 | -------------------------------------------------------------------------------- /src/Livewire.php: -------------------------------------------------------------------------------- 1 | exists()) return; 19 | 20 | $fromSession = $this->read(); 21 | 22 | $this->setValue($fromSession); 23 | } 24 | 25 | public function dehydrate($context) 26 | { 27 | $this->write(); 28 | } 29 | 30 | protected function exists() 31 | { 32 | return Session::exists($this->key()); 33 | } 34 | 35 | protected function read() 36 | { 37 | return Session::get($this->key()); 38 | } 39 | 40 | protected function write() 41 | { 42 | Session::put($this->key(), $this->getValue()); 43 | } 44 | 45 | protected function key() 46 | { 47 | if (! $this->key) { 48 | return (string) 'lw' . crc32($this->component->getName() . $this->getName()); 49 | } 50 | 51 | return self::replaceDynamicPlaceholders($this->key, $this->component); 52 | } 53 | 54 | static function replaceDynamicPlaceholders($key, $component) 55 | { 56 | return preg_replace_callback('/\{(.*)\}/U', function ($matches) use ($component) { 57 | return data_get($component, $matches[1], function () use ($matches) { 58 | throw new \Exception('Unable to evaluate dynamic session key placeholder: '.$matches[0]); 59 | }); 60 | }, $key); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/FileUploadController.php: -------------------------------------------------------------------------------- 1 | new Middleware($middleware), $middleware); 20 | } 21 | 22 | public function handle() 23 | { 24 | abort_unless(request()->hasValidSignature(), 401); 25 | 26 | $disk = FileUploadConfiguration::disk(); 27 | 28 | $filePaths = $this->validateAndStore(request('files'), $disk); 29 | 30 | return ['paths' => $filePaths]; 31 | } 32 | 33 | public function validateAndStore($files, $disk) 34 | { 35 | Validator::make(['files' => $files], [ 36 | 'files.*' => FileUploadConfiguration::rules() 37 | ])->validate(); 38 | 39 | $fileHashPaths = collect($files)->map(function ($file) use ($disk) { 40 | $filename = TemporaryUploadedFile::generateHashNameWithOriginalNameEmbedded($file); 41 | 42 | return $file->storeAs('/'.FileUploadConfiguration::path(), $filename, [ 43 | 'disk' => $disk 44 | ]); 45 | }); 46 | 47 | // Strip out the temporary upload directory from the paths. 48 | return $fileHashPaths->map(function ($path) { return str_replace(FileUploadConfiguration::path('/'), '', $path); }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/SupportConsoleCommands.php: -------------------------------------------------------------------------------- 1 | runningInConsole()) return; 13 | 14 | static::commands([ 15 | Commands\MakeLivewireCommand::class, // make:livewire 16 | Commands\MakeCommand::class, // livewire:make 17 | Commands\FormCommand::class, // livewire:form 18 | Commands\AttributeCommand::class, // livewire:attribute 19 | Commands\TouchCommand::class, // livewire:touch 20 | Commands\CopyCommand::class, // livewire:copy 21 | Commands\CpCommand::class, // livewire:cp 22 | Commands\DeleteCommand::class, // livewire:delete 23 | Commands\LayoutCommand::class, // livewire:layout 24 | Commands\RmCommand::class, // livewire:rm 25 | Commands\MoveCommand::class, // livewire:move 26 | Commands\MvCommand::class, // livewire:mv 27 | Commands\StubsCommand::class, // livewire:stubs 28 | Commands\S3CleanupCommand::class, // livewire:configure-s3-upload-cleanup 29 | Commands\PublishCommand::class, // livewire:publish 30 | Commands\UpgradeCommand::class, // livewire:upgrade 31 | ]); 32 | } 33 | 34 | static function commands($commands) 35 | { 36 | $commands = is_array($commands) ? $commands : func_get_args(); 37 | 38 | Artisan::starting(fn(Artisan $artisan) => $artisan->resolveCommands($commands)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ChangeDefaultLayoutView.php: -------------------------------------------------------------------------------- 1 | hasOldLayout()) 12 | { 13 | $console->line(' The Livewire default layout has changed. '); 14 | $console->newLine(); 15 | 16 | $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".'); 17 | 18 | $choice = $console->choice('Would you like to migrate or keep the old layout?', [ 19 | 'migrate', 20 | 'keep', 21 | ], 'migrate'); 22 | 23 | if($choice == 'keep') { 24 | $console->line('Keeping the old default layout...'); 25 | 26 | $this->publishConfigIfMissing($console); 27 | 28 | $console->line('Setting the default layout to "layouts.app"...'); 29 | 30 | $this->patternReplacement('/components\.layouts\.app/', 'layouts.app', 'config'); 31 | 32 | return $next($console); 33 | } 34 | 35 | $console->line('Setting the default layout to "components.layouts.app"...'); 36 | 37 | $this->patternReplacement('/layouts\.app/', 'components.layouts.app', 'config'); 38 | } 39 | 40 | return $next($console); 41 | } 42 | 43 | protected function hasOldLayout() 44 | { 45 | return config('livewire.class_namespace') === 'layouts.app' || $this->filesystem()->exists('resources/views/layouts/app.blade.php'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Features/SupportTesting/ComponentState.php: -------------------------------------------------------------------------------- 1 | component; 20 | } 21 | 22 | function getSnapshot() 23 | { 24 | return $this->snapshot; 25 | } 26 | 27 | function getSnapshotData() 28 | { 29 | return $this->untupleify($this->snapshot['data']); 30 | } 31 | 32 | function getEffects() 33 | { 34 | return $this->effects; 35 | } 36 | 37 | function getView() 38 | { 39 | return $this->view; 40 | } 41 | 42 | function getResponse() 43 | { 44 | return $this->response; 45 | } 46 | 47 | function untupleify($payload) { 48 | $value = Utils::isSyntheticTuple($payload) ? $payload[0] : $payload; 49 | 50 | if (is_array($value)) { 51 | foreach ($value as $key => $child) { 52 | $value[$key] = $this->untupleify($child); 53 | } 54 | } 55 | 56 | return $value; 57 | } 58 | 59 | function getHtml($stripInitialData = false) 60 | { 61 | $html = $this->html; 62 | 63 | if ($stripInitialData) { 64 | $removeMe = (string) str($html)->betweenFirst( 65 | 'wire:snapshot="', '"' 66 | ); 67 | 68 | $html = str_replace($removeMe, '', $html); 69 | 70 | $removeMe = (string) str($html)->betweenFirst( 71 | 'wire:effects="', '"' 72 | ); 73 | 74 | $html = str_replace($removeMe, '', $html); 75 | } 76 | 77 | return $html; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/StubsCommand.php: -------------------------------------------------------------------------------- 1 | makeDirectory($stubsPath); 22 | } 23 | 24 | file_put_contents( 25 | $stubsPath.'/livewire.stub', 26 | file_get_contents(__DIR__.'/livewire.stub') 27 | ); 28 | 29 | file_put_contents( 30 | $stubsPath.'/livewire.inline.stub', 31 | file_get_contents(__DIR__.'/livewire.inline.stub') 32 | ); 33 | 34 | file_put_contents( 35 | $stubsPath.'/livewire.view.stub', 36 | file_get_contents(__DIR__.'/livewire.view.stub') 37 | ); 38 | 39 | file_put_contents( 40 | $stubsPath.'/livewire.test.stub', 41 | file_get_contents(__DIR__.'/livewire.test.stub') 42 | ); 43 | 44 | file_put_contents( 45 | $stubsPath.'/livewire.pest.stub', 46 | file_get_contents(__DIR__.'/livewire.pest.stub') 47 | ); 48 | 49 | file_put_contents( 50 | $stubsPath.'/livewire.form.stub', 51 | file_get_contents(__DIR__.'/livewire.form.stub') 52 | ); 53 | 54 | file_put_contents( 55 | $stubsPath.'/livewire.attribute.stub', 56 | file_get_contents(__DIR__.'/livewire.attribute.stub') 57 | ); 58 | 59 | $this->info('Stubs published successfully.'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ChangeTestAssertionMethods.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'assertEmitted is now assertDispatched.', 14 | before: 'assertEmitted', 15 | after: 'assertDispatched', 16 | pattern: '/assertEmitted\((.*)\)/', 17 | replacement: 'assertDispatched($1)', 18 | directories: 'tests' 19 | ); 20 | 21 | $this->interactiveReplacement( 22 | console: $console, 23 | title: 'assertEmittedTo is now assertDispatchedTo.', 24 | before: 'assertEmittedTo', 25 | after: 'assertDispatchedTo', 26 | pattern: '/assertEmittedTo\((.*)\)/', 27 | replacement: 'assertDispatchedTo($1)', 28 | directories: 'tests' 29 | ); 30 | 31 | $this->interactiveReplacement( 32 | console: $console, 33 | title: 'assertNotEmitted is now assertNotDispatched.', 34 | before: 'assertNotEmitted', 35 | after: 'assertNotDispatched', 36 | pattern: '/assertNotEmitted\((.*)\)/', 37 | replacement: 'assertNotDispatched($1)', 38 | directories: 'tests' 39 | ); 40 | 41 | $this->interactiveReplacement( 42 | console: $console, 43 | title: 'assertEmittedUp is no longer available.', 44 | before: 'assertEmittedUp', 45 | after: '', 46 | pattern: '/->assertEmittedUp\(.*\)/', 47 | replacement: '', 48 | directories: 'tests' 49 | ); 50 | 51 | return $next($console); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Features/SupportPageComponents/PageComponentConfig.php: -------------------------------------------------------------------------------- 1 | view = $view ?: config('livewire.layout'); 21 | $this->viewContext = new ViewContext; 22 | } 23 | 24 | function mergeParams($toMerge) 25 | { 26 | $this->params = array_merge($toMerge, $this->params); 27 | } 28 | 29 | function normalizeViewNameAndParamsForBladeComponents() 30 | { 31 | // If a user passes the class name of a Blade component to the 32 | // layout macro (or uses inside their config), we need to 33 | // convert it to it's "view" name so Blade doesn't break. 34 | $view = $this->view; 35 | $params = $this->params; 36 | 37 | $attributes = $params['attributes'] ?? []; 38 | unset($params['attributes']); 39 | 40 | if (is_subclass_of($view, \Illuminate\View\Component::class)) { 41 | $layout = app()->makeWith($view, $params); 42 | $view = $layout->resolveView()->name(); 43 | $params = array_merge($params, $layout->resolveView()->getData()); 44 | } else { 45 | $layout = new AnonymousComponent($view, $params); 46 | } 47 | 48 | $layout->withAttributes($attributes); 49 | 50 | $params = array_merge($params, $layout->data()); 51 | 52 | $this->view = $view; 53 | $this->params = $params; 54 | 55 | // Remove default slot if present... 56 | if (isset($this->slots['default'])) unset($this->slots['default']); 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Features/SupportWireModelingNestedComponents/BaseModelable.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if (str($key)->startsWith('wire:model')) { 19 | $outer = $value; 20 | store($this->component)->push('bindings-directives', $key, $value); 21 | break; 22 | } 23 | } 24 | 25 | if ($outer === null) return; 26 | 27 | $inner = $this->getName(); 28 | 29 | store($this->component)->push('bindings', $inner, $outer); 30 | 31 | $this->setValue(data_get($parent, $outer)); 32 | } 33 | 34 | // This update hook is for the following scenario: 35 | // An modelable value has changed in the browser. 36 | // A network request is triggered from the parent. 37 | // The request contains both parent and child component updates. 38 | // The parent finishes it's request and the "updated" value is 39 | // overridden in the parent's lifecycle (ex. a form field being reset). 40 | // Without this hook, the child's value will not honor that change 41 | // and will instead still be updated to the old value, even though 42 | // the parent changed the bound value. This hook detects if the parent 43 | // has provided a value during this request and ensures that it is the 44 | // final value for the child's request... 45 | function update($fullPath, $newValue) 46 | { 47 | if (store($this->component)->get('hasBeenSeeded', false)) { 48 | $oldValue = $this->getValue(); 49 | 50 | return function () use ($oldValue) { 51 | $this->setValue($oldValue); 52 | }; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Features/SupportFormObjects/FormObjectSynth.php: -------------------------------------------------------------------------------- 1 | toArray(); 22 | 23 | foreach ($data as $key => $child) { 24 | $data[$key] = $dehydrateChild($key, $child); 25 | } 26 | 27 | return [$data, ['class' => get_class($target)]]; 28 | } 29 | 30 | function hydrate($data, $meta, $hydrateChild) 31 | { 32 | $form = new $meta['class']($this->context->component, $this->path); 33 | 34 | $callBootMethod = static::bootFormObject($this->context->component, $form, $this->path); 35 | 36 | foreach ($data as $key => $child) { 37 | if ($child === null && Utils::propertyIsTypedAndUninitialized($form, $key)) { 38 | continue; 39 | } 40 | 41 | $form->$key = $hydrateChild($key, $child); 42 | } 43 | 44 | $callBootMethod(); 45 | 46 | return $form; 47 | } 48 | 49 | function set(&$target, $key, $value) 50 | { 51 | if ($value === null && Utils::propertyIsTyped($target, $key) && ! Utils::getProperty($target, $key)->getType()->allowsNull()) { 52 | unset($target->$key); 53 | } else { 54 | $target->$key = $value; 55 | } 56 | } 57 | 58 | public static function bootFormObject($component, $form, $path) 59 | { 60 | $component->mergeOutsideAttributes( 61 | AttributeCollection::fromComponent($component, $form, $path . '.') 62 | ); 63 | 64 | return function () use ($form) { 65 | wrap($form)->boot(); 66 | }; 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/Synth.php: -------------------------------------------------------------------------------- 1 | $key; 35 | } 36 | 37 | function __call($method, $params) { 38 | if ($method === 'dehydrate') { 39 | throw new \Exception('You must define a "dehydrate" method'); 40 | } 41 | 42 | if ($method === 'hydrate') { 43 | throw new \Exception('You must define a "hydrate" method'); 44 | } 45 | 46 | if ($method === 'hydrateFromType') { 47 | throw new \Exception('You must define a "hydrateFromType" method'); 48 | } 49 | 50 | if ($method === 'get') { 51 | throw new \Exception('This synth doesn\'t support getting properties: '.get_class($this)); 52 | } 53 | 54 | if ($method === 'set') { 55 | throw new \Exception('This synth doesn\'t support setting properties: '.get_class($this)); 56 | } 57 | 58 | if ($method === 'unset') { 59 | throw new \Exception('This synth doesn\'t support unsetting properties: '.get_class($this)); 60 | } 61 | 62 | if ($method === 'call') { 63 | throw new \Exception('This synth doesn\'t support calling methods: '.get_class($this)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/TestsRedirects.php: -------------------------------------------------------------------------------- 1 | action($uri); 15 | } 16 | 17 | if (! app('livewire')->isLivewireRequest()) { 18 | $this->lastState->getResponse()->assertRedirect($uri); 19 | 20 | return $this; 21 | } 22 | 23 | PHPUnit::assertArrayHasKey( 24 | 'redirect', 25 | $this->effects, 26 | 'Component did not perform a redirect.' 27 | ); 28 | 29 | if (! is_null($uri)) { 30 | PHPUnit::assertSame(url($uri), url($this->effects['redirect'])); 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | public function assertRedirectContains($uri) 37 | { 38 | if (is_subclass_of($uri, Component::class)) { 39 | $uri = url()->action($uri); 40 | } 41 | 42 | if (! app('livewire')->isLivewireRequest()) { 43 | $this->lastState->getResponse()->assertRedirectContains($uri); 44 | 45 | return $this; 46 | } 47 | 48 | PHPUnit::assertArrayHasKey( 49 | 'redirect', 50 | $this->effects, 51 | 'Component did not perform a redirect.' 52 | ); 53 | 54 | PHPUnit::assertTrue( 55 | Str::contains($this->effects['redirect'], $uri), 'Redirect location ['.$this->effects['redirect'].'] does not contain ['.$uri.'].' 56 | ); 57 | 58 | return $this; 59 | } 60 | 61 | public function assertRedirectToRoute($name, $parameters = []) 62 | { 63 | $uri = route($name, $parameters); 64 | 65 | return $this->assertRedirect($uri); 66 | } 67 | 68 | public function assertNoRedirect() 69 | { 70 | PHPUnit::assertTrue(! isset($this->effects['redirect'])); 71 | 72 | return $this; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/ComponentParserFromExistingComponent.php: -------------------------------------------------------------------------------- 1 | existingParser = $existingParser; 12 | 13 | parent::__construct($classNamespace, $viewPath, $rawCommand); 14 | } 15 | 16 | public function classContents($inline = false) 17 | { 18 | $originalFile = file_get_contents($this->existingParser->classPath()); 19 | 20 | $escapedClassNamespace = preg_replace('/\\\/', '\\\\\\', $this->existingParser->classNamespace()); 21 | 22 | return preg_replace_array( 23 | ["/namespace {$escapedClassNamespace}/", "/class {$this->existingParser->className()}/", "/{$this->existingParser->viewName()}/"], 24 | ["namespace {$this->classNamespace()}", "class {$this->className()}", $this->viewName()], 25 | $originalFile 26 | ); 27 | } 28 | 29 | public function testContents($testType = 'phpunit') 30 | { 31 | $file_content = file_get_contents($this->existingParser->testPath()); 32 | 33 | $escapedTestNamespace = preg_replace('/\\\/', '\\\\\\', $this->existingParser->testNamespace()); 34 | $escapedClassWithNamespace = preg_replace('/\\\/', '\\\\\\', $this->existingParser->classNamespace() . '\\' . $this->existingParser->className()); 35 | 36 | $replaces = [ 37 | "/namespace {$escapedTestNamespace}/" => 'namespace ' . $this->testNamespace(), 38 | "/use {$escapedClassWithNamespace}/" => 'use ' . $this->classNamespace() . '\\' . $this->className(), 39 | "/class {$this->existingParser->testClassName()}/" => 'class ' . $this->testClassName(), 40 | "/{$this->existingParser->className()}::class/" => $this->className() . '::class', 41 | ]; 42 | 43 | return preg_replace(array_keys($replaces), array_values($replaces), $file_content); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Features/SupportQueryString/SupportQueryString.php: -------------------------------------------------------------------------------- 1 | getQueryString()) return; 18 | 19 | foreach ($queryString as $key => $value) { 20 | $key = is_string($key) ? $key : $value; 21 | $alias = $value['as'] ?? $key; 22 | $history = $value['history'] ?? true; 23 | $keep = $value['alwaysShow'] ?? $value['keep'] ?? false; 24 | $except = $value['except'] ?? null; 25 | 26 | $this->component->setPropertyAttribute($key, new BaseUrl(as: $alias, history: $history, keep: $keep, except: $except)); 27 | } 28 | } 29 | 30 | public function getQueryString() 31 | { 32 | if (isset($this->queryString)) return $this->queryString; 33 | 34 | $component = $this->component; 35 | 36 | $componentQueryString = []; 37 | 38 | if (method_exists($component, 'queryString')) $componentQueryString = invade($component)->queryString(); 39 | elseif (property_exists($component, 'queryString')) $componentQueryString = invade($component)->queryString; 40 | 41 | return $this->queryString = collect(class_uses_recursive($class = $component::class)) 42 | ->map(function ($trait) use ($class, $component) { 43 | $member = 'queryString' . class_basename($trait); 44 | 45 | if (method_exists($class, $member)) { 46 | return invade($component)->{$member}(); 47 | } 48 | 49 | if (property_exists($class, $member)) { 50 | return invade($component)->{$member}; 51 | } 52 | 53 | return []; 54 | }) 55 | ->values() 56 | ->mapWithKeys(function ($value) { 57 | return $value; 58 | }) 59 | ->merge($componentQueryString) 60 | ->toArray(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/FileUploadSynth.php: -------------------------------------------------------------------------------- 1 | dehydratePropertyFromWithFileUploads($target), []]; 17 | } 18 | 19 | public function dehydratePropertyFromWithFileUploads($value) 20 | { 21 | if (TemporaryUploadedFile::canUnserialize($value)) { 22 | return TemporaryUploadedFile::unserializeFromLivewireRequest($value); 23 | } 24 | 25 | if ($value instanceof TemporaryUploadedFile) { 26 | return $value->serializeForLivewireResponse(); 27 | } 28 | 29 | if (is_array($value) && isset(array_values($value)[0])) { 30 | $isValid = true; 31 | 32 | foreach ($value as $key => $arrayValue) { 33 | if (!($arrayValue instanceof TemporaryUploadedFile) || !is_numeric($key)) { 34 | $isValid = false; 35 | break; 36 | } 37 | } 38 | 39 | if ($isValid) { 40 | return array_values($value)[0]::serializeMultipleForLivewireResponse($value); 41 | } 42 | } 43 | 44 | if (is_array($value)) { 45 | foreach ($value as $key => $item) { 46 | $value[$key] = $this->dehydratePropertyFromWithFileUploads($item); 47 | } 48 | } 49 | 50 | if ($value instanceof \Livewire\Wireable) { 51 | $keys = array_keys(get_object_vars($value)); 52 | 53 | foreach ($keys as $key) { 54 | $value->{$key} = $this->dehydratePropertyFromWithFileUploads($value->{$key}); 55 | } 56 | } 57 | 58 | return $value; 59 | } 60 | 61 | function hydrate($value) { 62 | if (TemporaryUploadedFile::canUnserialize($value)) { 63 | return TemporaryUploadedFile::unserializeFromLivewireRequest($value); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/GenerateSignedUploadUrl.php: -------------------------------------------------------------------------------- 1 | addMinutes(FileUploadConfiguration::maxUploadTime()) 15 | ); 16 | } 17 | 18 | public function forS3($file, $visibility = 'private') 19 | { 20 | $driver = FileUploadConfiguration::storage()->getDriver(); 21 | 22 | // Flysystem V2+ doesn't allow direct access to adapter, so we need to invade instead. 23 | $adapter = invade($driver)->adapter; 24 | 25 | // Flysystem V2+ doesn't allow direct access to client, so we need to invade instead. 26 | $client = invade($adapter)->client; 27 | 28 | // Flysystem V2+ doesn't allow direct access to bucket, so we need to invade instead. 29 | $bucket = invade($adapter)->bucket; 30 | 31 | $fileType = $file->getMimeType(); 32 | $fileHashName = TemporaryUploadedFile::generateHashNameWithOriginalNameEmbedded($file); 33 | $path = FileUploadConfiguration::path($fileHashName); 34 | 35 | $command = $client->getCommand('putObject', array_filter([ 36 | 'Bucket' => $bucket, 37 | 'Key' => $path, 38 | 'ACL' => $visibility, 39 | 'ContentType' => $fileType ?: 'application/octet-stream', 40 | 'CacheControl' => null, 41 | 'Expires' => null, 42 | ])); 43 | 44 | $signedRequest = $client->createPresignedRequest( 45 | $command, 46 | '+' . FileUploadConfiguration::maxUploadTime() . ' minutes' 47 | ); 48 | 49 | return [ 50 | 'path' => $fileHashName, 51 | 'url' => (string) $signedRequest->getUri(), 52 | 'headers' => $this->headers($signedRequest, $fileType), 53 | ]; 54 | } 55 | 56 | protected function headers($signedRequest, $fileType) 57 | { 58 | return array_merge( 59 | $signedRequest->getHeaders(), 60 | [ 61 | 'Content-Type' => $fileType ?: 'application/octet-stream' 62 | ] 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Features/SupportPagination/HandlesPagination.php: -------------------------------------------------------------------------------- 1 | paginators)->mapWithKeys(function ($page, $pageName) { 15 | return ['paginators.'.$pageName => ['history' => true, 'as' => $pageName, 'keep' => false]]; 16 | })->toArray(); 17 | } 18 | 19 | public function getPage($pageName = 'page') 20 | { 21 | return $this->paginators[$pageName] ?? Paginator::resolveCurrentPage($pageName); 22 | } 23 | 24 | public function previousPage($pageName = 'page') 25 | { 26 | $this->setPage(max(($this->paginators[$pageName] ?? 1) - 1, 1), $pageName); 27 | } 28 | 29 | public function nextPage($pageName = 'page') 30 | { 31 | $this->setPage(($this->paginators[$pageName] ?? 1) + 1, $pageName); 32 | } 33 | 34 | public function gotoPage($page, $pageName = 'page') 35 | { 36 | $this->setPage($page, $pageName); 37 | } 38 | 39 | public function resetPage($pageName = 'page') 40 | { 41 | $this->setPage(1, $pageName); 42 | } 43 | 44 | public function setPage($page, $pageName = 'page') 45 | { 46 | if (is_numeric($page)) { 47 | $page = (int) ($page <= 0 ? 1 : $page); 48 | } 49 | 50 | $beforePaginatorMethod = 'updatingPaginators'; 51 | $afterPaginatorMethod = 'updatedPaginators'; 52 | 53 | $beforeMethod = 'updating' . ucfirst(Str::camel($pageName)); 54 | $afterMethod = 'updated' . ucfirst(Str::camel($pageName)); 55 | 56 | if (method_exists($this, $beforePaginatorMethod)) { 57 | $this->{$beforePaginatorMethod}($page, $pageName); 58 | } 59 | 60 | if (method_exists($this, $beforeMethod)) { 61 | $this->{$beforeMethod}($page, null); 62 | } 63 | 64 | $this->paginators[$pageName] = $page; 65 | 66 | if (method_exists($this, $afterPaginatorMethod)) { 67 | $this->{$afterPaginatorMethod}($page, $pageName); 68 | } 69 | 70 | if (method_exists($this, $afterMethod)) { 71 | $this->{$afterMethod}($page, null); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Features/SupportNestedComponentListeners/SupportNestedComponentListeners.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | if (str($key)->startsWith('@')) { 22 | // any kebab-cased parameters passed in will have been converted to camelCase 23 | // so we need to convert back to kebab to ensure events are valid in html 24 | $fullEvent = str($key)->after('@')->kebab(); 25 | $attributeKey = 'x-on:'.$fullEvent; 26 | $attributeValue = "\$wire.\$parent.".$value; 27 | 28 | store($this->component)->push('attributes', $attributeValue, $attributeKey); 29 | } 30 | } 31 | } 32 | 33 | public function render($view, $data) 34 | { 35 | return function ($html, $replaceHtml) { 36 | $attributes = store($this->component)->get('attributes', false); 37 | 38 | if (! $attributes) return; 39 | 40 | $replaceHtml(Utils::insertAttributesIntoHtmlRoot($html, $attributes)); 41 | }; 42 | } 43 | 44 | public function dehydrate($context) 45 | { 46 | $attributes = store($this->component)->get('attributes', false); 47 | 48 | if (! $attributes) return; 49 | 50 | $attributes && $context->addMemo('attributes', $attributes); 51 | } 52 | 53 | public function hydrate($memo) 54 | { 55 | if (! isset($memo['attributes'])) return; 56 | 57 | $attributes = $memo['attributes']; 58 | 59 | // Store the attributes for later dehydration... 60 | store($this->component)->set('attributes', $attributes); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Features/SupportAttributes/AttributeCollection.php: -------------------------------------------------------------------------------- 1 | push(tap($attribute->newInstance(), function ($attribute) use ($component, $subTarget) { 19 | $attribute->__boot($component, AttributeLevel::ROOT, null, null, $subTarget); 20 | })); 21 | } 22 | 23 | foreach ($reflected->getMethods() as $method) { 24 | foreach ($method->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 25 | $instance->push(tap($attribute->newInstance(), function ($attribute) use ($component, $method, $propertyNamePrefix, $subTarget) { 26 | $attribute->__boot($component, AttributeLevel::METHOD, $propertyNamePrefix . $method->getName(), $method->getName(), $subTarget); 27 | })); 28 | } 29 | } 30 | 31 | foreach ($reflected->getProperties() as $property) { 32 | foreach ($property->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 33 | $instance->push(tap($attribute->newInstance(), function ($attribute) use ($component, $property, $propertyNamePrefix, $subTarget) { 34 | $attribute->__boot($component, AttributeLevel::PROPERTY, $propertyNamePrefix . $property->getName(), $property->getName(), $subTarget); 35 | })); 36 | } 37 | } 38 | 39 | return $instance; 40 | } 41 | 42 | protected static function getClassAttributesRecursively($reflected) { 43 | $attributes = []; 44 | 45 | while ($reflected) { 46 | foreach ($reflected->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 47 | $attributes[] = $attribute; 48 | } 49 | 50 | $reflected = $reflected->getParentClass(); 51 | } 52 | 53 | return $attributes; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/SupportRedirects.php: -------------------------------------------------------------------------------- 1 | has('session.store')) { 21 | session()->forget(session()->get('_flash.new')); 22 | } 23 | }); 24 | 25 | on('flush-state', function () { 26 | static::$atLeastOneMountedComponentHasRedirected = false; 27 | }); 28 | } 29 | 30 | public function boot() 31 | { 32 | // Put Laravel's redirector aside and replace it with our own custom one. 33 | static::$redirectorCacheStack[] = app('redirect'); 34 | 35 | app()->bind('redirect', function () { 36 | $redirector = app(Redirector::class)->component($this->component); 37 | 38 | if (app()->has('session.store')) { 39 | $redirector->setSession(app('session.store')); 40 | } 41 | 42 | return $redirector; 43 | }); 44 | } 45 | 46 | public function dehydrate($context) 47 | { 48 | // Put the old redirector back into the container. 49 | app()->instance('redirect', array_pop(static::$redirectorCacheStack)); 50 | 51 | $to = $this->storeGet('redirect'); 52 | $usingNavigate = $this->storeGet('redirectUsingNavigate'); 53 | 54 | if (is_subclass_of($to, Component::class)) { 55 | $to = url()->action($to); 56 | } 57 | 58 | if ($to && ! app(HandleRequests::class)->isLivewireRequest()) { 59 | abort(redirect($to)); 60 | } 61 | 62 | if (! $to) return; 63 | 64 | $context->addEffect('redirect', $to); 65 | $usingNavigate && $context->addEffect('redirectUsingNavigate', true); 66 | 67 | if (! $context->isMounting()) { 68 | static::$atLeastOneMountedComponentHasRedirected = true; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Features/SupportModels/ModelSynth.php: -------------------------------------------------------------------------------- 1 | getMorphClass(); 26 | } catch (ClassMorphViolationException $e) { 27 | // If the model is not using morph classes, this exception is thrown 28 | $alias = $class; 29 | } 30 | 31 | $serializedModel = $target->exists 32 | ? (array) $this->getSerializedPropertyValue($target) 33 | : null; 34 | 35 | $meta = ['class' => $alias]; 36 | 37 | // If the model doesn't exist as it's an empty model or has been 38 | // recently deleted, then we don't want to include any key. 39 | if ($serializedModel) $meta['key'] = $serializedModel['id']; 40 | 41 | 42 | return [ 43 | null, 44 | $meta, 45 | ]; 46 | } 47 | 48 | function hydrate($data, $meta) { 49 | $class = $meta['class']; 50 | 51 | // If no alias found, this returns `null` 52 | $aliasClass = Relation::getMorphedModel($class); 53 | 54 | if (! is_null($aliasClass)) { 55 | $class = $aliasClass; 56 | } 57 | 58 | // If no key is provided then an empty model is returned 59 | if (! array_key_exists('key', $meta)) { 60 | return new $class; 61 | } 62 | 63 | $key = $meta['key']; 64 | 65 | $model = (new $class)->newQueryForRestoration($key)->useWritePdo()->firstOrFail(); 66 | 67 | return $model; 68 | } 69 | 70 | function get(&$target, $key) { 71 | throw new \Exception('Can\'t access model properties directly'); 72 | } 73 | 74 | function set(&$target, $key, $value, $pathThusFar, $fullPath) { 75 | throw new \Exception('Can\'t set model properties directly'); 76 | } 77 | 78 | function call($target, $method, $params, $addEffect) { 79 | throw new \Exception('Can\'t call model methods directly'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Features/SupportComputed/SupportLegacyComputedPropertySyntax.php: -------------------------------------------------------------------------------- 1 | unset('computedProperties', $property); 25 | } 26 | }); 27 | } 28 | 29 | public static function getComputedProperties($target) 30 | { 31 | return collect(static::getComputedPropertyNames($target)) 32 | ->mapWithKeys(function ($property) use ($target) { 33 | return [$property => static::getComputedProperty($target, $property)]; 34 | }) 35 | ->all(); 36 | } 37 | 38 | public static function hasComputedProperty($target, $property) 39 | { 40 | return array_search((string) str($property)->camel(), static::getComputedPropertyNames($target)) !== false; 41 | } 42 | 43 | public static function getComputedProperty($target, $property) 44 | { 45 | if (! static::hasComputedProperty($target, $property)) { 46 | throw new \Exception('No computed property found: $'.$property); 47 | } 48 | 49 | $method = 'get'.str($property)->studly().'Property'; 50 | 51 | store($target)->push( 52 | 'computedProperties', 53 | $value = store($target)->find('computedProperties', $property, fn() => wrap($target)->$method()), 54 | $property, 55 | ); 56 | 57 | return $value; 58 | } 59 | 60 | public static function getComputedPropertyNames($target) 61 | { 62 | $methodNames = SyntheticUtils::getPublicMethodsDefinedBySubClass($target); 63 | 64 | return collect($methodNames) 65 | ->filter(function ($method) { 66 | return str($method)->startsWith('get') 67 | && str($method)->endsWith('Property'); 68 | }) 69 | ->map(function ($method) { 70 | return (string) str($method)->between('get', 'Property')->camel(); 71 | }) 72 | ->all(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/fake-echo.js: -------------------------------------------------------------------------------- 1 | 2 | window.fakeEchoListeners = [] 3 | 4 | class FakeChannel { 5 | constructor(channel) { 6 | this.channel = channel 7 | 8 | this.type = 'public' 9 | } 10 | 11 | listen(eventName, callback) { 12 | window.fakeEchoListeners.push({ 13 | channel: this.channel, 14 | event: eventName, 15 | type: this.type, 16 | callback, 17 | }) 18 | 19 | return this 20 | } 21 | 22 | stopListening(eventName, callback) { 23 | window.fakeEchoListeners = window.fakeEchoListeners.filter(i => { 24 | if (callback) { 25 | return ! (i.event === eventName && i.callback === callback) 26 | } 27 | 28 | return ! (i.event === eventName) 29 | }) 30 | 31 | 32 | return this 33 | } 34 | } 35 | 36 | class FakePrivateChannel extends FakeChannel { 37 | constructor(channel) { 38 | super(channel) 39 | 40 | this.type = 'private' 41 | } 42 | 43 | whisper(eventName, data) { 44 | return this 45 | } 46 | } 47 | 48 | class FakePresenceChannel extends FakeChannel { 49 | constructor(channel) { 50 | super(channel) 51 | 52 | this.type = 'presence' 53 | } 54 | 55 | here(callback) { 56 | return this 57 | } 58 | 59 | joining(callback) { 60 | return this 61 | } 62 | 63 | whisper(eventName, data) { 64 | return this 65 | } 66 | 67 | leaving(callback) { 68 | return this 69 | } 70 | } 71 | 72 | class FakeEcho { 73 | join(channel) { 74 | return new FakePresenceChannel(channel); 75 | } 76 | 77 | channel(channel) { 78 | return new FakeChannel(channel); 79 | } 80 | 81 | private(channel) { 82 | return new FakePrivateChannel(channel); 83 | } 84 | 85 | encryptedPrivate(channel) { 86 | return new FakePrivateChannel(channel); 87 | } 88 | 89 | socketId() { 90 | return 'fake-socked-id' 91 | } 92 | 93 | // For dusk to trigger listeners... 94 | 95 | fakeTrigger({ channel, event, type, payload = {} }) { 96 | window.fakeEchoListeners.filter(listener => { 97 | if (event !== listener.event) return false 98 | if (channel !== listener.channel) return false 99 | if (type !== undefined && type !== listener.type) return false 100 | 101 | return true 102 | }).forEach(({ callback }) => { 103 | callback(payload) 104 | }) 105 | } 106 | } 107 | 108 | window.Echo = new FakeEcho 109 | -------------------------------------------------------------------------------- /src/Features/SupportTesting/SubsequentRender.php: -------------------------------------------------------------------------------- 1 | makeSubsequentRequest($calls, $updates, $cookies); 17 | } 18 | 19 | function makeSubsequentRequest($calls = [], $updates = [], $cookies = []) { 20 | $uri = app('livewire')->getUpdateUri(); 21 | 22 | $encodedSnapshot = json_encode($this->lastState->getSnapshot()); 23 | 24 | $payload = [ 25 | 'components' => [ 26 | [ 27 | 'snapshot' => $encodedSnapshot, 28 | 'calls' => $calls, 29 | 'updates' => $updates, 30 | ], 31 | ], 32 | ]; 33 | 34 | [$response, $componentInstance, $componentView] = $this->extractComponentAndBladeView(function () use ($uri, $payload, $cookies) { 35 | return $this->requestBroker->temporarilyDisableExceptionHandlingAndMiddleware(function ($requestBroker) use ($uri, $payload, $cookies) { 36 | return $requestBroker->addHeaders(['X-Livewire' => true])->call('POST', $uri, $payload, $cookies); 37 | }); 38 | }); 39 | 40 | app('livewire')->flushState(); 41 | 42 | if (! $response->isOk()) { 43 | return new ComponentState( 44 | $componentInstance, 45 | $response, 46 | null, 47 | '', 48 | [], 49 | [], 50 | ); 51 | } 52 | 53 | $json = $response->json(); 54 | 55 | // Set "original" to Blade view for assertions like "assertViewIs()"... 56 | $response->original = $componentView; 57 | 58 | $componentResponsePayload = $json['components'][0]; 59 | 60 | $snapshot = json_decode($componentResponsePayload['snapshot'], true); 61 | 62 | $effects = $componentResponsePayload['effects']; 63 | 64 | // If no new HTML has been rendered, let's forward the last known HTML... 65 | $html = $effects['html'] ?? $this->lastState->getHtml(stripInitialData: true); 66 | $view = $componentView ?? $this->lastState->getView(); 67 | 68 | return new ComponentState( 69 | $componentInstance, 70 | $response, 71 | $view, 72 | $html, 73 | $snapshot, 74 | $effects, 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/EventBus.php: -------------------------------------------------------------------------------- 1 | singleton($this::class); 14 | } 15 | 16 | function on($name, $callback) { 17 | if (! isset($this->listeners[$name])) $this->listeners[$name] = []; 18 | 19 | $this->listeners[$name][] = $callback; 20 | 21 | return fn() => $this->off($name, $callback); 22 | } 23 | 24 | function before($name, $callback) { 25 | if (! isset($this->listenersBefore[$name])) $this->listenersBefore[$name] = []; 26 | 27 | $this->listenersBefore[$name][] = $callback; 28 | 29 | return fn() => $this->off($name, $callback); 30 | } 31 | 32 | function after($name, $callback) { 33 | if (! isset($this->listenersAfter[$name])) $this->listenersAfter[$name] = []; 34 | 35 | $this->listenersAfter[$name][] = $callback; 36 | 37 | return fn() => $this->off($name, $callback); 38 | } 39 | 40 | function off($name, $callback) { 41 | $index = array_search($callback, $this->listeners[$name] ?? []); 42 | $indexAfter = array_search($callback, $this->listenersAfter[$name] ?? []); 43 | $indexBefore = array_search($callback, $this->listenersBefore[$name] ?? []); 44 | 45 | if ($index !== false) unset($this->listeners[$name][$index]); 46 | elseif ($indexAfter !== false) unset($this->listenersAfter[$name][$indexAfter]); 47 | elseif ($indexBefore !== false) unset($this->listenersBefore[$name][$indexBefore]); 48 | } 49 | 50 | function trigger($name, ...$params) { 51 | $middlewares = []; 52 | 53 | $listeners = array_merge( 54 | ($this->listenersBefore[$name] ?? []), 55 | ($this->listeners[$name] ?? []), 56 | ($this->listenersAfter[$name] ?? []), 57 | ); 58 | 59 | foreach ($listeners as $callback) { 60 | $result = $callback(...$params); 61 | 62 | if ($result) { 63 | $middlewares[] = $result; 64 | } 65 | } 66 | 67 | return function (&$forward = null, ...$extras) use ($middlewares) { 68 | foreach ($middlewares as $finisher) { 69 | if ($finisher === null) continue; 70 | 71 | $finisher = is_array($finisher) ? last($finisher) : $finisher; 72 | 73 | $result = $finisher($forward, ...$extras); 74 | 75 | // Only overwrite previous "forward" if something is returned from the callback. 76 | $forward = $result ?? $forward; 77 | } 78 | 79 | return $forward; 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Features/SupportAttributes/Attribute.php: -------------------------------------------------------------------------------- 1 | component = $component; 22 | $this->subName = $subName; 23 | $this->subTarget = $subTarget; 24 | $this->level = $level; 25 | $this->levelName = $name; 26 | } 27 | 28 | function getComponent() 29 | { 30 | return $this->component; 31 | } 32 | 33 | function getSubTarget() 34 | { 35 | return $this->subTarget; 36 | } 37 | 38 | function getSubName() 39 | { 40 | return $this->subName; 41 | } 42 | 43 | function getLevel() 44 | { 45 | return $this->level; 46 | } 47 | 48 | function getName() 49 | { 50 | return $this->levelName; 51 | } 52 | 53 | function getValue() 54 | { 55 | if ($this->level !== AttributeLevel::PROPERTY) { 56 | throw new \Exception('Can\'t set the value of a non-property attribute.'); 57 | } 58 | 59 | return data_get($this->component->all(), $this->levelName); 60 | } 61 | 62 | function setValue($value) 63 | { 64 | if ($this->level !== AttributeLevel::PROPERTY) { 65 | throw new \Exception('Can\'t set the value of a non-property attribute.'); 66 | } 67 | 68 | if ($enum = $this->tryingToSetStringOrIntegerToEnum($value)) { 69 | $value = $enum::from($value); 70 | } 71 | 72 | data_set($this->component, $this->levelName, $value); 73 | } 74 | 75 | protected function tryingToSetStringOrIntegerToEnum($subject) 76 | { 77 | if (! is_string($subject) && ! is_int($subject)) return; 78 | 79 | $target = $this->subTarget ?? $this->component; 80 | 81 | $name = $this->subName ?? $this->levelName; 82 | 83 | $property = str($name)->before('.')->toString(); 84 | 85 | $reflection = new \ReflectionProperty($target, $property); 86 | 87 | $type = $reflection->getType(); 88 | 89 | // If the type is available, display its name 90 | if ($type instanceof \ReflectionNamedType) { 91 | $name = $type->getName(); 92 | 93 | // If the type is a BackedEnum then return it's name 94 | if (is_subclass_of($name, \BackedEnum::class)) { 95 | return $name; 96 | } 97 | } 98 | 99 | return false; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/LayoutCommand.php: -------------------------------------------------------------------------------- 1 | layoutPath($baseViewPath, $layout); 23 | 24 | $relativeLayoutPath = $this->relativeLayoutPath($layoutPath); 25 | 26 | $force = $this->option('force'); 27 | 28 | $stubPath = $this->stubPath($this->option('stub')); 29 | 30 | if (File::exists($layoutPath) && ! $force) { 31 | $this->line("View already exists: {$relativeLayoutPath}"); 32 | 33 | return false; 34 | } 35 | 36 | $this->ensureDirectoryExists($layoutPath); 37 | 38 | $result = File::copy($stubPath, $layoutPath); 39 | 40 | if ($result) { 41 | $this->line(" LAYOUT CREATED 🤙\n"); 42 | $this->line("CLASS: {$relativeLayoutPath}"); 43 | } 44 | } 45 | 46 | protected function stubPath($stubSubDirectory = '') 47 | { 48 | $stubName = 'livewire.layout.stub'; 49 | 50 | if (! empty($stubSubDirectory) && str($stubSubDirectory)->startsWith('..')) { 51 | $stubDirectory = rtrim(str($stubSubDirectory)->replaceFirst('..' . DIRECTORY_SEPARATOR, ''), DIRECTORY_SEPARATOR) . '/'; 52 | } else { 53 | $stubDirectory = rtrim('stubs' . DIRECTORY_SEPARATOR . $stubSubDirectory, DIRECTORY_SEPARATOR) . '/'; 54 | } 55 | 56 | if (File::exists($stubPath = base_path($stubDirectory . $stubName))) { 57 | return $stubPath; 58 | } 59 | 60 | return __DIR__ . DIRECTORY_SEPARATOR . $stubName; 61 | } 62 | 63 | protected function layoutPath($baseViewPath, $layout) 64 | { 65 | $directories = $layout->explode('.'); 66 | 67 | $name = Str::kebab($directories->pop()); 68 | 69 | return $baseViewPath . DIRECTORY_SEPARATOR . collect() 70 | ->concat($directories) 71 | ->map([Str::class, 'kebab']) 72 | ->push("{$name}.blade.php") 73 | ->implode(DIRECTORY_SEPARATOR); 74 | } 75 | 76 | protected function relativeLayoutPath($layoutPath) 77 | { 78 | return (string) str($layoutPath)->replaceFirst(base_path() . '/', ''); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Features/SupportFileDownloads/SupportFileDownloads.php: -------------------------------------------------------------------------------- 1 | toResponse(request()); 17 | } 18 | 19 | if ($this->valueIsntAFileResponse($return)) return; 20 | 21 | $response = $return; 22 | 23 | $name = $this->getFilenameFromContentDispositionHeader( 24 | $response->headers->get('Content-Disposition') 25 | ); 26 | 27 | $binary = $this->captureOutput(function () use ($response) { 28 | $response->sendContent(); 29 | }); 30 | 31 | $content = base64_encode($binary); 32 | 33 | $this->storeSet('download', [ 34 | 'name' => $name, 35 | 'content' => $content, 36 | 'contentType' => $response->headers->get('Content-Type'), 37 | ]); 38 | }; 39 | } 40 | 41 | function dehydrate($context) 42 | { 43 | if (! $download = $this->storeGet('download')) return; 44 | 45 | $context->addEffect('download', $download); 46 | } 47 | 48 | function valueIsntAFileResponse($value) 49 | { 50 | return ! $value instanceof StreamedResponse 51 | && ! $value instanceof BinaryFileResponse; 52 | } 53 | 54 | function captureOutput($callback) 55 | { 56 | ob_start(); 57 | 58 | $callback(); 59 | 60 | return ob_get_clean(); 61 | } 62 | 63 | function getFilenameFromContentDispositionHeader($header) 64 | { 65 | /** 66 | * The following conditionals are here to allow for quoted and 67 | * non quoted filenames in the Content-Disposition header. 68 | * 69 | * Both of these values should return the correct filename without quotes. 70 | * 71 | * Content-Disposition: attachment; filename=filename.jpg 72 | * Content-Disposition: attachment; filename="test file.jpg" 73 | */ 74 | 75 | // Support foreign-language filenames (japanese, greek, etc..)... 76 | if (preg_match('/filename\*=utf-8\'\'(.+)$/i', $header, $matches)) { 77 | return rawurldecode($matches[1]); 78 | } 79 | 80 | if (preg_match('/.*?filename="(.+?)"/', $header, $matches)) { 81 | return $matches[1]; 82 | } 83 | 84 | if (preg_match('/.*?filename=([^; ]+)/', $header, $matches)) { 85 | return $matches[1]; 86 | } 87 | 88 | return 'download'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php: -------------------------------------------------------------------------------- 1 | handleViewException($e, $obLevel); 41 | } 42 | 43 | return ltrim(ob_get_clean()); 44 | } 45 | 46 | // Errors thrown while a view is rendering are caught by the Blade 47 | // compiler and wrapped in an "ErrorException". This makes Livewire errors 48 | // harder to read, AND causes issues like `abort(404)` not actually working. 49 | protected function handleViewException(\Throwable $e, $obLevel) 50 | { 51 | if ($this->shouldBypassExceptionForLivewire($e, $obLevel)) { 52 | // This is because there is no "parent::parent::". 53 | \Illuminate\View\Engines\PhpEngine::handleViewException($e, $obLevel); 54 | 55 | return; 56 | } 57 | 58 | parent::handleViewException($e, $obLevel); 59 | } 60 | 61 | public function shouldBypassExceptionForLivewire(\Throwable $e, $obLevel) 62 | { 63 | $uses = array_flip(class_uses_recursive($e)); 64 | 65 | return ( 66 | // Don't wrap "abort(403)". 67 | $e instanceof \Illuminate\Auth\Access\AuthorizationException 68 | // Don't wrap "abort(404)". 69 | || $e instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException 70 | // Don't wrap "abort(500)". 71 | || $e instanceof \Symfony\Component\HttpKernel\Exception\HttpException 72 | // Don't wrap most Livewire exceptions. 73 | || isset($uses[\Livewire\Exceptions\BypassViewHandler::class]) 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Features/SupportChecksumErrorDebugging/SupportChecksumErrorDebugging.php: -------------------------------------------------------------------------------- 1 | [], 'failure' => null])); 19 | 20 | $this->info('Monitoring for checksum errors...'); 21 | 22 | while (true) { 23 | $cache = json_decode(File::get($file), true); 24 | 25 | if ($cache['failure']) { 26 | $this->info('Failure: '.$cache['failure']); 27 | 28 | $cache['failure'] = null; 29 | } 30 | 31 | File::put($file, json_encode($cache)); 32 | 33 | sleep(1); 34 | } 35 | 36 | })->purpose('Debug checksum errors in Livewire'); 37 | 38 | on('checksum.fail', function ($checksum, $comparitor, $tamperedSnapshot) use ($file) { 39 | $cache = json_decode(File::get($file), true); 40 | 41 | if (! isset($cache['checksums'][$checksum])) return; 42 | 43 | $canonicalSnapshot = $cache['checksums'][$checksum]; 44 | 45 | $good = $this->array_diff_assoc_recursive($canonicalSnapshot, $tamperedSnapshot); 46 | $bad = $this->array_diff_assoc_recursive($tamperedSnapshot, $canonicalSnapshot); 47 | 48 | $cache['failure'] = "\nBefore: ".json_encode($good)."\nAfter: ".json_encode($bad); 49 | 50 | File::put($file, json_encode($cache)); 51 | }); 52 | 53 | on('checksum.generate', function ($checksum, $snapshot) use ($file) { 54 | $cache = json_decode(File::get($file), true); 55 | 56 | $cache['checksums'][$checksum] = $snapshot; 57 | 58 | File::put($file, json_encode($cache)); 59 | }); 60 | } 61 | 62 | // https://www.php.net/manual/en/function.array-diff-assoc.php#111675 63 | function array_diff_assoc_recursive($array1, $array2) { 64 | $difference=array(); 65 | 66 | foreach($array1 as $key => $value) { 67 | if( is_array($value) ) { 68 | if( !isset($array2[$key]) || !is_array($array2[$key]) ) { 69 | $difference[$key] = $value; 70 | } else { 71 | $new_diff = $this->array_diff_assoc_recursive($value, $array2[$key]); 72 | if( !empty($new_diff) ) 73 | $difference[$key] = $new_diff; 74 | } 75 | } else if( !array_key_exists($key,$array2) || $array2[$key] !== $value ) { 76 | $difference[$key] = $value; 77 | } 78 | } 79 | 80 | return $difference; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Features/SupportTesting/InitialRender.php: -------------------------------------------------------------------------------- 1 | makeInitialRequest($name, $params, $fromQueryString, $cookies, $headers); 18 | } 19 | 20 | function makeInitialRequest($name, $params, $fromQueryString = [], $cookies = [], $headers = []) 21 | { 22 | $uri = '/livewire-unit-test-endpoint/'.str()->random(20); 23 | 24 | $this->registerRouteBeforeExistingRoutes($uri, function () use ($name, $params) { 25 | return \Illuminate\Support\Facades\Blade::render('@livewire($name, $params)', [ 26 | 'name' => $name, 27 | 'params' => $params, 28 | ]); 29 | }); 30 | 31 | [$response, $componentInstance, $componentView] = $this->extractComponentAndBladeView(function () use ($uri, $fromQueryString, $cookies, $headers) { 32 | return $this->requestBroker->temporarilyDisableExceptionHandlingAndMiddleware(function ($requestBroker) use ($uri, $fromQueryString, $cookies, $headers) { 33 | return $requestBroker->addHeaders($headers)->call('GET', $uri, $fromQueryString, $cookies); 34 | }); 35 | }); 36 | 37 | app('livewire')->flushState(); 38 | 39 | $html = $response->getContent(); 40 | 41 | // Set "original" to Blade view for assertions like "assertViewIs()"... 42 | $response->original = $componentView; 43 | 44 | $snapshot = Utils::extractAttributeDataFromHtml($html, 'wire:snapshot'); 45 | $effects = Utils::extractAttributeDataFromHtml($html, 'wire:effects'); 46 | 47 | return new ComponentState($componentInstance, $response, $componentView, $html, $snapshot, $effects); 48 | } 49 | 50 | private function registerRouteBeforeExistingRoutes($path, $closure) 51 | { 52 | // To prevent this route from overriding wildcard routes registered within the application, 53 | // We have to make sure that this route is registered before other existing routes. 54 | $livewireTestingRoute = new \Illuminate\Routing\Route(['GET', 'HEAD'], $path, $closure); 55 | 56 | $existingRoutes = app('router')->getRoutes(); 57 | 58 | // Make an empty collection. 59 | $runningCollection = new \Illuminate\Routing\RouteCollection; 60 | 61 | // Add this testing route as the first one. 62 | $runningCollection->add($livewireTestingRoute); 63 | 64 | // Now add the existing routes after it. 65 | foreach ($existingRoutes as $route) { 66 | $runningCollection->add($route); 67 | } 68 | 69 | // Now set this route collection as THE route collection for the app. 70 | app('router')->setRoutes($runningCollection); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ComponentHook.php: -------------------------------------------------------------------------------- 1 | component = $component; 12 | } 13 | 14 | function callBoot(...$params) { 15 | if (method_exists($this, 'boot')) $this->boot(...$params); 16 | } 17 | 18 | function callMount(...$params) { 19 | if (method_exists($this, 'mount')) $this->mount(...$params); 20 | } 21 | 22 | function callHydrate(...$params) { 23 | if (method_exists($this, 'hydrate')) $this->hydrate(...$params); 24 | } 25 | 26 | function callUpdate($propertyName, $fullPath, $newValue) { 27 | $callbacks = []; 28 | 29 | if (method_exists($this, 'update')) $callbacks[] = $this->update($propertyName, $fullPath, $newValue); 30 | 31 | return function (...$params) use ($callbacks) { 32 | foreach ($callbacks as $callback) { 33 | if (is_callable($callback)) $callback(...$params); 34 | } 35 | }; 36 | } 37 | 38 | function callCall($method, $params, $returnEarly) { 39 | $callbacks = []; 40 | 41 | if (method_exists($this, 'call')) $callbacks[] = $this->call($method, $params, $returnEarly); 42 | 43 | return function (...$params) use ($callbacks) { 44 | foreach ($callbacks as $callback) { 45 | if (is_callable($callback)) $callback(...$params); 46 | } 47 | }; 48 | } 49 | 50 | function callRender(...$params) { 51 | $callbacks = []; 52 | 53 | if (method_exists($this, 'render')) $callbacks[] = $this->render(...$params); 54 | 55 | return function (...$params) use ($callbacks) { 56 | foreach ($callbacks as $callback) { 57 | if (is_callable($callback)) $callback(...$params); 58 | } 59 | }; 60 | } 61 | 62 | function callDehydrate(...$params) { 63 | if (method_exists($this, 'dehydrate')) $this->dehydrate(...$params); 64 | } 65 | 66 | function callDestroy(...$params) { 67 | if (method_exists($this, 'destroy')) $this->destroy(...$params); 68 | } 69 | 70 | function callException(...$params) { 71 | if (method_exists($this, 'exception')) $this->exception(...$params); 72 | } 73 | 74 | function getProperties() 75 | { 76 | return $this->component->all(); 77 | } 78 | 79 | function getProperty($name) 80 | { 81 | return data_get($this->getProperties(), $name); 82 | } 83 | 84 | function storeSet($key, $value) 85 | { 86 | store($this->component)->set($key, $value); 87 | } 88 | 89 | function storePush($key, $value, $iKey = null) 90 | { 91 | store($this->component)->push($key, $value, $iKey); 92 | } 93 | 94 | function storeGet($key, $default = null) 95 | { 96 | return store($this->component)->get($key, $default); 97 | } 98 | 99 | function storeHas($key) 100 | { 101 | return store($this->component)->has($key); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Features/SupportModels/EloquentCollectionSynth.php: -------------------------------------------------------------------------------- 1 | getQueueableClass(); 24 | 25 | /** 26 | * `getQueueableClass` above checks all models are the same and 27 | * then returns the class. We then instantiate a model object 28 | * so we can call `getMorphClass()` on it. 29 | * 30 | * If no alias is found, this just returns the class name 31 | */ 32 | $modelAlias = $modelClass ? (new $modelClass)->getMorphClass() : null; 33 | 34 | $meta = []; 35 | 36 | $serializedCollection = (array) $this->getSerializedPropertyValue($target); 37 | 38 | $meta['keys'] = $serializedCollection['id']; 39 | $meta['class'] = $class; 40 | $meta['modelClass'] = $modelAlias; 41 | 42 | return [ 43 | null, 44 | $meta 45 | ]; 46 | } 47 | 48 | function hydrate($data, $meta, $hydrateChild) 49 | { 50 | $class = $meta['class']; 51 | 52 | $modelClass = $meta['modelClass']; 53 | 54 | // If no alias found, this returns `null` 55 | $modelAlias = Relation::getMorphedModel($modelClass); 56 | 57 | if (! is_null($modelAlias)) { 58 | $modelClass = $modelAlias; 59 | } 60 | 61 | $keys = $meta['keys'] ?? []; 62 | 63 | if (count($keys) === 0) { 64 | return new $class(); 65 | } 66 | 67 | // We are using Laravel's method here for restoring the collection, which ensures 68 | // that all models in the collection are restored in one query, preventing n+1 69 | // issues and also only restores models that exist. 70 | $collection = (new $modelClass)->newQueryForRestoration($keys)->useWritePdo()->get(); 71 | 72 | $collection = $collection->keyBy->getKey(); 73 | 74 | return new $meta['class']( 75 | collect($meta['keys'])->map(function ($id) use ($collection) { 76 | return $collection[$id] ?? null; 77 | })->filter() 78 | ); 79 | } 80 | 81 | function get(&$target, $key) { 82 | throw new \Exception('Can\'t access model properties directly'); 83 | } 84 | 85 | function set(&$target, $key, $value, $pathThusFar, $fullPath) { 86 | throw new \Exception('Can\'t set model properties directly'); 87 | } 88 | 89 | function call($target, $method, $params, $addEffect) { 90 | throw new \Exception('Can\'t call model methods directly'); 91 | } 92 | } -------------------------------------------------------------------------------- /src/Features/SupportPagination/views/simple-bootstrap.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | if (! isset($scrollTo)) { 3 | $scrollTo = 'body'; 4 | } 5 | 6 | $scrollIntoViewJsSnippet = ($scrollTo !== false) 7 | ? << 14 | @if ($paginator->hasPages()) 15 | 52 | @endif 53 | 54 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/TestsEvents.php: -------------------------------------------------------------------------------- 1 | call('__dispatch', $event, $parameters); 13 | } 14 | 15 | public function fireEvent($event, ...$parameters) 16 | { 17 | return $this->dispatch($event, ...$parameters); 18 | } 19 | 20 | public function assertDispatched($event, ...$params) 21 | { 22 | $result = $this->testDispatched($event, $params); 23 | 24 | PHPUnit::assertTrue($result['test'], "Failed asserting that an event [{$event}] was fired{$result['assertionSuffix']}"); 25 | 26 | return $this; 27 | } 28 | 29 | public function assertNotDispatched($event, ...$params) 30 | { 31 | $result = $this->testDispatched($event, $params); 32 | 33 | PHPUnit::assertFalse($result['test'], "Failed asserting that an event [{$event}] was not fired{$result['assertionSuffix']}"); 34 | 35 | return $this; 36 | } 37 | 38 | public function assertDispatchedTo($target, $event, ...$params) 39 | { 40 | $this->assertDispatched($event, ...$params); 41 | $result = $this->testDispatchedTo($target, $event); 42 | 43 | PHPUnit::assertTrue($result, "Failed asserting that an event [{$event}] was fired to {$target}."); 44 | 45 | return $this; 46 | } 47 | 48 | protected function testDispatched($value, $params) 49 | { 50 | $assertionSuffix = '.'; 51 | 52 | if (empty($params)) { 53 | $test = collect(data_get($this->effects, 'dispatches'))->contains('name', '=', $value); 54 | } elseif (isset($params[0]) && ! is_string($params[0]) && is_callable($params[0])) { 55 | $event = collect(data_get($this->effects, 'dispatches'))->first(function ($item) use ($value) { 56 | return $item['name'] === $value; 57 | }); 58 | 59 | $test = $event && $params[0]($event['name'], $event['params']); 60 | } else { 61 | $test = (bool) collect(data_get($this->effects, 'dispatches'))->first(function ($item) use ($value, $params) { 62 | $commonParams = array_intersect_key($item['params'], $params); 63 | 64 | ksort($commonParams); 65 | ksort($params); 66 | 67 | return $item['name'] === $value 68 | && $commonParams === $params; 69 | }); 70 | 71 | $encodedParams = json_encode($params); 72 | $assertionSuffix = " with parameters: {$encodedParams}"; 73 | } 74 | 75 | return [ 76 | 'test' => $test, 77 | 'assertionSuffix' => $assertionSuffix, 78 | ]; 79 | } 80 | 81 | protected function testDispatchedTo($target, $value) 82 | { 83 | $name = app(ComponentRegistry::class)->getName($target); 84 | 85 | return (bool) collect(data_get($this->effects, 'dispatches'))->first(function ($item) use ($name, $value) { 86 | return $item['name'] === $value 87 | && $item['to'] === $name; 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/DeleteCommand.php: -------------------------------------------------------------------------------- 1 | parser = new ComponentParser( 18 | config('livewire.class_namespace'), 19 | config('livewire.view_path'), 20 | $this->argument('name') 21 | ); 22 | 23 | if (! $force = $this->option('force')) { 24 | $shouldContinue = $this->confirm( 25 | "Are you sure you want to delete the following files?\n\n{$this->parser->relativeClassPath()}\n{$this->parser->relativeViewPath()}\n" 26 | ); 27 | 28 | if (! $shouldContinue) { 29 | return; 30 | } 31 | } 32 | 33 | $inline = $this->option('inline'); 34 | $test = $this->option('test'); 35 | 36 | $class = $this->removeClass($force); 37 | if (! $inline) $view = $this->removeView($force); 38 | if ($test) $test = $this->removeTest($force); 39 | 40 | $this->line(" COMPONENT DESTROYED 🦖💫\n"); 41 | $class && $this->line("CLASS: {$this->parser->relativeClassPath()}"); 42 | if (! $inline) $view && $this->line("VIEW: {$this->parser->relativeViewPath()}"); 43 | if ($test) $test && $this->line("Test: {$this->parser->relativeTestPath()}"); 44 | } 45 | 46 | protected function removeTest($force = false) 47 | { 48 | $testPath = $this->parser->testPath(); 49 | 50 | if (! File::exists($testPath) && ! $force) { 51 | $this->line(" WHOOPS-IE-TOOTLES 😳 \n"); 52 | $this->line("Test doesn't exist: {$this->parser->relativeTestPath()}"); 53 | return false; 54 | } 55 | 56 | File::delete($testPath); 57 | 58 | return $testPath; 59 | } 60 | 61 | protected function removeClass($force = false) 62 | { 63 | $classPath = $this->parser->classPath(); 64 | 65 | if (! File::exists($classPath) && ! $force) { 66 | $this->line(" WHOOPS-IE-TOOTLES 😳 \n"); 67 | $this->line("Class doesn't exist: {$this->parser->relativeClassPath()}"); 68 | 69 | return false; 70 | } 71 | 72 | File::delete($classPath); 73 | 74 | return $classPath; 75 | } 76 | 77 | protected function removeView($force = false) 78 | { 79 | $viewPath = $this->parser->viewPath(); 80 | 81 | if (! File::exists($viewPath) && ! $force) { 82 | $this->line("View doesn't exist: {$this->parser->relativeViewPath()}"); 83 | 84 | return false; 85 | } 86 | 87 | File::delete($viewPath); 88 | 89 | return $viewPath; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Mechanisms/DataStore.php: -------------------------------------------------------------------------------- 1 | lookup = new WeakMap; 14 | } 15 | 16 | function set($instance, $key, $value) 17 | { 18 | if (! isset($this->lookup[$instance])) { 19 | $this->lookup[$instance] = []; 20 | } 21 | 22 | $this->lookup[$instance][$key] = $value; 23 | } 24 | 25 | function has($instance, $key, $iKey = null) { 26 | if (! isset($this->lookup[$instance])) { 27 | return false; 28 | } 29 | 30 | if (! isset($this->lookup[$instance][$key])) { 31 | return false; 32 | } 33 | 34 | if ($iKey !== null) { 35 | return !! ($this->lookup[$instance][$key][$iKey] ?? false); 36 | } 37 | 38 | return true; 39 | } 40 | 41 | function get($instance, $key, $default = null) 42 | { 43 | if (! isset($this->lookup[$instance])) { 44 | return value($default); 45 | } 46 | 47 | if (! isset($this->lookup[$instance][$key])) { 48 | return value($default); 49 | } 50 | 51 | return $this->lookup[$instance][$key]; 52 | } 53 | 54 | function find($instance, $key, $iKey = null, $default = null) 55 | { 56 | if (! isset($this->lookup[$instance])) { 57 | return value($default); 58 | } 59 | 60 | if (! isset($this->lookup[$instance][$key])) { 61 | return value($default); 62 | } 63 | 64 | if ($iKey !== null && ! isset($this->lookup[$instance][$key][$iKey])) { 65 | return value($default); 66 | } 67 | 68 | return $iKey !== null 69 | ? $this->lookup[$instance][$key][$iKey] 70 | : $this->lookup[$instance][$key]; 71 | } 72 | 73 | function push($instance, $key, $value, $iKey = null) 74 | { 75 | if (! isset($this->lookup[$instance])) { 76 | $this->lookup[$instance] = []; 77 | } 78 | 79 | if (! isset($this->lookup[$instance][$key])) { 80 | $this->lookup[$instance][$key] = []; 81 | } 82 | 83 | if ($iKey) { 84 | $this->lookup[$instance][$key][$iKey] = $value; 85 | } else { 86 | $this->lookup[$instance][$key][] = $value; 87 | } 88 | } 89 | 90 | function unset($instance, $key, $iKey = null) 91 | { 92 | if (! isset($this->lookup[$instance])) { 93 | return; 94 | } 95 | 96 | if (! isset($this->lookup[$instance][$key])) { 97 | return; 98 | } 99 | 100 | if ($iKey !== null) { 101 | // Set a local variable to avoid the "indirect modification" error. 102 | $keyValue = $this->lookup[$instance][$key]; 103 | 104 | unset($keyValue[$iKey]); 105 | 106 | $this->lookup[$instance][$key] = $keyValue; 107 | } else { 108 | // Set a local variable to avoid the "indirect modification" error. 109 | $instanceValue = $this->lookup[$instance]; 110 | 111 | unset($instanceValue[$key]); 112 | 113 | $this->lookup[$instance] = $instanceValue; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Features/SupportWireCurrent/test-views/navbar-sidebar.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 13 | 14 | 15 | 16 | 46 | 47 |
48 | {{ $slot }} 49 |
50 | 51 | 52 | --------------------------------------------------------------------------------