Showing preview only (497K chars total). Download the full file or copy to clipboard to get everything.
Repository: clickfwd/yoyo
Branch: develop
Commit: f8c14d260df0
Files: 249
Total size: 436.6 KB
Directory structure:
gitextract_fxf4xndl/
├── .actrc
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── package.json
├── phpunit.xml
├── src/
│ ├── assets/
│ │ └── js/
│ │ └── yoyo.js
│ └── yoyo/
│ ├── AnonymousComponent.php
│ ├── Blade/
│ │ ├── Application.php
│ │ ├── CreateBladeViewFromString.php
│ │ ├── YoyoBladeCompilerEngine.php
│ │ ├── YoyoBladeDirectives.php
│ │ ├── YoyoServiceProvider.php
│ │ └── yoyo-view.blade.php
│ ├── ClassHelpers.php
│ ├── Component.php
│ ├── ComponentManager.php
│ ├── ComponentResolver.php
│ ├── Concerns/
│ │ ├── BrowserEvents.php
│ │ ├── Redirector.php
│ │ ├── ResponseHeaders.php
│ │ └── Singleton.php
│ ├── ContainerResolver.php
│ ├── Containers/
│ │ ├── IlluminateContainer.php
│ │ └── YoyoContainer.php
│ ├── Exceptions/
│ │ ├── BindingNotFoundException.php
│ │ ├── BypassRenderMethod.php
│ │ ├── ComponentMethodNotFound.php
│ │ ├── ComponentNotFound.php
│ │ ├── ContainerResolutionException.php
│ │ ├── FailedToRegisterComponent.php
│ │ ├── HttpException.php
│ │ ├── IncompleteComponentParamInRequest.php
│ │ ├── MissingComponentTemplate.php
│ │ ├── NonPublicComponentMethodCall.php
│ │ └── NotFoundHttpException.php
│ ├── Interfaces/
│ │ ├── RequestInterface.php
│ │ ├── ViewProviderInterface.php
│ │ └── YoyoContainerInterface.php
│ ├── InvocableComponentVariable.php
│ ├── QueryString.php
│ ├── Request.php
│ ├── Services/
│ │ ├── BrowserEventsService.php
│ │ ├── Configuration.php
│ │ ├── PageRedirectService.php
│ │ ├── Response.php
│ │ └── UrlStateManagerService.php
│ ├── Twig/
│ │ └── YoyoTwigExtension.php
│ ├── View.php
│ ├── ViewProviders/
│ │ ├── BaseViewProvider.php
│ │ ├── BladeViewProvider.php
│ │ ├── PhalconViewProvider.php
│ │ ├── TwigViewProvider.php
│ │ └── YoyoViewProvider.php
│ ├── Yoyo.php
│ ├── YoyoCompiler.php
│ ├── YoyoHelpers.php
│ ├── YoyoPhalconController.php
│ ├── YoyoPhalconServiceProvider.php
│ └── helpers.php
└── tests/
├── Benchmark/
│ ├── PipelineBenchmarkTest.php
│ ├── RealWorldBenchmarkTest.php
│ ├── YoyoCompilerBenchmarkTest.php
│ └── profile-compiler.php
├── Browser/
│ ├── BrowserServer.php
│ ├── Components/
│ │ ├── ActionButton.php
│ │ ├── Counter.php
│ │ ├── DeleteItem.php
│ │ ├── DispatchBystander.php
│ │ ├── DispatchListener.php
│ │ ├── FavoriteButton.php
│ │ ├── Form.php
│ │ ├── LiveSearch.php
│ │ ├── ModalTrigger.php
│ │ ├── MultiScreen.php
│ │ ├── NotificationBadge.php
│ │ ├── NullProp.php
│ │ ├── Pagination.php
│ │ ├── ResponseHeaders.php
│ │ ├── StatusDropdown.php
│ │ └── TodoList.php
│ ├── CounterTest.php
│ ├── CrossComponentEventsTest.php
│ ├── DispatchTest.php
│ ├── FormTest.php
│ ├── InfrastructureTest.php
│ ├── LiveSearchTest.php
│ ├── ModalTest.php
│ ├── MultiScreenTest.php
│ ├── NullPropTest.php
│ ├── PaginationTest.php
│ ├── ProductListTest.php
│ ├── ResponseHeadersTest.php
│ ├── SkipRenderTest.php
│ ├── TodoListTest.php
│ ├── bootstrap.php
│ └── server/
│ ├── index.php
│ ├── layout.php
│ ├── pages/
│ │ ├── counter.php
│ │ ├── dispatch.php
│ │ ├── events.php
│ │ ├── form.php
│ │ ├── index.php
│ │ ├── live-search.php
│ │ ├── modal.php
│ │ ├── multi-screen.php
│ │ ├── null-prop.php
│ │ ├── pagination.php
│ │ ├── product-list.php
│ │ ├── response-headers.php
│ │ ├── skip-render.php
│ │ └── todo-list.php
│ └── views/
│ ├── action-button.php
│ ├── counter.php
│ ├── delete-item.php
│ ├── dispatch-bystander.php
│ ├── dispatch-listener.php
│ ├── favorite-button.php
│ ├── form.php
│ ├── live-search.php
│ ├── modal-trigger.php
│ ├── multi-screen.php
│ ├── notification-badge.php
│ ├── null-prop.php
│ ├── pagination.php
│ ├── response-headers.php
│ ├── status-dropdown.php
│ └── todo-list.php
├── Feature/
│ ├── AnonymousComponentTest.php
│ ├── BladeTest.php
│ ├── ComponentLifecycleTest.php
│ ├── ComponentResolverTest.php
│ ├── DispatchEventTest.php
│ ├── DynamicComponentTest.php
│ ├── NamespacedComponentTest.php
│ ├── NestedComponentTest.php
│ ├── ResponseHeadersComponentTest.php
│ └── TwigTest.php
├── Helpers.php
├── HelpersBlade.php
├── HelpersTwig.php
├── InitYoyoContainer.php
├── Pest.php
├── Unit/
│ ├── BrowserEventsServiceTest.php
│ ├── BrowserEventsTest.php
│ ├── ClassHelpersTest.php
│ ├── ComponentResolverTest.php
│ ├── ComponentTest.php
│ ├── ConfigurationTest.php
│ ├── ContainerResolverTest.php
│ ├── ExceptionsTest.php
│ ├── IlluminateContainerTest.php
│ ├── InvocableComponentVariableTest.php
│ ├── PageRedirectServiceTest.php
│ ├── ProtectedMethodTest.php
│ ├── PushUrlStateTest.php
│ ├── QueryStringTest.php
│ ├── RegressionTest.php
│ ├── RequestTest.php
│ ├── ResponseHeadersTest.php
│ ├── SecurityTest.php
│ ├── UrlStateManagerServiceTest.php
│ ├── ViewTest.php
│ ├── YoyoCompileTest.php
│ ├── YoyoCompilerEdgeCasesTest.php
│ ├── YoyoContainerTest.php
│ ├── YoyoHelpersTest.php
│ └── YoyoViewProviderTest.php
├── app/
│ ├── Comment.php
│ ├── Post.php
│ ├── Resolvers/
│ │ ├── BladeComponentResolver.php
│ │ ├── CustomComponentResolver.php
│ │ └── TwigComponentResolver.php
│ ├── Yoyo/
│ │ ├── Abort.php
│ │ ├── Account/
│ │ │ └── Register.php
│ │ ├── ActionArguments.php
│ │ ├── ComponentWithComputedArgs.php
│ │ ├── ComponentWithEmit.php
│ │ ├── ComponentWithListeners.php
│ │ ├── ComponentWithRedirect.php
│ │ ├── ComponentWithResponseHeaders.php
│ │ ├── ComponentWithSwapModifiers.php
│ │ ├── ComponentWithTrait.php
│ │ ├── ComputedProperty.php
│ │ ├── ComputedPropertyCache.php
│ │ ├── Counter.php
│ │ ├── CounterDynamicProperties.php
│ │ ├── DependencyInjectionAction.php
│ │ ├── DependencyInjectionClassWithNamedArgumentMapping.php
│ │ ├── DispatchListener.php
│ │ ├── EmptyResponse.php
│ │ ├── EmptyResponseAndRemove.php
│ │ ├── ProtectedMethods.php
│ │ ├── Registered.php
│ │ ├── SetViewData.php
│ │ └── VariadicParameters.php
│ └── resources/
│ └── views/
│ ├── components/
│ │ └── select.blade.php
│ └── yoyo/
│ ├── account/
│ │ ├── login.blade.php
│ │ ├── login.php
│ │ ├── register.blade.php
│ │ └── register.php
│ ├── action-arguments.php
│ ├── child.blade.php
│ ├── child.php
│ ├── child.twig
│ ├── component-with-computed-args.php
│ ├── component-with-emit.php
│ ├── component-with-listeners.php
│ ├── component-with-redirect.php
│ ├── component-with-response-headers.php
│ ├── component-with-swap-modifiers.php
│ ├── component-with-trait.php
│ ├── computed-property-cache.php
│ ├── computed-property.php
│ ├── counter.php
│ ├── dependency-injection-action.php
│ ├── dependency-injection-class-with-named-argument-mapping.php
│ ├── dispatch-listener.php
│ ├── foo.blade.php
│ ├── foo.php
│ ├── foo.twig
│ ├── layout-a.blade.php
│ ├── layout-b.blade.php
│ ├── parent.blade.php
│ ├── parent.php
│ ├── parent.twig
│ ├── registered-anon.php
│ ├── registered.php
│ ├── set-view-data.php
│ └── variadic-parameters.php
├── app-another/
│ ├── Yoyo/
│ │ └── Counter.php
│ └── views/
│ ├── components/
│ │ └── input.blade.php
│ ├── counter.blade.php
│ ├── counter.php
│ ├── counter.twig
│ ├── foo.blade.php
│ ├── foo.php
│ └── foo.twig
├── compiled/
│ └── .gitkeep
└── responses/
├── action-arguments.html
├── computed-property-cache.html
├── computed-property.html
├── nested.blade.html
├── nested.html
└── nested.twig.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .actrc
================================================
# For runs-on: ubuntu-latest
-P ubuntu-latest=shivammathur/node:latest
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
pull_request:
jobs:
style:
if: github.event_name == 'push'
runs-on: ubuntu-latest
name: Code Style
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Fix styling
unit:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: [ 8.4, 8.3 ]
name: Unit & Feature — PHP ${{ matrix.php }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Get Composer cache dir
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.lock') }}
restore-keys: php-${{ matrix.php }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run unit & feature tests
run: vendor/bin/pest --testsuite="Unit Suite,Feature Suite"
browser:
runs-on: ubuntu-latest
name: Browser E2E
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- name: Get Composer cache dir
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: php-8.3-composer-${{ hashFiles('composer.lock') }}
restore-keys: php-8.3-composer-
- name: Install PHP dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}-chromium
restore-keys: ${{ runner.os }}-playwright-
- name: Install Playwright
run: npm ci && npx playwright install --with-deps chromium
- name: Run browser tests
run: vendor/bin/pest --group=browser
================================================
FILE: .gitignore
================================================
/vendor
/node_modules
/.phpunit.result.cache
/.php-cs-fixer.cache
/tests/compiled/**/*.php
/tests/compiled/*.php
/tests/Browser/Screenshots
/tests/Benchmark/profile-report.html
.claude
================================================
FILE: .php-cs-fixer.dist.php
================================================
<?php
$finder = Symfony\Component\Finder\Finder::create()
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true)
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
]);
$config = new PhpCsFixer\Config();
return $config->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => ['elements' => ['method' => 'one']],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
])
->setFinder($finder);
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [Unreleased](https://github.com/clickfwd/yoyo/compare/0.15.0...develop)
## [0.15.0 (2026-04-10)](https://github.com/clickfwd/yoyo/compare/0.14.0...0.15.0)
### Added
- `Yoyo.dispatch()` and `Yoyo.dispatchTo()` JS API for triggering component events from JavaScript, matching Livewire 3's dispatch API. Thanks to @mucan54 for the original proposal in #35.
- Named parameter support for JS dispatch — object params like `{ postId: 2 }` map to listener method arguments by name.
- Required parameter validation for named dispatch params — throws `InvalidArgumentException` if a required parameter is missing.
- Documentation for all HTMX response header methods available via `$this->response` (`retarget`, `reswap`, `reselect`, `pushUrl`, `replaceUrl`, `location`, `redirect`, `refresh`, `trigger`, `triggerAfterSwap`, `triggerAfterSettle`).
### Fixed
- Fix `test_json` empty-params decode bug when `eventParams` is an empty JSON object (`'{}'`).
- Add defensive null-guard in `triggerServerEmittedEvent` to prevent TypeError when source element is not inside a component.
### Performance
- Add static cache to `getMethodParametersWithTypes()` for consistency with other `ClassHelpers` caches.
- Optimize props filtering in `YoyoCompiler` by replacing repeated `in_array` checks with a lookup map.
- Skip re-processing already compiled nested Yoyo child components (`yoyo:name` + `hx-vals`) during compile.
Benchmark update (5-run averages from `tests/Benchmark/RealWorldBenchmarkTest.php`):
- Listing List (10x3 children): `0.5064 -> 0.4488 ms/op` (`-11.4%`)
- Listing List (25x3 children): `1.1379 -> 1.0104 ms/op` (`-11.2%`)
- Listing List (50x3 children): `2.1507 -> 1.9204 ms/op` (`-10.7%`)
Other scenarios remained in the same range with normal benchmark variance.
## [0.14.0 (2025-10-27)](https://github.com/clickfwd/yoyo/compare/0.13.1...0.14.0)
### Breaking Changes
- Minimum PHP version is now 8.0+
- `illuminate/container` is now an optional dependency
### Added
- Built-in dependency injection container for standalone usage
### Changed
- Yoyo now automatically detects and uses `illuminate/container` if available, otherwise uses built-in container
### Migration Notes
If you're using Yoyo standalone and need advanced container features, install illuminate/container:
```bash
composer require illuminate/container
```
## [0.13.1 (2025-08-15)](https://github.com/clickfwd/yoyo/compare/0.13.0...0.13.1)
- Fix parameter validation to correctly handle optional parameters in component actions
## [0.13.0 (2025-08-15)](https://github.com/clickfwd/yoyo/compare/0.12.0...0.13.0)
- Fix spinners stop working when target is different than current element
- Add support for variadic parameters in component actions using PHP's `...$params` syntax
- Add automatic dependency injection for typed parameters in component actions
- Add new ClassHelpers methods: `methodHasVariadicParameter()` and `getMethodParametersWithTypes()`
- Enhanced ComponentManager to handle methods with variadic parameters
- Improved parameter validation to support methods with only typed (DI) parameters
- Container integration now properly handles mixed regular, typed, and variadic parameters
## [0.12.0 (2025-07-18)](https://github.com/clickfwd/yoyo/compare/0.11.1...0.12.0)
- Add compatibility up to illuminate/container v12
- Fix deprecated error for implicit nullable parameter value
## [0.11.1 (2025-05-28)](https://github.com/clickfwd/yoyo/compare/0.11.0...0.11.1)
- Improve parsing of yoyo attribute action arguments and fix error where they are incorreclty converted to null.
## [0.11.0 (2025-02-06)](https://github.com/clickfwd/yoyo/compare/0.10.0...0.11.0)
- Passing attributes to Yoyo\yoyo_render should only prefix HTMX attributes defined in `YoyoCompiler::YOYO_ATTRIBUTES`.
## [0.10.1 (2024-08-29)](https://github.com/clickfwd/yoyo/compare/0.10.0...0.10.1)
- Change default hx-include to `this` to improve event-to-request delay on forms with large number of elements.
## [0.10.0 (2024-06-20)](https://github.com/clickfwd/yoyo/compare/0.9.1...0.10.0)
- Merge PR to add Falcon framework implementation.
## [0.9.1 (2024-04-16)](https://github.com/clickfwd/yoyo/compare/0.9.0...0.9.1)
- Fix Safari/iOS errors due to evt.target and evt.srcElement now being null.
- Add support for port in UrlStateManagerService.php
- PHP 8.2/8.3 compat
- Fix ResponseHeaders::refresh error due to missing parameter.
- Fix headers already sent error when setting status code in response
- Ensure components are compiled only once.
- Bump htmx to v1.9.4 and include new config options.
- New Request::set, Request::triggerName and Request::header methods.
- New Response::reselect method for the HX-Reselect header.
- New New Yoyo::actionArgs method.
## [0.9.0 (2023-04-02)](https://github.com/clickfwd/yoyo/compare/0.8.1...0.9.0)
## New
- Added Component::actionMatches method.
- Add response HX header methods that can via accessed in Yoyo component via $this->response:
- location
- pushUrl
- redirect
- refresh
- replace
- reswap
- retarget
- trigger
- triggerAfterSwap
- triggerAfterSettle
## Changed
- Added composer support for illuminate/container v9.0
## Fixed
- Regex replacement in Yoyo compiler causing issues due to incorrect replacements.
## [0.8.2 (2021-07-07)](https://github.com/clickfwd/yoyo/compare/0.8.1...0.8.2)
### Changed
- Updated htmx to v1.8.4.
- Add support for new htmx attributes to the Yoyo compiler: `replace-url`, `select-oob`, `validate`.
- Add support for PHP 8.1 installs.
- Add `yoyo:history="remove"` attribute to allow excluding elements from browser history cache snapshot.
- Renamed `Component::addDynamicProperties` to `Component::getDynamicProperties` to make it consistent with other component methods.
- Expose all htmx configuration options to Yoyo via `Clickfwd\Yoyo\Services\Configuration`.
- Breaking change! Compiler no longer makes buttons, links or inputs reactive by default. Previously any button, link or input would automatically receive the yoyo:get="render" attribute unless it already had a different request attribute. Now it's necessary to explicitly add the yoyo:{method} attribute if you want to make the element reactive. You can also just add an empty `yoyo` attribute if you want to make a request to the default yoyo:get="render" attribute on the component.
### Fixed
- Allow queryString parameters with value of zero to be pushed to URL.
- Javascript `Yoyo.on` throws undefined error when event detail is of type object.
- Issues working with dynamic properties.
- Lots of other changes and improvements.
## [0.8.1 (2021-07-07)](https://github.com/clickfwd/yoyo/compare/0.8.0...0.8.1)
### Added
- Support for adding dynamic properties to components via Component::addDynamicProperties method, which returns an array of property names. NOTE: `renamed to Component::getDynamicProperties` in next update.
This can be useful when the names of the properties are not known in advanced (i.e. coming from the database). The code below shows how to use this together with queryStrings to push the dynamic property values to the URL. The dynamic properties are also available in templates like regular public properties.
```php
public function addDynamicProperties()
{
return ['width', 'length'];
}
public function getQueryString()
{
return array_merge($this->queryString, $this->addDynamicProperties());
}
```
## [0.8.0 (2021-07-07)](https://github.com/clickfwd/yoyo/compare/0.7.5...0.8.0)
### Changed
- Links that trigger Yoyo requests now automatically update the browser URL and push the component state to the browser history.
## [0.7.5 (2021-05-28)](https://github.com/clickfwd/yoyo/compare/0.7.4...0.7.5)
### Fixed
- Error retrieving parameter names for component action.
## [0.7.4 (2021-05-20)](https://github.com/clickfwd/yoyo/compare/0.7.3...0.7.4)
### Fixed
- Various fixes
## [0.7.3 (2021-04-04)](https://github.com/clickfwd/yoyo/compare/0.7.2...0.7.3)
### Fixed
- Allow component listeners to trigger the default `refresh` action.
```php
protected $listeners = ['updated' => 'refresh'];
```
## [0.7.2 (2021-03-22)](https://github.com/clickfwd/yoyo/compare/0.7.1...0.7.2)
### Fixed
- Initial component history snapshot taken even for components that don't push changes to the URL via `queryString`.
## [0.7.1 (2021-03-14)](https://github.com/clickfwd/yoyo/compare/0.7.0...0.7.1)
### Added
- Updated htmx to v1.3.1
- Component `emitToWithSelector` method to differentiate from `emitTo`. `emitTo` targets Yoyo components specifically, while `emitToWithSelector` can target elements using a CSS selector.
- Component `skipRenderAndRemove` method to allow removing components from the page.
- Component `addSwapModifiers` method to dynamically set [swap modifers](https://htmx.org/attributes/hx-swap/) when updating components.
- Additional component lifecycle hooks
- initialize - on component initialization, allows adding properties, setting listeners, etc.
- mount - after component initialization
- rendering - before component render method
- rendered - after component render method, receives component output
- Allow traits to implement lifecycle hooks which run after the component's equivalent. For example, a `trait WithValidation` in addition to adding its own properties and methods can also implement hooks with the format:
- `initializeWithValidation`
- `mountWithValidation`
- `renderingWithValidation`
- `renderedWithValidation`
- Depedency Injection for lifecycle hooks and listener methods.
- Yoyo\abort, Yoyo\abort_if, Yoyo\abort_unless functions allows throwing exceptions within components to stop execution while still sending any queued events back up to the browser.
- Namespace support for view templates and component classes
- Support for new htmx `hx-headers` attribute via `yoyo:headers`
- Tests for Blade and Twig
### Changed
- Automatically re-spawn dynamically created target elements if these are removed on swap.
When `yoyo:target="#some-element"` is used with an ID and the target element doesn't exist, Yoyo automatically creates an empty `<div id="some-element"></div>` and appends it to the document body.
- Refactored component resolver
- Events are sent to the browser even when throwing an exception within a component.
- Components are resolved from the container.
### Fixed
- Cannot use Array property as prop.
- Component props not persisted in POST request updates.
- Variables passed directly to `render` method leaking to component props.
================================================
FILE: LICENSE.md
================================================
## MIT License
Copyright © ClickFWD
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
================================================
FILE: README.md
================================================
# Yoyo
Yoyo is a full-stack PHP framework that you can use on any project to create rich dynamic interfaces using server-rendered HTML.
With Yoyo, you create reactive components that are seamlessly updated without the need to write any Javascript code.
Yoyo ships with a simple templating system, and offers out-of-the-box support for [Blade](https://laravel.com/docs/8.x/blade), without having to use Laravel, and [Twig](https://twig.symfony.com/).
Inspired by [Laravel Livewire](https://laravel-livewire.com/) and [Sprig](https://putyourlightson.com/plugins/sprig), and using [htmx](https://htmx.org/).
## 🚀 Yoyo Demo Apps
Check out the [Yoyo Demo App](https://app.getyoyo.dev) to get a better idea of what you can build with Yoyo. It showcases many different types of Yoyo components. You can also clone and install the demo apps:
- [Yoyo Blade App](https://github.com/clickfwd/yoyo-blade-app)
- [Yoyo Laravel App](https://github.com/clickfwd/yoyo-laravel-app)
- [Yoyo PHP template App](https://github.com/clickfwd/yoyo-app)
- [Yoyo Twig App](https://github.com/clickfwd/yoyo-twig-app)
## Documentation
- [How it Works](#how-it-works)
- [Installation](#installation)
- [Updating](#updating)
- [Configuring Yoyo](#configuring-yoyo)
- [Creating Components](#creating-components)
- [Rendering Components](#rendering-components)
- [Properties](#properties)
- [Actions](#actions)
- [View Data](#view-data)
- [Computed Properties](#computed-properties)
- [Events](#events)
- [Redirecting](#redirecting)
- [Component Props](#component-props)
- [Query String](#query-string)
- [Loading States](#loading-states)
- [Using Blade](#using-blade)
- [Using Twig](#using-twig)
- [License](#license)
## How it Works
Yoyo components are rendered on page load and can be individually updated, without the need for page-reloads, based on user interaction and specific events.
Component update requests are sent directly to a Yoyo-designated route, where it processes the request and then sends the updated component HTML partial back to the browser.
Yoyo can update the browser URL state and trigger browser events straight from the server.
Below you can see what a Counter component looks like:
**Component class**
```php
# /app/Yoyo/Counter.php
<?php
namespace App\Yoyo;
use Clickfwd\Yoyo\Component;
class Counter extends Component
{
public $count = 0;
protected $props = ['count'];
public function increment()
{
$this->count++;
}
}
```
**Component template**
```html
<!-- /app/resources/views/yoyo/counter.php -->
<div>
<button yoyo:get="increment">+</button>
<span><?php echo $count; ?></span>
</div>
```
Yes, it's that simple! One thing to note above is the use of the protected property `$props`. This indicates to Yoyo that the `count` variable, which is not explicitly available within the template, should be persisted and updated in every request.
## Installation
### Install the Package
```bash
composer require clickfwd/yoyo
```
#### Phalcon Framework Installation
For phalcon, you need to add di
```php
$di->register(new \Clickfwd\Yoyo\YoyoPhalconServiceProvider());
```
and you need to add router:
```php
$router->add('/yoyo', [
'controller' => 'yoyo',
'action' => 'handle',
]);
```
and you should create a controller and inherit from `Clickfwd\Yoyo\PhalconController` class.
## Updating
After performing the usual `composer update`, remember to also update the `yoyo.js` script per the [Load Assets](#load-assets) instructions.
## Configuring Yoyo
It's necessary to bootstrap Yoyo with a few configuration settings. This code should run when rendering and updating components.
```php
use Clickfwd\Yoyo\View;
use Clickfwd\Yoyo\ViewProviders\YoyoViewProvider;
use Clickfwd\Yoyo\Yoyo;
$yoyo = new Yoyo();
$yoyo->configure([
'url' => '/yoyo',
'scriptsPath' => 'app/resources/assets/js/',
'namespace' => 'App\\Yoyo\\'
]);
// Register the native Yoyo view provider
// Pass the Yoyo components' template directory path in the constructor
$yoyo->registerViewProvider(function() {
return new YoyoViewProvider(new View(__DIR__.'/resources/views/yoyo'));
});
```
**'url'**
Absolute or relative URL that will be used to request component updates.
**'scriptsPath'**
The location where you copied the `yoyo.js` script.
**'namespace'**
This is the PHP class namespace that will be used to discover auto-loaded dynamic components (components that use a PHP class).
If the namespace is not provided or components are in different namespaces, you need to register them manually:
```php
$yoyo->registerComponents([
'counter' => App\Yoyo\Counter::class,
];
```
You are required to load the component classes at run time, either using a `require` statement to load the component's PHP class file, or by including your component namespaces in you project's `composer.json`.
Anonymous components don't need to be registered, but the template name needs to match the component name.
### Dependency Injection Container
Yoyo includes a built-in container and automatically detects Laravel's `illuminate/container` if installed.
**Simple dependency injection:**
```php
class UserProfile extends Component
{
public function mount(UserRepository $users, $userId)
{
$this->user = $users->find($userId);
}
}
```
**For advanced container features**, install `illuminate/container` (automatically detected):
```bash
composer require illuminate/container
```
**Using a custom PSR-11 container:**
```php
use Clickfwd\Yoyo\ContainerResolver;
ContainerResolver::setPreferred($myContainer);
```
### Load Assets
Find `yoyo.js` in the following vendor path and copy it to your project's public assets directory.
```file
/vendor/clickfwd/yoyo/src/assets/js/yoyo.js
```
To load the necessary scripts in your template add the following code inside the `<head>` tag:
```php
<?php yoyo_scripts(); ?>
```
## Creating Components
Dynamic components require a class and a template. When using the Blade and Twig view providers, you can also use inline views, where the component markup is returned directly in the component's `render` method.
Anonymous components allow creating components with just a template file.
To create a simple search component that retrieves results from the server and updates itself, create the component template:
```html
// resources/views/yoyo/search.php
<form>
<input type="text" name="query" value="<?php echo $query ?? ''; ?>">
<button type="submit">Submit</button>
</form>
```
Yoyo will render the component output and compile it to add the necessary attributes that makes it dynamic and reactive.
When you submit the form, posted data is automatically made available within the component template. The template code can be expanded to show a list of results, or an empty state:
```php
<?php
$query = $query ?? '';
$entries = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$results = array_filter($entries, function($entry) use ($query) {
return $query && strpos($entry, $query) !== false;
});
?>
```
```html
<form>
<input type="text" name="query" value="<?php echo $query; ?>">
<button type="submit">Submit</button>
</form>
<ul>
<?php if ($query && empty($results)): ?>
<li>No results found</li>
<?php endif; ?>
<?php foreach ($results as $entry): ?>
<li><?php echo $entry; ?></li>
<?php endforeach; ?>
</ul>
```
The `$results` array can be populated from any source (i.e. database, API, etc.)
The example can be converted into a live search input, with a 300ms debounce to minimize the number of requests. Replace the `form` tag with:
```html
<input yoyo:on="keyup delay:300ms changed" type="text" name="query" value="<?php echo $query; ?>" />
```
The `yoyo:on="keyup delay:300ms change"` directive tells Yoyo to make a request on the keyup event, with a 300ms debounce, and only if the input text changed.
Now let's turn this into a dynamic component using a class.
```php
# /app/Yoyo/Search
<?php
namespace App\Yoyo;
use Clickfwd\Yoyo\Component;
class Search extends Component
{
public $query;
protected $queryString = ['query'];
public function render()
{
$query = $this->query;
// Perform your database query
$entries = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$results = array_filter($entries, function($entry) use ($query) {
return $query && stripos($entry, $query) !== false;
});
// Render the component view
return $this->view('search',['results' => $results]);
}
}
```
And the template:
```html
<!-- /app/resources/views/yoyo/search.php -->
<input yoyo:on="keyup delay:300ms changed" type="text" name="query" value="<?php echo $query; ?>" />
<ul yoyo:ignore>
<?php if ($query && empty($results)): ?>
<li>No results found</li>
<?php endif; ?>
<?php foreach ($results as $entry): ?>
<li><?php echo $entry; ?></li>
<?php endforeach; ?>
</ul>
```
A couple of things to note here that are covered in more detail in other sections.
1. The component class includes a `queryString` property that tells Yoyo to automatically include the queryString values in the browser URL after a component update. If you re-load the page with the `query` value in the URL, you'll automatically see the search results on the page.
2. Yoyo will automatically make available component class public properties as template variables. This allows using `$this->query` to access the search keyword in the component and `$query` in the template.
When you compare this search example to the counter example at the beginning, you can see that there are no action methods (i.e. increment, decrement). A component update will always default to the `render` method, unless an action is specified via one of the method attributes (i.e. yoyo:get, yoyo:post, etc.). In that case, the action method always runs before the render method.
## Rendering Components
There are two instances when components are rendered. On page load, and on component updates.
### Rendering on Page Load
To render any component on page load within your templates, use the `yoyo_render` function and pass the component name as the first parameter.
```php
<?php echo yoyo_render('search'); ?>
```
For dynamic components, the component name is a hyphenated version of the class name (i.e. LiveSearch → live-search). If you register components while bootstrapping Yoyo using the `registerComponents` method, then you can use the registered alias as the component name.
```php
$yoyo->registerComponent('search', App\Yoyo\LiveSearch::class);
```
For anonymous components, the component name should match the template name without the file extension. So if the template name is `form.php`, the component can be rendered with:
```php
<?php echo yoyo_render('form'); ?>
```
### Rendering on updates
Use the `yoyo_update` function to automatically process the component request and output the updated component.
```php
<?php echo yoyo_update(); ?>
```
You need to add this function call for requests routed to the Yoyo `url` used in the initial configuration.
## Properties
In dynamic components, all public properties in the component class are automatically made available to the view and tracked in component updates.
```php
class HelloWorld extends Component
{
public $message = 'Hello World!';
}
```
```html
<div>
<h1><?php echo $message; ?></h1>
<!-- Will output "Hello World!" -->
</div>
```
Public properties should only be of type: `string`, `int`, `array`, `boolean`, and should not contain any sensitive information because they can be used in component requests to keep the data in sync.
### Initializing Properties
You can initialize properties using the `mount` method of your component which runs right after the component is instantiated, and before the `render` method.
```php
class HelloWorld extends Component
{
public $message;
public function mount()
{
$this->message = 'Hello World!';
}
}
```
### Data Binding
You can automatically bind, or synchronize, the value of an HTML element with a component public property.
```php
class HelloWorld extends Component
{
public $message = 'Hello World!';
}
```
```html
<div>
<input yoyo name="message" type="text" value="<?php echo $message; ?>">
<h1><?php echo $message;?></h1>
</div>
```
Adding the `yoyo` attribute to any input will instantly make it reactive. Any changes to the input will be updated in the component.
By the default, the natural event of an element will be used as the event trigger.
- input, textarea and select elements are triggered on the change event.
- form elements are triggered on the submit event.
- All other elements are triggered on the click event.
You can modify this behavior using the `yoyo:on` directive which accepts multiple events separated by comma:
```html
<input yoyo:on="keyup" name="message" type="text" value="<?php echo $message; ?>">
```
### Debouncing and Throttling Requests
The are several ways to limit the requests to update components.
**`delay`** - debounces the request so it's made only after the specified period passes after the last trigger.
```html
<input yoyo:on="keyup delay:300ms" name="message" type="text" value="<?php echo $message; ?>">
```
**`throttle`** limits request to one dwithin the specified interval.
```html
<input yoyo:on="input throttle:2s" name="message" type="text" value="<?php echo $message; ?>">
```
**`changed`** - only makes the request when the input value has changed.
```html
<input yoyo:on="keyup delay:300ms changed" name="message" type="text" value="<?php echo $message; ?>">
```
## Actions
An action is a request made to a Yoyo component method to update (re-render) it as a result of a user interaction or page event (click, mouseover, scroll, load, etc.).
The `render` method is the default action when one is not provided explicitly. You can also override it in the component class to change the template name or when you need to send additional variables to the template in addition to the public properties.
```php
public function render()
{
return $this->view($this->componentName, ['foo' => 'bar']);
}
```
To specify an action you use one of the available action directives with the name of the action as the value.
- `yoyo:get`
- `yoyo:post`
- `yoyo:put`
- `yoyo:patch`
- `yoyo:delete`
For example:
```php
class Review extends Component
{
public Review $review;
public function helpful()
{
$this->review->userFoundHelpful($userId);
}
}
```
```html
<div>
<button yoyo:on="click" yoyo:get="helpful">Found Helpful</button>
</div>
```
All components automatically listen for the `refresh` event and trigger the `render` action to refresh the component state.
### Passing Data to Actions
You can include additional data to send to the server on component update requests using the `yoyo:vals` directive which accepts a JSON encoded list of name-value pairs.
```html
<button yoyo:on="click" yoyo:get="helpful" yoyo:vals='{"reviewId":100}'>Found Helpful</button>
<!-- Or use the encode_vals helper function to pass an array of name-value pairs -->
<button yoyo:on="click" yoyo:get="helpful" yoyo:vals='<?php Yoyo\encode_vals(["reviewId"=> 100]); ?>'>Found Helpful</button>
```
You can also use `yoyo:val.name` for individual values. kebab-case variable names are automatically converted to camel-case.
```html
<button yoyo:on="click" yoyo:get="helpful" yoyo:val.review-id="100">Found Helpful</button>
```
Yoyo will automatically track and send component public properties and input values with every request.
```php
class Review extends Component {
public $reviewId;
public function helpful()
{
// access reviewId via $this->reviewId
}
}
```
You can also pass extra parameters to an action as arguments using an expression, without having to define them as public properties in the component:
```html
<button yoyo:get="addToCart(<?php echo $productId; ?>, '<?php echo $style; ?>')">
Add Todo
</button>
```
Extra parameters passed to an action are made available to the component method as regular arguments:
```php
public function addToCart($productId, $style)
{
// ...
}
```
### Actions Without a Response
Sometimes you may want to use a component action only to make changes to a database and trigger events, without rendering a response. You can use the component `skipRender` method for this:
```php
public function savePost()
{
// Store the post to the database
// Send event to the browser to close modal, or trigger a notification
$this->emitSelf('PostSaved');
// Skip template rendering
$this->skipRender();
}
```
## View Data
Sometimes you want to send data to a view without declaring the variable as a public property. You can do this by defining a render method in your component and passing a data array as the second argument:
```php
public function render()
{
return $this->view($this->componentName, ['foo' => 'bar']);
}
```
Then access the $foo variable in your template.
You can also send data to the component view using the `set` method in any component action. For example:
```php
public function increment()
{
$this->set('foo', 'bar');
// or
$this->set(['foo' => 'bar']);
}
```
## Computed Properties
```php
class HelloWorld extends Component
{
public $message = 'Hello World!';
// Computed Property
public function getHelloWorldProperty()
{
return $message;
}
// Computed Property with argument
public function getErrorsProperty($name)
{
return [
'title' => 'Please enter a title',
'description' => 'Please enter a description',
][$name] ?? null;
}
}
```
Now, you can access `$this->hello_world` from either the component's class or template:
```php
<div>
<h1><?php echo $this->hello_world ;?></h1>
<!-- Will output "Hello World!" -->
</div>
```
Computed properties with arguments behave like normal class methods that you can call in your templates:
```php
<div>
<h1><?php echo $this->errors('title') ;?></h1>
<!-- Will output "Please enter a title" -->
</div>
```
The output of computed properties is cached within the same component request, allowing you to perform complex tasks like querying the database and not duplicating the tasks if the property is accessed multiple times. If you need to clear the cache for a computed property:
```php
// Clear all computed properties, including those with arguments
$this->forgetComputed();
// Clear a single property
$this->forgetComputed($property);
// Clear multiple properties
$this->forgetComputed([$property1, $property2]);
// Clear a single computed property with arguments
$this->forgetComputedWithArgs($property, $arg1, $arg2);
```
## Component Props
Yoyo can persist and update variables in requests without the need to explicitly include an input element.
For an anonymous component, it's possible to specify the props directly in the component root node using a comma-separated list of variable names and this allows implementing a counter without the need for a component class:
```php
<?php $count = $count ?? 0 ; ?>
<div yoyo:props="count">
<button yoyo:val.count="<?php echo $count + 1; ?>">+</button>
<p><?php echo $count; ?></p>
</div>
```
By adding the `yoyo:props="count"`, Yoyo knows to automatically include the value of `count` in every request.
For dynamic components, there's no need to use the `yoyo:props` attribute because we use the protected method $props in the component class with an array of variable names.
```php
class Counter extends Component
{
public $count = 0;
protected $props = ['count'];
public function increment()
{
$this->count++;
}
}
```
Since the `$count` variable is also defined as a public property, it's already available in the template and the value is incremented throgh the `increment` method in the component class without having to use `yoyo:val.count`.
```html
<div>
<button yoyo:get="increment">+</button>
<span><?php echo $count; ?></span>
</div>
```
## Query String
Components have the ability to automatically update the browser's query string on state changes.
```php
class Search extends Component
{
public $query;
protected $queryString = ['query'];
}
```
Yoyo is smart enough to automatically remove the query string when the current state value matches the property's default value.
For example, in a pagination component, you don't need the `?page=1` query string to appear in the URL.
```php
class Posts extends Component
{
public $page = 1;
protected $queryString = ['page'];
}
```
## Loading States
Updating Yoyo components requires an Ajax request to the server and depending on what the component does, the response time will vary. The `yoyo:spinning` directive allows you to do all sorts of cool things when a component is updating to provide a visual indicator to end-users.
### Toggling Elements During Loading States
To show an element at the start of a Yoyo update request and hide it again when the update is complete:
```html
<div>
<button yoyo:post="submit">Submit</button>
<div yoyo:spinning>
Processing your submission...
</div>
</div>
```
Yoyo adds some CSS to the page to automatically hide the element with the `yoyo:spinning` directive.
To hide a visible element while the component is updating you can add the `remove` modifier:
```html
<div>
<button yoyo:post="submit">Submit</button>
<div yoyo:spinning.remove>
Text hidden while updating ...
</div>
</div>
```
## Delaying Loading States
Some actions may update quickly and showing a loading state in these cases may be more of a distraction. The `delay` modifier ensures that the loading state changes are applied only after 200ms if the component hasn't finished updating.
```html
<div>
<button yoyo:post="submit">Submit</button>
<div yoyo:spinning.delay>
Processing your submission...
</div>
</div>
```
### Targeting Specific Actions
If you need to toggle different indicators for different component actions, you can add the `yoyo:spin-on` directive and pass a comma separated list of action names. For example:
```html
<div>
<button yoyo:get="edit">Edit</button>
<button yoyo:get="like">Like</button>
<div yoyo:spinning yoyo:spin-on="edit">
Show for edit action
</div>
<div yoyo:spinning yoyo:spin-on="like">
Show for like action
</div>
<div yoyo:spinning yoyo:spin-on="edit, like">
Show for edit and like actions
</div>
</div>
```
## Toggling Element CSS Classes
Instead of toggling the visibility of an element you can also add specific CSS classes while the component updates. Use the `class` modifier and include the space-separated class names as the attribute value:
```html
<div>
<button yoyo:post="submit" yoyo:spinning.class="text-gray-300">
Submit
</button>
</div>
```
You can also remove specific class names by adding the `remove` modifier:
```html
<div>
<button yoyo:post="submit" yoyo:spinning.class.remove="bg-blue-200" class="bg-blue-200">
Submit
</button>
</div>
```
## Toggling Element Attributes
Similar to CSS class toggling, you can also add or remove attributes while the component is updating.
```html
<div>
<button yoyo:post="submit" yoyo:spinning.attr="disabled">
Submit
</button>
</div>
```
## Events
Events are a great way to establish communication between Yoyo components on the same page, where one or more components can listen to events fired by another component.
Events can be fired from component methods and templates using a variety of emit methods.
All emit methods accept any number of arguments that allow sending data (string, number, array) to listeners.
### Emitting an Event to All Yoyo Components
From a component method.
```php
public function increment()
{
$this->count++;
$this->emit('counter-updated', $count);
}
```
From a template
```php
<?php $this->emit('counter-updated', $count) ; ?>
```
### Emitting an Event to Parent Components
When dealing with nested components you can emit events to parents and not children or sibling components.
```php
$this->emitUp('postWascreated', $arg1, $arg2);
```
### Emitting an Event to a Specific Component
When you need to emit an event to a specific component using the component name (e.g. `cart`).
```php
$this->emitTo('cart', 'productAddedToCart', $arg1, $arg2);
```
### Emitting an Event to an Element Using a Selector
The `emitTo` method also works with selectors. When a component is not found, the selector is used instead. Emitting events using selectors doesn't support passing arguments.
```php
$this->emitTo('.cart', 'productAddedToCart');
$this->emitTo('#cart', 'productAddedToCart');
$this->emitTo('.post-100', 'saved');
```
#### Emitting an Event to Itself
When you need to emit an event on the same component.
```php
$this->emitSelf('productAddedToCart', $arg1, $arg2);
```
### Listening for Events
To register listeners in Yoyo, use the `$listeners` protected property of the component.
Listeners are a key->value pair where the key is the event to listen for, and the value is the method to call on the component. If the event and method are the same, you can leave out the key.
```php
class Counter extends Component {
public $message;
protected $listeners = ['counter-updated' => 'showNewCount'];
protected function showNewCount($count)
{
$this->message = "The new count is: $count";
}
}
```
### Listening For Events In JavaScript
Yoyo allows registering event listeners for component emitted events:
```js
<script>
Yoyo.on('productAddedToCart', id => {
alert('A product was added to the cart with ID:' + id
});
</script>
```
With this feature you can control toasters, alerts, modals, etc. directly from a component action on the server by emitting the event and listening for it on the browser.
### Dispatching Events From JavaScript
You can also trigger component events directly from your custom JavaScript using the `Yoyo.dispatch()` method. This is useful when you need to interact with other JavaScript libraries or custom browser APIs.
```js
// Dispatch an event to all Yoyo components listening for it
Yoyo.dispatch('post-created');
// You can also pass named parameters to the event listener
Yoyo.dispatch('post-created', { postId: 2 });
// Dispatch an event to a specific component by name
Yoyo.dispatchTo('dashboard', 'post-created', { postId: 2 });
```
Parameters are passed as named arguments that match the listener method's parameter names:
```php
protected $listeners = [
'post-created' => 'handlePostCreated',
];
public function handlePostCreated($postId)
{
// $postId will be 2
}
```
### Dispatching Browser Events
In addition to allowing components to communicate with each other, you can also send browser window events directly from a component method or template:
```php
// passing single value
$this->dispatchBrowserEvent('counter-updated', $count);
// Passing an array
$this->dispatchBrowserEvent('counter-updated', ['count' => $count]);
```
And listen for the event anywhere on the page:
```js
<script>
window.addEventListener('counter-updated', event => {
// Reading a single value
alert('Counter is now: ' + event.detail);
// Reading from an array
alert('Counter is now: ' + event.detail.count);
})
</script>
```
## Redirecting
Sometimes you may want to redirect the user to a different page after performing an action within a Yoyo component.
```php
class Registration extends Component
{
public function register()
{
// Create the user
$this->redirect('/welcome');
}
}
```
## Response Headers
Yoyo components have access to `$this->response` which provides methods for controlling how HTMX handles the response. These map directly to [HTMX response headers](https://htmx.org/reference/#response_headers).
### Retargeting
Override which element receives the swap:
```php
public function save()
{
// Swap the response into a different element instead of the component itself
$this->response->retarget('#notification-area');
}
```
### Changing Swap Strategy
Override the swap strategy for the response:
```php
public function update()
{
// Use innerHTML instead of the default outerHTML swap
$this->response->reswap('innerHTML');
}
```
### Selecting Response Content
Select a subset of the response HTML to swap:
```php
public function load()
{
// Only swap the #content portion of the response
$this->response->reselect('#content');
}
```
### URL Management
Push or replace the browser URL without a full page reload:
```php
public function navigate()
{
// Push a new URL to browser history
$this->response->pushUrl('/new-page');
}
public function filter()
{
// Replace the current URL without adding a history entry
$this->response->replaceUrl('/results?q=search');
}
```
### Client-Side Navigation
Perform a client-side redirect (AJAX-style, no full reload) or a full redirect:
```php
public function softRedirect()
{
// AJAX navigation — loads content without a full page reload
$this->response->location('/dashboard');
}
public function fullRedirect()
{
// Full page redirect via HX-Redirect header
$this->response->redirect('/login');
}
```
> **Note:** `$this->response->redirect()` sets the `HX-Redirect` header (HTMX native redirect). This is different from `$this->redirect()` which uses Yoyo's own `Yoyo-Redirect` header. Both achieve a full page redirect but through different mechanisms.
### Triggering Client-Side Events
Trigger browser events from the server that JavaScript can listen for:
```php
public function save()
{
// Trigger immediately after the response is received
$this->response->trigger('item-saved');
// Trigger after the swap is complete
$this->response->triggerAfterSwap('swap-complete');
// Trigger after the settle phase (CSS transitions finished)
$this->response->triggerAfterSettle('settle-complete');
}
```
Listen for these events in JavaScript:
```js
document.body.addEventListener('item-saved', function() {
// Show a toast notification, update a counter, etc.
});
```
### Full Page Refresh
Force a full page refresh from a component action:
```php
public function reset()
{
$this->response->refresh();
}
```
## Using Blade
You can use Yoyo with Laravel's [Blade](https://laravel.com/docs/8.x/blade) templating engine, without having to use Laravel.
### Installation
To get started install the following packages in your project:
```bash
composer require clickfwd/yoyo
composer require jenssegers/blade
```
### Configuration
Create a Blade instance and set it as the view provider for Yoyo. We also add the `YoyoServiceProvider` for Blade.
This code should run when rendering and updating components.
```php
<?php
use Clickfwd\Yoyo\Blade\Application;
use Clickfwd\Yoyo\Blade\YoyoServiceProvider;
use Clickfwd\Yoyo\ViewProviders\BladeViewProvider;
use Clickfwd\Yoyo\Yoyo;
use Illuminate\Contracts\Foundation\Application as ApplicationContract;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Support\Fluent;
use Jenssegers\Blade\Blade;
define('APP_PATH', __DIR__);
$yoyo = new Yoyo();
$yoyo->configure([
'url' => 'yoyo',
'scriptsPath' => APP_PATH.'/app/resources/assets/js/',
'namespace' => 'App\\Yoyo\\',
]);
// Create a Blade instance
$app = Application::getInstance();
$app->bind(ApplicationContract::class, Application::class);
// Needed for Blade anonymous components
$app->alias('view', ViewFactory::class);
$app->extend('config', function (array $config) {
return new Fluent($config);
});
$blade = new Blade(
[
APP_PATH.'/resources/views',
APP_PATH.'/resources/views/yoyo',
APP_PATH.'/resources/views/components',
],
APP_PATH.'/../cache',
$app
);
$app->bind('view', function () use ($blade) {
return $blade;
});
(new YoyoServiceProvider($app))->boot();
// Optionally register Blade components
$blade->compiler()->components([
// 'button' => 'button',
]);
// Register Blade view provider for Yoyo
$yoyo->registerViewProvider(function() use ($blade) {
return new BladeViewProvider($blade);
});
```
### Load Assets
Find `yoyo.js` in the following vendor path and copy it to your project's public assets directory.
```file
/vendor/clickfwd/yoyo/src/assets/js/yoyo.js
```
To load the necessary scripts in your Blade template you can use the `yoyo_scripts` directive in the `<head>` tag:
```blade
@yoyo_scripts
```
### Rendering a Blade View
You can use the Blade instance to render any Blade view.
```
$blade = \Clickfwd\Yoyo\Yoyo::getViewProvider()->getProviderInstance();
echo $blade->render('home');
```
### Rendering Yoyo Blade Components
To render Yoyo components inside Blade views, use the `@yoyo` directive.
```blade
@yoyo('search')
```
### Updating Yoyo Blade Components
To update Yoyo components in the Yoyo-designated route.
```php
echo (new \Clickfwd\Yoyo\Blade\Yoyo())->update();
```
### Inline Views
When dealing with simple templates, you can create components without a template file and instead return an inline view in the component's `render` method.
```php
class HelloWorld extends Component
{
public $message = 'Hello World!';
}
public function render()
{
return <<<'yoyo'
<div>
<input yoyo name="message" type="text" value="{{ $message }}">
<h1>{{ $message }}</h1>
</div>
yoyo;
}
```
### Other Blade Features
Yoyo implements several Blade directives that can be used within Yoyo component templates.
- `@spinning` and `@endspinning` - Check if a component is being re-rendered.
```blade
@spinnning
Component updated
@endspinning
@spinning($liked == 1)
Component updated and liked == 1
@endspinning
```
- All event methods are available as directives within blade components
```blade
@emit('eventName', ['foo' => 'bar']);
@emitUp('eventName', ['foo' => 'bar']);
@emitSelf('eventName', ['foo' => 'bar']);
@emitTo('component-name', 'eventName', ['foo' => 'bar']);
```
- Computed properties
```php
class HelloWorld extends Component
{
public $message = 'Hello World!';
public function getHelloWorldProperty()
{
return $message;
}
}
```
```blade
<div>
<h1>{{ $this->hello_world }}</h1>
<!-- Will output "Hello World!" -->
</div>
```
## Using Twig
You can use Yoyo with Symfony's [Twig](https://twig.symfony.com/) templating engine.
### Installation
To get started install the following packages in your project:
```bash
composer require clickfwd/yoyo
composer require twig/twig
```
### Configuration
Create a Twig instance and set it as the view provider for Yoyo. We also add the `YoyoTwigExtension` to Twig.
This code should run when rendering and updating components.
```php
use Clickfwd\Yoyo\Twig\YoyoTwigExtension;
use Clickfwd\Yoyo\ViewProviders\TwigViewProvider;
use Clickfwd\Yoyo\Yoyo;
use Twig\Extension\DebugExtension;
define('APP_PATH', __DIR__);
$yoyo = new Yoyo();
$yoyo->configure([
'url' => 'yoyo',
'scriptsPath' => APP_PATH.'/app/resources/assets/js/',
'namespace' => 'App\\Yoyo\\',
]);
$loader = new \Twig\Loader\FilesystemLoader([
APP_PATH.'/resources/views',
APP_PATH.'/resources/views/yoyo',
]);
$twig = new \Twig\Environment($loader, [
'cache' => APP_PATH.'/../cache',
'auto_reload' => true,
// 'debug' => true
]);
// Add Yoyo's Twig Extension
$twig->addExtension(new YoyoTwigExtension());
// Register Twig view provider for Yoyo
$yoyo->registerViewProvider(function() use ($twig) {
return new TwigViewProvider($twig);
});
```
### Load Assets
Find `yoyo.js` in the following vendor path and copy it to your project's public assets directory.
```file
/vendor/clickfwd/yoyo/src/assets/js/yoyo.js
```
To load the necessary scripts in your Twig template you can use the `yoyo_scripts` function in the `<head>` tag:
```twig
{{ yoyo_scripts() }}
```
### Rendering a Twig View
You can use the Twig instance to render any Twig view.
```
$twig = \Clickfwd\Yoyo\Yoyo::getViewProvider()->getProviderInstance();
echo $twig->render('home');
```
### Rendering Yoyo Twig Components
To render Yoyo components inside Twig views, use the `yoyo` function.
```twig
yoyo('search')
```
### Updating Yoyo Twig Components
To update Yoyo components in the Yoyo-designated route.
```php
echo (new \Clickfwd\Yoyo\Yoyo())->update();
```
### Inline Views
When dealing with simple templates, you can create components without a template file and instead return an inline view in the component's `render` method.
```php
class HelloWorld extends Component
{
public $message = 'Hello World!';
}
public function render()
{
return <<<'twig'
<div>
<input yoyo name="message" type="text" value="{{ message }}">
<h1>{{ message }}</h1>
</div>
twig;
}
```
### Other Twig Features
Yoyo adds a few functions and variables that can be used within Yoyo component templates.
- The `spinning` variable can be used to check if a component is being re-rendered.
```twig
{% if spinning %}
Component updated
{% endif %}
```
- All event methods are available as functions within blade components
```twig
{{ emit('eventName', {'foo':'bar'}) }}
{{ emitUp('eventName', {'foo':'bar'}) }}
{{ emitSelf('eventName', {'foo':'bar'}) }}
{{ emitTo('component-name', 'eventName', {'foo':'bar'}) }}
```
- Computed properties
```php
class HelloWorld extends Component
{
public $message = 'Hello World!';
public function getHelloWorldProperty()
{
return $this->message;
}
}
```
```twig
<div>
<h1>{{ this.hello_world }}</h1>
<!-- Will output "Hello World!" -->
</div>
```
## License
Copyright © ClickFWD
Yoyo is open-sourced software licensed under the [MIT license](LICENSE.md).
================================================
FILE: composer.json
================================================
{
"name": "clickfwd/yoyo",
"description": "Framework to build dynamic interfaces with seamless communication between frontend and backend.",
"keywords": [
"framework",
"yoyo"
],
"license": "MIT",
"homepage": "https://github.com/Clickfwd/yoyo",
"support": {
"issues": "https://github.com/Clickfwd/yoyo/issues",
"source": "https://github.com/Clickfwd/yoyo"
},
"authors": [
{
"name": "Clickfwd"
}
],
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": "^8.0",
"psr/container": "^1.1.1|^2.0.1"
},
"suggest": {
"illuminate/container": "Required for advanced dependency injection or Blade integration"
},
"require-dev": {
"phpunit/phpunit": "^12.0",
"pestphp/pest": "^4.0",
"pestphp/pest-plugin-browser": "^4.0",
"jenssegers/blade": "^2.0",
"symfony/var-dumper": "^5.2|^6.0|^7.0",
"spatie/ray": "^1.20",
"twig/twig": "^3.4",
"illuminate/container": "^8.0||^9.0||^10.0||^11.0||^12.0",
"friendsofphp/php-cs-fixer": "^3.39"
},
"autoload": {
"files": [
"src/yoyo/helpers.php"
],
"psr-4": {
"Clickfwd\\Yoyo\\": "src/yoyo"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\App\\": "tests/app",
"Tests\\AppAnother\\": "tests/app-another",
"Tests\\Browser\\": "tests/Browser",
"Tests\\Browser\\Components\\": "tests/Browser/Components"
}
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
}
}
================================================
FILE: package.json
================================================
{
"name": "yoyo-tests",
"private": true,
"scripts": {
"test:browser": "./vendor/bin/pest --testsuite Browser",
"test:browser:headed": "./vendor/bin/pest --testsuite Browser --headed"
},
"devDependencies": {
"playwright": "^1.54.1"
}
}
================================================
FILE: phpunit.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit Suite">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature Suite">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Browser">
<directory suffix="Test.php">./tests/Browser</directory>
</testsuite>
<testsuite name="Benchmark">
<directory suffix="Test.php">./tests/Benchmark</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
================================================
FILE: src/assets/js/yoyo.js
================================================
; (function (global, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory)
} else {
global.Yoyo = factory()
}
})(typeof self !== 'undefined' ? self : this, function () {
return (function () {
'use strict'
window.YoyoEngine = window.htmx
window.addEventListener('popstate', (event) => {
event?.state?.yoyo?.forEach((state) =>
restoreComponentStateFromHistory(state)
)
})
var Yoyo = {
url: null,
config(options) {
Object.keys(options).forEach((key) => {
YoyoEngine.config[key] = options[key]
})
},
on(name, callback) {
YoyoEngine.on(window, name, (event) => {
delete event.detail.elt
callback(event.detail)
})
},
dispatch(eventName, params = null) {
this.processEmitEvents(document.body, [
{ event: eventName, params: params }
])
},
dispatchTo(componentName, eventName, params = null) {
if (!/^[a-zA-Z0-9._-]+$/.test(componentName)) return
this.processEmitEvents(document.body, [
{ event: eventName, params: params, component: componentName }
])
},
createNonExistentIdTarget(targetId) {
// Dynamically create non-existent target IDs by appending them to document body
if (
targetId &&
targetId[0] == '#' &&
document.querySelector(targetId) === null
) {
let targetDiv = document.createElement('div')
targetDiv.setAttribute('id', targetId.replace('#', ''))
document.body.appendChild(targetDiv)
}
},
afterProcessNode(evt) {
// Create non-existent target
if (evt.detail.elt) {
this.createNonExistentIdTarget(
evt.detail.elt.getAttribute('hx-target')
)
}
// Initialize spinners
let component
if (!evt.detail.elt || !isComponent(evt.detail.elt)) {
// For innerHTML swap find the component root node
component = YoyoEngine.closest(
evt.detail.elt,
'[hx-swap~=innerHTML]'
)
} else {
component = getComponent(evt.detail.elt)
}
// Fallback: try to find the nearest component from evt.detail.elt or evt.detail.target
if (!component && evt.detail.elt) {
component = getComponent(evt.detail.elt);
}
if (!component && evt.detail.target) {
component = getComponent(evt.detail.target);
}
if (!component) {
return;
}
initializeComponentSpinners(component)
},
bootstrapRequest(evt) {
const elt = evt.detail.elt
let component = getComponent(elt)
const componentName = getComponentName(component)
if (evt.detail.path === document.location.href) {
evt.detail.path = 'render'
}
// Includes the commonly-used X-Requested-With header that identifies ajax requests in many backend frameworks
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest'
const action = getActionAndParseArguments(evt.detail)
evt.detail.parameters[
'component'
] = `${componentName}/${action}`
evt.detail.path = Yoyo.url
// Make request info available to other events
componentAddYoyoData(component, { action })
eventsMiddleware(evt)
},
processRedirectHeader(xhr) {
if (xhr.getAllResponseHeaders().match(/Yoyo-Redirect:/i)) {
const url = xhr.getResponseHeader('Yoyo-Redirect')
if (url) {
window.location = url
}
}
},
processEmitEvents(elt, events) {
if (!events || events == '[]') return
events = typeof events == 'string' ? JSON.parse(events) : events
yoyoEventCache.clear()
events.forEach((event) => {
triggerServerEmittedEvent(elt, event)
})
},
processBrowserEvents(events) {
if (!events) return
events = typeof events == 'string' ? JSON.parse(events) : events
events.forEach((event) => {
window.dispatchEvent(
new CustomEvent(event.event, {
detail: event.params,
})
)
})
},
beforeRequestActions(elt) {
let component = getComponent(elt)
spinningStart(component)
},
afterOnLoadActions(evt) {
let component = getComponentById(evt.detail.target.id)
if (!component) {
if (!evt.detail.elt) {
return
}
// Needed when using yoyo:select to replace a specific part of the response
// so stop spinning callbacks are run to remove animations in the parts of the component
// that were not replaced
component = getComponent(evt.detail.elt)
if (component) {
spinningStop(component)
}
return
}
componentCopyYoyoDataFromTo(evt.detail.target, component)
// For 204 No Content responses or empty responses, explicitly stop spinners
// since no DOM swap occurs that would naturally clean up spinner states
const xhr = evt.detail.xhr
if (xhr.status === 204 || !xhr.responseText) {
spinningStop(component)
}
// This isn't needed at this time because the CSS classes/attributes are
// automatically removed when a component is updated from the server
// however, could be useful to improve transitions in the future. It would
// be necessary to add back spinner classes before new HTML is swapped in
// spinningStop(component)
// Timeout needed for targets outside of Yoyo component
setTimeout(() => {
removeEventListenerData(component)
}, 125)
},
afterSettleActions(evt) {
/// HISTORY
// At this time, browser history support only works with outerHTML swaps
const component = getComponentById(evt.detail.elt.id)
if (!component) return
const xhr = evt.detail.xhr
// Browser history automatically enabled for components with queryStrings
let history = component.hasAttribute('yoyo:history')
let pushedUrl = xhr.getResponseHeader('Yoyo-Push')
let triggerId =
evt.detail.requestConfig?.triggerEltInfo?.id ||
evt.detail.requestConfig.headers['HX-Trigger']
let href = triggerId
? evt.detail.requestConfig?.triggerEltInfo?.href
: false
// If reactive element has an href tag, override history setting and add the component and href to browser history
if (triggerId && href) {
pushedUrl = href
history = true
}
const url =
pushedUrl !== null ? pushedUrl : window.location.href
if (
!history ||
!pushedUrl ||
component?.__yoyo?.replayingHistory
) {
component.__yoyo.replayingHistory = false
return
}
componentAddYoyoData(component, {
effects: {
browserEvents:
xhr.getResponseHeader('Yoyo-Browser-Event'),
emitEvents: xhr.getResponseHeader('Yoyo-Emit'),
},
})
const componentName = getComponentName(component)
YoyoEngine.findAll(
evt.detail.target,
'[yoyo\\:history=remove]'
).forEach((node) => node.remove())
// Before pushing a component to the browser history, we need to take a snapshot
// of its initial rendered-HTML to store it in the current state
// This also works for components loaded dynamically onto the page, like modals
if (!componentAlreadyInCurrentHistoryState(component)) {
updateState(
'replaceState',
document.location.href,
component,
true,
evt.detail.target.outerHTML
)
}
if (
!history?.state?.yoyo ||
history?.state?.initialState ||
url !== window.location.href
) {
updateState('pushState', url, component)
} else {
updateState('replaceState', url, component)
}
},
}
/**
* Tracking for elements receiving multiple emitted events to only trigger the first one
*/
let yoyoEventCache = new Set()
let yoyoSpinners = {}
function getActionAndParseArguments(detail) {
const m = detail.path.match(/^(.+?)\((.*)\)$/);
if (!m) {
// no “(…)” → action is the full path
return detail.path;
}
const [, actionName, rawArgs] = m;
const args = rawArgs.trim() === ''
? []
: rawArgs
.split(/\s*,\s*/) // split on commas + trim
.map(parseArg);
detail.parameters.actionArgs = JSON.stringify(args);
return actionName;
}
function parseArg(token) {
// quoted string?
if (/^['"].*['"]$/.test(token)) {
return token.slice(1, -1);
}
// boolean literals?
if (token === 'true') return true;
if (token === 'false') return false;
// try number
const num = Number(token);
if (!isNaN(num) && isFinite(num)) {
return num;
}
// fallback to raw string
return token;
}
function isComponent(elt) {
return elt?.hasAttribute('yoyo:name')
}
function getComponent(elt) {
let component = elt.closest('[yoyo\\:name]')
if (component) {
component.__yoyo = component?.__yoyo || {}
}
return component
}
function getAllcomponents() {
return document.querySelectorAll('[yoyo\\:name]')
}
function getComponentById(componentId) {
if (!componentId) return null
const component = document.querySelector(`#${componentId}`)
return isComponent(component) ? component : null
}
function getComponentName(component) {
return component.getAttribute('yoyo:name')
}
function getComponentFingerprint(component) {
return `${getComponentName(
component
)}:${getComponentIndex(component)}`
}
function getComponentsByName(name) {
return Array.from(
document.querySelectorAll(`[yoyo\\:name="${name}"]`)
)
}
// Index as it appears on the page relative to other same-named components
function getComponentIndex(component) {
const name = getComponentName(component)
const components = getComponentsByName(name)
return components.indexOf(component)
}
function getAncestorcomponents(selector) {
let ancestor = getComponent(document.querySelector(selector))
let ancestors = []
while (ancestor) {
ancestors.push(ancestor)
ancestor = getComponent(ancestor.parentElement)
}
// Remove the current component
ancestors.shift()
return ancestors
}
function shouldTriggerYoyoEvent(elt, eventName) {
let key
if (isComponent(elt)) {
key = `${elt.id}${eventName}`
} else if (elt.selector !== undefined) {
return true
}
if (key && !yoyoEventCache.has(key)) {
yoyoEventCache.add(key)
return true
}
return false
}
function eventsMiddleware(evt) {
const component = getComponent(evt.detail.elt)
const componentName = getComponentName(component)
const eventData = component.__yoyo.eventListener
if (!eventData) return
evt.detail.parameters[
'component'
] = `${componentName}/${eventData.name}`
if (eventData.params) {
delete eventData.params.elt
}
evt.detail.parameters = {
...evt.detail.parameters,
...{
eventParams: eventData.params
? JSON.stringify(eventData.params)
: [],
},
}
}
function addEmittedEventParametersToListenerComponent(
component,
event,
params
) {
// Check if Yoyo component is listening for the event
let componentListeningFor = component
.getAttribute('hx-trigger')
.split(',')
.filter((name) => name.trim())
if (componentListeningFor.indexOf(event) === -1) {
return
}
componentAddYoyoData(component, {
eventListener: { name: event, params: params },
})
}
function triggerServerEmittedEvent(elt, event) {
const component = getComponent(elt)
const eventName = event.event
const params = event.params
const selector = event.selector || null
const componentName = event.component || null
const propagation = event.propagation || null
let elements
if (!component && (propagation === 'self' || selector)) return
// emit
if (!selector && !componentName) {
elements = getAllcomponents()
} else if (componentName) {
// emitUp
if (propagation == 'ancestorsOnly') {
elements = getAncestorcomponents(selector)
// emitSelf
} else if (propagation == 'self') {
elements = [component]
// emitTo
} else {
elements = getComponentsByName(componentName)
}
// emitWithSelector, excludes current component to allow replication without udpating the current component twice
} else if (selector) {
elements = document.querySelectorAll(selector)
elements = Array.from(elements).filter(
(element) => !component.contains(element)
)
elements.forEach((elt) => (elt.selector = selector))
}
if (elements.length) {
elements.forEach((elt) => {
if (shouldTriggerYoyoEvent(elt, eventName)) {
addEmittedEventParametersToListenerComponent(
getComponent(elt),
eventName,
params
)
YoyoEngine.trigger(elt, eventName, params)
}
})
}
}
function removeEventListenerData(component) {
delete component.__yoyo.eventListener
}
/**
* Component loading state spinners
*/
function spinningStart(component) {
const componentId = component.id
if (!yoyoSpinners[componentId]) {
return
}
let spinningElts = yoyoSpinners[componentId].generic || []
spinningElts = spinningElts.concat(
yoyoSpinners[componentId]?.actions[component.__yoyo.action] ||
[]
)
delete yoyoSpinners[component.id]
spinningElts.forEach((directive) => {
const spinnerElt = directive.elt
if (directive.modifiers.includes('class')) {
let classes = directive.value.split(' ').filter(Boolean)
doAndSetCallbackOnElToUndo(
component,
directive,
() => directive.elt.classList.add(...classes),
() => spinnerElt.classList.remove(...classes)
)
} else if (directive.modifiers.includes('attr')) {
doAndSetCallbackOnElToUndo(
component,
directive,
() => directive.elt.setAttribute(directive.value, true),
() => spinnerElt.removeAttribute(directive.value)
)
} else {
doAndSetCallbackOnElToUndo(
component,
directive,
() => (spinnerElt.style.display = 'inline-block'),
() => (spinnerElt.style.display = 'none')
)
}
})
}
function spinningStop(component) {
while (component.__yoyo_on_finish_loading.length > 0) {
component.__yoyo_on_finish_loading.shift()()
}
}
function initializeComponentSpinners(component) {
const componentId = component.id
component.__yoyo_on_finish_loading = []
walk(component, (elt) => {
const directive = extractModifiersAndValue(elt, 'spinning')
if (directive) {
const yoyoSpinOnAction = elt.getAttribute('yoyo:spin-on')
if (yoyoSpinOnAction) {
yoyoSpinOnAction
.replace(' ', '')
.split(',')
.forEach((action) => {
addActionSpinner(componentId, action, directive)
})
} else {
addGenericSpinner(componentId, directive)
}
}
})
}
function checkSpinnerInitialized(componentId, action) {
yoyoSpinners[componentId] = yoyoSpinners[componentId] || {
actions: {},
generic: [],
}
if (
action &&
yoyoSpinners?.[componentId]?.actions?.[action] === undefined
) {
yoyoSpinners[componentId].actions[action] = []
}
}
function addActionSpinner(componentId, action, directive) {
checkSpinnerInitialized(componentId, action)
yoyoSpinners[componentId].actions[action].push(directive)
}
function addGenericSpinner(componentId, directive) {
checkSpinnerInitialized(componentId)
yoyoSpinners[componentId].generic.push(directive)
}
// https://github.com/livewire/livewire
function doAndSetCallbackOnElToUndo(
el,
directive,
doCallback,
undoCallback
) {
if (directive.modifiers.includes('remove'))
[doCallback, undoCallback] = [undoCallback, doCallback]
if (directive.modifiers.includes('delay')) {
let timeout = setTimeout(() => {
doCallback()
el.__yoyo_on_finish_loading.push(() => undoCallback())
}, 200)
el.__yoyo_on_finish_loading.push(() => clearTimeout(timeout))
} else {
doCallback()
el.__yoyo_on_finish_loading.push(() => undoCallback())
}
}
function componentAlreadyInCurrentHistoryState(component) {
if (!history?.state?.yoyo) return false
history.state.yoyo.forEach((state) => {
if (state.fingerprint == getComponentFingerprint(component)) {
return true
}
})
return false
}
/**
* Component state caching for browser history
*/
function updateState(
method,
url,
component,
initialState,
originalHTML
) {
const id = component.id
const componentName = getComponentName(component)
const componentIndex = getComponentIndex(component)
const fingerprint = getComponentFingerprint(component)
const html = originalHTML ? originalHTML : component.outerHTML
const effects = component.__yoyo.effects || {}
const newState = {
url,
id,
componentName,
componentIndex,
fingerprint,
html,
effects,
initialState,
}
const stateArray =
method == 'pushState'
? [newState]
: replaceStateByComponentIndex(newState)
history[method](
{ yoyo: stateArray, initialState: initialState },
'',
url
)
}
function replaceStateByComponentIndex(newState) {
let stateArray = history?.state?.yoyo || []
let fingerprintFound = false
stateArray.map((state) => {
if (state.fingerprint == newState.fingerprint) {
fingerprintFound = true
return newState
}
return state
})
if (!fingerprintFound) {
stateArray.push(newState)
}
return stateArray
}
function restoreComponentStateFromHistory(state) {
const componentName = state.componentName
const componentsWithSameName = getComponentsByName(componentName)
let component = componentsWithSameName[state.componentIndex]
// If the component cannot be found by index, try a simple ID check
// This is needed for components dynamically added to the page, like modals
// and it works when the component id is pre-determined (i.e. not randomly generated)
if (!component) {
component = getComponentById(state.id)
if (!component) return
}
var parser = new DOMParser()
var cached = parser.parseFromString(state.html, 'text/html').body
.firstElementChild
component.replaceWith(cached)
htmx.process(cached)
// Trigger full server refresh when coming back to the original state
// so server-sent events on the render/refresh method are run
if (state.initialState) {
componentAddYoyoData(cached, { replayingHistory: true })
YoyoEngine.trigger(cached, 'refresh')
} else {
Yoyo.processBrowserEvents(state?.effects?.browserEvents)
Yoyo.processEmitEvents(component, state?.effects?.emitEvents)
}
}
function componentCopyYoyoDataFromTo(from, to) {
to.__yoyo = from?.__yoyo || {}
to.__yoyo_on_finish_loading = from?.__yoyo_on_finish_loading || []
}
function componentAddYoyoData(component, data) {
if (!data) return
component.__yoyo = Object.assign(component.__yoyo, data)
}
// https://github.com/alpinejs/alpine/
function walk(el, callback) {
if (callback(el) === false) return
let node = el.firstElementChild
while (node) {
walk(node, callback)
node = node.nextElementSibling
}
}
function extractModifiersAndValue(elt, type) {
const attr = elt
.getAttributeNames()
// Filter only the Yoyo spinning directives.
.filter((name) => name.match(new RegExp(`yoyo:${type}`)))
if (attr.length) {
const name = attr[0]
const [ntype, ...modifiers] = name
.replace(new RegExp(`yoyo:${type}`), '')
.split('.')
const value = elt.getAttribute(name)
return { elt, name, value, modifiers }
}
return false
}
return Yoyo
})()
})
YoyoEngine.defineExtension('yoyo', {
onEvent: function (name, evt) {
if (name === 'htmx:afterProcessNode') {
Yoyo.afterProcessNode(evt)
}
if (name === 'htmx:configRequest') {
if (!evt.detail.elt) return
Yoyo.bootstrapRequest(evt)
}
if (name === 'htmx:beforeRequest') {
if (!Yoyo.url) {
console.error('The yoyo URL needs to be defined')
evt.preventDefault()
}
Yoyo.beforeRequestActions(evt.detail.elt)
}
if (name === 'htmx:afterOnLoad') {
Yoyo.afterOnLoadActions(evt)
const xhr = evt.detail.xhr
Yoyo.processEmitEvents(
evt.detail.elt,
xhr.getResponseHeader('Yoyo-Emit')
)
Yoyo.processBrowserEvents(
xhr.getResponseHeader('Yoyo-Browser-Event')
)
Yoyo.processRedirectHeader(xhr)
// Re-spawn targets removed from the page and take into account swap delays
let modifier = xhr.getResponseHeader('Yoyo-Swap-Modifier')
if (!modifier) return
let swap = modifier.match(/swap:([0-9.]+)s/)
let time = swap[1] ? swap[1] * 1000 + 1 : 0
setTimeout(() => {
if (
!evt.detail.target.isConnected &&
document.querySelector(
`[hx-target="#${evt.detail.target.id}"]`
)
) {
Yoyo.createNonExistentIdTarget(`#${evt.detail.target.id}`)
}
}, time)
}
if (name === 'htmx:beforeSwap') {
if (!evt.detail.elt) return
// Add triggering element info to event detail so it can be read in after swap events
// For example to push the href url to browser history using the href from the element that's no longer present on the page
let triggerId =
evt.detail.requestConfig.headers['HX-Trigger'] || null
let triggeringElt = htmx.find(`#${triggerId}`)
if (triggerId && triggeringElt) {
evt.detail.requestConfig.triggerEltInfo = {
id: triggerId,
href: triggeringElt.getAttribute('href'),
}
}
const modifier =
evt.detail.xhr.getResponseHeader('Yoyo-Swap-Modifier')
if (modifier) {
const swap =
evt.detail.elt.getAttribute('hx-swap') ||
YoyoEngine.config.defaultSwapStyle
evt.detail.elt.setAttribute('hx-swap', `${swap} ${modifier}`)
}
Yoyo.processBrowserEvents(
evt.detail.xhr.getResponseHeader('Yoyo-Browser-Event')
)
}
if (name === 'htmx:afterSettle') {
// Push component response to history cache
// Make sure we trigger once for the new element - this was failing in Safari mobile
// Causing a duplicate snapshot
if (!evt.detail.elt || !evt.detail.elt.isConnected) return
Yoyo.afterSettleActions(evt)
}
},
// Add support for morphdom swap when using Alpine JS to be able to
// maintain the Alpine component state after a swap
isInlineSwap: function (swapStyle) {
return swapStyle === 'morphdom'
},
handleSwap: function (swapStyle, target, fragment) {
if (typeof morphdom === 'function' && swapStyle === 'morphdom') {
morphdom(target, fragment.outerHTML, {
onBeforeElUpdated: (from, to) => {
// From Livewire - deal with Alpine component updates
if (from.__x) {
window.Alpine.clone(from.__x, to)
}
},
})
return [target] // let htmx handle the new content
}
},
})
================================================
FILE: src/yoyo/AnonymousComponent.php
================================================
<?php
namespace Clickfwd\Yoyo;
class AnonymousComponent extends Component
{
public function render()
{
$data = array_merge($this->variables, $this->request->all());
return $this->view($this->componentName, $data);
}
}
================================================
FILE: src/yoyo/Blade/Application.php
================================================
<?php
namespace Clickfwd\Yoyo\Blade;
use Closure;
use Illuminate\Container\Container;
class Application extends Container
{
protected array $terminatingCallbacks = [];
public function getNamespace()
{
return '';
}
public function terminating(Closure $callback)
{
$this->terminatingCallbacks[] = $callback;
return $this;
}
public function terminate()
{
foreach ($this->terminatingCallbacks as $terminatingCallback) {
$terminatingCallback();
}
}
}
================================================
FILE: src/yoyo/Blade/CreateBladeViewFromString.php
================================================
<?php
namespace Clickfwd\Yoyo\Blade;
use Illuminate\View\Component;
class CreateBladeViewFromString extends Component
{
public function __invoke($view, $contents)
{
return $this->createBladeViewFromString($view, $contents);
}
public function render()
{
//
}
}
================================================
FILE: src/yoyo/Blade/YoyoBladeCompilerEngine.php
================================================
<?php
namespace Clickfwd\Yoyo\Blade;
use Illuminate\View\Engines\CompilerEngine as LaravelCompilerEngine;
class YoyoBladeCompilerEngine extends LaravelCompilerEngine
{
protected $yoyoComponent;
protected $isRenderingYoyoComponent;
public function startYoyoRendering($component)
{
$this->yoyoComponent = $component;
$this->isRenderingYoyoComponent = true;
}
public function stopYoyoRendering()
{
$this->isRenderingYoyoComponent = false;
}
/**
* /vendor/illuminate/view/Engines/PhpEngine.php.
*/
protected function evaluatePath($__path, $__data)
{
if (! $this->isRenderingYoyoComponent) {
return parent::evaluatePath($__path, $__data);
}
$obLevel = ob_get_level();
ob_start();
try {
\Closure::bind(function () use ($__path, $__data) {
extract($__data, EXTR_SKIP);
include $__path;
}, $this->yoyoComponent ? $this->yoyoComponent : $this)();
} catch (\Throwable $e) {
$this->handleViewException($e, $obLevel);
}
return ltrim(ob_get_clean());
}
}
================================================
FILE: src/yoyo/Blade/YoyoBladeDirectives.php
================================================
<?php
namespace Clickfwd\Yoyo\Blade;
class YoyoBladeDirectives
{
public function __construct($blade)
{
$blade->directive('yoyo', [$this, 'yoyo']);
$blade->directive('yoyo_scripts', [$this, 'yoyo_scripts']);
$blade->directive('spinning', [$this, 'spinning']);
$blade->directive('endspinning', [$this, 'endspinning']);
$blade->directive('emit', [$this, 'emit']);
$blade->directive('emitTo', [$this, 'emitTo']);
$blade->directive('emitToWithSelector', [$this, 'emitToWithSelector']);
$blade->directive('emitSelf', [$this, 'emitToWithSelector']);
$blade->directive('emitUp', [$this, 'emitToWithSelector']);
}
public function yoyo($expression)
{
return <<<yoyo
<?php
\$yoyo = \Clickfwd\Yoyo\Yoyo::getInstance();
if (Yoyo\is_spinning()) {
echo \$yoyo->mount({$expression})->refresh();
} else {
echo \$yoyo->mount({$expression})->render();
}
?>
yoyo;
}
public function yoyo_scripts()
{
return '<?php Yoyo\yoyo_scripts(); ?>';
}
public function spinning($expression)
{
return $expression !== ''
? "<?php if(\$spinning && {$expression}): ?>"
: '<?php if($spinning): ?>';
}
public function endspinning()
{
return '<?php endif; ?>';
}
public function emit($expression)
{
return "<?php \$this->emit({$expression}); ?>";
}
public function emitTo($expression)
{
return "<?php \$this->emitTo({$expression}); ?>";
}
public function emitToWithSelector($expression)
{
return "<?php \$this->emitToWithSelector({$expression}); ?>";
}
public function emitSelf($expression)
{
return "<?php \$this->emitSelf({$expression}); ?>";
}
public function emitUp($expression)
{
return "<?php \$this->emitUp({$expression}); ?>";
}
}
================================================
FILE: src/yoyo/Blade/YoyoServiceProvider.php
================================================
<?php
namespace Clickfwd\Yoyo\Blade;
use Illuminate\Support\ServiceProvider;
class YoyoServiceProvider extends ServiceProvider
{
public function boot()
{
$this->registerBladeDirectives();
$this->registerViewCompilerEngine();
}
protected function registerViewCompilerEngine()
{
$this->app->make('view.engine.resolver')->register('blade', function () {
return new YoyoBladeCompilerEngine($this->app['blade.compiler']);
});
}
protected function registerBladeDirectives()
{
if (method_exists($this->app->get('view'), 'directive')) {
$blade = $this->app->get('view');
} else {
$blade = $this->app->get('view')->getEngineResolver()->resolve('blade')->getCompiler();
}
new YoyoBladeDirectives($blade);
}
}
================================================
FILE: src/yoyo/Blade/yoyo-view.blade.php
================================================
@yoyo($name, $variables, $attributes, $action)
================================================
FILE: src/yoyo/ClassHelpers.php
================================================
<?php
namespace Clickfwd\Yoyo;
use ReflectionClass;
use ReflectionMethod;
class ClassHelpers
{
private static array $propertyCache = [];
private static array $defaultVarCache = [];
private static array $methodCache = [];
private static array $traitCache = [];
private static array $paramTypeCache = [];
public static function getDefaultPublicVars($instance, $baseClass = null)
{
$className = get_class($instance);
$cacheKey = $className . ':' . ($baseClass ?? '');
if (isset(static::$defaultVarCache[$cacheKey])) {
return static::$defaultVarCache[$cacheKey];
}
$class = new ReflectionClass($className);
$names = self::getPublicProperties($instance, $baseClass);
$values = $class->getDefaultProperties();
return static::$defaultVarCache[$cacheKey] = array_intersect_key($values, array_flip($names));
}
public static function getPublicVars($instance, $baseClass = null)
{
$publicProperties = self::getPublicProperties($instance, $baseClass);
$vars = call_user_func('get_object_vars', $instance);
$publicVars = [];
foreach ($vars as $key => $value) {
if (in_array($key, $publicProperties)) {
$publicVars[$key] = $vars[$key];
}
}
return $publicVars;
}
public static function getPublicProperties($instance, $baseClass = null)
{
$className = get_class($instance);
$cacheKey = $className . ':' . ($baseClass ?? '');
if (isset(static::$propertyCache[$cacheKey])) {
return static::$propertyCache[$cacheKey];
}
$class = new ReflectionClass($className);
$properties = $class->getProperties(ReflectionMethod::IS_PUBLIC);
$publicProperties = [];
foreach ($properties as $prop) {
// Only include the property if it's different from the base class when passed as 2d parameter
// This allows extending component classes with public properties
if (($baseClass && $prop->class !== $baseClass) || $prop->class == $className) {
$publicProperties[] = $prop->name;
}
}
return static::$propertyCache[$cacheKey] = $publicProperties;
}
public static function getPublicMethods($instance, $exceptions = [])
{
$className = is_string($instance) ? $instance : get_class($instance);
$cacheKey = $className . ':' . implode(',', $exceptions);
if (isset(static::$methodCache[$cacheKey])) {
return static::$methodCache[$cacheKey];
}
$class = new ReflectionClass($className);
$methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if ($method->class == $className && ! in_array($method->name, $exceptions)) {
$publicMethods[] = $method->name;
}
}
return static::$methodCache[$cacheKey] = $publicMethods ?? [];
}
public static function methodIsPrivate($instance, $method)
{
$reflection = new ReflectionMethod($instance, $method);
return ! $reflection->isPublic();
}
public static function classImplementsInterface($name, $instance)
{
$class = new ReflectionClass($name);
return in_array($instance, $class->getInterfaceNames());
}
/**
* Laravel Support helper
*/
public static function classUsesRecursive($class)
{
if (is_object($class)) {
$class = get_class($class);
}
$className = $class;
if (isset(static::$traitCache[$className])) {
return static::$traitCache[$className];
}
$results = [];
foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {
$results += static::traitUsesRecursive($class);
}
return static::$traitCache[$className] = array_unique($results);
}
/**
* Laravel Support helper
*/
public static function traitUsesRecursive($trait)
{
$traits = class_uses($trait);
foreach ($traits as $trait) {
$traits += static::traitUsesRecursive($trait);
}
return $traits;
}
/**
* Laravel Support helper
*/
public static function classBasename($class)
{
$class = is_object($class) ? get_class($class) : $class;
return basename(str_replace('\\', '/', $class));
}
public static function getMethodParameterNames($class, $method)
{
$names = [];
$reflector = new ReflectionClass($class);
$method = $reflector->getMethod($method);
foreach ($method->getParameters() as $parameter) {
if (! $parameter->getType() || ($parameter->getType() && $parameter->getType()->isBuiltin())) {
$names[] = $parameter->getName();
}
}
return $names;
}
public static function methodHasVariadicParameter($class, $method)
{
$reflector = new ReflectionClass($class);
$method = $reflector->getMethod($method);
$parameters = $method->getParameters();
if (empty($parameters)) {
return false;
}
// Check if the last parameter is variadic
$lastParam = end($parameters);
return $lastParam->isVariadic();
}
/**
* Get all method parameters with type information
* Returns an array with 'typed' and 'regular' parameters
*/
public static function getMethodParametersWithTypes($class, $method)
{
$className = is_object($class) ? get_class($class) : $class;
$cacheKey = $className . ':' . $method;
if (isset(static::$paramTypeCache[$cacheKey])) {
return static::$paramTypeCache[$cacheKey];
}
$typed = []; // Parameters with class type hints (for DI)
$regular = []; // Parameters without type hints or with builtin types
$reflector = new ReflectionClass($class);
$method = $reflector->getMethod($method);
foreach ($method->getParameters() as $parameter) {
$paramInfo = [
'name' => $parameter->getName(),
'optional' => $parameter->isOptional(),
'variadic' => $parameter->isVariadic(),
];
if (! $parameter->getType() || ($parameter->getType() && $parameter->getType()->isBuiltin())) {
// Regular parameter (no type or builtin type)
$regular[] = $paramInfo;
} else {
// Typed parameter (class type hint for DI)
$paramInfo['type'] = $parameter->getType()->getName();
$typed[] = $paramInfo;
}
}
return static::$paramTypeCache[$cacheKey] = [
'typed' => $typed,
'regular' => $regular,
];
}
public static function flushCache(): void
{
static::$propertyCache = [];
static::$defaultVarCache = [];
static::$methodCache = [];
static::$traitCache = [];
static::$paramTypeCache = [];
}
}
================================================
FILE: src/yoyo/Component.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Concerns\BrowserEvents;
use Clickfwd\Yoyo\Concerns\Redirector;
use Clickfwd\Yoyo\Exceptions\BypassRenderMethod;
use Clickfwd\Yoyo\Exceptions\ComponentMethodNotFound;
use Clickfwd\Yoyo\Exceptions\MissingComponentTemplate;
use Clickfwd\Yoyo\Interfaces\ViewProviderInterface;
use Clickfwd\Yoyo\Services\Response;
use Closure;
use ReflectionMethod;
abstract class Component
{
use BrowserEvents;
use Redirector;
protected $yoyo_id;
protected $componentName;
protected $componentAction;
protected $variables;
protected $request;
protected $response;
protected $spinning;
protected $queryString = [];
protected $props = [];
protected $listeners = [];
protected $omitResponse = false;
protected $computedPropertyCache = [];
protected $attributes;
protected $resolver;
protected $viewData = [];
private static $excludePublicMethods = [
'__construct',
'spinning',
];
public function __construct(ComponentResolver $resolver, string $id, string $name)
{
$this->yoyo_id = $id;
$this->componentName = $name;
$this->request = Yoyo::request();
$this->response = Response::getInstance();
$this->resolver = $resolver;
}
public function spinning(bool $spinning)
{
$this->spinning = $spinning;
return $this;
}
public function boot(array $variables, array $attributes)
{
$data = array_merge($variables, $this->request->all());
$this->variables = $variables;
$this->attributes = $attributes;
$publicProperties = ClassHelpers::getPublicProperties($this, __CLASS__);
foreach ($publicProperties as $property) {
$this->{$property} = $data[$property] ?? $this->{$property};
}
// Set an initial value for dynamic properties
foreach ($this->getDynamicProperties() as $property) {
$this->{$property} = $data[$property] ?? null;
}
return $this;
}
public function getName()
{
return $this->componentName;
}
public function getDynamicProperties()
{
return [];
}
public function getInitialAttributes()
{
$attributes = $this->attributes;
return $attributes;
}
public function getVariables()
{
return $this->variables;
}
public function getQueryParam($key, $default = null)
{
return $this->request->get($key, $default);
}
public function getQueryString()
{
return $this->queryString;
}
public function getProps()
{
return $this->props;
}
public function setAction($action)
{
$this->componentAction = $action;
}
public function actionMatches($action)
{
if (is_array($action)) {
return in_array($this->componentAction, $action);
}
return $this->componentAction == $action;
}
public function getListeners()
{
$listeners = [];
foreach ($this->listeners as $key => $value) {
if (is_numeric($key)) {
$listeners[$value] = $value;
} else {
$listeners[$key] = $value;
}
}
return $listeners;
}
public function getComponentId()
{
return $this->yoyo_id;
}
public function set($key, $value = null)
{
if (is_array($key)) {
$this->viewData = array_merge($this->viewData, $key);
} else {
$this->viewData[$key] = $value;
}
return $this;
}
public function render()
{
if ($this->omitResponse) {
throw new BypassRenderMethod($this->response->getStatusCode());
}
return $this->view($this->componentName);
}
public function addSwapModifiers($modifier)
{
$this->response->header('Yoyo-Swap-Modifier', $modifier);
return $this;
}
public function skipRender()
{
$this->response->status(204);
$this->omitResponse = true;
return $this;
}
public function skipRenderAndRemove($modifier = 'swap:1s')
{
if ($modifier) {
$this->addSwapModifiers($modifier);
}
$this->response->status(200);
$this->omitResponse = true;
return $this;
}
protected function view($template, $vars = []): ViewProviderInterface
{
$view = $this->resolver->resolveViewProvider();
if (! $view->exists($template)) {
throw new MissingComponentTemplate($template, get_class($this));
}
$view->startYoyoRendering($this);
// Make public properties and methods available to views
$vars = array_merge($this->viewVars(), $vars);
$view->render($template, $vars);
return $view;
}
public function createViewFromString($content): string
{
$view = $this->resolver->resolveViewProvider();
$view->startYoyoRendering($this);
$html = $view->makeFromString($content, $this->viewVars());
$view->stopYoyoRendering();
return $html;
}
protected function viewVars(): array
{
$vars = [];
$vars['spinning'] = $this->spinning;
$properties = ClassHelpers::getPublicVars($this, __CLASS__);
$properties = array_merge($properties, array_fill_keys($this->getDynamicProperties(), null));
return array_merge($this->viewData, $vars, $properties);
}
protected function createVariableFromMethod(ReflectionMethod $method)
{
return $method->getNumberOfParameters() === 0
? $this->createInvocableVariable($method->getName())
: Closure::fromCallable([$this, $method->getName()]);
}
protected function createInvocableVariable(string $method)
{
return new InvocableComponentVariable(function () use ($method) {
return $this->{$method}();
});
}
// For computed properties with arguments
// For Twig compatibility, because computed properties are not resolved through __get
public function __call(string $name, array $arguments)
{
$key = static::makeCacheKey($name, $arguments);
if (isset($this->computedPropertyCache[$key])) {
return $this->computedPropertyCache[$key];
}
$studlyProperty = YoyoHelpers::studly($name);
if (method_exists($this, $computedMethodName = 'get'.$studlyProperty.'Property')) {
return $this->computedPropertyCache[$key] = call_user_func_array([$this, $computedMethodName], $arguments);
}
throw new ComponentMethodNotFound($this->getName(), $name);
}
public function __get($property)
{
if (isset($this->computedPropertyCache[$property])) {
return $this->computedPropertyCache[$property];
}
$studlyProperty = YoyoHelpers::studly($property);
if (method_exists($this, $computedMethodName = 'get'.$studlyProperty.'Property')) {
return $this->computedPropertyCache[$property] = $this->$computedMethodName();
}
throw new ComponentMethodNotFound($this->getName(), $property);
}
public function forgetComputed($key = null)
{
if (is_null($key)) {
$this->computedPropertyCache = [];
return;
}
$keys = is_array($key) ? $key : func_get_args();
foreach ($keys as $keyName) {
unset($this->computedPropertyCache[$keyName]);
}
}
public function forgetComputedWithArgs($name, ...$args)
{
$this->forgetComputed(static::makeCacheKey($name, $args));
}
protected static function makeCacheKey($name, $arguments)
{
return md5($name.json_encode($arguments));
}
}
================================================
FILE: src/yoyo/ComponentManager.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Exceptions\ComponentMethodNotFound;
use Clickfwd\Yoyo\Exceptions\ComponentNotFound;
use Clickfwd\Yoyo\Exceptions\NonPublicComponentMethodCall;
class ComponentManager
{
private $id;
private $name;
private $request;
private $component;
private $resolver;
private $spinning;
public function __construct($resolver, $request, $spinning)
{
$this->request = $request;
$this->spinning = $spinning;
$this->resolver = $resolver;
}
public function getDefaultPublicVars()
{
return ClassHelpers::getDefaultPublicVars($this->component, Component::class);
}
public function getPublicVars()
{
if ($this->isAnonymousComponent()) {
return $this->request->except(['component', YoyoCompiler::yoprefix('id')]);
}
$vars = ClassHelpers::getPublicVars($this->component, Component::class);
foreach ($this->component->getDynamicProperties() as $name) {
$vars[$name] = property_exists($this->component, $name) ? $this->component->{$name} : null;
}
$vars = array_merge($vars, $this->request->startsWith(YoyoCompiler::yoprefix('')));
return $vars;
}
public function getQueryString()
{
if ($this->isAnonymousComponent()) {
return $this->request->method() == 'GET'
? array_keys($this->request->except(['component', YoyoCompiler::yoprefix('id')]))
: [];
}
return $this->component->getQueryString();
}
public function getProps()
{
return $this->component->getProps();
}
public function getListeners()
{
return $this->component->getListeners();
}
public function process($id, $name, $action, $variables, $attributes)
{
if (! ($this->component = $this->resolver->resolveComponent($id, $name, $variables))) {
throw new ComponentNotFound($name);
}
if ($this->isAnonymousComponent()) {
return $this->processAnonymousComponent($variables, $attributes);
}
return $this->processDynamicComponent($action, $variables, $attributes);
}
public function isAnonymousComponent(): bool
{
return is_a($this->component, AnonymousComponent::class);
}
public function isDynamicComponent(): bool
{
return ! $this->isAnonymousComponent();
}
private function processDynamicComponent($action, $variables = [], $attributes = [])
{
$class = get_class($this->component);
$this->component->setAction($action);
$isEventListenerAction = false;
$eventParams = $this->request->get('eventParams', []);
// Guard: Request::get() returns raw string when test_json decodes to falsy value ([], {})
// TODO: Root cause is test_json falsy check in Request::get() — track as separate fix
if (is_string($eventParams)) {
$decoded = json_decode($eventParams, true, 32);
$eventParams = is_array($decoded) ? $decoded : [];
}
$this->component->spinning($this->spinning)->boot($variables, $attributes);
$hookStack = [
'initialize' => ['initialize'],
'mount' => ['mount'],
'rendering' => ['rendering'],
'rendered' => ['rendered'],
];
$parameters = array_merge($variables, $this->request->all());
// Build stack of trait lifecycle hooks to run after the component hook of the same name
foreach (ClassHelpers::classUsesRecursive($this->component) as $trait) {
foreach (array_keys($hookStack) as $hook) {
$hookStack[$hook][] = $hook.ClassHelpers::classBasename($trait);
}
}
foreach ($hookStack['initialize'] as $method) {
if (method_exists($this->component, $method)) {
Yoyo::container()->call([$this->component, $method], $parameters);
}
}
$listeners = $this->component->getListeners();
if (! empty($listeners[$action]) || in_array($action, $listeners)) {
// If action is an event listener, re-route it to the listener method
$action = ! empty($listeners[$action]) ? $listeners[$action] : $action;
$isEventListenerAction = true;
} elseif (! method_exists($this->component, $action)) {
throw new ComponentMethodNotFound($class, $action);
}
$excludedActions = ClassHelpers::getPublicMethods(Component::class, ['render']);
if (in_array($action, $excludedActions) ||
(! $isEventListenerAction && ClassHelpers::methodIsPrivate($this->component, $action))) {
throw new NonPublicComponentMethodCall($class, $action);
}
foreach ($hookStack['mount'] as $method) {
if (method_exists($this->component, $method)) {
Yoyo::container()->call([$this->component, $method], $parameters);
}
}
if (! in_array($action, ['render', 'refresh'])) {
$parameters = $isEventListenerAction ? $eventParams : $this->parseActionArguments();
// Empty params must fall through to existing no-params handling in the else branch
if ($isEventListenerAction && is_array($parameters) && ! empty($parameters) && array_values($parameters) !== $parameters) {
// Associative array from JS dispatch — validate required params then pass as named args
$paramInfo = ClassHelpers::getMethodParametersWithTypes($this->component, $action);
$regularParams = $paramInfo['regular'];
foreach ($regularParams as $param) {
if (! $param['optional'] && ! $param['variadic'] && ! isset($parameters[$param['name']])) {
throw new \InvalidArgumentException(
"Missing required parameter [{$param['name']}] for [{$this->name}::{$action}]"
);
}
}
$args = $parameters;
} else {
// Get parameter information with types
$paramInfo = ClassHelpers::getMethodParametersWithTypes($this->component, $action);
$regularParams = $paramInfo['regular'];
$typedParams = $paramInfo['typed'];
// Extract just the names of regular parameters for backwards compatibility
$parameterNames = array_column($regularParams, 'name');
// Check if the last regular parameter is variadic
$hasVariadic = ! empty($regularParams) && end($regularParams)['variadic'];
// Handle variadic parameters
if ($hasVariadic && count($parameterNames) > 0) {
$regularParamCount = count($parameterNames) - 1; // Exclude the variadic parameter
if (count($parameters) >= $regularParamCount) {
// Split parameters into regular and variadic
$regularParamValues = array_slice($parameters, 0, $regularParamCount);
$variadicParamValues = array_slice($parameters, $regularParamCount);
// Create args array with named regular parameters and indexed variadic parameters
$args = [];
for ($i = 0; $i < $regularParamCount; $i++) {
$args[$parameterNames[$i]] = $regularParamValues[$i] ?? null;
}
// Add variadic parameters as indexed values (not named)
foreach ($variadicParamValues as $value) {
$args[] = $value;
}
} else {
throw new \InvalidArgumentException("Too few parameters passed to [{$this->name}::{$action}]");
}
} else {
// Check if all regular parameters are optional
$requiredCount = 0;
foreach ($regularParams as $param) {
if (! $param['optional']) {
$requiredCount++;
}
}
// Only validate regular parameters (not typed/DI parameters)
if (count($parameters) >= $requiredCount && count($parameters) <= count($parameterNames)) {
// Parameters count is valid (between required and total)
$args = [];
for ($i = 0; $i < count($parameterNames); $i++) {
$args[$parameterNames[$i]] = $parameters[$i] ?? null;
}
} elseif (empty($parameterNames) && empty($parameters)) {
// Method has only typed parameters (or no parameters at all)
$args = [];
} else {
throw new \InvalidArgumentException("Incorrect number of parameters passed to [{$this->name}::{$action}]");
}
}
}
// The container will handle dependency injection for typed parameters
$actionResponse = Yoyo::container()->call([$this->component, $action], $args);
$type = gettype($actionResponse);
if ($type !== 'string' && $type !== 'NULL') {
throw new \Exception("Component [{$class}] action [{$action}] response should be a string, instead was [{$type}]");
}
}
foreach ($hookStack['rendering'] as $method) {
if (method_exists($this->component, $method)) {
Yoyo::container()->call([$this->component, $method]);
}
}
$view = $this->component->render();
if (is_null($view)) {
return '';
}
// For string based templates
if (is_string($view)) {
$view = $this->component->createViewFromString($view);
}
foreach ($hookStack['rendered'] as $method) {
if (method_exists($this->component, $method)) {
$view = Yoyo::container()->call([$this->component, $method], ['view' => $view]);
}
}
return (string) $view;
}
private function parseActionArguments()
{
$args = $this->request->get('actionArgs', []);
return is_array($args) ? $args : [$args];
}
private function processAnonymousComponent($variables = [], $attributes = [])
{
$this->component->spinning($this->spinning)->boot($variables, $attributes);
$view = (string) $this->component->render();
return $view;
}
public function getComponentInstance()
{
return $this->component;
}
}
================================================
FILE: src/yoyo/ComponentResolver.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Interfaces\ViewProviderInterface;
use Clickfwd\Yoyo\Interfaces\YoyoContainerInterface;
use Psr\Container\ContainerExceptionInterface;
class ComponentResolver
{
protected $name = 'default';
protected $variables;
protected $registered;
protected $hints;
protected $container;
public function __invoke(YoyoContainerInterface $container, array $registered = [], array $hints = [])
{
$this->container = $container;
$this->registered = $registered;
$this->hints = $hints;
return $this;
}
public function getName()
{
return $this->name;
}
public function resolving($id, $name, $variables)
{
}
public function resolveComponent($id, $name, $variables): ?Component
{
$this->resolving($id, $name, $variables);
if ($instance = $this->resolveDynamic($id, $name)) {
return $instance;
}
return $this->resolveAnonymous($id, $name);
}
public function resolveDynamic($id, $name): ?Component
{
$classNames = [];
$args = ['resolver' => $this, 'id' => $id, 'name' => $name];
// Check namespaced components
if (strpos($name, ViewProviderInterface::HINT_PATH_DELIMITER) > 0) {
[$namespaceAlias, $name] = explode(ViewProviderInterface::HINT_PATH_DELIMITER, $name);
if (isset($this->hints[$namespaceAlias])) {
foreach ($this->hints[$namespaceAlias] as $namespaceHint) {
$classNames[] = $namespaceHint . '\\' . $this->dotNotationToClass($name);
}
}
}
$classNames[] = $this->registered[$name] ?? null;
$configurationNamespaces = (array) \Clickfwd\Yoyo\Services\Configuration::get('namespace');
foreach ($configurationNamespaces as $namespaceHint) {
$classNames[] = $namespaceHint . $this->dotNotationToClass($name);
}
$classNames = array_filter(array_unique($classNames));
foreach ($classNames as $className) {
if (class_exists($className)) {
break;
}
}
try {
return $this->container->make($className, $args);
} catch (ContainerExceptionInterface $e) {
return null;
}
}
public function resolveAnonymous($id, $name): ?Component
{
$args = ['resolver' => $this, 'id' => $id, 'name' => $name];
if ($this->registered[$name] ?? null) {
$args['name'] = $this->registered[$name] ?? $name;
return $this->container->make(AnonymousComponent::class, $args);
}
$view = $this->resolveViewProvider();
if ($view->exists($name)) {
return $this->container->make(AnonymousComponent::class, $args);
}
return null;
}
public function dotNotationToClass($name)
{
return implode('\\', array_map(function ($name) {
return YoyoHelpers::studly($name);
}, explode('.', $name)));
}
public function resolveViewProvider(): ViewProviderInterface
{
return $this->container->get('yoyo.view.'.$this->getName());
}
}
================================================
FILE: src/yoyo/Concerns/BrowserEvents.php
================================================
<?php
namespace Clickfwd\Yoyo\Concerns;
use Clickfwd\Yoyo\Services\BrowserEventsService;
trait BrowserEvents
{
public function emit($event, ...$params)
{
(BrowserEventsService::getInstance())->emit($event, $params);
}
public function emitTo($target, $event, ...$params)
{
(BrowserEventsService::getInstance())->emitTo($target, $event, $params);
}
public function emitToWithSelector($target, $event, ...$params)
{
(BrowserEventsService::getInstance())->emitToWithSelector($target, $event, $params);
}
public function emitSelf($event, ...$params)
{
(BrowserEventsService::getInstance())->emitSelf($event, $params);
}
public function emitUp($event, ...$params)
{
(BrowserEventsService::getInstance())->emitUp($event, $params);
}
public function dispatchBrowserEvent($event, $params = [])
{
(BrowserEventsService::getInstance())->dispatchBrowserEvent($event, $params);
}
}
================================================
FILE: src/yoyo/Concerns/Redirector.php
================================================
<?php
namespace Clickfwd\Yoyo\Concerns;
trait Redirector
{
public $redirectTo;
public function redirect($url)
{
$this->redirectTo = $url;
return $this;
}
}
================================================
FILE: src/yoyo/Concerns/ResponseHeaders.php
================================================
<?php
namespace Clickfwd\Yoyo\Concerns;
trait ResponseHeaders
{
public function location($path)
{
$this->header('HX-Location', $path);
return $this;
}
public function pushUrl($url)
{
$this->header('HX-Push-Url', $url);
return $this;
}
public function redirect($url)
{
$this->header('HX-Redirect', $url);
return $this;
}
public function refresh()
{
$this->header('HX-Refresh', 'true');
return $this;
}
public function replaceUrl($url)
{
$this->header('HX-Replace-Url', $url);
return $this;
}
public function reswap($swap)
{
$this->header('HX-Reswap', $swap);
return $this;
}
public function reselect($selector)
{
$this->header('HX-Reselect', $selector);
return $this;
}
public function retarget($selector)
{
$this->header('HX-Retarget', $selector);
return $this;
}
public function trigger($event)
{
$this->header('HX-Trigger', $event);
return $this;
}
public function triggerAfterSwap($event)
{
$this->header('HX-Trigger-After-Swap', $event);
return $this;
}
public function triggerAfterSettle($event)
{
$this->header('HX-Trigger-After-Settle', $event);
return $this;
}
}
================================================
FILE: src/yoyo/Concerns/Singleton.php
================================================
<?php
namespace Clickfwd\Yoyo\Concerns;
trait Singleton
{
/**
* @var reference to singleton instance
*/
private static $instance;
/**
* Creates a new instance of a singleton class (via late static binding),
* accepting a variable-length argument list.
*
* @return self
*/
final public static function getInstance(...$params)
{
if (! isset(static::$instance)) {
static::$instance = new self(...$params);
}
return static::$instance;
}
/**
* Prevents cloning the singleton instance.
*
* @return void
*/
public function __clone()
{
}
/**
* Prevents unserializing the singleton instance.
*
* @return void
*/
public function __wakeup()
{
}
}
================================================
FILE: src/yoyo/ContainerResolver.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Containers\IlluminateContainer;
use Clickfwd\Yoyo\Containers\YoyoContainer;
use Clickfwd\Yoyo\Interfaces\YoyoContainerInterface;
use Illuminate\Container\Container;
class ContainerResolver
{
protected static $preferred = null;
public static function setPreferred(?YoyoContainerInterface $container)
{
static::$preferred = $container;
}
public static function getPreferred()
{
return static::$preferred;
}
public static function resolve(): YoyoContainerInterface
{
if (static::$preferred) {
return static::$preferred;
}
if (class_exists(Container::class)) {
return new IlluminateContainer(Container::getInstance());
}
return YoyoContainer::getInstance();
}
}
================================================
FILE: src/yoyo/Containers/IlluminateContainer.php
================================================
<?php
namespace Clickfwd\Yoyo\Containers;
use Clickfwd\Yoyo\Interfaces\YoyoContainerInterface;
use Closure;
use Illuminate\Container\Container;
class IlluminateContainer implements YoyoContainerInterface
{
protected static $instance;
protected $container;
public static function getInstance()
{
return static::$instance = static::$instance ?? new static(Container::getInstance());
}
public function __construct(Container $container)
{
$this->container = $container;
}
public function get(string $id)
{
return $this->container->get($id);
}
public function has(string $id): bool
{
return $this->container->has($id);
}
public function set(string $id, $value)
{
if (is_object($value) && ! ($value instanceof Closure)) {
return $this->container->instance($id, $value);
}
$this->container->bind($id, $value, true);
return $value;
}
public function make(string $class, array $args = [])
{
return $this->container->make($class, $args);
}
public function call($method, array $args = [])
{
return $this->container->call($method, $args);
}
}
================================================
FILE: src/yoyo/Containers/YoyoContainer.php
================================================
<?php
namespace Clickfwd\Yoyo\Containers;
use Clickfwd\Yoyo\Exceptions\BindingNotFoundException;
use Clickfwd\Yoyo\Exceptions\ContainerResolutionException;
use Clickfwd\Yoyo\Interfaces\YoyoContainerInterface;
class YoyoContainer implements YoyoContainerInterface
{
protected static $instance;
protected $bindings = [];
public static function getInstance()
{
return static::$instance = static::$instance ?? new static();
}
public function get(string $id)
{
if (! $this->has($id)) {
throw new BindingNotFoundException("[$id] is not bound to the container");
}
$resolved = $this->bindings[$id];
if ($resolved instanceof \Closure) {
$this->bindings[$id] = $resolved($this);
}
if (is_string($resolved) && class_exists($resolved)) {
return $this->make($resolved);
}
return $this->bindings[$id];
}
public function has(string $id): bool
{
return isset($this->bindings[$id]);
}
public function set(string $id, $value)
{
$this->bindings[$id] = $value;
return $value;
}
public function make(string $class, array $args = [])
{
try {
$class = $this->has($class) ? $this->get($class) : $class;
return is_object($class) ? $class : new $class(...$this->extractArgs($class, '__construct', $args));
} catch (\Throwable $e) {
throw new ContainerResolutionException("[$class] could not be resolved", $e);
}
}
public function call(callable $method, array $args = [])
{
if (! is_array($method) || count($method) !== 2) {
throw new \InvalidArgumentException("Callable must be in [class, method] format");
}
return $method(...$this->extractArgs($method[0], $method[1], $args));
}
protected function extractArgs($class, $method, $arguments)
{
try {
$result = [];
$reflector = new \ReflectionClass($class);
$parameters = $reflector->getMethod($method)->getParameters();
} catch (\ReflectionException $e) {
return $arguments;
}
foreach ($parameters as $parameter) {
// Variadic arguments
if ($parameter->isVariadic()) {
return array_merge($result, array_values($arguments));
}
// Named argument
elseif (isset($arguments[$parameter->getName()])) {
$result[] = $arguments[$parameter->getName()];
unset($arguments[$parameter->getName()]);
}
// Typed argument
elseif ($this->isResolvableType($parameter)) {
$result[] = $this->make($parameter->getType()->getName());
}
// Argument with default value
elseif ($parameter->isDefaultValueAvailable()) {
$result[] = $parameter->getDefaultValue();
}
// Nullable argument
elseif ($parameter->allowsNull()) {
$result[] = null;
}
// Default - assume explicit arguments
else {
return $arguments;
}
}
return $result;
}
protected function isResolvableType(\ReflectionParameter $parameter): bool
{
if ($parameter->getType() instanceof \ReflectionNamedType) {
$name = $parameter->getType()->getName();
return $name && (class_exists($name) || interface_exists($name));
}
return false;
}
}
================================================
FILE: src/yoyo/Exceptions/BindingNotFoundException.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
use Psr\Container\NotFoundExceptionInterface;
class BindingNotFoundException extends \Exception implements NotFoundExceptionInterface
{
public function __construct(string $message, ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}
================================================
FILE: src/yoyo/Exceptions/BypassRenderMethod.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class BypassRenderMethod extends \Exception
{
public function __construct($statusCode)
{
parent::__construct('', $statusCode);
}
}
================================================
FILE: src/yoyo/Exceptions/ComponentMethodNotFound.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class ComponentMethodNotFound extends \Exception
{
public function __construct($component, $method)
{
parent::__construct(
"Public method [{$method}] not found on Yoyo component [{$component}]"
);
}
}
================================================
FILE: src/yoyo/Exceptions/ComponentNotFound.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class ComponentNotFound extends \Exception
{
public function __construct($alias)
{
parent::__construct("Yoyo component with alias [$alias] not found.");
}
}
================================================
FILE: src/yoyo/Exceptions/ContainerResolutionException.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
use Psr\Container\ContainerExceptionInterface;
class ContainerResolutionException extends \Exception implements ContainerExceptionInterface
{
public function __construct(string $message, ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}
================================================
FILE: src/yoyo/Exceptions/FailedToRegisterComponent.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class FailedToRegisterComponent extends \Exception
{
public function __construct($alias, $componentClassName)
{
$message = 'Component registration failed.';
if ($componentClassName == 'Anonymous') {
$message = PHP_EOL."[$alias] template not found for Yoyo component [$componentClassName].";
} else {
$message = PHP_EOL."Yoyo component class [$componentClassName] provided for alias [$alias] not found.";
}
parent::__construct($message);
}
}
================================================
FILE: src/yoyo/Exceptions/HttpException.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class HttpException extends \Exception
{
protected $statusCode;
protected $headers;
public function __construct(int $statusCode, ?string $message = '', array $headers = [])
{
$this->statusCode = $statusCode;
$this->headers = $headers;
parent::__construct($message, $statusCode);
}
public function getStatusCode()
{
return $this->statusCode;
}
public function getHeaders()
{
return $this->headers;
}
}
================================================
FILE: src/yoyo/Exceptions/IncompleteComponentParamInRequest.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class IncompleteComponentParamInRequest extends \Exception
{
public function __construct()
{
parent::__construct('The component parameter is missing the component name or action.');
}
}
================================================
FILE: src/yoyo/Exceptions/MissingComponentTemplate.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class MissingComponentTemplate extends \Exception
{
public function __construct($template, $componentName)
{
parent::__construct("Unable to find template [$template] for [$componentName] component.");
}
}
================================================
FILE: src/yoyo/Exceptions/NonPublicComponentMethodCall.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class NonPublicComponentMethodCall extends \Exception
{
public function __construct($componentName, $method)
{
parent::__construct("Unable to call non-public method [$method] in Yoyo component [$componentName].");
}
}
================================================
FILE: src/yoyo/Exceptions/NotFoundHttpException.php
================================================
<?php
namespace Clickfwd\Yoyo\Exceptions;
class NotFoundHttpException extends HttpException
{
public function __construct(?string $message = '', array $headers = [])
{
parent::__construct(404, $message, $headers);
}
}
================================================
FILE: src/yoyo/Interfaces/RequestInterface.php
================================================
<?php
namespace Clickfwd\Yoyo\Interfaces;
interface RequestInterface
{
public function all();
public function except($keys);
public function get($key, $default = null);
public function drop($key);
public function method();
public function fullUrl();
public function isYoyoRequest();
public function windUp();
public function triggerId();
}
================================================
FILE: src/yoyo/Interfaces/ViewProviderInterface.php
================================================
<?php
namespace Clickfwd\Yoyo\Interfaces;
interface ViewProviderInterface
{
public const HINT_PATH_DELIMITER = '::';
public function __construct($view);
public function render($template, $vars = []): self;
public function makeFromString($content, $vars = []): string;
public function exists($template): bool;
public function getProviderInstance();
public function startYoyoRendering($component): void;
public function stopYoyoRendering(): void;
}
================================================
FILE: src/yoyo/Interfaces/YoyoContainerInterface.php
================================================
<?php
namespace Clickfwd\Yoyo\Interfaces;
use Closure;
use Psr\Container\ContainerInterface;
interface YoyoContainerInterface extends ContainerInterface
{
/**
* Binds value to the container for later resolution
*
* @param string $id
* @param Closure|object|string $value
* @return void
*/
public function set(string $id, $value);
/**
* Makes the class with provided attributes
*
* @param string $class
* @param array $args
* @return mixed
*/
public function make(string $class, array $args = []);
/**
* Calls the method with provided attributes
*
* @param callable $method
* @param array $args
* @return mixed
*/
public function call(callable $method, array $args = []);
}
================================================
FILE: src/yoyo/InvocableComponentVariable.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Closure;
class InvocableComponentVariable
{
protected $callable;
public function __construct(Closure $callable)
{
$this->callable = $callable;
}
public function __get($key)
{
return $this->__invoke()->{$key};
}
public function __call($method, $parameters)
{
return $this->__invoke()->{$method}(...$parameters);
}
public function __invoke()
{
return call_user_func($this->callable);
}
public function __toString()
{
return (string) $this->__invoke();
}
}
================================================
FILE: src/yoyo/QueryString.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Services\Request;
class QueryString
{
private $defaults;
private $new;
private $keys;
private $currentUrl;
public function __construct($defaults, $new, $keys)
{
$this->defaults = $defaults;
$this->new = $new;
$this->keys = $keys;
$this->currentUrl = Yoyo::request()->fullUrl();
}
/**
* Used to pass variables to the component request.
*/
public function getQueryParams()
{
$queryParams = array_merge($this->defaults, $this->new);
// Filter out keys that are not explicitly set in the component queryString property
$queryParams = array_intersect_key($queryParams, array_flip($this->keys));
return $queryParams;
}
/**
* Used to update the browser URL state.
*/
public function getPageQueryParams()
{
if (! $this->currentUrl) {
return [];
}
// Filter out keys that are not explicitly set in the component queryString property
$new = array_intersect_key($this->new, array_flip($this->keys));
// Get current query string values and merge them with new ones
$queryString = parse_url(htmlspecialchars_decode($this->currentUrl), PHP_URL_QUERY) ?? '';
parse_str($queryString, $args);
$queryParams = array_merge($args, $new);
// If a query string value matches the default value, remove it from the URL
foreach ($queryParams as $key => $val) {
if (is_object($val) && method_exists($val, 'toArray')) {
$queryParams[$key] = $val->toArray();
}
if ((isset($this->defaults[$key]) && $val === $this->defaults[$key]) || $val === '') {
unset($queryParams[$key]);
}
}
return $queryParams;
}
}
================================================
FILE: src/yoyo/Request.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Interfaces\RequestInterface;
class Request implements RequestInterface
{
private $request;
private $server;
private $dropped = [];
private $decodedRequest = null;
public function __construct()
{
$this->request = $_REQUEST;
$this->server = $_SERVER;
}
public function mock($request, $server)
{
$this->request = $request;
$this->server = $server;
$this->decodedRequest = null;
return $this;
}
public function reset()
{
$this->request = [];
$this->server = [];
$this->decodedRequest = null;
}
public function all()
{
if ($this->decodedRequest !== null) {
return $this->decodedRequest;
}
return $this->decodedRequest = array_map(function ($value) {
$validJson = false;
$decoded = YoyoHelpers::test_json($value, $validJson);
if ($validJson) {
return $decoded;
}
return $value;
}, $this->request);
}
public function except($keys)
{
$keys = is_array($keys) ? $keys : [$keys];
$all = $this->all();
$output = [];
foreach ($all as $key => $value) {
if (in_array($key, $keys)) {
continue;
}
$output[$key] = $value;
}
return $output;
}
public function only($keys)
{
return array_intersect_key($this->all(), array_flip($keys));
}
public function get($key, $default = null)
{
if (in_array($key, $this->dropped)) {
return $default;
}
$value = $this->request[$key] ?? $default;
$validJson = false;
$decoded = YoyoHelpers::test_json($value, $validJson);
if ($validJson) {
return $decoded;
}
return $value;
}
public function startsWith($prefix)
{
$vars = [];
foreach ($this->all() as $key => $value) {
if (strpos($key, $prefix) === 0) {
$vars[$key] = $value;
}
}
return $vars;
}
public function set($key, $value)
{
$this->request[$key] = $value;
$this->decodedRequest = null;
return $this;
}
public function merge($data)
{
$this->request = array_merge($this->request, $data);
$this->decodedRequest = null;
return $this;
}
public function drop($key)
{
$this->dropped[] = $key;
}
public function method()
{
return $this->server['REQUEST_METHOD'] ?? 'GET';
}
public function fullUrl()
{
if (isset($this->server['HTTP_HX_CURRENT_URL'])) {
return $this->server['HTTP_HX_CURRENT_URL'];
}
if (empty($this->server['HTTP_HOST'])) {
return null;
}
$protocol = 'http';
if (isset($this->server['HTTPS']) && $this->server['HTTPS'] === 'on') {
$protocol = 'https';
}
$host = $this->server['HTTP_HOST'];
$path = rtrim($this->server['REQUEST_URI'] ?? '', '?');
return "{$protocol}://{$host}{$path}";
}
public function isYoyoRequest()
{
return $this->server['HTTP_HX_REQUEST'] ?? false;
}
public function windUp()
{
unset($this->server['HTTP_HX_REQUEST']);
}
public function triggerId()
{
return $this->server['HTTP_HX_TRIGGER'];
}
public function triggerName()
{
return $this->server['HTTP_HX_TRIGGER_NAME'] ?? null;
}
public function header($name)
{
return $this->server['HTTP_'.strtoupper($name)] ?? null;
}
}
================================================
FILE: src/yoyo/Services/BrowserEventsService.php
================================================
<?php
namespace Clickfwd\Yoyo\Services;
use Clickfwd\Yoyo\Concerns\Singleton;
use Clickfwd\Yoyo\Yoyo;
class BrowserEventsService
{
use Singleton;
private $request;
private $response;
private $eventQueue = [];
private $browserEventQueue = [];
public function __construct()
{
$this->request = Yoyo::request();
$this->response = Response::getInstance();
}
public function emit($event, ...$params)
{
$this->queue($event, $params);
}
public function emitTo($target, $event, ...$params)
{
$this->queue($event, $params, null, $target);
}
public function emitToWithSelector($target, $event, ...$params)
{
$this->queue($event, $params, $target);
}
public function emitSelf($event, ...$params)
{
if ($component = $this->getComponentNameFromRequest()) {
$this->queue($event, $params, $selector = null, $component, 'self');
}
}
public function emitUp($event, ...$params)
{
$targetId = $this->request->triggerId();
if ($component = $this->getComponentNameFromRequest()) {
$this->queue($event, $params, "#{$targetId}", $component, 'ancestorsOnly');
}
}
public function queue($event, $params, $selector = null, $component = null, $propagation = null)
{
$params = $params[0];
$payload = array_filter(compact('event', 'params', 'selector', 'component', 'propagation'));
$this->eventQueue[] = $payload;
}
public function dispatchBrowserEvent($event, $params = [])
{
$this->browserEventQueue[] = compact('event', 'params');
}
public function dispatch()
{
$this->response->header('Yoyo-Emit', json_encode($this->eventQueue));
$this->response->header('Yoyo-Browser-Event', json_encode($this->browserEventQueue));
}
protected function getComponentNameFromRequest()
{
if ($name = $this->request->get('component')) {
return explode('/', $name)[0];
}
return false;
}
}
================================================
FILE: src/yoyo/Services/Configuration.php
================================================
<?php
namespace Clickfwd\Yoyo\Services;
use Clickfwd\Yoyo\Concerns\Singleton;
class Configuration
{
use Singleton;
private static $options;
public static $htmx = '1.9.4';
protected static $allowedConfigOptions = [
'historyEnabled',
'historyCacheSize',
'refreshOnHistoryMiss',
'defaultSwapStyle',
'defaultSwapDelay',
'defaultSettleDelay',
'includeIndicatorStyles',
'indicatorClass',
'requestClass',
'addedClass',
'settlingClass',
'swappingClass',
'allowEval',
'inlineScriptNonce',
'attributesToSettle',
'withCredentials',
'timeout',
'wsReconnectDelay',
'wsBinaryType',
'disableSelector',
'useTemplateFragments',
'scrollBehavior',
'defaultFocusScroll',
'getCacheBusterParam',
'globalViewTransitions',
'methodsThatUseUrlParams',
];
public function __construct($options)
{
self::$options = array_merge([
'namespace' => 'App\\Yoyo\\',
'defaultSwapStyle' => 'outerHTML',
'historyEnabled' => false,
'indicatorClass' => 'yoyo-indicator',
'requestClass' => 'yoyo-request',
'settlingClass' => 'yoyo-settling',
'swappingClass' => 'yoyo-swapping',
], $options);
}
public static function get($key, $default = null)
{
return self::$options[$key] ?? $default;
}
public static function scripts($return = false)
{
return self::minify(self::javascriptAssets());
}
public static function styles()
{
return self::minify(self::cssStyle());
}
public static function htmxSrc(): string
{
if (empty($htmxSrc = self::get('htmx'))) {
$htmxSrc = 'https://unpkg.com/htmx.org@'.self::$htmx.'/dist/htmx.min.js';
}
return $htmxSrc;
}
public static function yoyoSrc(): string
{
return rtrim(self::get('scriptsPath', ''), '/').'/yoyo.js';
}
public static function javascriptAssets(): string
{
$htmxSrc = self::htmxSrc();
$yoyoSrc = self::yoyoSrc();
$initCode = self::javascriptInitCode();
return <<<HTML
<script src="{$htmxSrc}"></script>
<script src="{$yoyoSrc}"></script>
{$initCode}
HTML;
}
public static function javascriptInitCode($includeScriptTag = true): string
{
$yoyoRoute = self::get('url', '');
$configuration = array_intersect_key(static::$options, array_flip(static::$allowedConfigOptions));
$yoyoConfig = json_encode($configuration);
$yoyoRouteJs = json_encode($yoyoRoute);
$script = <<<HTML
Yoyo.url = {$yoyoRouteJs};
Yoyo.config($yoyoConfig);
HTML;
if ($includeScriptTag) {
$script = "<script>{$script}</script>";
}
return $script;
}
public static function cssStyle($includeStyleTag = true)
{
$style = <<<HTML
[yoyo\:spinning], [yoyo\:spinning\.delay] {
display: none;
}
HTML;
if ($includeStyleTag) {
$style = "<style>$style</style>";
}
return $style;
}
protected static function minify($string)
{
return preg_replace('~(\v|\t|\s{2,})~m', '', $string);
}
}
================================================
FILE: src/yoyo/Services/PageRedirectService.php
================================================
<?php
namespace Clickfwd\Yoyo\Services;
use Clickfwd\Yoyo\Concerns\Singleton;
class PageRedirectService
{
use Singleton;
private $response;
public function __construct()
{
$this->response = Response::getInstance();
}
public function redirect($url)
{
if ($url) {
$this->response->header('Yoyo-Redirect', $url);
}
}
}
================================================
FILE: src/yoyo/Services/Response.php
================================================
<?php
namespace Clickfwd\Yoyo\Services;
use Clickfwd\Yoyo\Concerns;
class Response
{
use Concerns\Singleton;
use Concerns\ResponseHeaders;
protected $headers = [];
protected $statusCode = 200;
public function __construct()
{
}
public function header($name, $value)
{
// Sanitize header name and value to prevent header injection
$name = str_replace(["\r", "\n", "\0"], '', (string) $name);
$value = is_array($value) ? $value : str_replace(["\r", "\n", "\0"], '', (string) $value);
$this->headers[$name] = $value;
return $this;
}
public function status($statusCode)
{
$this->statusCode = $statusCode;
return $this;
}
public function send(string $content = ''): string
{
if (! headers_sent()) {
foreach ($this->headers as $key => $value) {
if (is_array($value)) {
$value = json_encode($value);
}
header("$key: $value");
}
http_response_code($this->statusCode ?? 200);
}
return $content ?: '';
}
public function setHeaders($headers)
{
$this->headers = array_merge($this->headers, $headers);
return $this;
}
public function getHeaders()
{
return $this->headers;
}
public function getStatusCode()
{
return $this->statusCode;
}
}
================================================
FILE: src/yoyo/Services/UrlStateManagerService.php
================================================
<?php
namespace Clickfwd\Yoyo\Services;
use Clickfwd\Yoyo\Yoyo;
class UrlStateManagerService
{
private $request;
private $currentUrl;
public function __construct()
{
$this->request = Yoyo::request();
$this->currentUrl = $this->request->fullUrl();
}
public function pushState($queryParams)
{
$response = Response::getInstance();
if (! $this->currentUrl || $this->request->method() !== 'GET') {
return;
}
// Don't override if the component already set an explicit URL
$headers = $response->getHeaders();
if (isset($headers['HX-Replace-Url']) || isset($headers['HX-Push-Url'])) {
return;
}
$parsedUrl = parse_url($this->currentUrl);
$port = isset($parsedUrl['port']) ? (':'.$parsedUrl['port']) : '';
$url = $parsedUrl['scheme'].'://'.$parsedUrl['host'].$port.$parsedUrl['path'].($queryParams ? '?'.http_build_query($queryParams) : '');
if ($url !== $this->currentUrl) {
$response->header('Yoyo-Push', $url);
}
}
}
================================================
FILE: src/yoyo/Twig/YoyoTwigExtension.php
================================================
<?php
namespace Clickfwd\Yoyo\Twig;
use Clickfwd\Yoyo\Services\BrowserEventsService;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
use Twig\Markup;
use Twig\TwigFunction;
use function Yoyo\yoyo_render;
use function Yoyo\yoyo_scripts;
class YoyoTwigExtension extends AbstractExtension implements GlobalsInterface
{
public function getFunctions()
{
return [
$this->yoyo_scripts(),
$this->yoyo(),
$this->emit(),
$this->emitTo(),
$this->emitToWithSelector(),
$this->emitSelf(),
$this->emitUp(),
];
}
public function getGlobals(): array
{
return [
];
}
private function yoyo_scripts()
{
return new TwigFunction('yoyo_scripts', function (): Markup {
return self::raw(yoyo_scripts());
});
}
private function yoyo()
{
return new TwigFunction('yoyo', function ($name, $variables = [], $attributes = []): Markup {
$variables = $variables ?? [];
$attributes = $attributes ?? [];
$output = yoyo_render($name, $variables, $attributes);
return self::raw($output);
});
}
private function emit()
{
return new TwigFunction('emit', function ($eventName, $payload = []) {
(BrowserEventsService::getInstance())->emit($eventName, $payload);
});
}
private function emitTo()
{
return new TwigFunction('emitTo', function ($target, $eventName, $payload = []) {
(BrowserEventsService::getInstance())->emitTo($target, $eventName, $payload);
});
}
private function emitToWithSelector()
{
return new TwigFunction('emitToWithSelector', function ($target, $eventName, $payload = []) {
(BrowserEventsService::getInstance())->emitToWithSelector($target, $eventName, $payload);
});
}
private function emitSelf()
{
return new TwigFunction('emitSelf', function ($eventName, $payload = []) {
(BrowserEventsService::getInstance())->emitSelf($eventName, $payload);
});
}
private function emitUp()
{
return new TwigFunction('emitUp', function ($eventName, $payload = []) {
(BrowserEventsService::getInstance())->emitUp($eventName, $payload);
});
}
private static function raw($string)
{
return new Markup($string, 'UTF-8');
}
}
================================================
FILE: src/yoyo/View.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Interfaces\ViewProviderInterface;
use InvalidArgumentException;
class View
{
protected $paths;
protected $views;
protected $yoyoComponent;
protected static $hints;
public function __construct($paths)
{
$paths = (array) $paths;
$this->paths = array_map([$this, 'resolvePath'], $paths);
}
/**
* Forward method calls to their property closure function equivalent
* Used for eventManager emit methods dynamically added to the view class.
*/
public function __call($name, $args)
{
return call_user_func_array($this->$name, $args);
}
public function startYoyoRendering($component)
{
$this->yoyoComponent = $component;
return $this;
}
public function render($name, $vars = []): string
{
$path = $this->exists($name);
ob_start();
\Closure::bind(function () use ($path, $vars) {
extract($vars, EXTR_SKIP);
include $path;
}, $this->yoyoComponent ? $this->yoyoComponent : $this)();
return ltrim(ob_get_clean());
}
public function makeFromString($content, $vars = []): string
{
throw new \Exception('Views from strings are not supported with the native Yoyo view provider.');
}
public function exists($name)
{
if (isset($this->views[$name])) {
return $this->views[$name];
}
if ($this->hasHintInformation($name = trim($name))) {
return $this->views[$name] = $this->findNamespacedView($name);
}
return $this->views[$name] = $this->findInPaths($name, $this->paths);
}
public function addLocation($location)
{
$this->paths[] = $this->resolvePath($location);
}
public function prependLocation($location)
{
array_unshift($this->paths, $this->resolvePath($location));
}
protected function findInPaths($name, $paths)
{
$templatePath = str_replace('.', '/', $name);
foreach ($paths as $path) {
if (file_exists($location = "{$path}/{$templatePath}.php")) {
return $location;
}
}
throw new InvalidArgumentException("View [{$name}] not found.");
}
protected function findNamespacedView($name)
{
[$namespace, $view] = $this->parseNamespaceSegments($name);
return $this->findInPaths($view, static::$hints[$namespace]);
}
protected function parseNamespaceSegments($name)
{
$segments = explode(ViewProviderInterface::HINT_PATH_DELIMITER, $name);
if (count($segments) !== 2) {
throw new InvalidArgumentException("View [{$name}] has an invalid name.");
}
if (! isset(static::$hints[$segments[0]])) {
throw new InvalidArgumentException("No hint path defined for [{$segments[0]}].");
}
return $segments;
}
public function addNamespace($namespace, $hints)
{
$hints = (array) $hints;
if (isset(static::$hints[$namespace])) {
$hints = array_merge(static::$hints[$namespace], $hints);
}
static::$hints[$namespace] = $hints;
}
public function prependNamespace($namespace, $hints)
{
$hints = (array) $hints;
if (isset(static::$hints[$namespace])) {
$hints = array_merge($hints, static::$hints[$namespace]);
}
static::$hints[$namespace] = $hints;
}
public function hasHintInformation($name)
{
return strpos($name, ViewProviderInterface::HINT_PATH_DELIMITER) > 0;
}
protected function resolvePath($path)
{
return realpath($path) ?: $path;
}
}
================================================
FILE: src/yoyo/ViewProviders/BaseViewProvider.php
================================================
<?php
namespace Clickfwd\Yoyo\ViewProviders;
abstract class BaseViewProvider
{
protected $view;
public function getProviderInstance()
{
return $this->view;
}
}
================================================
FILE: src/yoyo/ViewProviders/BladeViewProvider.php
================================================
<?php
namespace Clickfwd\Yoyo\ViewProviders;
use Clickfwd\Yoyo\Interfaces\ViewProviderInterface;
class BladeViewProvider extends BaseViewProvider implements ViewProviderInterface
{
protected $view;
protected $template;
protected $vars;
protected $engine;
public function __construct($view)
{
$this->view = $view;
}
public function startYoyoRendering($component): void
{
$this->engine = $this->view->getContainer()->get('view.engine.resolver')->resolve('blade');
$this->engine->startYoyoRendering($component);
}
public function stopYoyoRendering(): void
{
$this->engine->stopYoyoRendering();
}
public function render($template, $vars = []): ViewProviderInterface
{
$this->template = $template;
$this->vars = $vars;
return $this;
}
public function makeFromString($content, $vars = []): string
{
$view = $this->view->make((new \Clickfwd\Yoyo\Blade\CreateBladeViewFromString())($this->view, $content));
return $view->with($vars)->render();
}
public function exists($template): bool
{
return $this->view->exists($template);
}
public function getFinder()
{
return $this->view->getFinder();
}
public function addNamespace($namespace, $hints)
{
$this->getFinder()->addNamespace($namespace, $hints);
return $this;
}
public function prependNamespace($namespace, $hints)
{
$this->getFinder()->prependNamespace($namespace, $hints);
return $this;
}
public function addLocation($location)
{
$this->getFinder()->addLocation($location);
return $this;
}
public function prependLocation($location)
{
$this->getFinder()->prependLocation($location);
return $this;
}
public function __call(string $method, array $params)
{
return call_user_func_array([$this->view, $method], $params);
}
public function __toString()
{
$output = (string) $this->view->make($this->template, $this->vars);
$this->stopYoyoRendering();
return $output;
}
}
================================================
FILE: src/yoyo/ViewProviders/PhalconViewProvider.php
================================================
<?php
namespace Clickfwd\Yoyo\ViewProviders;
use Clickfwd\Yoyo\Interfaces\ViewProviderInterface;
class PhalconViewProvider extends BaseViewProvider implements ViewProviderInterface
{
protected $view;
protected $template;
protected $vars;
private $viewExtention = '.phtml';
public function __construct($view)
{
$this->view = $view;
}
public function exists($view): bool
{
return file_exists($this->view->getViewsDir() . $view . $this->viewExtention);
}
public function render($template, $vars = []): ViewProviderInterface
{
$this->template = $template;
$this->vars = $vars;
return $this;
}
public function setViewExtention($viewExtention)
{
$this->viewExtention = $viewExtention;
return $this;
}
public function makeFromString($content, $vars = []): string
{
$this->view->start();
$this->view->setContent($content);
$this->view->setVars($vars);
$this->view->finish();
return $this->view->render();
}
public function startYoyoRendering($component): void
{
}
public function stopYoyoRendering(): void
{
}
public function __toString()
{
return $this->view->render($this->template, $this->vars);
}
}
================================================
FILE: src/yoyo/ViewProviders/TwigViewProvider.php
================================================
<?php
namespace Clickfwd\Yoyo\ViewProviders;
use Clickfwd\Yoyo\Interfaces\ViewProviderInterface;
class TwigViewProvider extends BaseViewProvider implements ViewProviderInterface
{
protected $view;
protected $template;
protected $vars;
protected $yoyoComponent;
public static $twig_template_extension = 'twig';
public function __construct($view)
{
$this->view = $view;
}
public function startYoyoRendering($component): void
{
$this->yoyoComponent = $component;
}
public function stopYoyoRendering(): void
{
//
}
public function normalizeName($template)
{
if (strpos($template, ViewProviderInterface::HINT_PATH_DELIMITER) > 0) {
[$namespace, $name] = explode(ViewProviderInterface::HINT_PATH_DELIMITER, $template);
return "@{$namespace}/{$name}";
}
return $template;
}
public function render($template, $vars = []): ViewProviderInterface
{
$this->template = $this->normalizeName($template);
$this->vars = $vars;
return $this;
}
public function makeFromString($content, $vars = []): string
{
$template = $this->view->createTemplate((string) $content);
return $template->render($vars);
}
public function exists($template): bool
{
$template = $this->normalizeName($template);
return $this->getLoader()->exists($template.'.'.self::$twig_template_extension);
}
public function getLoader()
{
return $this->view->getLoader();
}
public function addNamespace($namespace, $path)
{
$this->getLoader()->addPath($path, $namespace);
return $this;
}
public function prependNamespace($namespace, $path)
{
$this->getLoader()->prependPath($path, $namespace);
return $this;
}
public function addLocation($location)
{
$this->getLoader()->addPath($location);
return $this;
}
public function prependLocation($location)
{
$this->getLoader()->prependPath($location);
return $this;
}
public function __call(string $method, array $params)
{
return call_user_func_array([$this->view, $method], $params);
}
public function __toString()
{
$this->vars['this'] = $this->yoyoComponent;
return (string) $this->view->render($this->template.'.'.self::$twig_template_extension, $this->vars);
}
}
================================================
FILE: src/yoyo/ViewProviders/YoyoViewProvider.php
================================================
<?php
namespace Clickfwd\Yoyo\ViewProviders;
use Clickfwd\Yoyo\Exceptions\ComponentNotFound;
use Clickfwd\Yoyo\Interfaces\ViewProviderInterface;
use InvalidArgumentException;
class YoyoViewProvider extends BaseViewProvider implements ViewProviderInterface
{
protected $view;
protected $name;
protected $vars;
public function __construct($view)
{
$this->view = $view;
}
public function startYoyoRendering($component): void
{
$this->view->startYoyoRendering($component);
}
public function stopYoyoRendering(): void
{
//
}
public function render($name, $vars = []): ViewProviderInterface
{
$this->name = $name;
$this->vars = $vars;
return $this;
}
public function makeFromString($content, $vars = []): string
{
return $this->view->makeFromString($content, $vars);
}
public function exists($name): bool
{
try {
return $this->view->exists($name);
} catch (InvalidArgumentException $e) {
throw new ComponentNotFound($name);
}
}
public function addNamespace($namespace, $hints)
{
$this->view->addNamespace($namespace, $hints);
return $this;
}
public function prependNamespace($namespace, $hints)
{
$this->view->prependNamespace($namespace, $hints);
return $this;
}
public function addLocation($location)
{
$this->view->addLocation($location);
return $this;
}
public function prependLocation($location)
{
$this->view->prependLocation($location);
return $this;
}
public function __call(string $method, array $params)
{
return call_user_func_array([$this->view, $method], $params);
}
public function __toString()
{
return $this->view->render($this->name, $this->vars);
}
}
================================================
FILE: src/yoyo/Yoyo.php
================================================
<?php
namespace Clickfwd\Yoyo;
use Clickfwd\Yoyo\Exceptions\BypassRenderMethod;
use Clickfwd\Yoyo\Exceptions\HttpException;
use Clickfwd\Yoyo\Exceptions\NotFoundHttpException;
use Clickfwd\Yoyo\Interfaces\RequestInterface;
use Clickfwd\Yoyo\Interfaces\YoyoContainerInterface;
use Clickfwd\Yoyo\Services\BrowserEventsService;
use Clickfwd\Yoyo\Services\Configuration;
use Clickfwd\Yoyo\Services\PageRedirectService;
use Clickfwd\Yoyo\Services\Response;
use Clickfwd\Yoyo\Services\UrlStateManagerService;
class Yoyo
{
private $action;
private $attributes = [];
private $id;
private $name;
private $variables = [];
private static $container;
private static $request;
private static $registeredComponents = [];
private static $componentNamespaces = [];
private static $resolverInstances = [];
public function __construct(?YoyoContainerInterface $container = null)
{
static::$container = $container ?? ContainerResolver::resolve();
}
/**
* Not really an instance, but we avoid having to call `new` with an empty constructor
* Nested components don't work when re-using an instance
*/
public static function getInstance()
{
return new self(static::$container);
}
public function bindRequest(RequestInterface $request)
{
static::$request = $request;
}
public static function request()
{
if (! static::$request) {
static::$request = new Request();
}
return static::$request;
}
public function configure($options): void
{
Configuration::getInstance($options);
}
public static function container()
{
return static::$container;
}
public function getComponentId($attributes): string
{
if (isset($attributes['id'])) {
$id = $attributes['id'];
} else {
$id = static::request()->get(YoyoCompiler::yoprefix_value('id'), YoyoCompiler::yoprefix_value(YoyoHelpers::randString()));
}
// Remove the component ID from the request so it's not passed to child components
static::request()->drop(YoyoCompiler::yoprefix_value('id'));
return $id;
}
private function getComponentResolver()
{
$name = $this->variables[YoyoCompiler::yoprefix('resolver')]
?? static::request()->get(YoyoCompiler::yoprefix('resolver'));
if ($name && static::$container->has("yoyo.resolver.{$name}")) {
return static::$container->get("yoyo.resolver.{$name}")(static::$container, static::$registeredComponents, static::$componentNamespaces);
}
$resolver = ! $name ? new ComponentResolver() : static::$resolverInstances[$name];
$name = $name ?? 'default';
static::$container->set("yoyo.resolver.{$name}", $resolver);
return $resolver(static::$container, static::$registeredComponents, static::$componentNamespaces);
}
public function registerViewProvider($name, $provider = null)
{
if (is_null($provider)) {
$provider = $name;
$name = 'default';
}
static::$container->set("yoyo.view.{$name}", $provider);
}
public function registerViewProviders($providers)
{
foreach ($providers as $name => $provider) {
$this->registerViewProvider($name, $provider);
}
}
public static function getViewProvider($name = 'default')
{
return static::$container->get("yoyo.view.{$name}");
}
public function registerComponentResolver($resolver)
{
if (is_string($resolver)) {
$resolver = self::$container->make($resolver);
}
$this->registerViewProvider($name = $resolver->getName(), function () use ($resolver) {
return $resolver->getViewProvider();
});
static::$resolverInstances[$name] = $resolver;
}
/**
* Register component namespace hints
*
* @param string $namespace
* @param mixed $class
* @return void
*/
public static function componentNamespace(string $namespace, $class): void
{
$class = array_filter((array) $class);
static::$componentNamespaces[$namespace] = array_merge(static::$componentNamespaces[$namespace] ?? [], $class);
}
public static function registerComponent($name, $class = null): void
{
static::$registeredComponents[$name] = $class;
}
public static function registerComponents($components): void
{
foreach ($components as $name => $class) {
if (is_numeric($name)) {
$name = $class;
$class = null;
}
static::registerComponent($name, $class);
}
}
public static function abort($code, $message = '', array $headers = [])
{
if ($code == 404) {
throw new NotFoundHttpException($message, $headers);
}
throw new HttpException($code, $message, $headers);
}
public function mount($name, $variables = [], $attributes = [], $action = 'render'): self
{
$this->action($action);
$this->id = $this->getComponentId($attributes);
unset($attributes['id']);
$this->name = $name;
$this->variables = $variables;
$this->attributes = $attributes;
return $this;
}
public function action($action): self
{
$this->action = $action == 'refresh' ? 'render' : $action;
return $this;
}
public function actionArgs(...$args)
{
$this->request()->merge(['actionArgs' => $args]);
return $this;
}
/**
* Renders the component on initial page load.
*/
public function render(): string
{
return $this->output($spinning = false);
}
/**
* Renders the component on dynamic updates (ajax) to send back to the browser.
*/
public function refresh(): string
{
$output = $this->output($spinning = true);
return $output;
}
public function update(): string
{
[$name, $action] = $this->parseUpdateRequest();
return $this->mount($name, $variables = [], $attributes = [], $action)->refresh();
}
protected function parseUpdateRequest()
{
$component = static::request()->get('component');
$parts = array_filter(explode('/', $component));
if (empty($parts)) {
throw new Exceptions\IncompleteComponentParamInRequest();
}
$name = $parts[0];
$action = $parts[1] ?? 'render';
return [$name, $action];
}
public function output($spinning = false)
{
$variables = [];
$componentManager = new ComponentManager($this->getComponentResolver(), static::request(), $spinning);
try {
try {
$html = $componentManager->process($this->id, $this->name, $this->action ?? YoyoCompiler::COMPONENT_DEFAULT_ACTION, $this->variables, $this->attributes);
} finally {
if ($componentManager->getComponentInstance()) {
// Get all data needed to pass the rendered HTML through the Yoyo compiler to make it reactive
$defaultValues = $componentManager->getDefaultPublicVars();
$newValues = $componentManager->getPublicVars();
// Get dynamic component public properties anonymous components vars to pass them to the compiler
// Any matching parameter names in yoyo:props will be automatically added to yoyo:vals
$variables = array_merge($defaultValues, $newValues);
$variables = YoyoHelpers::removeEmptyValues($variables);
$listeners = $componentManager->getListeners();
$componentType = $componentManager->isDynamicComponent() ? 'dynamic' : 'anonymous';
// For dynamic components, filter variables based on component props
$props = $componentManager->getProps();
$postComponentProcessingActions = function () use ($componentManager, $defaultValues, $newValues) {
$queryStringKeys = $componentManager->getQueryString();
$queryString = new QueryString($defaultValues, $newValues, $queryStringKeys);
// Browser URL State
$urlStateManager = new UrlStateManagerService();
if ($componentManager->isDynamicComponent()) {
$urlStateManager->pushState($queryString->getPageQueryParams());
}
// Browser Events
$eventsService = BrowserEventsService::getInstance();
$eventsService->dispatch();
// Browser Redirect
(PageRedirectService::getInstance())->redirect($componentManager->getComponentInstance()->redirectTo);
};
}
}
} catch (BypassRenderMethod $e) {
if (isset($postComponentProcessingActions)) {
$postComponentProcessingActions();
}
if ($e->getCode() == 204) {
Response::getInstance()->status(204)->send();
// Need to throw exception to stop execution and send 204 status code
throw $e;
} else {
return Response::getInstance()->status(200)->send();
}
} catch (HttpException $e) {
if (isset($postComponentProcessingActions)) {
$postComponentProcessingActions();
}
Response::getInstance()->status($e->getStatusCode())->setHeaders($e->getHeaders())->send();
throw $e;
} catch (\Exception $e) {
if (isset($postComponentProcessingActions)) {
$postComponentProcessingActions();
}
Response::getInstance()->send();
throw $e;
}
$cacheHistory = ! empty(array_filter(
$componentManager->getQueryString(),
function ($key) {
return strpos($key, 'yoyo-id') !== 0;
}
)
);
$compiledHtml = $this->compile($componentType, $html, $spinning, $variables, $listeners, $props, $cacheHistory);
if ($spinning) {
$postComponentProcessingActions();
}
return (Response::getInstance())->send($compiledHtml);
}
public function compile($componentType, $html, $spinning = null, $variables = [], $listeners = [], $props = [], $cacheHistory = false): string
{
$spinning = $spinning ?? $this->is_spinning();
$variables = array_merge($this->variables, $variables);
$output = (new YoyoCompiler($componentType, $this->id, $this->name, $variables, $this->attributes, $spinning))
->withHistory($cacheHistory)
->addComponentListeners($listeners)
->addComponentProps($props)
->compile($html);
return $output;
}
/**
* Is this a request to update the component?
*/
private function is_spinning(): bool
{
$spinning = static::request()->isYoyoRequest();
// Stop spinning of child components when parent is refreshed
static::request()->windUp();
return $spinning;
}
}
================================================
FILE: src/yoyo/YoyoCompiler.php
================================================
<?php
namespace Clickfwd\Yoyo;
use DOMDocument;
use DOMXPath;
class YoyoCompiler
{
protected $componentType;
protected $componentId;
protected $name;
protected $variables;
protected $attributes;
protected $spinning;
protected $listeners;
protected $props;
protected $withHistory;
protected $idCounter = 1;
public const HTMX_REQUEST_METHOD_ATTRIBUTES = [
'boost',
'delete',
'get',
'patch',
'post',
'put',
'sse',
'ws',
];
public const YOYO_ATTRIBUTES = [
'confirm',
'disable',
'disinherit',
'encoding',
'ext',
'headers',
'history-elt',
'include',
'indicator',
'on',
'params',
'preserve',
'prompt',
'push-url',
'replace-url',
'request',
'select-oob',
'select',
'swap-oob',
'swap',
'sync',
'target',
'trigger',
'validate',
'vals',
];
public const YOYO_TO_HX_ATTRIBUTE_REMAP = [
'on' => 'trigger',
];
public const COMPONENT_DEFAULT_ACTION = 'render';
public const COMPONENT_WRAPPER_CLASS = 'yoyo-wrapper';
public const YOYO_PREFIX = 'yoyo';
public const YOYO_PREFIX_FINDER = 'yoyo-finder';
public const HTMX_PREFIX = 'hx';
// Pre-built lookup tables for O(1) checks instead of in_array()
private static $yoyoAttributeMap;
private static $htmxMethodMap;
// Cached prefix strings to avoid repeated concatenation
private static $yoprefixCache = [];
private static $hxprefixCache = [];
public function __construct($componentType, $componentId, $name, $variables, $attributes, $spinning)
{
$this->componentType = $componentType;
$this->componentId = $componentId;
$this->name = $name;
$this->variables = $variables;
$this->attributes = $attributes;
$this->spinning = $spinning;
// Build lookup maps once
if (self::$yoyoAttributeMap === null) {
self::$yoyoAttributeMap = array_flip(self::YOYO_ATTRIBUTES);
self::$htmxMethodMap = array_flip(self::HTMX_REQUEST_METHOD_ATTRIBUTES);
}
}
public function addComponentListeners($listeners = [])
{
$this->listeners = $listeners;
return $this;
}
public function addComponentProps($props = [])
{
$this->props = $props;
return $this;
}
public function withHistory($cacheHistory = false)
{
$this->withHistory = $cacheHistory;
return $this;
}
public function compile($html): string
{
if (! trim($html)) {
return $html;
}
// Add yoyo-finder marker attributes for XPath discovery
// (XPath cannot query attributes with colons)
// Combined regex handles both double and single quoted attributes
$prefix = self::YOYO_PREFIX;
$prefix_finder = self::YOYO_PREFIX_FINDER;
$html = preg_replace(
[
'/ '.$prefix.':(.*)="(.*)"/U',
'/ '.$prefix.':(.*)=\'(.*)\'/U',
],
[
" $prefix_finder $prefix:\$1=\"\$2\"",
" $prefix_finder $prefix:\$1='\$2'",
],
$html
);
// Convert non-ASCII characters to numeric HTML entities only when needed
if (preg_match('/[\x80-\xff]/', $html)) {
$html = mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, ~0], 'UTF-8');
}
$dom = new DOMDocument();
$internalErrors = libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_use_internal_errors($internalErrors);
// Reuse a single XPath instance throughout
$xpath = new DOMXPath($dom);
if (! ($node = $this->getComponentRootNode($xpath))) {
$html = $this->getOuterHTML($dom, $xpath);
unset($dom);
return $this->compile('<div>'.$html.'</div>');
}
$elements = $xpath->query('//form');
foreach ($elements as $element) {
if (! $element->hasAttribute(self::yoprefix('ignore'))) {
$this->addFormBehavior($element, $xpath);
}
}
// Prevent infinite loop with on 'load' event on root node with outerHTML swap
$this->removeOnLoadEventWhenSpinning($node);
$this->addComponentRootAttributes($node);
$this->addComponentChildrenAttributes($xpath);
// Cleanup
$node->removeAttribute(self::YOYO_PREFIX_FINDER);
$doOuterHtmlSwap = ! $this->elementHasAttributeWithValue($node, self::hxprefix('swap'), 'innerHTML');
if ($this->spinning && ! $doOuterHtmlSwap) {
$output = $this->getInnerHTML($dom, $xpath);
} else {
$output = $this->getOuterHTML($dom, $xpath);
}
return trim($output);
}
protected function addComponentRootAttributes($element)
{
if ($element->hasAttribute(self::yoprefix('ignore'))) {
$element->removeAttribute(self::yoprefix('ignore'));
return;
}
// Skip when component already compiled
if ($element->hasAttribute(self::yoprefix('name')) && $element->hasAttribute(self::hxprefix('vals'))) {
return;
}
$element->setAttribute(self::YOYO_PREFIX, '');
$element->setAttribute(self::YOYO_PREFIX_FINDER, '');
// Discard generated component ID and use hardcoded one if found
$id = $element->getAttribute('id');
if ($id !== '') {
$this->componentId = $id;
}
$this->addRequestMethodAttribute($element, true);
// Get default attributes
$attributes = $this->getComponentAttributes($this->componentId);
// Merge, or in some cases replace, defaults with existing attributes at the root node level
if (! $element->hasAttribute('id')) {
$element->setAttribute('id', $this->componentId);
}
foreach (['target', 'include'] as $attr) {
if ($value = $element->getAttribute(self::yoprefix($attr))) {
$attributes[$attr] = $value;
}
}
// Add yoyo extension attribute and merge existing extensions
if ($ext = $element->getAttribute(self::yoprefix('ext'))) {
$element->removeAttribute(self::yoprefix('ext'));
$attributes['ext'] .= ', '.$ext;
}
$class = $element->getAttribute('class');
$element->setAttribute('class', self::COMPONENT_WRAPPER_CLASS.($class ? ' '.$class : ''));
$element->setAttribute(self::yoprefix('name'), $this->name);
if ($this->withHistory) {
$element->setAttribute(self::yoprefix('history'), 1);
}
if ($trigger = $element->getAttribute(self::yoprefix('on'))) {
$attributes['on'] .= ', '.$trigger;
}
// Process variables
if ($vars = $element->getAttribute(self::yoprefix('vals'))) {
$element->removeAttribute(self::yoprefix('vals'));
$vars = YoyoHelpers::decode_vals($vars);
$attributes['vals'] = array_merge($attributes['vals'], $vars);
}
// Process invididual variables added through yoyo:val.key
$attributes['vals'] = array_merge($attributes['vals'] ?? [], $this->parseIndividualValAttributes($element));
// Process public props
if ($props = $element->getAttribute(self::yoprefix('props')) ?: []) {
$props = explode(',', str_replace(' ', '', $props));
$element->removeAttribute(self::yoprefix('props'));
}
$props = array_merge($props, $this->props, [
self::yoprefix('resolver'),
self::yoprefix('source'),
]);
$propsLookup = [];
foreach ($props as $prop) {
if (is_string($prop) && $prop !== '') {
$propsLookup[$prop] = true;
}
}
$variables = array_filter($this->variables, function ($key) use ($propsLookup) {
return isset($propsLookup[$key]);
}, ARRAY_FILTER_USE_KEY);
$attributes['vals'] = array_merge($attributes['vals'], $variables);
// Add all attributes
$attributes['vals'] = YoyoHelpers::encode_vals($attributes['vals']);
foreach ($attributes as $attr => $value) {
if (! $value) {
$value = $element->getAttribute(self::yoprefix($attr));
}
if ($value) {
$this->remapAndReplaceAttribute($element, $attr, $value);
}
}
}
protected function addComponentChildrenAttributes($xpath)
{
$elements = $xpath->query('//*[@'.self::YOYO_PREFIX.']|//*[@'.self::YOYO_PREFIX_FINDER.']');
$valPrefixLen = strlen(self::yoprefix('val').'.'); // "yoyo:val." = 9
foreach ($elements as $key => $element) {
// Skip the component root because it's processed separately
if ($key == 0) {
continue;
}
// Skip already-compiled nested components.
// Keep cleanup behavior consistent with the existing loop.
if ($element->hasAttribute(self::yoprefix('name')) && $element->hasAttribute(self::hxprefix('vals'))) {
$element->removeAttribute(self::YOYO_PREFIX);
$element->removeAttribute(self::YOYO_PREFIX_FINDER);
continue;
}
// Single pass over element attributes: categorize everything at once
// instead of 3 separate passes (addRequestMethodAttribute + yoyo scan + val scan)
$yoyoAttrs = [];
$valAttrs = [];
$valToRemove = [];
$hasHxMethod = false;
$yoyoMethod = null;
$yoyoMethodValue = null;
$hasYoyoMarker = false;
foreach ($element->attributes as $attr) {
$name = $attr->name;
if ($name === self::YOYO_PREFIX) {
$hasYoyoMarker = true;
continue;
}
// Check for existing hx-{method} — skip method assignment if found
if (str_starts_with($name, 'hx-') && isset(self::$htmxMethodMap[substr($name, 3)])) {
$hasHxMethod = true;
continue;
}
if (! str_starts_with($name, 'yoyo:')) {
continue;
}
$yoyoAttr = substr($name, 5);
// yoyo:val.{key} — collect for val processing
if (str_starts_with($yoyoAttr, 'val.')) {
$valKey = substr($name, $valPrefixLen);
$valAttrs[YoyoHelpers::camel($valKey, '-')] = YoyoHelpers::decode_val($attr->value);
$valToRemove[] = $name;
continue;
}
// yoyo:{method} — request method to remap
if (isset(self::$htmxMethodMap[$yoyoAttr])) {
$yoyoMethod = $yoyoAttr;
$yoyoMethodValue = $attr->value;
continue;
}
// yoyo:{attr} — collect for hx- remapping
if (isset(self::$yoyoAttributeMap[$yoyoAttr])) {
$yoyoAttrs[$yoyoAttr] = $attr->value;
}
}
// Handle request method
if (! $hasHxMethod) {
if ($yoyoMethod !== null) {
$element->removeAttribute(self::yoprefix($yoyoMethod));
$element->setAttribute(self::hxprefix($yoyoMethod), $yoyoMethodValue);
$this->checkForIdAttribute($element);
} elseif ($hasYoyoMarker) {
$element->setAttribute(self::hxprefix('get'), self::COMPONENT_DEFAULT_ACTION);
$this->checkForIdAttribute($element);
}
}
// Remap yoyo: attributes to hx-
foreach ($yoyoAttrs as $yoyoAttr => $value) {
$this->remapAndReplaceAttribute($element, $yoyoAttr, $value);
}
// Process val attributes
if ($valAttrs) {
foreach ($valToRemove as $name) {
$element->removeAttribute($name);
}
$element->setAttribute(self::hxprefix('vals'), YoyoHelpers::encode_vals($valAttrs));
}
// Cleanup
$element->removeAttribute(self::YOYO_PREFIX);
$element->removeAttribute(self::YOYO_PREFIX_FINDER);
}
}
protected function parseIndividualValAttributes($element)
{
$attributes = [];
$valPrefix = self::yoprefix('val').'.';
$valPrefixLen = strlen($valPrefix);
// Collect matching attributes first to avoid modifying
// the live DOMNamedNodeMap while iterating
$toRemove = [];
foreach ($element->attributes as $attr) {
$name = $attr->name;
if (! str_starts_with($name, $valPrefix)) {
continue;
}
$key = substr($name, $valPrefixLen);
$attributes[YoyoHelpers::camel($key, '-')] = YoyoHelpers::decode_val($attr->value);
$toRemove[] = $name;
}
foreach ($toRemove as $name) {
$element->removeAttribute($name);
}
return $attributes;
}
protected function removeOnLoadEventWhenSpinning($element)
{
if ($this->spinning && $element->hasAttribute(self::yoprefix('on'))) {
$on = $element->getAttribute(self::yoprefix('on'));
$events = explode(',', $on);
$events = array_filter($events, function ($event) {
return $event !== 'load';
});
$element->setAttribute(self::yoprefix('on'), implode(',', $events));
}
}
protected function addFormBehavior($element, $xpath = null)
{
if ($element->tagName == 'form' && ! $element->hasAttribute(self::yoprefix('on'))) {
$element->setAttribute(self::YOYO_PREFIX, '');
$element->setAttribute(self::yoprefix('on'), 'submit');
// If the form has an upload input, set the encoding to multipart/form-data
if ($xpath === null) {
$xpath = new DOMXPath($element->ownerDocument);
}
$inputs = $xpath->query('.//*[@type="file"]', $element);
if ($inputs->item(0)) {
$element->setAttribute(self::yoprefix('encoding'), 'multipart/form-data');
}
// If the form tag doesn't have a method set, set POST by default
foreach ($element->attributes as $attr) {
if (($parts = explode(':', $attr->name))[0] == self::YOYO_PREFIX && ! empty($parts[1])) {
if (isset(self::$htmxMethodMap[$parts[1]])) {
return;
}
}
}
$element->setAttribute(self::yoprefix('post'), self::COMPONENT_DEFAULT_ACTION);
}
}
protected function checkForIdAttribute($element)
{
if (! $element->hasAttribute('id')) {
$element->setAttribute('id', $this->componentId.'-'.$this->idCounter++);
}
}
protected function remapAndReplaceAttribute($element, $attr, $value)
{
if (str_starts_with($attr, self::YOYO_PREFIX) || isset(self::$yoyoAttributeMap[$attr])) {
$element->removeAttribute(self::yoprefix($attr));
$remappedAttr = self::YOYO_TO_HX_ATTRIBUTE_REMAP[$attr] ?? $attr;
$element->setAttribute(self::hxprefix($remappedAttr), $value);
} elseif ($value) {
$currentValue = $element->getAttribute($attr);
if ($currentValue) {
$value = $currentValue . ' ' . $value;
}
$element->setAttribute($attr, $value);
}
}
protected function addRequestMethodAttribute($element, $isRootNode = false)
{
// Skip if element already has an hx-[request] attribute (no yoyo:[request] which is processed below)
foreach (self::HTMX_REQUEST_METHOD_ATTRIBUTES as $attr) {
$hxattr = self::hxprefix($attr);
if ($element->hasAttribute($hxattr)) {
return;
}
}
// Look for existing method attribute, otherwise set 'get' as default
foreach (self::HTMX_REQUEST_METHOD_ATTRIBUTES as $attr) {
$yoattr = self::yoprefix($attr);
if ($value = $element->getAttribute($yoattr)) {
$element->removeAttribute($yoattr);
$element->setAttribute(self::hxprefix($attr), $value);
// Add an ID attribute for elements that trigger requests
if (! $isRootNode) {
$this->checkForIdAttribute($element);
}
return;
}
}
// Automatically add the default hx-get="render" request to component root nodes and any child with the `yoyo` attribute
if ($element->hasAttribute(self::YOYO_PREFIX)) {
$element->setAttribute(self::hxprefix('get'), self::COMPONENT_DEFAULT_ACTION);
if (! $isRootNode) {
// Ensure re-active tags have an ID to improve swapping
$this->checkForIdAttribute($element);
}
}
}
protected function getComponentAttributes($componentId): array
{
$attributes = array_merge(
array_fill_keys(self::YOYO_ATTRIBUTES, ''),
[
'ext' => 'yoyo',
// Adding refresh trigger to prevent default click trigger
'on' => 'refresh',
'target' => 'this',
'include' => "this",
'vals' => [self::yoprefix_value('id') => $componentId],
],
$this->attributes
);
// Include component listeners in trigger attribute
if (! empty($this->listeners)) {
$listeners = array_keys($this->listeners);
$attributes['on'] .= ','.implode(',', $listeners);
}
return $attributes;
}
public static function yoprefix($attr): string
{
return self::$yoprefixCache[$attr] ??= self::YOYO_PREFIX.':'.$attr;
}
public static function yoprefix_value($string): string
{
return self::YOYO_PREFIX.'-'.$string;
}
public static function hxprefix($attr): string
{
return self::$hxprefixCache[$attr] ??= self::HTMX_PREFIX.'-'.$attr;
}
protected function getComponentRootNode($xpath)
{
$count = 0;
foreach ($xpath->query('/html/body/*') as $node) {
if ($node->nodeType === XML_ELEMENT_NODE) {
$count++;
}
}
return $count == 1 ? $node : false;
}
protected static function elementHasAttributeWithValue($element, $attr, $value)
{
if (! $element->hasAttribute($attr)) {
return false;
}
$string = $element->getAttribute($attr);
return strpos($string, $value) !== false;
}
protected function getOuterHTML($dom, $xpath = null): string
{
$output = '';
if ($xpath === null) {
$xpath = new DOMXPath($dom);
}
$elements = $xpath->query("//*[starts-with(name(@*),'hx-')]");
foreach ($elements as $node) {
$setDefaultAction = true;
foreach (['get', 'post', 'put', 'delete', 'patch', 'ws', 'sse'] as $verb) {
if ($node->hasAttribute('hx-'.$verb)) {
$setDefaultAction = false;
break;
}
}
if ($setDefaultAction) {
$node->setAttribute('hx-get', 'render');
}
}
foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $node) {
$output .= $dom->saveHTML($node);
}
return $output;
}
protected function getInnerHTML($dom, $xpath = null): string
{
$output = '';
if ($xpath === null) {
$xpath = new DOMXPath($dom);
}
$elements = $xpath->query("//*[contains(concat(' ', normalize-space(@class), ' '), ' ".self::COMPONENT_WRAPPER_CLASS." ')]");
if (! $elements->length) {
return $this->getOuterHTML($dom, $xpath);
}
foreach ($elements->item(0)->childNodes as $node) {
$output .= $dom->saveHTML($node);
}
return $output;
}
}
================================================
FILE: src/yoyo/YoyoHelpers.php
================================================
<?php
namespace Clickfwd\Yoyo;
class YoyoHelpers
{
public static function encode_vals(array $vars): string
{
$adjusted = [];
foreach ($vars as $key => $val) {
$newKey = is_array($val) ? $key.'[]' : $key;
$adjusted[$newKey] = $val;
}
return json_encode($adjusted, JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);
}
public static function decode_vals(string $string): array
{
if (empty($string)) {
return [];
}
return json_decode((string) $string, true);
}
public static function decode_val(string $string)
{
$validJson = false;
$json = self::test_json($string, $validJson);
if ($validJson) {
return $json;
}
return $string === '0' ? 0 : $string;
}
public static function test_json($string, ?bool &$validJson = null)
{
$validJson = false;
if (is_array($string)) {
$validJson = true;
return $string;
}
if (! is_string($string)) {
return null;
}
$decoded = json_decode($string, true);
if (json_last_error() === JSON_ERROR_NONE) {
$validJson = true;
return $decoded;
}
// Retry after stripping slashes (handles WordPress magic quotes)
$unslashed = stripslashes($string);
if ($unslashed !== $string) {
$decoded = json_decode($unslashed, true);
if (json_last_err
gitextract_fxf4xndl/
├── .actrc
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── package.json
├── phpunit.xml
├── src/
│ ├── assets/
│ │ └── js/
│ │ └── yoyo.js
│ └── yoyo/
│ ├── AnonymousComponent.php
│ ├── Blade/
│ │ ├── Application.php
│ │ ├── CreateBladeViewFromString.php
│ │ ├── YoyoBladeCompilerEngine.php
│ │ ├── YoyoBladeDirectives.php
│ │ ├── YoyoServiceProvider.php
│ │ └── yoyo-view.blade.php
│ ├── ClassHelpers.php
│ ├── Component.php
│ ├── ComponentManager.php
│ ├── ComponentResolver.php
│ ├── Concerns/
│ │ ├── BrowserEvents.php
│ │ ├── Redirector.php
│ │ ├── ResponseHeaders.php
│ │ └── Singleton.php
│ ├── ContainerResolver.php
│ ├── Containers/
│ │ ├── IlluminateContainer.php
│ │ └── YoyoContainer.php
│ ├── Exceptions/
│ │ ├── BindingNotFoundException.php
│ │ ├── BypassRenderMethod.php
│ │ ├── ComponentMethodNotFound.php
│ │ ├── ComponentNotFound.php
│ │ ├── ContainerResolutionException.php
│ │ ├── FailedToRegisterComponent.php
│ │ ├── HttpException.php
│ │ ├── IncompleteComponentParamInRequest.php
│ │ ├── MissingComponentTemplate.php
│ │ ├── NonPublicComponentMethodCall.php
│ │ └── NotFoundHttpException.php
│ ├── Interfaces/
│ │ ├── RequestInterface.php
│ │ ├── ViewProviderInterface.php
│ │ └── YoyoContainerInterface.php
│ ├── InvocableComponentVariable.php
│ ├── QueryString.php
│ ├── Request.php
│ ├── Services/
│ │ ├── BrowserEventsService.php
│ │ ├── Configuration.php
│ │ ├── PageRedirectService.php
│ │ ├── Response.php
│ │ └── UrlStateManagerService.php
│ ├── Twig/
│ │ └── YoyoTwigExtension.php
│ ├── View.php
│ ├── ViewProviders/
│ │ ├── BaseViewProvider.php
│ │ ├── BladeViewProvider.php
│ │ ├── PhalconViewProvider.php
│ │ ├── TwigViewProvider.php
│ │ └── YoyoViewProvider.php
│ ├── Yoyo.php
│ ├── YoyoCompiler.php
│ ├── YoyoHelpers.php
│ ├── YoyoPhalconController.php
│ ├── YoyoPhalconServiceProvider.php
│ └── helpers.php
└── tests/
├── Benchmark/
│ ├── PipelineBenchmarkTest.php
│ ├── RealWorldBenchmarkTest.php
│ ├── YoyoCompilerBenchmarkTest.php
│ └── profile-compiler.php
├── Browser/
│ ├── BrowserServer.php
│ ├── Components/
│ │ ├── ActionButton.php
│ │ ├── Counter.php
│ │ ├── DeleteItem.php
│ │ ├── DispatchBystander.php
│ │ ├── DispatchListener.php
│ │ ├── FavoriteButton.php
│ │ ├── Form.php
│ │ ├── LiveSearch.php
│ │ ├── ModalTrigger.php
│ │ ├── MultiScreen.php
│ │ ├── NotificationBadge.php
│ │ ├── NullProp.php
│ │ ├── Pagination.php
│ │ ├── ResponseHeaders.php
│ │ ├── StatusDropdown.php
│ │ └── TodoList.php
│ ├── CounterTest.php
│ ├── CrossComponentEventsTest.php
│ ├── DispatchTest.php
│ ├── FormTest.php
│ ├── InfrastructureTest.php
│ ├── LiveSearchTest.php
│ ├── ModalTest.php
│ ├── MultiScreenTest.php
│ ├── NullPropTest.php
│ ├── PaginationTest.php
│ ├── ProductListTest.php
│ ├── ResponseHeadersTest.php
│ ├── SkipRenderTest.php
│ ├── TodoListTest.php
│ ├── bootstrap.php
│ └── server/
│ ├── index.php
│ ├── layout.php
│ ├── pages/
│ │ ├── counter.php
│ │ ├── dispatch.php
│ │ ├── events.php
│ │ ├── form.php
│ │ ├── index.php
│ │ ├── live-search.php
│ │ ├── modal.php
│ │ ├── multi-screen.php
│ │ ├── null-prop.php
│ │ ├── pagination.php
│ │ ├── product-list.php
│ │ ├── response-headers.php
│ │ ├── skip-render.php
│ │ └── todo-list.php
│ └── views/
│ ├── action-button.php
│ ├── counter.php
│ ├── delete-item.php
│ ├── dispatch-bystander.php
│ ├── dispatch-listener.php
│ ├── favorite-button.php
│ ├── form.php
│ ├── live-search.php
│ ├── modal-trigger.php
│ ├── multi-screen.php
│ ├── notification-badge.php
│ ├── null-prop.php
│ ├── pagination.php
│ ├── response-headers.php
│ ├── status-dropdown.php
│ └── todo-list.php
├── Feature/
│ ├── AnonymousComponentTest.php
│ ├── BladeTest.php
│ ├── ComponentLifecycleTest.php
│ ├── ComponentResolverTest.php
│ ├── DispatchEventTest.php
│ ├── DynamicComponentTest.php
│ ├── NamespacedComponentTest.php
│ ├── NestedComponentTest.php
│ ├── ResponseHeadersComponentTest.php
│ └── TwigTest.php
├── Helpers.php
├── HelpersBlade.php
├── HelpersTwig.php
├── InitYoyoContainer.php
├── Pest.php
├── Unit/
│ ├── BrowserEventsServiceTest.php
│ ├── BrowserEventsTest.php
│ ├── ClassHelpersTest.php
│ ├── ComponentResolverTest.php
│ ├── ComponentTest.php
│ ├── ConfigurationTest.php
│ ├── ContainerResolverTest.php
│ ├── ExceptionsTest.php
│ ├── IlluminateContainerTest.php
│ ├── InvocableComponentVariableTest.php
│ ├── PageRedirectServiceTest.php
│ ├── ProtectedMethodTest.php
│ ├── PushUrlStateTest.php
│ ├── QueryStringTest.php
│ ├── RegressionTest.php
│ ├── RequestTest.php
│ ├── ResponseHeadersTest.php
│ ├── SecurityTest.php
│ ├── UrlStateManagerServiceTest.php
│ ├── ViewTest.php
│ ├── YoyoCompileTest.php
│ ├── YoyoCompilerEdgeCasesTest.php
│ ├── YoyoContainerTest.php
│ ├── YoyoHelpersTest.php
│ └── YoyoViewProviderTest.php
├── app/
│ ├── Comment.php
│ ├── Post.php
│ ├── Resolvers/
│ │ ├── BladeComponentResolver.php
│ │ ├── CustomComponentResolver.php
│ │ └── TwigComponentResolver.php
│ ├── Yoyo/
│ │ ├── Abort.php
│ │ ├── Account/
│ │ │ └── Register.php
│ │ ├── ActionArguments.php
│ │ ├── ComponentWithComputedArgs.php
│ │ ├── ComponentWithEmit.php
│ │ ├── ComponentWithListeners.php
│ │ ├── ComponentWithRedirect.php
│ │ ├── ComponentWithResponseHeaders.php
│ │ ├── ComponentWithSwapModifiers.php
│ │ ├── ComponentWithTrait.php
│ │ ├── ComputedProperty.php
│ │ ├── ComputedPropertyCache.php
│ │ ├── Counter.php
│ │ ├── CounterDynamicProperties.php
│ │ ├── DependencyInjectionAction.php
│ │ ├── DependencyInjectionClassWithNamedArgumentMapping.php
│ │ ├── DispatchListener.php
│ │ ├── EmptyResponse.php
│ │ ├── EmptyResponseAndRemove.php
│ │ ├── ProtectedMethods.php
│ │ ├── Registered.php
│ │ ├── SetViewData.php
│ │ └── VariadicParameters.php
│ └── resources/
│ └── views/
│ ├── components/
│ │ └── select.blade.php
│ └── yoyo/
│ ├── account/
│ │ ├── login.blade.php
│ │ ├── login.php
│ │ ├── register.blade.php
│ │ └── register.php
│ ├── action-arguments.php
│ ├── child.blade.php
│ ├── child.php
│ ├── child.twig
│ ├── component-with-computed-args.php
│ ├── component-with-emit.php
│ ├── component-with-listeners.php
│ ├── component-with-redirect.php
│ ├── component-with-response-headers.php
│ ├── component-with-swap-modifiers.php
│ ├── component-with-trait.php
│ ├── computed-property-cache.php
│ ├── computed-property.php
│ ├── counter.php
│ ├── dependency-injection-action.php
│ ├── dependency-injection-class-with-named-argument-mapping.php
│ ├── dispatch-listener.php
│ ├── foo.blade.php
│ ├── foo.php
│ ├── foo.twig
│ ├── layout-a.blade.php
│ ├── layout-b.blade.php
│ ├── parent.blade.php
│ ├── parent.php
│ ├── parent.twig
│ ├── registered-anon.php
│ ├── registered.php
│ ├── set-view-data.php
│ └── variadic-parameters.php
├── app-another/
│ ├── Yoyo/
│ │ └── Counter.php
│ └── views/
│ ├── components/
│ │ └── input.blade.php
│ ├── counter.blade.php
│ ├── counter.php
│ ├── counter.twig
│ ├── foo.blade.php
│ ├── foo.php
│ └── foo.twig
├── compiled/
│ └── .gitkeep
└── responses/
├── action-arguments.html
├── computed-property-cache.html
├── computed-property.html
├── nested.blade.html
├── nested.html
└── nested.twig.html
SYMBOL INDEX (647 symbols across 111 files)
FILE: src/assets/js/yoyo.js
method config (line 21) | config(options) {
method on (line 26) | on(name, callback) {
method dispatch (line 32) | dispatch(eventName, params = null) {
method dispatchTo (line 37) | dispatchTo(componentName, eventName, params = null) {
method createNonExistentIdTarget (line 43) | createNonExistentIdTarget(targetId) {
method afterProcessNode (line 55) | afterProcessNode(evt) {
method bootstrapRequest (line 90) | bootstrapRequest(evt) {
method processRedirectHeader (line 114) | processRedirectHeader(xhr) {
method processEmitEvents (line 122) | processEmitEvents(elt, events) {
method processBrowserEvents (line 133) | processBrowserEvents(events) {
method beforeRequestActions (line 146) | beforeRequestActions(elt) {
method afterOnLoadActions (line 151) | afterOnLoadActions(evt) {
method afterSettleActions (line 188) | afterSettleActions(evt) {
function getActionAndParseArguments (line 271) | function getActionAndParseArguments(detail) {
function parseArg (line 289) | function parseArg(token) {
function isComponent (line 306) | function isComponent(elt) {
function getComponent (line 310) | function getComponent(elt) {
function getAllcomponents (line 318) | function getAllcomponents() {
function getComponentById (line 322) | function getComponentById(componentId) {
function getComponentName (line 330) | function getComponentName(component) {
function getComponentFingerprint (line 334) | function getComponentFingerprint(component) {
function getComponentsByName (line 340) | function getComponentsByName(name) {
function getComponentIndex (line 347) | function getComponentIndex(component) {
function getAncestorcomponents (line 353) | function getAncestorcomponents(selector) {
function shouldTriggerYoyoEvent (line 367) | function shouldTriggerYoyoEvent(elt, eventName) {
function eventsMiddleware (line 383) | function eventsMiddleware(evt) {
function addEmittedEventParametersToListenerComponent (line 408) | function addEmittedEventParametersToListenerComponent(
function triggerServerEmittedEvent (line 428) | function triggerServerEmittedEvent(elt, event) {
function removeEventListenerData (line 476) | function removeEventListenerData(component) {
function spinningStart (line 484) | function spinningStart(component) {
function spinningStop (line 529) | function spinningStop(component) {
function initializeComponentSpinners (line 535) | function initializeComponentSpinners(component) {
function checkSpinnerInitialized (line 557) | function checkSpinnerInitialized(componentId, action) {
function addActionSpinner (line 570) | function addActionSpinner(componentId, action, directive) {
function addGenericSpinner (line 575) | function addGenericSpinner(componentId, directive) {
function doAndSetCallbackOnElToUndo (line 581) | function doAndSetCallbackOnElToUndo(
function componentAlreadyInCurrentHistoryState (line 603) | function componentAlreadyInCurrentHistoryState(component) {
function updateState (line 619) | function updateState(
function replaceStateByComponentIndex (line 656) | function replaceStateByComponentIndex(newState) {
function restoreComponentStateFromHistory (line 675) | function restoreComponentStateFromHistory(state) {
function componentCopyYoyoDataFromTo (line 708) | function componentCopyYoyoDataFromTo(from, to) {
function componentAddYoyoData (line 713) | function componentAddYoyoData(component, data) {
function walk (line 719) | function walk(el, callback) {
function extractModifiersAndValue (line 731) | function extractModifiersAndValue(elt, type) {
FILE: src/yoyo/AnonymousComponent.php
class AnonymousComponent (line 5) | class AnonymousComponent extends Component
method render (line 7) | public function render()
FILE: src/yoyo/Blade/Application.php
class Application (line 8) | class Application extends Container
method getNamespace (line 12) | public function getNamespace()
method terminating (line 17) | public function terminating(Closure $callback)
method terminate (line 24) | public function terminate()
FILE: src/yoyo/Blade/CreateBladeViewFromString.php
class CreateBladeViewFromString (line 7) | class CreateBladeViewFromString extends Component
method __invoke (line 9) | public function __invoke($view, $contents)
method render (line 14) | public function render()
FILE: src/yoyo/Blade/YoyoBladeCompilerEngine.php
class YoyoBladeCompilerEngine (line 7) | class YoyoBladeCompilerEngine extends LaravelCompilerEngine
method startYoyoRendering (line 13) | public function startYoyoRendering($component)
method stopYoyoRendering (line 20) | public function stopYoyoRendering()
method evaluatePath (line 28) | protected function evaluatePath($__path, $__data)
FILE: src/yoyo/Blade/YoyoBladeDirectives.php
class YoyoBladeDirectives (line 5) | class YoyoBladeDirectives
method __construct (line 7) | public function __construct($blade)
method yoyo (line 28) | public function yoyo($expression)
method yoyo_scripts (line 42) | public function yoyo_scripts()
method spinning (line 47) | public function spinning($expression)
method endspinning (line 54) | public function endspinning()
method emit (line 59) | public function emit($expression)
method emitTo (line 64) | public function emitTo($expression)
method emitToWithSelector (line 69) | public function emitToWithSelector($expression)
method emitSelf (line 74) | public function emitSelf($expression)
method emitUp (line 79) | public function emitUp($expression)
FILE: src/yoyo/Blade/YoyoServiceProvider.php
class YoyoServiceProvider (line 7) | class YoyoServiceProvider extends ServiceProvider
method boot (line 9) | public function boot()
method registerViewCompilerEngine (line 15) | protected function registerViewCompilerEngine()
method registerBladeDirectives (line 22) | protected function registerBladeDirectives()
FILE: src/yoyo/ClassHelpers.php
class ClassHelpers (line 8) | class ClassHelpers
method getDefaultPublicVars (line 20) | public static function getDefaultPublicVars($instance, $baseClass = null)
method getPublicVars (line 38) | public static function getPublicVars($instance, $baseClass = null)
method getPublicProperties (line 55) | public static function getPublicProperties($instance, $baseClass = null)
method getPublicMethods (line 81) | public static function getPublicMethods($instance, $exceptions = [])
method methodIsPrivate (line 103) | public static function methodIsPrivate($instance, $method)
method classImplementsInterface (line 110) | public static function classImplementsInterface($name, $instance)
method classUsesRecursive (line 120) | public static function classUsesRecursive($class)
method traitUsesRecursive (line 144) | public static function traitUsesRecursive($trait)
method classBasename (line 158) | public static function classBasename($class)
method getMethodParameterNames (line 165) | public static function getMethodParameterNames($class, $method)
method methodHasVariadicParameter (line 182) | public static function methodHasVariadicParameter($class, $method)
method getMethodParametersWithTypes (line 201) | public static function getMethodParametersWithTypes($class, $method)
method flushCache (line 239) | public static function flushCache(): void
FILE: src/yoyo/Component.php
class Component (line 15) | abstract class Component
method __construct (line 55) | public function __construct(ComponentResolver $resolver, string $id, s...
method spinning (line 68) | public function spinning(bool $spinning)
method boot (line 75) | public function boot(array $variables, array $attributes)
method getName (line 97) | public function getName()
method getDynamicProperties (line 102) | public function getDynamicProperties()
method getInitialAttributes (line 107) | public function getInitialAttributes()
method getVariables (line 114) | public function getVariables()
method getQueryParam (line 119) | public function getQueryParam($key, $default = null)
method getQueryString (line 124) | public function getQueryString()
method getProps (line 129) | public function getProps()
method setAction (line 134) | public function setAction($action)
method actionMatches (line 139) | public function actionMatches($action)
method getListeners (line 148) | public function getListeners()
method getComponentId (line 163) | public function getComponentId()
method set (line 168) | public function set($key, $value = null)
method render (line 179) | public function render()
method addSwapModifiers (line 188) | public function addSwapModifiers($modifier)
method skipRender (line 195) | public function skipRender()
method skipRenderAndRemove (line 203) | public function skipRenderAndRemove($modifier = 'swap:1s')
method view (line 215) | protected function view($template, $vars = []): ViewProviderInterface
method createViewFromString (line 234) | public function createViewFromString($content): string
method viewVars (line 247) | protected function viewVars(): array
method createVariableFromMethod (line 260) | protected function createVariableFromMethod(ReflectionMethod $method)
method createInvocableVariable (line 267) | protected function createInvocableVariable(string $method)
method __call (line 277) | public function __call(string $name, array $arguments)
method __get (line 294) | public function __get($property)
method forgetComputed (line 309) | public function forgetComputed($key = null)
method forgetComputedWithArgs (line 324) | public function forgetComputedWithArgs($name, ...$args)
method makeCacheKey (line 329) | protected static function makeCacheKey($name, $arguments)
FILE: src/yoyo/ComponentManager.php
class ComponentManager (line 9) | class ComponentManager
method __construct (line 23) | public function __construct($resolver, $request, $spinning)
method getDefaultPublicVars (line 32) | public function getDefaultPublicVars()
method getPublicVars (line 37) | public function getPublicVars()
method getQueryString (line 54) | public function getQueryString()
method getProps (line 65) | public function getProps()
method getListeners (line 70) | public function getListeners()
method process (line 75) | public function process($id, $name, $action, $variables, $attributes)
method isAnonymousComponent (line 88) | public function isAnonymousComponent(): bool
method isDynamicComponent (line 93) | public function isDynamicComponent(): bool
method processDynamicComponent (line 98) | private function processDynamicComponent($action, $variables = [], $at...
method parseActionArguments (line 277) | private function parseActionArguments()
method processAnonymousComponent (line 284) | private function processAnonymousComponent($variables = [], $attribute...
method getComponentInstance (line 293) | public function getComponentInstance()
FILE: src/yoyo/ComponentResolver.php
class ComponentResolver (line 9) | class ComponentResolver
method __invoke (line 21) | public function __invoke(YoyoContainerInterface $container, array $reg...
method getName (line 32) | public function getName()
method resolving (line 37) | public function resolving($id, $name, $variables)
method resolveComponent (line 41) | public function resolveComponent($id, $name, $variables): ?Component
method resolveDynamic (line 52) | public function resolveDynamic($id, $name): ?Component
method resolveAnonymous (line 91) | public function resolveAnonymous($id, $name): ?Component
method dotNotationToClass (line 110) | public function dotNotationToClass($name)
method resolveViewProvider (line 117) | public function resolveViewProvider(): ViewProviderInterface
FILE: src/yoyo/Concerns/BrowserEvents.php
type BrowserEvents (line 7) | trait BrowserEvents
method emit (line 9) | public function emit($event, ...$params)
method emitTo (line 14) | public function emitTo($target, $event, ...$params)
method emitToWithSelector (line 19) | public function emitToWithSelector($target, $event, ...$params)
method emitSelf (line 24) | public function emitSelf($event, ...$params)
method emitUp (line 29) | public function emitUp($event, ...$params)
method dispatchBrowserEvent (line 34) | public function dispatchBrowserEvent($event, $params = [])
FILE: src/yoyo/Concerns/Redirector.php
type Redirector (line 5) | trait Redirector
method redirect (line 9) | public function redirect($url)
FILE: src/yoyo/Concerns/ResponseHeaders.php
type ResponseHeaders (line 5) | trait ResponseHeaders
method location (line 7) | public function location($path)
method pushUrl (line 14) | public function pushUrl($url)
method redirect (line 21) | public function redirect($url)
method refresh (line 28) | public function refresh()
method replaceUrl (line 35) | public function replaceUrl($url)
method reswap (line 42) | public function reswap($swap)
method reselect (line 49) | public function reselect($selector)
method retarget (line 56) | public function retarget($selector)
method trigger (line 63) | public function trigger($event)
method triggerAfterSwap (line 70) | public function triggerAfterSwap($event)
method triggerAfterSettle (line 77) | public function triggerAfterSettle($event)
FILE: src/yoyo/Concerns/Singleton.php
type Singleton (line 5) | trait Singleton
method getInstance (line 18) | final public static function getInstance(...$params)
method __clone (line 32) | public function __clone()
method __wakeup (line 41) | public function __wakeup()
FILE: src/yoyo/ContainerResolver.php
class ContainerResolver (line 10) | class ContainerResolver
method setPreferred (line 14) | public static function setPreferred(?YoyoContainerInterface $container)
method getPreferred (line 19) | public static function getPreferred()
method resolve (line 24) | public static function resolve(): YoyoContainerInterface
FILE: src/yoyo/Containers/IlluminateContainer.php
class IlluminateContainer (line 9) | class IlluminateContainer implements YoyoContainerInterface
method getInstance (line 15) | public static function getInstance()
method __construct (line 20) | public function __construct(Container $container)
method get (line 25) | public function get(string $id)
method has (line 30) | public function has(string $id): bool
method set (line 35) | public function set(string $id, $value)
method make (line 46) | public function make(string $class, array $args = [])
method call (line 51) | public function call($method, array $args = [])
FILE: src/yoyo/Containers/YoyoContainer.php
class YoyoContainer (line 9) | class YoyoContainer implements YoyoContainerInterface
method getInstance (line 15) | public static function getInstance()
method get (line 20) | public function get(string $id)
method has (line 39) | public function has(string $id): bool
method set (line 44) | public function set(string $id, $value)
method make (line 51) | public function make(string $class, array $args = [])
method call (line 61) | public function call(callable $method, array $args = [])
method extractArgs (line 70) | protected function extractArgs($class, $method, $arguments)
method isResolvableType (line 111) | protected function isResolvableType(\ReflectionParameter $parameter): ...
FILE: src/yoyo/Exceptions/BindingNotFoundException.php
class BindingNotFoundException (line 7) | class BindingNotFoundException extends \Exception implements NotFoundExc...
method __construct (line 9) | public function __construct(string $message, ?\Throwable $previous = n...
FILE: src/yoyo/Exceptions/BypassRenderMethod.php
class BypassRenderMethod (line 5) | class BypassRenderMethod extends \Exception
method __construct (line 7) | public function __construct($statusCode)
FILE: src/yoyo/Exceptions/ComponentMethodNotFound.php
class ComponentMethodNotFound (line 5) | class ComponentMethodNotFound extends \Exception
method __construct (line 7) | public function __construct($component, $method)
FILE: src/yoyo/Exceptions/ComponentNotFound.php
class ComponentNotFound (line 5) | class ComponentNotFound extends \Exception
method __construct (line 7) | public function __construct($alias)
FILE: src/yoyo/Exceptions/ContainerResolutionException.php
class ContainerResolutionException (line 7) | class ContainerResolutionException extends \Exception implements Contain...
method __construct (line 9) | public function __construct(string $message, ?\Throwable $previous = n...
FILE: src/yoyo/Exceptions/FailedToRegisterComponent.php
class FailedToRegisterComponent (line 5) | class FailedToRegisterComponent extends \Exception
method __construct (line 7) | public function __construct($alias, $componentClassName)
FILE: src/yoyo/Exceptions/HttpException.php
class HttpException (line 5) | class HttpException extends \Exception
method __construct (line 11) | public function __construct(int $statusCode, ?string $message = '', ar...
method getStatusCode (line 20) | public function getStatusCode()
method getHeaders (line 25) | public function getHeaders()
FILE: src/yoyo/Exceptions/IncompleteComponentParamInRequest.php
class IncompleteComponentParamInRequest (line 5) | class IncompleteComponentParamInRequest extends \Exception
method __construct (line 7) | public function __construct()
FILE: src/yoyo/Exceptions/MissingComponentTemplate.php
class MissingComponentTemplate (line 5) | class MissingComponentTemplate extends \Exception
method __construct (line 7) | public function __construct($template, $componentName)
FILE: src/yoyo/Exceptions/NonPublicComponentMethodCall.php
class NonPublicComponentMethodCall (line 5) | class NonPublicComponentMethodCall extends \Exception
method __construct (line 7) | public function __construct($componentName, $method)
FILE: src/yoyo/Exceptions/NotFoundHttpException.php
class NotFoundHttpException (line 5) | class NotFoundHttpException extends HttpException
method __construct (line 7) | public function __construct(?string $message = '', array $headers = [])
FILE: src/yoyo/Interfaces/RequestInterface.php
type RequestInterface (line 5) | interface RequestInterface
method all (line 7) | public function all();
method except (line 9) | public function except($keys);
method get (line 11) | public function get($key, $default = null);
method drop (line 13) | public function drop($key);
method method (line 15) | public function method();
method fullUrl (line 17) | public function fullUrl();
method isYoyoRequest (line 19) | public function isYoyoRequest();
method windUp (line 21) | public function windUp();
method triggerId (line 23) | public function triggerId();
FILE: src/yoyo/Interfaces/ViewProviderInterface.php
type ViewProviderInterface (line 5) | interface ViewProviderInterface
method __construct (line 9) | public function __construct($view);
method render (line 11) | public function render($template, $vars = []): self;
method makeFromString (line 13) | public function makeFromString($content, $vars = []): string;
method exists (line 15) | public function exists($template): bool;
method getProviderInstance (line 17) | public function getProviderInstance();
method startYoyoRendering (line 19) | public function startYoyoRendering($component): void;
method stopYoyoRendering (line 21) | public function stopYoyoRendering(): void;
FILE: src/yoyo/Interfaces/YoyoContainerInterface.php
type YoyoContainerInterface (line 8) | interface YoyoContainerInterface extends ContainerInterface
method set (line 17) | public function set(string $id, $value);
method make (line 26) | public function make(string $class, array $args = []);
method call (line 35) | public function call(callable $method, array $args = []);
FILE: src/yoyo/InvocableComponentVariable.php
class InvocableComponentVariable (line 7) | class InvocableComponentVariable
method __construct (line 11) | public function __construct(Closure $callable)
method __get (line 16) | public function __get($key)
method __call (line 21) | public function __call($method, $parameters)
method __invoke (line 26) | public function __invoke()
method __toString (line 31) | public function __toString()
FILE: src/yoyo/QueryString.php
class QueryString (line 7) | class QueryString
method __construct (line 17) | public function __construct($defaults, $new, $keys)
method getQueryParams (line 31) | public function getQueryParams()
method getPageQueryParams (line 45) | public function getPageQueryParams()
FILE: src/yoyo/Request.php
class Request (line 7) | class Request implements RequestInterface
method __construct (line 17) | public function __construct()
method mock (line 24) | public function mock($request, $server)
method reset (line 35) | public function reset()
method all (line 44) | public function all()
method except (line 62) | public function except($keys)
method only (line 81) | public function only($keys)
method get (line 86) | public function get($key, $default = null)
method startsWith (line 104) | public function startsWith($prefix)
method set (line 117) | public function set($key, $value)
method merge (line 126) | public function merge($data)
method drop (line 135) | public function drop($key)
method method (line 140) | public function method()
method fullUrl (line 145) | public function fullUrl()
method isYoyoRequest (line 168) | public function isYoyoRequest()
method windUp (line 173) | public function windUp()
method triggerId (line 178) | public function triggerId()
method triggerName (line 183) | public function triggerName()
method header (line 188) | public function header($name)
FILE: src/yoyo/Services/BrowserEventsService.php
class BrowserEventsService (line 8) | class BrowserEventsService
method __construct (line 20) | public function __construct()
method emit (line 27) | public function emit($event, ...$params)
method emitTo (line 32) | public function emitTo($target, $event, ...$params)
method emitToWithSelector (line 37) | public function emitToWithSelector($target, $event, ...$params)
method emitSelf (line 42) | public function emitSelf($event, ...$params)
method emitUp (line 49) | public function emitUp($event, ...$params)
method queue (line 57) | public function queue($event, $params, $selector = null, $component = ...
method dispatchBrowserEvent (line 66) | public function dispatchBrowserEvent($event, $params = [])
method dispatch (line 71) | public function dispatch()
method getComponentNameFromRequest (line 78) | protected function getComponentNameFromRequest()
FILE: src/yoyo/Services/Configuration.php
class Configuration (line 7) | class Configuration
method __construct (line 44) | public function __construct($options)
method get (line 57) | public static function get($key, $default = null)
method scripts (line 62) | public static function scripts($return = false)
method styles (line 67) | public static function styles()
method htmxSrc (line 72) | public static function htmxSrc(): string
method yoyoSrc (line 81) | public static function yoyoSrc(): string
method javascriptAssets (line 86) | public static function javascriptAssets(): string
method javascriptInitCode (line 99) | public static function javascriptInitCode($includeScriptTag = true): s...
method cssStyle (line 119) | public static function cssStyle($includeStyleTag = true)
method minify (line 134) | protected static function minify($string)
FILE: src/yoyo/Services/PageRedirectService.php
class PageRedirectService (line 7) | class PageRedirectService
method __construct (line 13) | public function __construct()
method redirect (line 18) | public function redirect($url)
FILE: src/yoyo/Services/Response.php
class Response (line 7) | class Response
method __construct (line 16) | public function __construct()
method header (line 20) | public function header($name, $value)
method status (line 31) | public function status($statusCode)
method send (line 38) | public function send(string $content = ''): string
method setHeaders (line 55) | public function setHeaders($headers)
method getHeaders (line 62) | public function getHeaders()
method getStatusCode (line 67) | public function getStatusCode()
FILE: src/yoyo/Services/UrlStateManagerService.php
class UrlStateManagerService (line 7) | class UrlStateManagerService
method __construct (line 13) | public function __construct()
method pushState (line 20) | public function pushState($queryParams)
FILE: src/yoyo/Twig/YoyoTwigExtension.php
class YoyoTwigExtension (line 14) | class YoyoTwigExtension extends AbstractExtension implements GlobalsInte...
method getFunctions (line 16) | public function getFunctions()
method getGlobals (line 29) | public function getGlobals(): array
method yoyo_scripts (line 35) | private function yoyo_scripts()
method yoyo (line 42) | private function yoyo()
method emit (line 55) | private function emit()
method emitTo (line 62) | private function emitTo()
method emitToWithSelector (line 69) | private function emitToWithSelector()
method emitSelf (line 76) | private function emitSelf()
method emitUp (line 83) | private function emitUp()
method raw (line 90) | private static function raw($string)
FILE: src/yoyo/View.php
class View (line 8) | class View
method __construct (line 18) | public function __construct($paths)
method __call (line 29) | public function __call($name, $args)
method startYoyoRendering (line 34) | public function startYoyoRendering($component)
method render (line 41) | public function render($name, $vars = []): string
method makeFromString (line 55) | public function makeFromString($content, $vars = []): string
method exists (line 60) | public function exists($name)
method addLocation (line 73) | public function addLocation($location)
method prependLocation (line 78) | public function prependLocation($location)
method findInPaths (line 83) | protected function findInPaths($name, $paths)
method findNamespacedView (line 96) | protected function findNamespacedView($name)
method parseNamespaceSegments (line 103) | protected function parseNamespaceSegments($name)
method addNamespace (line 118) | public function addNamespace($namespace, $hints)
method prependNamespace (line 129) | public function prependNamespace($namespace, $hints)
method hasHintInformation (line 140) | public function hasHintInformation($name)
method resolvePath (line 145) | protected function resolvePath($path)
FILE: src/yoyo/ViewProviders/BaseViewProvider.php
class BaseViewProvider (line 5) | abstract class BaseViewProvider
method getProviderInstance (line 9) | public function getProviderInstance()
FILE: src/yoyo/ViewProviders/BladeViewProvider.php
class BladeViewProvider (line 7) | class BladeViewProvider extends BaseViewProvider implements ViewProvider...
method __construct (line 17) | public function __construct($view)
method startYoyoRendering (line 22) | public function startYoyoRendering($component): void
method stopYoyoRendering (line 29) | public function stopYoyoRendering(): void
method render (line 34) | public function render($template, $vars = []): ViewProviderInterface
method makeFromString (line 43) | public function makeFromString($content, $vars = []): string
method exists (line 50) | public function exists($template): bool
method getFinder (line 55) | public function getFinder()
method addNamespace (line 60) | public function addNamespace($namespace, $hints)
method prependNamespace (line 67) | public function prependNamespace($namespace, $hints)
method addLocation (line 74) | public function addLocation($location)
method prependLocation (line 81) | public function prependLocation($location)
method __call (line 88) | public function __call(string $method, array $params)
method __toString (line 93) | public function __toString()
FILE: src/yoyo/ViewProviders/PhalconViewProvider.php
class PhalconViewProvider (line 7) | class PhalconViewProvider extends BaseViewProvider implements ViewProvid...
method __construct (line 17) | public function __construct($view)
method exists (line 22) | public function exists($view): bool
method render (line 27) | public function render($template, $vars = []): ViewProviderInterface
method setViewExtention (line 35) | public function setViewExtention($viewExtention)
method makeFromString (line 42) | public function makeFromString($content, $vars = []): string
method startYoyoRendering (line 51) | public function startYoyoRendering($component): void
method stopYoyoRendering (line 55) | public function stopYoyoRendering(): void
method __toString (line 59) | public function __toString()
FILE: src/yoyo/ViewProviders/TwigViewProvider.php
class TwigViewProvider (line 7) | class TwigViewProvider extends BaseViewProvider implements ViewProviderI...
method __construct (line 19) | public function __construct($view)
method startYoyoRendering (line 24) | public function startYoyoRendering($component): void
method stopYoyoRendering (line 29) | public function stopYoyoRendering(): void
method normalizeName (line 34) | public function normalizeName($template)
method render (line 45) | public function render($template, $vars = []): ViewProviderInterface
method makeFromString (line 54) | public function makeFromString($content, $vars = []): string
method exists (line 61) | public function exists($template): bool
method getLoader (line 68) | public function getLoader()
method addNamespace (line 73) | public function addNamespace($namespace, $path)
method prependNamespace (line 80) | public function prependNamespace($namespace, $path)
method addLocation (line 87) | public function addLocation($location)
method prependLocation (line 94) | public function prependLocation($location)
method __call (line 101) | public function __call(string $method, array $params)
method __toString (line 106) | public function __toString()
FILE: src/yoyo/ViewProviders/YoyoViewProvider.php
class YoyoViewProvider (line 9) | class YoyoViewProvider extends BaseViewProvider implements ViewProviderI...
method __construct (line 17) | public function __construct($view)
method startYoyoRendering (line 22) | public function startYoyoRendering($component): void
method stopYoyoRendering (line 27) | public function stopYoyoRendering(): void
method render (line 32) | public function render($name, $vars = []): ViewProviderInterface
method makeFromString (line 41) | public function makeFromString($content, $vars = []): string
method exists (line 46) | public function exists($name): bool
method addNamespace (line 55) | public function addNamespace($namespace, $hints)
method prependNamespace (line 62) | public function prependNamespace($namespace, $hints)
method addLocation (line 69) | public function addLocation($location)
method prependLocation (line 76) | public function prependLocation($location)
method __call (line 83) | public function __call(string $method, array $params)
method __toString (line 88) | public function __toString()
FILE: src/yoyo/Yoyo.php
class Yoyo (line 16) | class Yoyo
method __construct (line 38) | public function __construct(?YoyoContainerInterface $container = null)
method getInstance (line 47) | public static function getInstance()
method bindRequest (line 52) | public function bindRequest(RequestInterface $request)
method request (line 57) | public static function request()
method configure (line 66) | public function configure($options): void
method container (line 71) | public static function container()
method getComponentId (line 76) | public function getComponentId($attributes): string
method getComponentResolver (line 90) | private function getComponentResolver()
method registerViewProvider (line 108) | public function registerViewProvider($name, $provider = null)
method registerViewProviders (line 118) | public function registerViewProviders($providers)
method getViewProvider (line 125) | public static function getViewProvider($name = 'default')
method registerComponentResolver (line 130) | public function registerComponentResolver($resolver)
method componentNamespace (line 150) | public static function componentNamespace(string $namespace, $class): ...
method registerComponent (line 157) | public static function registerComponent($name, $class = null): void
method registerComponents (line 162) | public static function registerComponents($components): void
method abort (line 173) | public static function abort($code, $message = '', array $headers = [])
method mount (line 182) | public function mount($name, $variables = [], $attributes = [], $actio...
method action (line 199) | public function action($action): self
method actionArgs (line 206) | public function actionArgs(...$args)
method render (line 216) | public function render(): string
method refresh (line 224) | public function refresh(): string
method update (line 231) | public function update(): string
method parseUpdateRequest (line 238) | protected function parseUpdateRequest()
method output (line 255) | public function output($spinning = false)
method compile (line 351) | public function compile($componentType, $html, $spinning = null, $vari...
method is_spinning (line 369) | private function is_spinning(): bool
FILE: src/yoyo/YoyoCompiler.php
class YoyoCompiler (line 8) | class YoyoCompiler
method __construct (line 93) | public function __construct($componentType, $componentId, $name, $vari...
method addComponentListeners (line 114) | public function addComponentListeners($listeners = [])
method addComponentProps (line 121) | public function addComponentProps($props = [])
method withHistory (line 128) | public function withHistory($cacheHistory = false)
method compile (line 135) | public function compile($html): string
method addComponentRootAttributes (line 213) | protected function addComponentRootAttributes($element)
method addComponentChildrenAttributes (line 330) | protected function addComponentChildrenAttributes($xpath)
method parseIndividualValAttributes (line 431) | protected function parseIndividualValAttributes($element)
method removeOnLoadEventWhenSpinning (line 459) | protected function removeOnLoadEventWhenSpinning($element)
method addFormBehavior (line 474) | protected function addFormBehavior($element, $xpath = null)
method checkForIdAttribute (line 507) | protected function checkForIdAttribute($element)
method remapAndReplaceAttribute (line 514) | protected function remapAndReplaceAttribute($element, $attr, $value)
method addRequestMethodAttribute (line 529) | protected function addRequestMethodAttribute($element, $isRootNode = f...
method getComponentAttributes (line 569) | protected function getComponentAttributes($componentId): array
method yoprefix (line 595) | public static function yoprefix($attr): string
method yoprefix_value (line 600) | public static function yoprefix_value($string): string
method hxprefix (line 605) | public static function hxprefix($attr): string
method getComponentRootNode (line 610) | protected function getComponentRootNode($xpath)
method elementHasAttributeWithValue (line 623) | protected static function elementHasAttributeWithValue($element, $attr...
method getOuterHTML (line 634) | protected function getOuterHTML($dom, $xpath = null): string
method getInnerHTML (line 667) | protected function getInnerHTML($dom, $xpath = null): string
FILE: src/yoyo/YoyoHelpers.php
class YoyoHelpers (line 5) | class YoyoHelpers
method encode_vals (line 7) | public static function encode_vals(array $vars): string
method decode_vals (line 18) | public static function decode_vals(string $string): array
method decode_val (line 27) | public static function decode_val(string $string)
method test_json (line 39) | public static function test_json($string, ?bool &$validJson = null)
method studly (line 77) | public static function studly($str, $delimiter = ['-', '_'])
method camel (line 84) | public static function camel($str, $delimiter = ['-', '_'])
method snake (line 89) | public static function snake($str, $delimiter = '_')
method randString (line 99) | public static function randString($length = 8)
method removeEmptyValues (line 111) | public static function removeEmptyValues(array &$array)
FILE: src/yoyo/YoyoPhalconController.php
class YoyoPhalconController (line 7) | class YoyoPhalconController extends Controller
method handleAction (line 9) | public function handleAction()
FILE: src/yoyo/YoyoPhalconServiceProvider.php
class YoyoPhalconServiceProvider (line 10) | class YoyoPhalconServiceProvider implements ServiceProviderInterface
method setYoyoConfig (line 16) | public function setYoyoConfig($yoyoConfig)
method setViewExtention (line 23) | public function setViewExtention($viewExtention)
method register (line 30) | public function register(DiInterface $di): void
FILE: src/yoyo/helpers.php
function yoyo_render (line 9) | function yoyo_render($name, $variables = [], $attributes = []): string
function yoyo_scripts (line 17) | function yoyo_scripts($return = false)
function yoyo_styles (line 26) | function yoyo_styles($return = false)
function abort (line 35) | function abort($code, $message = '', array $headers = [])
function abort_if (line 40) | function abort_if($boolean, $code, $message = '', array $headers = [])
function abort_unless (line 47) | function abort_unless($boolean, $code, $message = '', array $headers = [])
function encode_vals (line 54) | function encode_vals($vals)
function is_spinning (line 59) | function is_spinning($expression = null)
function not_spinning (line 74) | function not_spinning($expression = null)
FILE: tests/Benchmark/PipelineBenchmarkTest.php
function benchmark (line 22) | function benchmark(string $label, int $iterations, Closure $fn): array
function createComponentInstance (line 48) | function createComponentInstance(string $class = Counter::class, string ...
FILE: tests/Benchmark/RealWorldBenchmarkTest.php
function bench_run (line 10) | function bench_run(string $label, int $iterations, Closure $fn): array
function childComponent (line 31) | function childComponent(string $name, int $id, string $innerHtml, array ...
function buildListingList (line 75) | function buildListingList(int $rows): string
function buildAdminTable (line 189) | function buildAdminTable(int $rows): string
FILE: tests/Benchmark/YoyoCompilerBenchmarkTest.php
function bench (line 11) | function bench(string $label, int $iterations, Closure $fn): array
FILE: tests/Benchmark/profile-compiler.php
function bench (line 18) | function bench(string $label, int $iterations, Closure $fn): array
function buildHtml (line 39) | function buildHtml(int $rows): string
function buildTodoHtml (line 51) | function buildTodoHtml(): string
function profilePhases (line 71) | function profilePhases(string $html, int $iters): array
function profileScaling (line 201) | function profileScaling(): array
function phaseBar (line 247) | function phaseBar(array $profile, array $colors): string
function phaseTable (line 278) | function phaseTable(array $profile): string
function scalingChart (line 317) | function scalingChart(array $scaling): string
FILE: tests/Browser/BrowserServer.php
class BrowserServer (line 9) | class BrowserServer
method __construct (line 23) | private function __construct()
method url (line 27) | public static function url(): string
method ensureRunning (line 35) | public static function ensureRunning(): void
method start (line 56) | private function start(): void
method isListening (line 104) | public static function isListening(): bool
method stop (line 120) | public static function stop(): void
FILE: tests/Browser/Components/ActionButton.php
class ActionButton (line 7) | class ActionButton extends Component
method fire (line 13) | public function fire()
FILE: tests/Browser/Components/Counter.php
class Counter (line 7) | class Counter extends Component
method increment (line 15) | public function increment()
method decrement (line 20) | public function decrement()
FILE: tests/Browser/Components/DeleteItem.php
class DeleteItem (line 7) | class DeleteItem extends Component
method delete (line 15) | public function delete()
FILE: tests/Browser/Components/DispatchBystander.php
class DispatchBystander (line 7) | class DispatchBystander extends Component
FILE: tests/Browser/Components/DispatchListener.php
class DispatchListener (line 7) | class DispatchListener extends Component
method handlePostCreated (line 17) | public function handlePostCreated($postId)
method handleStatusChanged (line 22) | public function handleStatusChanged($status, $reason = 'none')
method handleSimpleRefresh (line 27) | public function handleSimpleRefresh()
FILE: tests/Browser/Components/FavoriteButton.php
class FavoriteButton (line 7) | class FavoriteButton extends Component
method toggle (line 15) | public function toggle()
FILE: tests/Browser/Components/Form.php
class Form (line 7) | class Form extends Component
method register (line 17) | public function register()
FILE: tests/Browser/Components/LiveSearch.php
class LiveSearch (line 7) | class LiveSearch extends Component
method getResultsProperty (line 26) | protected function getResultsProperty()
FILE: tests/Browser/Components/ModalTrigger.php
class ModalTrigger (line 7) | class ModalTrigger extends Component
method openModal (line 13) | public function openModal()
method closeModal (line 18) | public function closeModal()
FILE: tests/Browser/Components/MultiScreen.php
class MultiScreen (line 7) | class MultiScreen extends Component
method open (line 11) | public function open()
method submit (line 16) | public function submit()
FILE: tests/Browser/Components/NotificationBadge.php
class NotificationBadge (line 7) | class NotificationBadge extends Component
method onNotification (line 15) | public function onNotification()
FILE: tests/Browser/Components/NullProp.php
class NullProp (line 7) | class NullProp extends Component
method increment (line 17) | public function increment()
FILE: tests/Browser/Components/Pagination.php
class Pagination (line 7) | class Pagination extends Component
method getResultsProperty (line 19) | protected function getResultsProperty()
method getTotalPagesProperty (line 32) | protected function getTotalPagesProperty()
method getStartProperty (line 37) | protected function getStartProperty()
method getEndProperty (line 42) | protected function getEndProperty()
FILE: tests/Browser/Components/ResponseHeaders.php
class ResponseHeaders (line 7) | class ResponseHeaders extends Component
method doRetarget (line 11) | public function doRetarget()
method doReswap (line 17) | public function doReswap()
method doTrigger (line 23) | public function doTrigger()
method doTriggerAfterSettle (line 29) | public function doTriggerAfterSettle()
method doPushUrl (line 35) | public function doPushUrl()
method doReplaceUrl (line 41) | public function doReplaceUrl()
FILE: tests/Browser/Components/StatusDropdown.php
class StatusDropdown (line 7) | class StatusDropdown extends Component
method toggleMenu (line 19) | public function toggleMenu()
method setStatus (line 24) | public function setStatus()
FILE: tests/Browser/Components/TodoList.php
class TodoList (line 7) | class TodoList extends Component
method mount (line 16) | public function mount()
method add (line 31) | public function add()
method toggle (line 41) | public function toggle()
method delete (line 53) | public function delete()
method getEntriesProperty (line 62) | protected function getEntriesProperty()
method getCountProperty (line 77) | protected function getCountProperty()
method getActiveCountProperty (line 82) | protected function getActiveCountProperty()
FILE: tests/Browser/server/layout.php
function render_page (line 7) | function render_page(string $title, string $componentHtml): void
FILE: tests/Helpers.php
function yoyo_view (line 15) | function yoyo_view()
function yoyo_instance (line 22) | function yoyo_instance()
function compile_html (line 29) | function compile_html($name, $html, $spinning = false)
function compile_html_with_vars (line 36) | function compile_html_with_vars($name, $html, $vars, $spinning = false)
function render (line 43) | function render($name, $variables = [], $attributes = [])
function update (line 50) | function update($name, $action = 'render', $variables = [], $attributes ...
function yoyo_update (line 57) | function yoyo_update()
function normalizeDomOutput (line 68) | function normalizeDomOutput($html)
function mockYoyoGetRequest (line 79) | function mockYoyoGetRequest($url, $component, $target = '', $parameters ...
function mockYoyoPostRequest (line 97) | function mockYoyoPostRequest($url, $component, $target = '', $parameters...
function resetYoyoRequest (line 115) | function resetYoyoRequest()
function headers (line 120) | function headers()
function hxattr (line 125) | function hxattr($name, $value = '')
function yoattr (line 130) | function yoattr($name, $value = '')
function yoprefix_value (line 135) | function yoprefix_value($value)
function encode_vals (line 140) | function encode_vals($vars)
function addValue (line 145) | function addValue($value = '')
function response (line 158) | function response($filename)
function htmlformat (line 165) | function htmlformat($html)
FILE: tests/HelpersBlade.php
function yoyo_blade (line 14) | function yoyo_blade()
function blade (line 23) | function blade()
FILE: tests/HelpersTwig.php
function yoyo_twig (line 8) | function yoyo_twig()
function twig (line 17) | function twig()
FILE: tests/Unit/ClassHelpersTest.php
function resolveComponent (line 11) | function resolveComponent(string $class = Counter::class, string $name =...
FILE: tests/Unit/ComponentTest.php
function makeComponent (line 9) | function makeComponent(string $class, string $id = 'test', string $name ...
FILE: tests/Unit/InvocableComponentVariableTest.php
method greet (line 34) | public function greet()
method add (line 49) | public function add($a, $b)
FILE: tests/Unit/YoyoContainerTest.php
method __construct (line 31) | public function __construct(?string $optional)
method __construct (line 52) | public function __construct(YoyoContainerInterface $container)
FILE: tests/app-another/Yoyo/Counter.php
class Counter (line 7) | class Counter extends Component
method increment (line 15) | public function increment()
method getCurrentCountProperty (line 22) | public function getCurrentCountProperty()
FILE: tests/app/Comment.php
class Comment (line 5) | class Comment
method title (line 7) | public function title()
method body (line 12) | public function body()
FILE: tests/app/Post.php
class Post (line 5) | class Post
method __construct (line 9) | public function __construct(Comment $comment)
method title (line 14) | public function title()
FILE: tests/app/Resolvers/BladeComponentResolver.php
class BladeComponentResolver (line 10) | class BladeComponentResolver extends ComponentResolver
method getViewProvider (line 14) | public function getViewProvider()
FILE: tests/app/Resolvers/CustomComponentResolver.php
class CustomComponentResolver (line 9) | class CustomComponentResolver extends ComponentResolver
method getViewProvider (line 13) | public function getViewProvider()
FILE: tests/app/Resolvers/TwigComponentResolver.php
class TwigComponentResolver (line 10) | class TwigComponentResolver extends ComponentResolver
method getViewProvider (line 14) | public function getViewProvider()
FILE: tests/app/Yoyo/Abort.php
class Abort (line 9) | class Abort extends Component
method initialize (line 11) | public function initialize()
FILE: tests/app/Yoyo/Account/Register.php
class Register (line 7) | class Register extends Component
FILE: tests/app/Yoyo/ActionArguments.php
class ActionArguments (line 7) | class ActionArguments extends Component
method someAction (line 13) | public function someAction($a, $b)
method render (line 20) | public function render()
FILE: tests/app/Yoyo/ComponentWithComputedArgs.php
class ComponentWithComputedArgs (line 7) | class ComponentWithComputedArgs extends Component
method getGreetingProperty (line 11) | protected function getGreetingProperty($name = 'World')
method getExpensiveProperty (line 16) | protected function getExpensiveProperty()
FILE: tests/app/Yoyo/ComponentWithEmit.php
class ComponentWithEmit (line 7) | class ComponentWithEmit extends Component
method doEmit (line 9) | public function doEmit()
method doEmitTo (line 14) | public function doEmitTo()
method doBrowserEvent (line 19) | public function doBrowserEvent()
FILE: tests/app/Yoyo/ComponentWithListeners.php
class ComponentWithListeners (line 7) | class ComponentWithListeners extends Component
method onItemAdded (line 16) | public function onItemAdded()
FILE: tests/app/Yoyo/ComponentWithRedirect.php
class ComponentWithRedirect (line 7) | class ComponentWithRedirect extends Component
method save (line 9) | public function save()
FILE: tests/app/Yoyo/ComponentWithResponseHeaders.php
class ComponentWithResponseHeaders (line 7) | class ComponentWithResponseHeaders extends Component
method doRetarget (line 11) | public function doRetarget()
method doReswap (line 17) | public function doReswap()
method doReselect (line 23) | public function doReselect()
method doLocation (line 29) | public function doLocation()
method doPushUrl (line 34) | public function doPushUrl()
method doReplaceUrl (line 40) | public function doReplaceUrl()
method doRedirect (line 46) | public function doRedirect()
method doRefresh (line 51) | public function doRefresh()
method doTrigger (line 56) | public function doTrigger()
method doTriggerAfterSwap (line 62) | public function doTriggerAfterSwap()
method doTriggerAfterSettle (line 68) | public function doTriggerAfterSettle()
FILE: tests/app/Yoyo/ComponentWithSwapModifiers.php
class ComponentWithSwapModifiers (line 7) | class ComponentWithSwapModifiers extends Component
method doSwap (line 9) | public function doSwap()
FILE: tests/app/Yoyo/ComponentWithTrait.php
class ComponentWithTrait (line 7) | class ComponentWithTrait extends Component
method mount (line 13) | public function mount()
type WithFramework (line 19) | trait WithFramework
method mountWithFramework (line 21) | public function mountWithFramework()
method renderedWithFramework (line 26) | public function renderedWithFramework($view)
FILE: tests/app/Yoyo/ComputedProperty.php
class ComputedProperty (line 7) | class ComputedProperty extends Component
method getFooBarProperty (line 11) | protected function getFooBarProperty()
FILE: tests/app/Yoyo/ComputedPropertyCache.php
class ComputedPropertyCache (line 7) | class ComputedPropertyCache extends Component
method getTestCountProperty (line 11) | public function getTestCountProperty()
FILE: tests/app/Yoyo/Counter.php
class Counter (line 7) | class Counter extends Component
method increment (line 15) | public function increment()
method getCurrentCountProperty (line 22) | public function getCurrentCountProperty()
FILE: tests/app/Yoyo/CounterDynamicProperties.php
class CounterDynamicProperties (line 7) | #[\AllowDynamicProperties]
method getQueryString (line 10) | public function getQueryString()
method getDynamicProperties (line 20) | public function getDynamicProperties()
method increment (line 25) | public function increment()
method getCurrentCountProperty (line 30) | public function getCurrentCountProperty()
method render (line 35) | public function render()
FILE: tests/app/Yoyo/DependencyInjectionAction.php
class DependencyInjectionAction (line 9) | class DependencyInjectionAction extends Component
method onlyTyped (line 16) | public function onlyTyped(Post $post)
method multipleTyped (line 24) | public function multipleTyped(Post $post, Comment $comment)
method mixedTypedAndRegular (line 32) | public function mixedTypedAndRegular(Post $post, $id, $status = 'active')
method typedWithVariadic (line 40) | public function typedWithVariadic(Post $post, ...$tags)
method typedWithOptional (line 48) | public function typedWithOptional(Post $post, ?string $status = null)
method render (line 54) | public function render()
FILE: tests/app/Yoyo/DependencyInjectionClassWithNamedArgumentMapping.php
class DependencyInjectionClassWithNamedArgumentMapping (line 8) | class DependencyInjectionClassWithNamedArgumentMapping extends Component
method mount (line 16) | public function mount(Post $post, $id)
method getOutputProperty (line 23) | public function getOutputProperty()
FILE: tests/app/Yoyo/DispatchListener.php
class DispatchListener (line 7) | class DispatchListener extends Component
method handlePostCreated (line 22) | public function handlePostCreated($postId)
method handleStatusChanged (line 28) | public function handleStatusChanged($status, $reason = 'none')
method handleSimpleRefresh (line 34) | public function handleSimpleRefresh()
method handleMultiParam (line 39) | public function handleMultiParam($title, $body, $categoryId)
FILE: tests/app/Yoyo/EmptyResponse.php
class EmptyResponse (line 7) | class EmptyResponse extends Component
method mount (line 9) | public function mount()
FILE: tests/app/Yoyo/EmptyResponseAndRemove.php
class EmptyResponseAndRemove (line 7) | class EmptyResponseAndRemove extends Component
method mount (line 9) | public function mount()
FILE: tests/app/Yoyo/ProtectedMethods.php
class ProtectedMethods (line 7) | class ProtectedMethods extends Component
method secret (line 9) | protected function secret()
FILE: tests/app/Yoyo/Registered.php
class Registered (line 7) | class Registered extends Component
method render (line 9) | public function render()
FILE: tests/app/Yoyo/SetViewData.php
class SetViewData (line 7) | class SetViewData extends Component
method mount (line 9) | public function mount()
FILE: tests/app/Yoyo/VariadicParameters.php
class VariadicParameters (line 7) | class VariadicParameters extends Component
method onlyVariadic (line 14) | public function onlyVariadic(...$params)
method mixedVariadic (line 22) | public function mixedVariadic($first, ...$rest)
method optionalAndVariadic (line 30) | public function optionalAndVariadic($required, $optional = 'default', ...
method render (line 35) | public function render()
Condensed preview — 249 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (487K chars).
[
{
"path": ".actrc",
"chars": 70,
"preview": "# For runs-on: ubuntu-latest\n-P ubuntu-latest=shivammathur/node:latest"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2945,
"preview": "name: CI\n\non:\n push:\n pull_request:\n\njobs:\n style:\n if: github.event_name == 'push'\n runs-on: ubuntu-latest\n "
},
{
"path": ".gitignore",
"chars": 185,
"preview": "/vendor\n/node_modules\n/.phpunit.result.cache\n/.php-cs-fixer.cache\n/tests/compiled/**/*.php\n/tests/compiled/*.php\n/tests/"
},
{
"path": ".php-cs-fixer.dist.php",
"chars": 1056,
"preview": "<?php\n\n$finder = Symfony\\Component\\Finder\\Finder::create()\n ->name('*.php')\n ->notName('*.blade.php')\n ->ignore"
},
{
"path": "CHANGELOG.md",
"chars": 10710,
"preview": "# Changelog\n\n## [Unreleased](https://github.com/clickfwd/yoyo/compare/0.15.0...develop)\n\n## [0.15.0 (2026-04-10)](https:"
},
{
"path": "LICENSE.md",
"chars": 1060,
"preview": "## MIT License\n\nCopyright © ClickFWD\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of th"
},
{
"path": "README.md",
"chars": 37969,
"preview": "# Yoyo\n\nYoyo is a full-stack PHP framework that you can use on any project to create rich dynamic interfaces using serve"
},
{
"path": "composer.json",
"chars": 1703,
"preview": "{\n \"name\": \"clickfwd/yoyo\",\n \"description\": \"Framework to build dynamic interfaces with seamless communication bet"
},
{
"path": "package.json",
"chars": 259,
"preview": "{\n \"name\": \"yoyo-tests\",\n \"private\": true,\n \"scripts\": {\n \"test:browser\": \"./vendor/bin/pest --testsuite Browser\","
},
{
"path": "phpunit.xml",
"chars": 920,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:noNam"
},
{
"path": "src/assets/js/yoyo.js",
"chars": 22705,
"preview": "; (function (global, factory) {\n\tif (typeof define === 'function' && define.amd) {\n\t\tdefine([], factory)\n\t} else {\n\t\tglo"
},
{
"path": "src/yoyo/AnonymousComponent.php",
"chars": 249,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nclass AnonymousComponent extends Component\n{\n public function render()\n {\n "
},
{
"path": "src/yoyo/Blade/Application.php",
"chars": 545,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Closure;\nuse Illuminate\\Container\\Container;\n\nclass Application extends Conta"
},
{
"path": "src/yoyo/Blade/CreateBladeViewFromString.php",
"chars": 304,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Illuminate\\View\\Component;\n\nclass CreateBladeViewFromString extends Component"
},
{
"path": "src/yoyo/Blade/YoyoBladeCompilerEngine.php",
"chars": 1180,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Illuminate\\View\\Engines\\CompilerEngine as LaravelCompilerEngine;\n\nclass YoyoB"
},
{
"path": "src/yoyo/Blade/YoyoBladeDirectives.php",
"chars": 1907,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nclass YoyoBladeDirectives\n{\n public function __construct($blade)\n {\n "
},
{
"path": "src/yoyo/Blade/YoyoServiceProvider.php",
"chars": 841,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Illuminate\\Support\\ServiceProvider;\n\nclass YoyoServiceProvider extends Servic"
},
{
"path": "src/yoyo/Blade/yoyo-view.blade.php",
"chars": 46,
"preview": "@yoyo($name, $variables, $attributes, $action)"
},
{
"path": "src/yoyo/ClassHelpers.php",
"chars": 7229,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse ReflectionClass;\nuse ReflectionMethod;\n\nclass ClassHelpers\n{\n private static arr"
},
{
"path": "src/yoyo/Component.php",
"chars": 7957,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Concerns\\BrowserEvents;\nuse Clickfwd\\Yoyo\\Concerns\\Redirector;\nuse Cl"
},
{
"path": "src/yoyo/ComponentManager.php",
"chars": 10974,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\Comp"
},
{
"path": "src/yoyo/ComponentResolver.php",
"chars": 3248,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoCo"
},
{
"path": "src/yoyo/Concerns/BrowserEvents.php",
"chars": 997,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\n\ntrait BrowserEvents\n{\n pu"
},
{
"path": "src/yoyo/Concerns/Redirector.php",
"chars": 192,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\ntrait Redirector\n{\n public $redirectTo;\n\n public function redirect($url)"
},
{
"path": "src/yoyo/Concerns/ResponseHeaders.php",
"chars": 1402,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\ntrait ResponseHeaders\n{\n public function location($path)\n {\n $thi"
},
{
"path": "src/yoyo/Concerns/Singleton.php",
"chars": 809,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\ntrait Singleton\n{\n /**\n * @var reference to singleton instance\n */\n"
},
{
"path": "src/yoyo/ContainerResolver.php",
"chars": 835,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Clickfwd\\Yoyo\\Containers\\YoyoCont"
},
{
"path": "src/yoyo/Containers/IlluminateContainer.php",
"chars": 1228,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Containers;\n\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoContainerInterface;\nuse Closure;\nuse Illumi"
},
{
"path": "src/yoyo/Containers/YoyoContainer.php",
"chars": 3617,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Containers;\n\nuse Clickfwd\\Yoyo\\Exceptions\\BindingNotFoundException;\nuse Clickfwd\\Yoyo\\Exc"
},
{
"path": "src/yoyo/Exceptions/BindingNotFoundException.php",
"chars": 327,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass BindingNotFoundExceptio"
},
{
"path": "src/yoyo/Exceptions/BypassRenderMethod.php",
"chars": 195,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass BypassRenderMethod extends \\Exception\n{\n public function __construc"
},
{
"path": "src/yoyo/Exceptions/ComponentMethodNotFound.php",
"chars": 285,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass ComponentMethodNotFound extends \\Exception\n{\n public function __con"
},
{
"path": "src/yoyo/Exceptions/ComponentNotFound.php",
"chars": 221,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass ComponentNotFound extends \\Exception\n{\n public function __construct"
},
{
"path": "src/yoyo/Exceptions/ContainerResolutionException.php",
"chars": 333,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nuse Psr\\Container\\ContainerExceptionInterface;\n\nclass ContainerResolutionExc"
},
{
"path": "src/yoyo/Exceptions/FailedToRegisterComponent.php",
"chars": 563,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass FailedToRegisterComponent extends \\Exception\n{\n public function __c"
},
{
"path": "src/yoyo/Exceptions/HttpException.php",
"chars": 535,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass HttpException extends \\Exception\n{\n protected $statusCode;\n\n pro"
},
{
"path": "src/yoyo/Exceptions/IncompleteComponentParamInRequest.php",
"chars": 250,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass IncompleteComponentParamInRequest extends \\Exception\n{\n public func"
},
{
"path": "src/yoyo/Exceptions/MissingComponentTemplate.php",
"chars": 269,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass MissingComponentTemplate extends \\Exception\n{\n public function __co"
},
{
"path": "src/yoyo/Exceptions/NonPublicComponentMethodCall.php",
"chars": 282,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass NonPublicComponentMethodCall extends \\Exception\n{\n public function "
},
{
"path": "src/yoyo/Exceptions/NotFoundHttpException.php",
"chars": 240,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass NotFoundHttpException extends HttpException\n{\n public function __co"
},
{
"path": "src/yoyo/Interfaces/RequestInterface.php",
"chars": 386,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Interfaces;\n\ninterface RequestInterface\n{\n public function all();\n\n public function"
},
{
"path": "src/yoyo/Interfaces/ViewProviderInterface.php",
"chars": 488,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Interfaces;\n\ninterface ViewProviderInterface\n{\n public const HINT_PATH_DELIMITER = '::"
},
{
"path": "src/yoyo/Interfaces/YoyoContainerInterface.php",
"chars": 792,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Interfaces;\n\nuse Closure;\nuse Psr\\Container\\ContainerInterface;\n\ninterface YoyoContainerI"
},
{
"path": "src/yoyo/InvocableComponentVariable.php",
"chars": 601,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Closure;\n\nclass InvocableComponentVariable\n{\n protected $callable;\n\n public f"
},
{
"path": "src/yoyo/QueryString.php",
"chars": 1881,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Services\\Request;\n\nclass QueryString\n{\n private $defaults;\n\n pr"
},
{
"path": "src/yoyo/Request.php",
"chars": 3793,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Interfaces\\RequestInterface;\n\nclass Request implements RequestInterfa"
},
{
"path": "src/yoyo/Services/BrowserEventsService.php",
"chars": 2094,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns\\Singleton;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nclass BrowserEv"
},
{
"path": "src/yoyo/Services/Configuration.php",
"chars": 3408,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns\\Singleton;\n\nclass Configuration\n{\n use Singleton"
},
{
"path": "src/yoyo/Services/PageRedirectService.php",
"chars": 390,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns\\Singleton;\n\nclass PageRedirectService\n{\n use Sin"
},
{
"path": "src/yoyo/Services/Response.php",
"chars": 1459,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns;\n\nclass Response\n{\n use Concerns\\Singleton;\n "
},
{
"path": "src/yoyo/Services/UrlStateManagerService.php",
"chars": 1103,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\nclass UrlStateManagerService\n{\n private $request;\n"
},
{
"path": "src/yoyo/Twig/YoyoTwigExtension.php",
"chars": 2509,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\Twig;\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Twig\\Extension\\AbstractExtens"
},
{
"path": "src/yoyo/View.php",
"chars": 3761,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\nuse InvalidArgumentException;\n\nclas"
},
{
"path": "src/yoyo/ViewProviders/BaseViewProvider.php",
"chars": 187,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nabstract class BaseViewProvider\n{\n protected $view;\n\n public functi"
},
{
"path": "src/yoyo/ViewProviders/BladeViewProvider.php",
"chars": 2198,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\n\nclass BladeViewProvi"
},
{
"path": "src/yoyo/ViewProviders/PhalconViewProvider.php",
"chars": 1327,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\n\nclass PhalconViewPro"
},
{
"path": "src/yoyo/ViewProviders/TwigViewProvider.php",
"chars": 2502,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\n\nclass TwigViewProvid"
},
{
"path": "src/yoyo/ViewProviders/YoyoViewProvider.php",
"chars": 1926,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentNotFound;\nuse Clickfwd\\Yoyo\\Interfa"
},
{
"path": "src/yoyo/Yoyo.php",
"chars": 11577,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Exceptions\\BypassRenderMethod;\nuse Clickfwd\\Yoyo\\Exceptions\\HttpExcep"
},
{
"path": "src/yoyo/YoyoCompiler.php",
"chars": 20861,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse DOMDocument;\nuse DOMXPath;\n\nclass YoyoCompiler\n{\n protected $componentType;\n\n "
},
{
"path": "src/yoyo/YoyoHelpers.php",
"chars": 3203,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nclass YoyoHelpers\n{\n public static function encode_vals(array $vars): string\n {\n "
},
{
"path": "src/yoyo/YoyoPhalconController.php",
"chars": 478,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Phalcon\\Mvc\\Controller;\n\nclass YoyoPhalconController extends Controller\n{\n publi"
},
{
"path": "src/yoyo/YoyoPhalconServiceProvider.php",
"chars": 1651,
"preview": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\ViewProviders\\PhalconViewProvider;\nuse Phalcon\\Di\\DiInterface;\nuse Ph"
},
{
"path": "src/yoyo/helpers.php",
"chars": 1672,
"preview": "<?php\n\nnamespace Yoyo;\n\nuse Clickfwd\\Yoyo\\Services\\Configuration;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nif (! function_exists('Yoyo\\y"
},
{
"path": "tests/Benchmark/PipelineBenchmarkTest.php",
"chars": 5560,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\ClassHelpers;\nuse Clickfwd\\Yoyo\\Component;\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yo"
},
{
"path": "tests/Benchmark/RealWorldBenchmarkTest.php",
"chars": 14281,
"preview": "<?php\n\nuse function Tests\\compile_html;\nuse function Tests\\yoyo_view;\n\nbeforeAll(function () {\n yoyo_view();\n});\n\nfun"
},
{
"path": "tests/Benchmark/YoyoCompilerBenchmarkTest.php",
"chars": 9594,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoCompiler;\n\nuse function Tests\\compile_html;\n\nbeforeAll(function () {\n Tests\\yoyo_view();"
},
{
"path": "tests/Benchmark/profile-compiler.php",
"chars": 18692,
"preview": "<?php\n\n/**\n * YoyoCompiler Performance Profiler\n *\n * Generates an HTML report with visual breakdown of where time is sp"
},
{
"path": "tests/Browser/BrowserServer.php",
"chars": 3276,
"preview": "<?php\n\nnamespace Tests\\Browser;\n\n/**\n * Manages the PHP built-in server for browser tests.\n * Auto-starts if not already"
},
{
"path": "tests/Browser/Components/ActionButton.php",
"chars": 246,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ActionButton extends Component\n{\n pub"
},
{
"path": "tests/Browser/Components/Counter.php",
"chars": 347,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Counter extends Component\n{\n public $"
},
{
"path": "tests/Browser/Components/DeleteItem.php",
"chars": 269,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DeleteItem extends Component\n{\n publi"
},
{
"path": "tests/Browser/Components/DispatchBystander.php",
"chars": 188,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DispatchBystander extends Component\n{\n "
},
{
"path": "tests/Browser/Components/DispatchListener.php",
"chars": 704,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DispatchListener extends Component\n{\n "
},
{
"path": "tests/Browser/Components/FavoriteButton.php",
"chars": 317,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass FavoriteButton extends Component\n{\n p"
},
{
"path": "tests/Browser/Components/Form.php",
"chars": 572,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Form extends Component\n{\n public $nam"
},
{
"path": "tests/Browser/Components/LiveSearch.php",
"chars": 790,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass LiveSearch extends Component\n{\n publi"
},
{
"path": "tests/Browser/Components/ModalTrigger.php",
"chars": 314,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ModalTrigger extends Component\n{\n pub"
},
{
"path": "tests/Browser/Components/MultiScreen.php",
"chars": 349,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass MultiScreen extends Component\n{\n publ"
},
{
"path": "tests/Browser/Components/NotificationBadge.php",
"chars": 335,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass NotificationBadge extends Component\n{\n "
},
{
"path": "tests/Browser/Components/NullProp.php",
"chars": 314,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass NullProp extends Component\n{\n public "
},
{
"path": "tests/Browser/Components/Pagination.php",
"chars": 931,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Pagination extends Component\n{\n publi"
},
{
"path": "tests/Browser/Components/ResponseHeaders.php",
"chars": 997,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ResponseHeaders extends Component\n{\n "
},
{
"path": "tests/Browser/Components/StatusDropdown.php",
"chars": 472,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass StatusDropdown extends Component\n{\n p"
},
{
"path": "tests/Browser/Components/TodoList.php",
"chars": 2185,
"preview": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass TodoList extends Component\n{\n public "
},
{
"path": "tests/Browser/CounterTest.php",
"chars": 1110,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with initial count of 0', function () {\n $this->visit(BASE_URL."
},
{
"path": "tests/Browser/CrossComponentEventsTest.php",
"chars": 1178,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders badge and action buttons', function () {\n $this->visit(BASE_URL"
},
{
"path": "tests/Browser/DispatchTest.php",
"chars": 1766,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('registers listener events in hx-trigger attribute', function () {\n $thi"
},
{
"path": "tests/Browser/FormTest.php",
"chars": 1302,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with empty fields', function () {\n $this->visit(BASE_URL.'/form"
},
{
"path": "tests/Browser/InfrastructureTest.php",
"chars": 1064,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('includes htmx script', function () {\n $this->visit(BASE_URL.'/counter')"
},
{
"path": "tests/Browser/LiveSearchTest.php",
"chars": 1094,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with empty search input', function () {\n $this->visit(BASE_URL."
},
{
"path": "tests/Browser/ModalTest.php",
"chars": 1482,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders without modal visible', function () {\n $this->visit(BASE_URL.'/"
},
{
"path": "tests/Browser/MultiScreenTest.php",
"chars": 2189,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders initial screen with open button', function () {\n $this->visit(B"
},
{
"path": "tests/Browser/NullPropTest.php",
"chars": 1550,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('preserves PHP null prop after request roundtrip', function () {\n $this-"
},
{
"path": "tests/Browser/PaginationTest.php",
"chars": 1137,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders first page of results', function () {\n $this->visit(BASE_URL.'/"
},
{
"path": "tests/Browser/ProductListTest.php",
"chars": 4006,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders all products with their own component instances', function () {\n "
},
{
"path": "tests/Browser/ResponseHeadersTest.php",
"chars": 2130,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders response headers test page', function () {\n $this->visit(BASE_U"
},
{
"path": "tests/Browser/SkipRenderTest.php",
"chars": 1217,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders deleteable items', function () {\n $this->visit(BASE_URL.'/skip-"
},
{
"path": "tests/Browser/TodoListTest.php",
"chars": 1203,
"preview": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with default entries', function () {\n $this->visit(BASE_URL.'/t"
},
{
"path": "tests/Browser/bootstrap.php",
"chars": 264,
"preview": "<?php\n\n// Shared bootstrap for browser tests.\n// Each test file requires this to start the server and define BASE_URL.\n\n"
},
{
"path": "tests/Browser/server/index.php",
"chars": 1153,
"preview": "<?php\n\n// Minimal Yoyo test server for isolated component browser tests.\n// Usage: php -S localhost:8765 tests/Browser/s"
},
{
"path": "tests/Browser/server/layout.php",
"chars": 484,
"preview": "<?php\n\n/**\n * Minimal layout for browser test pages.\n * Includes Yoyo scripts/styles and renders a single component in i"
},
{
"path": "tests/Browser/server/pages/counter.php",
"chars": 95,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Counter', Yoyo\\yoyo_render('counter'));\n"
},
{
"path": "tests/Browser/server/pages/dispatch.php",
"chars": 1122,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"dispatch-test\">\n <div data-listener-area>\n <"
},
{
"path": "tests/Browser/server/pages/events.php",
"chars": 580,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"events-test\">\n <div data-badge-area>\n <?php "
},
{
"path": "tests/Browser/server/pages/form.php",
"chars": 89,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Form', Yoyo\\yoyo_render('form'));\n"
},
{
"path": "tests/Browser/server/pages/index.php",
"chars": 418,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\n$links = [\n 'counter' => 'Counter',\n 'live-search' => 'Live Search',\n "
},
{
"path": "tests/Browser/server/pages/live-search.php",
"chars": 103,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Live Search', Yoyo\\yoyo_render('live-search'));\n"
},
{
"path": "tests/Browser/server/pages/modal.php",
"chars": 215,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"modal-test\">\n <?php echo Yoyo\\yoyo_render('modal-tr"
},
{
"path": "tests/Browser/server/pages/multi-screen.php",
"chars": 221,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"multi-screen-test\">\n <?php echo Yoyo\\yoyo_render('m"
},
{
"path": "tests/Browser/server/pages/null-prop.php",
"chars": 99,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Null Prop', Yoyo\\yoyo_render('null-prop'));\n"
},
{
"path": "tests/Browser/server/pages/pagination.php",
"chars": 101,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Pagination', Yoyo\\yoyo_render('pagination'));\n"
},
{
"path": "tests/Browser/server/pages/product-list.php",
"chars": 1222,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\n// Simulated product data — each row gets its own Yoyo components\n$products = "
},
{
"path": "tests/Browser/server/pages/response-headers.php",
"chars": 831,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"response-headers-test\">\n <div data-component-area>\n"
},
{
"path": "tests/Browser/server/pages/skip-render.php",
"chars": 480,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"skip-render-test\">\n <div data-items>\n <?php "
},
{
"path": "tests/Browser/server/pages/todo-list.php",
"chars": 99,
"preview": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Todo List', Yoyo\\yoyo_render('todo-list'));\n"
},
{
"path": "tests/Browser/server/views/action-button.php",
"chars": 140,
"preview": "<div data-component=\"action-button\">\n <button data-action=\"fire\" yoyo:get=\"fire\"><?php echo htmlspecialchars($label);"
},
{
"path": "tests/Browser/server/views/counter.php",
"chars": 212,
"preview": "<div id=\"counter\">\n <button data-action=\"decrement\" yoyo:get=\"decrement\">-</button>\n <span data-count><?php echo $"
},
{
"path": "tests/Browser/server/views/delete-item.php",
"chars": 212,
"preview": "<div data-component=\"delete-item\" data-item=\"<?php echo $itemId; ?>\">\n <span data-title><?php echo htmlspecialchars($"
},
{
"path": "tests/Browser/server/views/dispatch-bystander.php",
"chars": 91,
"preview": "<div id=\"dispatch-bystander\">\n <span id=\"bystander\"><?php echo $label; ?></span>\n</div>\n"
},
{
"path": "tests/Browser/server/views/dispatch-listener.php",
"chars": 90,
"preview": "<div id=\"dispatch-listener\">\n <span id=\"message\"><?php echo $message; ?></span>\n</div>\n"
},
{
"path": "tests/Browser/server/views/favorite-button.php",
"chars": 248,
"preview": "<div data-component=\"favorite\" data-item=\"<?php echo $itemId; ?>\">\n <button\n data-action=\"toggle\"\n data"
},
{
"path": "tests/Browser/server/views/form.php",
"chars": 979,
"preview": "<?php if ($success): ?>\n <div id=\"form\" data-success>\n <p>Thank you for registering!</p>\n </div>\n<?php else"
},
{
"path": "tests/Browser/server/views/live-search.php",
"chars": 530,
"preview": "<div id=\"live-search\" yoyo:trigger=\"input delay:300ms from:input[name='q']\">\n <input type=\"text\" name=\"q\" value=\"<?ph"
},
{
"path": "tests/Browser/server/views/modal-trigger.php",
"chars": 358,
"preview": "<div data-component=\"modal-trigger\">\n <button data-action=\"open\" yoyo:get=\"openModal\">Open Modal</button>\n<?php if ($"
},
{
"path": "tests/Browser/server/views/multi-screen.php",
"chars": 875,
"preview": "<div data-component=\"multi-screen\">\n<?php if ($this->actionMatches(['render', 'closeModal'])): ?>\n <div data-screen=\""
},
{
"path": "tests/Browser/server/views/notification-badge.php",
"chars": 86,
"preview": "<div data-component=\"badge\">\n <span data-count><?php echo $count; ?></span>\n</div>\n"
},
{
"path": "tests/Browser/server/views/null-prop.php",
"chars": 625,
"preview": "<div id=\"null-prop\">\n <span data-icon-slot-type=\"<?php echo gettype($iconSlot); ?>\"></span>\n <span data-icon-slot-"
},
{
"path": "tests/Browser/server/views/pagination.php",
"chars": 905,
"preview": "<div id=\"pagination\">\n <?php if ($this->results): ?>\n <ul data-results>\n <?php foreach ($this->resu"
},
{
"path": "tests/Browser/server/views/response-headers.php",
"chars": 534,
"preview": "<div id=\"response-headers\">\n <span id=\"rh-message\"><?php echo $message; ?></span>\n <button id=\"btn-retarget\" yoyo:"
},
{
"path": "tests/Browser/server/views/status-dropdown.php",
"chars": 744,
"preview": "<div data-component=\"status\" data-item=\"<?php echo $itemId; ?>\">\n <button\n data-action=\"toggle-menu\"\n d"
},
{
"path": "tests/Browser/server/views/todo-list.php",
"chars": 1349,
"preview": "<div id=\"todo-list\">\n <div>\n <input type=\"text\" name=\"task\" placeholder=\"What needs to be done?\"\n y"
},
{
"path": "tests/Feature/AnonymousComponentTest.php",
"chars": 899,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentNotFound;\n\nuse function Tests\\render;\nuse function Tests\\update;\nuse functi"
},
{
"path": "tests/Feature/BladeTest.php",
"chars": 2687,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\nuse function Tests\\htmlformat;\nuse function Tests\\render;\nuse function Tests\\response;\nu"
},
{
"path": "tests/Feature/ComponentLifecycleTest.php",
"chars": 5264,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse C"
},
{
"path": "tests/Feature/ComponentResolverTest.php",
"chars": 772,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Tests\\App\\Resolvers\\BladeComponentResolver;\nuse Tests\\App\\Resolvers\\CustomComponentRe"
},
{
"path": "tests/Feature/DispatchEventTest.php",
"chars": 5472,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickf"
},
{
"path": "tests/Feature/DynamicComponentTest.php",
"chars": 7867,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\BypassRenderMethod;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickf"
},
{
"path": "tests/Feature/NamespacedComponentTest.php",
"chars": 1838,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Illuminate\\Container\\Container;\nuse Tests\\App\\Resolvers\\BladeComponentResolver;\n\nuse "
},
{
"path": "tests/Feature/NestedComponentTest.php",
"chars": 392,
"preview": "<?php\n\nuse function Tests\\htmlformat;\nuse function Tests\\render;\nuse function Tests\\response;\nuse function Tests\\yoyo_vi"
},
{
"path": "tests/Feature/ResponseHeadersComponentTest.php",
"chars": 5140,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nuse function Tests\\headers"
},
{
"path": "tests/Feature/TwigTest.php",
"chars": 1597,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\nuse function Tests\\htmlformat;\nuse function Tests\\render;\nuse function Tests\\response;\nu"
},
{
"path": "tests/Helpers.php",
"chars": 3761,
"preview": "<?php\n\nnamespace Tests;\n\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\ViewProviders\\Yo"
},
{
"path": "tests/HelpersBlade.php",
"chars": 1184,
"preview": "<?php\n\nnamespace Tests;\n\nuse Clickfwd\\Yoyo\\Blade\\Application;\nuse Clickfwd\\Yoyo\\Blade\\YoyoServiceProvider;\nuse Clickfwd\\"
},
{
"path": "tests/HelpersTwig.php",
"chars": 653,
"preview": "<?php\n\nnamespace Tests;\n\nuse Clickfwd\\Yoyo\\Twig\\YoyoTwigExtension;\nuse Clickfwd\\Yoyo\\ViewProviders\\TwigViewProvider;\n\nfu"
},
{
"path": "tests/InitYoyoContainer.php",
"chars": 289,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\ContainerResolver;\nuse Clickfwd\\Yoyo\\Containers\\YoyoContainer;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nContain"
},
{
"path": "tests/Pest.php",
"chars": 159,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\n$yoyo = new Yoyo();\n\n$yoyo->configure([\n 'namespace' => 'Tests\\\\App\\\\Yoyo\\\\',\n]);\n\nus"
},
{
"path": "tests/Unit/BrowserEventsServiceTest.php",
"chars": 5094,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nus"
},
{
"path": "tests/Unit/BrowserEventsTest.php",
"chars": 508,
"preview": "<?php\n\nuse function Tests\\headers;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\yoyo_update;\nuse function T"
},
{
"path": "tests/Unit/ClassHelpersTest.php",
"chars": 4866,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\ClassHelpers;\nuse Clickfwd\\Yoyo\\Component;\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yo"
},
{
"path": "tests/Unit/ComponentResolverTest.php",
"chars": 929,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\ContainerResolver;\n\nit('resolves dynamic component', function () {\n $resolver = (new Clickfw"
},
{
"path": "tests/Unit/ComponentTest.php",
"chars": 9261,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\ContainerResolver;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentM"
},
{
"path": "tests/Unit/ConfigurationTest.php",
"chars": 3657,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\Configuration;\n\n// Tests work against the configuration set in Pest.php (namespace = T"
},
{
"path": "tests/Unit/ContainerResolverTest.php",
"chars": 1140,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\ContainerResolver;\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Clickfwd\\Yoyo\\Containe"
},
{
"path": "tests/Unit/ExceptionsTest.php",
"chars": 4958,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\BindingNotFoundException;\nuse Clickfwd\\Yoyo\\Exceptions\\BypassRenderMethod;\nuse Click"
},
{
"path": "tests/Unit/IlluminateContainerTest.php",
"chars": 352,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Illuminate\\Container\\Container;\n\nit('delegates to illuminat"
},
{
"path": "tests/Unit/InvocableComponentVariableTest.php",
"chars": 1596,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\InvocableComponentVariable;\n\nit('invokes the callable when used as a function', function () {\n "
},
{
"path": "tests/Unit/PageRedirectServiceTest.php",
"chars": 1748,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\PageRedirectService;\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nuses()->group('services');\n"
},
{
"path": "tests/Unit/ProtectedMethodTest.php",
"chars": 289,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\n\nuse function Tests\\update;\n\nit('throws exception when"
},
{
"path": "tests/Unit/PushUrlStateTest.php",
"chars": 408,
"preview": "<?php\n\nuse function Tests\\headers;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\yoyo_update;\n\nit('pushes ne"
},
{
"path": "tests/Unit/QueryStringTest.php",
"chars": 2900,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\QueryString;\nuse Clickfwd\\Yoyo\\Request;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nbeforeEach(function () {\n $"
},
{
"path": "tests/Unit/RegressionTest.php",
"chars": 6638,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\ClassHelpers;\nuse Clickfwd\\Yoyo\\Component;\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yo"
},
{
"path": "tests/Unit/RequestTest.php",
"chars": 5176,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Request;\n\nbeforeEach(function () {\n $_REQUEST = ['name' => 'test', 'count' => '5', 'data' =>"
},
{
"path": "tests/Unit/ResponseHeadersTest.php",
"chars": 3868,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nit('sets HX-Location header', function () {\n $response = new Response();"
},
{
"path": "tests/Unit/SecurityTest.php",
"chars": 5443,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\n"
},
{
"path": "tests/Unit/UrlStateManagerServiceTest.php",
"chars": 3989,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Clickfwd\\Yoyo\\Services\\UrlStateManagerService;\nuse Clickfwd\\Yoyo\\Yoyo;\n\n"
},
{
"path": "tests/Unit/ViewTest.php",
"chars": 4083,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\View;\n\nit('renders a template with variables', function () {\n $view = new View(__DIR__.'/../"
},
{
"path": "tests/Unit/YoyoCompileTest.php",
"chars": 5852,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoCompiler;\n\nuse function Tests\\compile_html;\nuse function Tests\\compile_html_with_vars;\nuse "
},
{
"path": "tests/Unit/YoyoCompilerEdgeCasesTest.php",
"chars": 10652,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoCompiler;\n\nuse function Tests\\compile_html;\nuse function Tests\\compile_html_with_vars;\nuse "
},
{
"path": "tests/Unit/YoyoContainerTest.php",
"chars": 3076,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Clickfwd\\Yoyo\\Containers\\YoyoContainer;\nuse Clickfwd\\Yoyo\\E"
},
{
"path": "tests/Unit/YoyoHelpersTest.php",
"chars": 6506,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoHelpers;\n\n// --- encode_vals ---\n\nit('encodes simple key-value pairs to JSON', function () "
},
{
"path": "tests/Unit/YoyoViewProviderTest.php",
"chars": 956,
"preview": "<?php\n\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\ViewProviders\\YoyoViewProvider;\n\ntest('can render a template', function"
},
{
"path": "tests/app/Comment.php",
"chars": 198,
"preview": "<?php\n\nnamespace Tests\\App;\n\nclass Comment\n{\n public function title()\n {\n return 'the comment title';\n }"
},
{
"path": "tests/app/Post.php",
"chars": 247,
"preview": "<?php\n\nnamespace Tests\\App;\n\nclass Post\n{\n protected $comment;\n\n public function __construct(Comment $comment)\n "
},
{
"path": "tests/app/Resolvers/BladeComponentResolver.php",
"chars": 343,
"preview": "<?php\n\nnamespace Tests\\App\\Resolvers;\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\ViewProviders\\BladeViewPro"
},
{
"path": "tests/app/Resolvers/CustomComponentResolver.php",
"chars": 377,
"preview": "<?php\n\nnamespace Tests\\App\\Resolvers;\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\Vi"
},
{
"path": "tests/app/Resolvers/TwigComponentResolver.php",
"chars": 337,
"preview": "<?php\n\nnamespace Tests\\App\\Resolvers;\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\ViewProviders\\TwigViewProv"
},
{
"path": "tests/app/Yoyo/Abort.php",
"chars": 220,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nuse function Yoyo\\abort;\n\nclass Abort extends Component\n"
},
{
"path": "tests/app/Yoyo/Account/Register.php",
"chars": 170,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo\\Account;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Register extends Component\n{\n public $m"
},
{
"path": "tests/app/Yoyo/ActionArguments.php",
"chars": 370,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ActionArguments extends Component\n{\n protected "
},
{
"path": "tests/app/Yoyo/ComponentWithComputedArgs.php",
"chars": 416,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithComputedArgs extends Component\n{\n "
},
{
"path": "tests/app/Yoyo/ComponentWithEmit.php",
"chars": 445,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithEmit extends Component\n{\n public f"
},
{
"path": "tests/app/Yoyo/ComponentWithListeners.php",
"chars": 325,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithListeners extends Component\n{\n pub"
},
{
"path": "tests/app/Yoyo/ComponentWithRedirect.php",
"chars": 190,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithRedirect extends Component\n{\n publ"
},
{
"path": "tests/app/Yoyo/ComponentWithResponseHeaders.php",
"chars": 1565,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithResponseHeaders extends Component\n{\n "
},
{
"path": "tests/app/Yoyo/ComponentWithSwapModifiers.php",
"chars": 223,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithSwapModifiers extends Component\n{\n "
},
{
"path": "tests/app/Yoyo/ComponentWithTrait.php",
"chars": 513,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithTrait extends Component\n{\n use Wit"
},
{
"path": "tests/app/Yoyo/ComputedProperty.php",
"chars": 217,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComputedProperty extends Component\n{\n public $f"
},
{
"path": "tests/app/Yoyo/ComputedPropertyCache.php",
"chars": 227,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComputedPropertyCache extends Component\n{\n prot"
},
{
"path": "tests/app/Yoyo/Counter.php",
"chars": 444,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Counter extends Component\n{\n public $count = 0;"
},
{
"path": "tests/app/Yoyo/CounterDynamicProperties.php",
"chars": 715,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\n#[\\AllowDynamicProperties]\nclass CounterDynamicPropertie"
},
{
"path": "tests/app/Yoyo/DependencyInjectionAction.php",
"chars": 1491,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\nuse Tests\\App\\Comment;\nuse Tests\\App\\Post;\n\nclass Depende"
},
{
"path": "tests/app/Yoyo/DependencyInjectionClassWithNamedArgumentMapping.php",
"chars": 540,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\nuse Tests\\App\\Post;\n\nclass DependencyInjectionClassWithNa"
},
{
"path": "tests/app/Yoyo/DispatchListener.php",
"chars": 1022,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DispatchListener extends Component\n{\n public $m"
},
{
"path": "tests/app/Yoyo/EmptyResponse.php",
"chars": 182,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass EmptyResponse extends Component\n{\n public funct"
},
{
"path": "tests/app/Yoyo/EmptyResponseAndRemove.php",
"chars": 200,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass EmptyResponseAndRemove extends Component\n{\n pub"
},
{
"path": "tests/app/Yoyo/ProtectedMethods.php",
"chars": 206,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ProtectedMethods extends Component\n{\n protected"
},
{
"path": "tests/app/Yoyo/Registered.php",
"chars": 186,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Registered extends Component\n{\n public function"
},
{
"path": "tests/app/Yoyo/SetViewData.php",
"chars": 217,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass SetViewData extends Component\n{\n public functio"
},
{
"path": "tests/app/Yoyo/VariadicParameters.php",
"chars": 925,
"preview": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass VariadicParameters extends Component\n{\n public "
}
]
// ... and 49 more files (download for full content)
About this extraction
This page contains the full source code of the clickfwd/yoyo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 249 files (436.6 KB), approximately 119.0k tokens, and a symbol index with 647 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.