Repository: Z3d0X/filament-fabricator Branch: 4.x Commit: a28e68ce883c Files: 88 Total size: 193.2 KB Directory structure: gitextract_u1aprfa_/ ├── .devcontainer/ │ ├── config/ │ │ └── xdebug.ini │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ └── config.yml │ ├── SECURITY.md │ ├── dependabot.yml │ └── workflows/ │ ├── dependabot-auto-merge.yml │ ├── fix-php-code-style-issues.yml │ ├── phpstan.yml │ ├── run-tests.yml │ └── update-changelog.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bootstrap/ │ └── app.php ├── composer.json ├── config/ │ └── filament-fabricator.php ├── database/ │ ├── factories/ │ │ └── ModelFactory.php │ └── migrations/ │ ├── create_pages_table.php.stub │ └── fix_slug_unique_constraint_on_pages_table.php.stub ├── docs/ │ └── README.md ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── phpunit.xml.dist ├── pint.json ├── rector.php ├── resources/ │ ├── lang/ │ │ ├── ar/ │ │ │ └── page-resource.php │ │ ├── en/ │ │ │ └── page-resource.php │ │ ├── fr/ │ │ │ └── page-resource.php │ │ ├── id/ │ │ │ └── page-resource.php │ │ ├── nl/ │ │ │ └── page-resource.php │ │ ├── pl/ │ │ │ └── page-resource.php │ │ ├── ru/ │ │ │ └── page-resource.php │ │ ├── tr/ │ │ │ └── page-resource.php │ │ └── uk/ │ │ └── page-resource.php │ └── views/ │ ├── components/ │ │ ├── forms/ │ │ │ └── components/ │ │ │ ├── page-builder/ │ │ │ │ ├── dropdown-block-picker.blade.php │ │ │ │ └── modal-block-picker.blade.php │ │ │ └── page-builder.blade.php │ │ ├── layouts/ │ │ │ └── base.blade.php │ │ └── page-blocks.blade.php │ ├── preview.blade.php │ └── tests/ │ └── fixtures/ │ └── blade-wrapper.blade.php ├── routes/ │ └── web.php ├── src/ │ ├── Commands/ │ │ ├── Aliases/ │ │ │ ├── MakeLayoutCommand.php │ │ │ └── MakePageBlockCommand.php │ │ ├── ClearRoutesCacheCommand.php │ │ ├── MakeLayoutCommand.php │ │ └── MakePageBlockCommand.php │ ├── Enums/ │ │ └── BlockPickerStyle.php │ ├── Facades/ │ │ └── FilamentFabricator.php │ ├── FilamentFabricatorManager.php │ ├── FilamentFabricatorPlugin.php │ ├── FilamentFabricatorServiceProvider.php │ ├── Forms/ │ │ └── Components/ │ │ └── PageBuilder.php │ ├── Helpers.php │ ├── Http/ │ │ └── Controllers/ │ │ └── PageController.php │ ├── Layouts/ │ │ └── Layout.php │ ├── Listeners/ │ │ └── OptimizeWithLaravel.php │ ├── Models/ │ │ ├── Concerns/ │ │ │ └── HandlesPageUrls.php │ │ ├── Contracts/ │ │ │ ├── HasPageUrls.php │ │ │ └── Page.php │ │ └── Page.php │ ├── Observers/ │ │ └── PageRoutesObserver.php │ ├── PageBlocks/ │ │ └── PageBlock.php │ ├── Resources/ │ │ ├── PageResource/ │ │ │ └── Pages/ │ │ │ ├── Concerns/ │ │ │ │ └── HasPreviewModal.php │ │ │ ├── CreatePage.php │ │ │ ├── EditPage.php │ │ │ ├── ListPages.php │ │ │ └── ViewPage.php │ │ └── PageResource.php │ ├── Services/ │ │ └── PageRoutesService.php │ └── View/ │ ├── LayoutRenderHook.php │ └── ResourceSchemaSlot.php ├── stubs/ │ ├── Layout.stub │ ├── LayoutView.stub │ ├── PageBlock.stub │ └── PageBlockView.stub └── tests/ ├── Commands/ │ └── ClearRoutesCacheCommand.test.php ├── ExampleTest.php ├── Fixtures/ │ └── PageBuilderTestComponent.php ├── Forms/ │ └── Components/ │ └── PageBuilder.test.php ├── Observers/ │ └── PageRoutesObserver.test.php ├── Pest.php └── TestCase.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/config/xdebug.ini ================================================ zend_extension=xdebug.so xdebug.mode=develop,debug,profile,trace,gcstats xdebug.discover_client_host=1 xdebug.client_port=9003 ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "image": "mcr.microsoft.com/devcontainers/php:8.3", "forwardPorts": [ 8000, 5173, 9000, 9003 ], "remoteEnv": { "COMPOSER_MEMORY_LIMIT": "-1", "COMPOSER_PROCESS_TIMEOUT": "0", "PHPUNIT_TELEMETRY": "off" }, "features": { "ghcr.io/devcontainers/features/node:1": {} } } ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_size = 4 indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 [*.{neon,neon.dist}] indent_size = 4 ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.blade.php diff=html *.css diff=css *.html diff=html *.md diff=markdown *.php diff=php # Path-based git attributes # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". /.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore /phpunit.xml.dist export-ignore /art export-ignore /docs export-ignore /tests export-ignore /.editorconfig export-ignore /.php_cs.dist.php export-ignore /psalm.xml export-ignore /psalm.xml.dist export-ignore /testbench.yaml export-ignore /UPGRADING.md export-ignore /phpstan.neon.dist export-ignore /phpstan-baseline.neon export-ignore /.travis.yml export-ignore /.scrutinizer.yml export-ignore /.idea export-ignore /.prettierrc export-ignore /.package-lock.json export-ignore /.editorconfig export-ignore /.php_cs.dist.php export-ignore /.vscode export-ignore /images export-ignore /package.json export-ignore /postcss.config.js export-ignore /phpunit.xml.dist export-ignore /pint.json export-ignore /psalm.xml export-ignore /psalm.xml.dist export-ignore /tailwind.config.js export-ignore /testbench.yaml export-ignore /UPGRADING.md export-ignore ## GITATTRIBUTES FOR WEB PROJECTS # # These settings are for any web project. # # Details per file setting: # text These files should be normalized (i.e. convert CRLF to LF). # binary These files are binary and should be left untouched. # # Note that binary is a macro for -text -diff. ###################################################################### # Auto detect ## Handle line endings automatically for files detected as ## text and leave all files detected as binary untouched. ## This will handle all files NOT defined below. # Source code *.js text *.mjs text *.cjs text *.json text *.ls text *.map text -diff *.sass text *.scss text diff=css *.sh text eol=lf .husky/* text eol=lf *.sql text *.ts text # Docker Dockerfile text # Documentation *.ipynb text eol=lf *.markdown text diff=markdown *.md text diff=markdown *.mdwn text diff=markdown *.mdown text diff=markdown *.mkd text diff=markdown *.mkdn text diff=markdown *.mdtxt text *.mdtext text *.txt text AUTHORS text CHANGELOG text CHANGES text CONTRIBUTING text COPYING text copyright text *COPYRIGHT* text INSTALL text license text LICENSE text NEWS text readme text *README* text TODO text # Templates *.dot text *.ejs text *.erb text *.haml text *.handlebars text *.hbs text *.hbt text *.jade text *.latte text *.mustache text *.njk text *.phtml text *.svelte text *.tmpl text *.tpl text *.twig text *.vue text # Configs *.cnf text *.conf text *.config text .editorconfig text *.env text .gitattributes text .gitconfig text .htaccess text *.lock text -diff package.json text eol=lf package-lock.json text eol=lf -diff pnpm-lock.yaml text eol=lf -diff .prettierrc text yarn.lock text -diff *.toml text *.yaml text *.yml text browserslist text Makefile text makefile text # Fixes syntax highlighting on GitHub to allow comments tsconfig.json linguist-language=JSON-with-Comments # Heroku Procfile text # Graphics *.ai binary *.bmp binary *.eps binary *.gif binary *.gifv binary *.ico binary *.jng binary *.jp2 binary *.jpg binary *.jpeg binary *.jpx binary *.jxr binary *.pdf binary *.png binary *.psb binary *.psd binary # SVG treated as an asset (binary) by default. *.svg text # If you want to treat it as binary, # use the following line instead. *.svgz binary *.tif binary *.tiff binary *.wbmp binary *.webp binary # Audio *.kar binary *.m4a binary *.mid binary *.midi binary *.mp3 binary *.ogg binary *.ra binary # Video *.3gpp binary *.3gp binary *.as binary *.asf binary *.asx binary *.avi binary *.fla binary *.flv binary *.m4v binary *.mng binary *.mov binary *.mp4 binary *.mpeg binary *.mpg binary *.ogv binary *.swc binary *.swf binary *.webm binary # Archives *.7z binary *.gz binary *.jar binary *.rar binary *.tar binary *.zip binary # Fonts *.ttf binary *.eot binary *.otf binary *.woff binary *.woff2 binary # Executables *.exe binary *.pyc binary # Prevents massive diffs caused by vendored, minified files **/.yarn/releases/** binary **/.yarn/plugins/** binary # RC files (like .babelrc or .eslintrc) *.*rc text # Ignore files (like .npmignore or .gitignore) *.*ignore text ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Contributions are **welcome** and will be fully **credited**. Please read and understand the contribution guide before creating an issue or pull request. ## Etiquette This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. ## Viability When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. ## Procedure Before filing an issue: - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. - Check to make sure your feature suggestion isn't already present within the project. - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. - Check the pull requests tab to ensure that the feature isn't already in progress. Before submitting a pull request: - Check the codebase to ensure that your feature doesn't already exist. - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. ## Requirements If the project maintainer has any additional requirements, you will find them listed here. - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). - **Add tests!** - Your patch won't be accepted if it doesn't have tests. - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. **Happy coding**! ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a question url: https://github.com/Z3d0X/filament-fabricator/discussions/new?category=q-a about: Ask the community for help - name: Request a feature url: https://github.com/Z3d0X/filament-fabricator/discussions/new?category=ideas about: Share ideas for new features - name: Report a security issue url: https://github.com/Z3d0X/filament-fabricator/security/policy about: Learn how to notify us for sensitive bugs - name: Report a bug url: https://github.com/Z3d0X/filament-fabricator/issues/new about: Report a reproducable bug ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy If you discover any security related issues, please email ziyaan2010@gmail.com instead of using the issue tracker. ================================================ FILE: .github/dependabot.yml ================================================ # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" labels: - "dependencies" ================================================ FILE: .github/workflows/dependabot-auto-merge.yml ================================================ name: dependabot-auto-merge on: pull_request_target permissions: pull-requests: write contents: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2.5.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Auto-merge Dependabot PRs for semver-minor updates if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Auto-merge Dependabot PRs for semver-patch updates if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} ================================================ FILE: .github/workflows/fix-php-code-style-issues.yml ================================================ name: Fix PHP code style issues on: [push] jobs: php-code-styling: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} - name: Fix PHP code style issues uses: aglipanci/laravel-pint-action@2.6 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Fix styling ================================================ FILE: .github/workflows/phpstan.yml ================================================ name: PHPStan on: push: paths: - '**.php' - 'phpstan.neon.dist' jobs: phpstan: name: phpstan runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' coverage: none - name: Install composer dependencies uses: ramsey/composer-install@v3 - name: Run PHPStan run: ./vendor/bin/phpstan ================================================ FILE: .github/workflows/run-tests.yml ================================================ name: run-tests on: push: branches: - 1.x - 2.x pull_request: branches: - 1.x - 2.x jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: os: [ubuntu-latest] php: [8.1, '8.2', '8.3'] laravel: ['10.*', '11.*', '12.*', '13.*'] stability: [prefer-stable] include: - laravel: 12.* testbench: 10.* - laravel: 11.* testbench: 9.* - laravel: 10.* testbench: 8.* - laravel: 13.* testbench: 11.* exclude: - laravel: 12.* php: 8.1 - laravel: 11.* php: 8.1 - laravel: 13.* php: 8.1 - laravel: 13.* php: '8.2' name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v6 - name: Cache dependencies uses: actions/cache@v5 with: path: ~/.composer/cache/files key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo coverage: none - name: Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Execute tests run: ./vendor/bin/pest ================================================ FILE: .github/workflows/update-changelog.yml ================================================ name: "Update Changelog" on: release: types: [released] jobs: update: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Update Changelog uses: stefanzweifel/changelog-updater-action@v1 with: latest-version: ${{ github.event.release.name }} release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG uses: stefanzweifel/git-auto-commit-action@v7 with: branch: ${{ github.event.release.target_commitish }} commit_message: Update CHANGELOG file_pattern: CHANGELOG.md ================================================ FILE: .gitignore ================================================ .idea .php_cs .php_cs.cache .phpunit.result.cache build composer.lock coverage phpunit.xml phpstan.neon testbench.yaml vendor node_modules .php-cs-fixer.cache **/.DS_Store ================================================ FILE: .vscode/settings.json ================================================ { "intelephense.environment.includePaths": [ "/workspaces/filament-fabricator/vendor" ] } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to `filament-fabricator` will be documented in this file. ## v4.0.0 - Filament v5 Compatibility - 2026-02-16 ### What's Changed * build(deps): bump dependabot/fetch-metadata from 2.4.0 to 2.5.0 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/258 * Migrate to Filament v5 by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/260 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v3.0.3...v4.0.0 ## v3.0.3 - 2025-12-31 ### What's Changed * hotfix: 3.0.2 mishaps by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/257 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v3.0.2...v3.0.3 ## v3.0.2 - 2025-12-23 ### What's Changed * build(deps): bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/252 * build(deps): bump actions/cache from 4 to 5 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/253 * hotfix: Filament v4 migration mishaps by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/254 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v3.0.1...v3.0.2 ## v3.0.1 - 2025-10-24 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.7.0...v2.7.1 ### What's Changed * build(deps): bump actions/cache from 3 to 4 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/239 * build(deps): bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/238 * fix: wrong argument used on getPageUrlFromId breaks page listing on f… by @Reapious in https://github.com/Z3d0X/filament-fabricator/pull/245 * fix: update PageBuilder for Filament v4 compatibility by @mckenziearts in https://github.com/Z3d0X/filament-fabricator/pull/244 * Add polish translations by @KaminskiDaniell in https://github.com/Z3d0X/filament-fabricator/pull/247 * build(deps): bump stefanzweifel/git-auto-commit-action from 6 to 7 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/246 ### New Contributors * @Reapious made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/245 * @mckenziearts made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/244 * @KaminskiDaniell made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/247 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v3.0...v3.0.1 ## v3.0 - Filament v4 Support - 2025-09-29 ### What's Changed * Migrate 3.x codebase to Filament v4 by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/237 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.6.1...v3.0 ## v2.6.1 - 2025-09-09 ### What's Changed * hotfix: Revert changes made in v2.6.0 by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/236 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.6.0...v2.6.1 ## v2.5.1 - 2025-08-03 ### What's Changed * build(deps): bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/225 * build(deps): bump aglipanci/laravel-pint-action from 2.5 to 2.6 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/231 * Fix: bind page route when runnning unit tests by @thecrazybob in https://github.com/Z3d0X/filament-fabricator/pull/224 * Fix: resolve PHP 8 deprecation warning in namespace handling by @thecrazybob in https://github.com/Z3d0X/filament-fabricator/pull/219 * Add dutch translations by @lbovit in https://github.com/Z3d0X/filament-fabricator/pull/220 * build(deps): bump stefanzweifel/git-auto-commit-action from 5 to 6 by @dependabot[bot] in https://github.com/Z3d0X/filament-fabricator/pull/229 * Fix issue when removing old urls from uncached pages by @Voltra in https://github.com/Z3d0X/filament-fabricator/commit/2e09ce5d6b5a9cb04c9d1c9a17f519e76febb7e4 ### New Contributors * @thecrazybob made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/224 * @lbovit made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/220 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.5...v2.5.1 ## v2.5 - 2025-03-04 ### What's Changed * build(deps): bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/210 * build(deps): bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/211 * Laravel 12.x Compatibility by @laravel-shift in https://github.com/Z3d0X/filament-fabricator/pull/216 * Null Page Reference in FilamentFabricator Block Preloading by @rsandipermana in https://github.com/Z3d0X/filament-fabricator/pull/214 ### New Contributors * @rsandipermana made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/214 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.4.3...v2.5 ## v2.4.3 - 2025-01-22 ### What's Changed * Add Indonesian language translations for page resource by @cuinc99 in https://github.com/Z3d0X/filament-fabricator/pull/208 ### New Contributors * @cuinc99 made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/208 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.4.2...v2.4.3 ## v2.4.2 - 2025-01-08 ### What's Changed * Rename ResourcheSchemaSlot.php to ResourceSchemaSlot.php by @rezadindar in https://github.com/Z3d0X/filament-fabricator/pull/206 * proposal: Replace the default middleware list by the 'web' middleware group by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/207 ### New Contributors * @rezadindar made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/206 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.4.1...v2.4.2 ## v2.4.1 - 2025-01-07 ### What's Changed * Add constants for layout render hooks by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/193 * Fix n+1 query by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/199 * Fix modal picker style by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/200 * Fix PHPStan by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/201 * hotfix: Proper clearing of the ID<->URI mappings by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/204 * Add constants for page resource schema slots by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/203 * feat ( Localization ) : add Turkish lang. by @AzizEmir in https://github.com/Z3d0X/filament-fabricator/pull/202 ### New Contributors * @AzizEmir made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/202 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.4.0...v2.4.1 ## v2.4.0 - 2024-12-24 ### What's Changed * "Smart" route URLs caching by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/119 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.3.0...v2.4.0 ## v2.3.0 - 2024-12-22 ### What's Changed * allow layout live switching by @phpsa in https://github.com/Z3d0X/filament-fabricator/pull/188 * Add exception for runningInConsole in FilamentFabricatorServiceProvid… by @yolanmees in https://github.com/Z3d0X/filament-fabricator/pull/160 * Add a hook to allow mass-preload/batch-load of related data when rendering a page's blocks by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/166 ### New Contributors * @phpsa made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/188 * @yolanmees made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/160 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.2.2...v2.3.0 ## v2.2.2 - 2024-05-12 ### What's Changed * build(deps): bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/152 * build(deps): bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/156 * Allow string IDs by @rojtjo in https://github.com/Z3d0X/filament-fabricator/pull/158 ### New Contributors * @rojtjo made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/158 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.2.1...v2.2.2 ## v2.2.1 - 2024-04-15 ### What's Changed * build(deps): bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/148 * Add note to README regarding the plugin assets by @pboivin in https://github.com/Z3d0X/filament-fabricator/pull/149 * Fixed incorrect table name during migration by @witaway in https://github.com/Z3d0X/filament-fabricator/pull/151 ### New Contributors * @pboivin made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/149 * @witaway made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/151 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.2.0...v2.2.1 ## v2.2.0 - 2024-03-12 ### Laravel 11.x compatibility added #### What's Changed * build(deps): bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/143 * Laravel 11.x Compatibility by @laravel-shift in https://github.com/Z3d0X/filament-fabricator/pull/142 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.1.1...v2.2.0 ## v2.1.1 - 2024-02-19 ### What's Changed * Fix Resource Registration by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/140 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.1.0...v2.1.1 ## v2.1.0 - 2024-02-09 ### What's Changed * Fix Slug Unique Constraint by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/135 * Feature Block Picker Styles by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/136 Note: to apply the fix for unique slug issue from #135 please publish the migrations using `php artisan vendor:publish --tag=filament-fabricator-migrations` & then run migrations **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.0.6...v2.1.0 ## v2.0.6 - 2024-02-03 ### What's Changed * Fix octane issues change registering package from scoped to singleton by @ksimenic in https://github.com/Z3d0X/filament-fabricator/pull/130 ### New Contributors * @ksimenic made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/130 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.0.5...v2.0.6 ## v2.0.5 - 2024-01-18 ### What's Changed * Respect app locale for slug generation by @flolanger in https://github.com/Z3d0X/filament-fabricator/pull/109 * build(deps): bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/124 * [ar]: Add Arabic translations by @mohamedsabil83 in https://github.com/Z3d0X/filament-fabricator/pull/127 ### New Contributors * @flolanger made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/109 * @mohamedsabil83 made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/127 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.0.4...v2.0.5 ## v2.0.4 - 2023-11-26 ### What's Changed * Update README.md by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/105 * Fix getLabel() and getPluralLabel() deprecation by @devhoussam1998 in https://github.com/Z3d0X/filament-fabricator/pull/113 * Allow easier subclassing of the Page model by @Voltra in https://github.com/Z3d0X/filament-fabricator/pull/115 ### New Contributors * @devhoussam1998 made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/113 * @Voltra made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/115 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.0.3...v2.0.4 ## v2.0.3 - 2023-10-14 ### What's Changed - build(deps): bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/97 - fix: default layout resolving by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/103 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.0.2...v2.0.3 ## v1.2.2 - 2023-10-14 ### What's Changed - fix: default layout resolving by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/103 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.2.1...v1.2.2 ## v2.0.2 - 2023-09-25 ### What's Changed - [Docs]: Remove Peek from installation instructions by @viraljetani in https://github.com/Z3d0X/filament-fabricator/pull/87 - build(deps): bump actions/checkout from 3 to 4 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/89 - Update PageResource.php by @RibesAlexandre in https://github.com/Z3d0X/filament-fabricator/pull/93 - Fix: Lazy Loading in Model Strict Mode by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/96 ### New Contributors - @viraljetani made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/87 - @RibesAlexandre made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/93 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.0.1...v2.0.2 ## v2.0.1 - 2023-08-20 ### What's Changed - Fix deprecated code by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/83 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v2.0.0...v2.0.1 ## v1.1.5 - 2023-04-18 ### What's Changed - Prevented lazy loading. by @danielbehrendt in https://github.com/Z3d0X/filament-fabricator/pull/56 ### New Contributors - @danielbehrendt made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/56 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.1.4...v1.1.5 ## v1.1.4 - 2023-03-30 ### What's Changed - build(deps): bump aglipanci/laravel-pint-action from 2.1.0 to 2.2.0 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/54 - Translatable Resource Labels in https://github.com/Z3d0X/filament-fabricator/commit/073fb9d4935951b6f59fd01b8426ddf8321344be **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.1.3...v1.1.4 ## v1.1.3 - 2023-03-05 ### What's Changed - Fix: Page Middleware by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/51 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.1.2...v1.1.3 ## v1.1.2 - 2023-02-27 ### What's Changed - Make `$page` instance accessible in blocks by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/45 - Shorter Commands by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/46 - Fix Routing Urls by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/47 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.1.1...v1.1.2 ## v1.1.1 - 2023-02-17 ### What's Changed - Fix: ignore deleted blocks by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/41 - Refactor to PageController by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/42 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.1.0...v1.1.1 ## v1.1.0 - 2023-02-15 ### What's Changed - Laravel 10 Support by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/38 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.0.4...v1.1.0 ## v1.0.4 - 2023-02-14 ### What's Changed - build(deps): bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/35 - Fix custom PageResource not being considered on Pages by @lucasgiovanny in https://github.com/Z3d0X/filament-fabricator/pull/37 ### New Contributors - @lucasgiovanny made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/37 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.0.3...v1.0.4 ## v1.0.3 - 2023-01-07 ### What's Changed - build(deps): bump ramsey/composer-install from 1 to 2 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/28 - Add a save button at the top of the page by @jvkassi in https://github.com/Z3d0X/filament-fabricator/pull/31 - build(deps): bump aglipanci/laravel-pint-action from 1.0.0 to 2.1.0 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/32 - Configurable Database Table & Migration Down Method by @mrlinnth in https://github.com/Z3d0X/filament-fabricator/pull/19 - Fix: Frontend Routing by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/34 ### New Contributors - @jvkassi made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/31 - @mrlinnth made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/19 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.0.2...v1.0.3 ## v1.0.2 - 2022-11-14 ### What's Changed - Fix: Homepage Routing by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/24 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.0.1...v1.0.2 ## v1.0.1 - 2022-11-13 ### What's Changed - build(deps): bump dependabot/fetch-metadata from 1.3.4 to 1.3.5 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/21 - Feature: Configurable `PageResource` by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/22 - Fix: Hide page urls when routing disabled by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/23 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v1.0.0...v1.0.1 ## v1.0.0 - 2022-10-25 ### What's Changed - Feature: `PageResource` Translations by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/7 - Feature: `PageBuilder` Field by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/8 - Feature: Base Layout by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/10 - Feature: Configurable `Page` Model by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/11 - Fix: HomePage Routing by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/12 - build(deps): bump dependabot/fetch-metadata from 1.3.3 to 1.3.4 by @dependabot in https://github.com/Z3d0X/filament-fabricator/pull/15 - Feature: Nested Pages by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/13 - adding prefix for pages by @MuhamadSelim in https://github.com/Z3d0X/filament-fabricator/pull/14 - fixing Dynamic Page Model for FilamentFabricatorManager.php setPageUr… by @MuhamadSelim in https://github.com/Z3d0X/filament-fabricator/pull/16 - Feature: Page Model Contract by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/17 - Feature: PageResource custom fieldslots by @Z3d0X in https://github.com/Z3d0X/filament-fabricator/pull/18 ### New Contributors - @Z3d0X made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/7 - @dependabot made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/15 - @MuhamadSelim made their first contribution in https://github.com/Z3d0X/filament-fabricator/pull/14 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/compare/v0.1.0...v1.0.0 ## v0.1.0 - 2022-09-19 **Full Changelog**: https://github.com/Z3d0X/filament-fabricator/commits/v0.1.0 ## 1.0.0 - 202X-XX-XX - initial release ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) Z3d0X 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 ================================================ # Block-Based Page Builder Skeleton for your Filament Apps [![Latest Version on Packagist](https://img.shields.io/packagist/v/z3d0x/filament-fabricator.svg?style=for-the-badge)](https://packagist.org/packages/z3d0x/filament-fabricator) [![Total Downloads](https://img.shields.io/packagist/dt/z3d0x/filament-fabricator.svg?style=for-the-badge)](https://packagist.org/packages/z3d0x/filament-fabricator)

fabricator banner

**_What is Filament Fabricator?_** Filament Fabricator is simply said a block-based page builder skeleton. Filament Fabricator takes care of the PageResource & frontend routing, so you can focus on what really matters: your [Layouts](https://filamentphp.com/plugins/z3d0x-fabricator#layouts) & [Page Blocks](https://filamentphp.com/plugins/z3d0x-fabricator#page-blocks). ## Compatibility | Fabricator | Filament | PHP | | ------------------------------------------------------------ | -------- | ---- | | [1.x](https://github.com/z3d0x/filament-fabricator/tree/1.x) | ^2.0 | ^8.0 | | [2.x](https://github.com/z3d0x/filament-fabricator/tree/2.x) | ^3.0 | ^8.1 | | [3.x](https://github.com/z3d0x/filament-fabricator/tree/3.x) | ^4.0 | ^8.2 | | [4.x](https://github.com/z3d0x/filament-fabricator/tree/4.x) | ^5.0 | ^8.3 | ## Installation You can install the package via composer: ```bash composer require z3d0x/filament-fabricator ``` After that run the install command: ```bash php artisan filament-fabricator:install ``` Register a `FilamentFabricatorPlugin` instance in your Panel provider: ```php use Z3d0X\FilamentFabricator\FilamentFabricatorPlugin; //.. public function panel(Panel $panel): Panel { return $panel // ... ->plugins([ FilamentFabricatorPlugin::make(), ]); } ``` Then, publish the registered plugin assets: ``` php artisan filament:assets ``` ## Documentation Documentation can be viewed at: https://filamentphp.com/plugins/z3d0x-fabricator ## Screenshots fabricator-index fabricator-edit-1 fabricator-edit-2 ## Migrate ### From 3.x to 4.x - Relies on PHP 8.3 as the minimum version ### From 2.x to 3.x - There is no longer a default value for the `pages.layout` database column - `FilamentFabricatorManager#getPageUrlFromId` no longer has a `prefixSlash` parameter - Relies on PHP 8.2 as the minimum version ### From Filament v4 to Filament v5 Following [the upgrade guide from Filament](https://filamentphp.com/docs/5.x/upgrade-guide) should be enough. In case it isn't, you can run the following commands: ```bash composer require filament/upgrade:"^5.0" -W --dev ./vendor/bin/filament-v5 # Run the commands output by the upgrade script, they are unique to your app composer require filament/filament:"^5.0" z3d0x/filament-fabricator:"^5.0" -W --no-update composer update ``` ### From Filament v3 to Filament v4 Following [the upgrade guide from Filament](https://filamentphp.com/docs/4.x/upgrade-guide) should be enough. In case it isn't, you can run the following commands: ```bash composer require filament/upgrade:"^4.0" -W --dev ./vendor/bin/filament-v4 # Run the commands output by the upgrade script, they are unique to your app composer require filament/filament:"^4.0" z3d0x/filament-fabricator:"^4.0" -W --no-update composer update ``` For more info on breaking changes, please refer to the [CHANGELOG](CHANGELOG.md) ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Contributing Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. ## Security Vulnerabilities Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits - [ZedoX](https://github.com/Z3d0X) - [Voltra](https://github.com/Voltra) - [Patrick Boivin](https://github.com/pboivin) - Filament Peek - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. ================================================ FILE: bootstrap/app.php ================================================ configure([ 'enables_package_discoveries' => true, ]) ->createApplication(); $app->register(LivewireServiceProvider::class); $app->register(FilamentServiceProvider::class); $app->register(SupportServiceProvider::class); $app->register(FilamentFabricatorServiceProvider::class); return $app; ================================================ FILE: composer.json ================================================ { "name": "z3d0x/filament-fabricator", "description": "Block-Based Page Builder Skeleton for your Filament Apps", "keywords": [ "Z3d0X", "laravel", "filament-fabricator" ], "homepage": "https://github.com/z3d0x/filament-fabricator", "license": "MIT", "authors": [ { "name": "Ziyaan Hassan", "email": "ziyaan2010@gmail.com", "role": "Developer" } ], "require": { "php": "^8.3", "filament/filament": "^5.0", "illuminate/contracts": "^11.0 | ^12.0 | ^13.0", "livewire/livewire": "^4.0", "pboivin/filament-peek": "^4.0", "spatie/laravel-package-tools": "^1.13.5" }, "require-dev": { "driftingly/rector-laravel": "^2.0", "filament/upgrade": "^5.0", "larastan/larastan": "^2.9 | ^3.7 | dev-l13", "laravel/pint": "^1.24", "nunomaduro/collision": "^8.0", "orchestra/testbench": "^10.0 | ^11.0", "pestphp/pest": "^4.0", "pestphp/pest-plugin-laravel": "^4.0", "pestphp/pest-plugin-livewire": "^4.0", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", "rector/rector": "^2.1", "spatie/laravel-ray": "^1.26" }, "autoload": { "psr-4": { "Z3d0X\\FilamentFabricator\\": "src", "Z3d0X\\FilamentFabricator\\Database\\Factories\\": "database/factories" } }, "autoload-dev": { "psr-4": { "Z3d0X\\FilamentFabricator\\Tests\\": "tests" } }, "scripts": { "pint": "@php ./vendor/bin/pint", "rector": [ "@php ./vendor/bin/rector", "@pint" ], "test:pest": "@php ./vendor/bin/pest --parallel", "test:phpstan": "@php ./vendor/bin/phpstan analyse", "test": [ "@test:pest", "@test:phpstan" ] }, "config": { "sort-packages": true, "allow-plugins": { "composer/package-versions-deprecated": true, "pestphp/pest-plugin": true } }, "extra": { "laravel": { "providers": [ "Z3d0X\\FilamentFabricator\\FilamentFabricatorServiceProvider" ], "aliases": { "FilamentFabricator": "Z3d0X\\FilamentFabricator\\Facades\\FilamentFabricator" } } }, "minimum-stability": "dev", "prefer-stable": true } ================================================ FILE: config/filament-fabricator.php ================================================ [ /** * Whether routing should be automatically handled. * Disable if you want finer and manual control over how the routing to your pages is done. */ 'enabled' => true, /** * The prefix to use for all pages' routes. * Leave to null if you don't want them to have a prefix. * A prefix set to '/pages' means that a page of slug 'page-1' * would be accessed through `/pages/page-1` if routing is enabled. */ 'prefix' => null, // /pages ], 'layouts' => [ /** * The base namespace for all your * filament-fabricator page layouts */ 'namespace' => 'App\\Filament\\Fabricator\\Layouts', /** * The path to your layouts (folder or glob) * This is used when auto-registering them */ 'path' => app_path('Filament/Fabricator/Layouts'), /** * A list of layout classes you want to manually register * in addition to those that are auto-registered */ 'register' => [ // ], ], 'page-blocks' => [ /** * The base namespace for all your filament-fabricator page blocks */ 'namespace' => 'App\\Filament\\Fabricator\\PageBlocks', /** * The path to your blocks (folder or glob) * This is used when auto-registering them */ 'path' => app_path('Filament/Fabricator/PageBlocks'), /** * A list of block classes you want to manually register * in addition to those that are auto-registered */ 'register' => [ // ], ], /** * The middleware(s) to apply to your pages when routing is enabled */ 'middleware' => [ 'web', ], /** * The page model to be used by the package. * Replace this if you ever extend it */ 'page-model' => Page::class, /** * The page filament resource to be used by the package. * Replace this if you ever extend it */ 'page-resource' => PageResource::class, /** * Whether you want to have a view page as part of your PageResource */ 'enable-view-page' => false, /** * Whether to hook into artisan's core commands to clear and refresh page route caches along with the rest. * Disable for manual control over cache. * * This is the list of commands that will be hooked into: * - cache:clear -> clear routes cache * - config:cache -> refresh routes cache * - config:clear -> clear routes cache * - optimize -> refresh routes cache * - optimize:clear -> clear routes cache * - route:clear -> clear routes cache */ 'hook-to-commands' => true, /** * This is the name of the table that will be created by the migration and * used by the above page-model shipped with this package. */ 'table_name' => 'pages', ]; ================================================ FILE: database/factories/ModelFactory.php ================================================ id(); $table->string('title')->index(); $table->string('slug')->unique(); $table->string('layout')->index(); $table->json('blocks'); $table->foreignId('parent_id')->nullable()->constrained(config('filament-fabricator.table_name', 'pages'))->cascadeOnDelete()->cascadeOnUpdate(); $table->timestamps(); }); } public function down() { Schema::dropIfExists(config('filament-fabricator.table_name', 'pages')); } }; ================================================ FILE: database/migrations/fix_slug_unique_constraint_on_pages_table.php.stub ================================================ dropUnique(['slug']); $table->unique(['slug', 'parent_id']); }); } public function down() { Schema::table(config('filament-fabricator.table_name', 'pages'), function (Blueprint $table) { $table->dropUnique(['slug', 'parent_id']); $table->unique(['slug']); }); } }; ================================================ FILE: docs/README.md ================================================ ## Introduction

fabricator banner

**What is Filament Fabricator?** Filament Fabricator is simply said a block-based page builder skeleton. Filament Fabricator takes care of the `PageResource` & frontend routing, so you can focus on what really matters: your [Layouts](#layouts) & [Page Blocks](#page-blocks). ## Screenshots fabricator-index fabricator-edit-1 fabricator-edit-2 ## Installation Once you have [Filament Panels](https://filamentphp.com/docs/3.x/panels/installation) configured. You can install this package via composer: ```bash composer require z3d0x/filament-fabricator ``` After that run the install command: (this will publish the config & migrations) ```bash php artisan filament-fabricator:install ``` Register a `FilamentFabricatorPlugin` instance in your Panel provider: ```php use Z3d0X\FilamentFabricator\FilamentFabricatorPlugin; //.. public function panel(Panel $panel): Panel { return $panel // ... ->plugins([ FilamentFabricatorPlugin::make(), ]); } ``` Then, publish the registered plugin assets: ``` php artisan filament:assets ``` To get started create a [Layout](#layouts) and then [Page Blocks](#page-blocks) ## Layouts ### Creating a Layout Use the following command to create a new Layout: ```bash php artisan filament-fabricator:layout DefaultLayout ``` This will create the following Layout class: ```php use Z3d0X\FilamentFabricator\Layouts\Layout; class DefaultLayout extends Layout { protected static ?string $name = 'default'; } ``` and its corresponding blade component: ```blade @props(['page']) {{-- Header Here --}} {{-- Footer Here --}} ``` You may edit this layout blade file however you want, as long as you are using the `filament-fabricator::page-blocks` to show the page blocks > Pro Tip 💡: Use the `$page` instance to build your layout ### Base Layouts You may noticed that layouts created are wrapped in a `filament-fabricator::layouts.base` component. This is the [Base Layout](https://github.com/Z3d0X/filament-fabricator/blob/main/resources/views/components/layouts/base.blade.php). You can use the following, in the `boot()` of a ServiceProvider, to inject additional data to the base layout: ```php use Z3d0X\FilamentFabricator\Facades\FilamentFabricator; use Illuminate\Foundation\Vite; //Add custom tags (like `` & ``) FilamentFabricator::pushMeta([ new HtmlString(''), ]); //Register scripts FilamentFabricator::registerScripts([ 'https://unpkg.com/browse/tippy.js@6.3.7/dist/tippy.esm.js', //external url mix('js/app.js'), //laravel-mix app(Vite::class)('resources/css/app.js'), //vite asset('js/app.js'), // asset from public folder ]); //Register styles FilamentFabricator::registerStyles([ 'https://unpkg.com/tippy.js@6/dist/tippy.css', //external url mix('css/app.css'), //laravel-mix app(Vite::class)('resources/css/app.css'), //vite asset('css/app.css'), // asset from public folder ]); FilamentFabricator::favicon(asset('favicon.ico')); ``` Apart from these this plugin also adds the following [Filament's Render Hooks](https://filamentphp.com/docs/3.x/support/render-hooks) to the base layout: - `filament-fabricator::head.start` - after `` - `filament-fabricator::head.end` - before `` - `filament-fabricator::body.start` - after `` - `filament-fabricator::body.end` - before `` - `filament-fabricator::scripts.start` - before scripts are defined - `filament-fabricator::scripts.end` - after scripts are defined > Pro Tip 💡: Using a base layout is completely optional, if you don't need it you may just remove it from the generated layout blade file. If you prefer, You may also use your own base layout. > Pro Tip 💡: You might prefer using the corresponding constants defined in `\Z3d0X\FilamentFabricator\View\LayoutRenderHook` instead of hard-coded strings. ## Page Blocks ### Creating a Page Block Use the following command to create a new Page Block: ```bash php artisan filament-fabricator:block MyBlock ``` This will create the following Page Block class (& its corresponding blade component view): ```php use Filament\Forms\Components\Builder\Block; use Z3d0X\FilamentFabricator\PageBlocks\PageBlock; class MyBlock extends PageBlock { protected static string $name = 'my-block'; public static function defineBlock(Block $block): Block { return block ->schema([ // ]); } public static function mutateData(array $data): array { return $data; } } ``` > Pro Tip 💡: You can access the `$page` instance in the block, by using the [`@aware` blade directive](https://laravel.com/docs/blade#accessing-parent-data) > ```blade > {{-- `my-block.blade.php` --}} > @aware(['page']) // make sure this line exists, in order to access `$page` > > @dump($page) > ``` ### Page Block Schema Define your block schema in this method: ```php public static function defineBlock(Block $block): Block ``` You may use any [Fields](https://filamentphp.com/docs/3.x/forms/fields/getting-started#available-fields) to make up your schema. > Pro Tip 💡: You can conditionally allow blocks based on a layout using: > ```php > Block::make('foo') > ->visible(fn ($get) => $get('../layout') == 'special') > ``` ### Mutate Data By default, your blade component will receive raw data from all the fields as props Example: ```php //Given the following schema public static function defineBlock(Block $block): Block { return block ->schema([ TextInput::make('name'), ]); } ``` ```blade {{-- Your blade component would receive the following props --}} @dump($name) ``` However you may customize this behavior using: ```php //`$data` is the raw block data. public static function mutateData(array $data): array ``` The array keys from this would be your blade component props. Example: ```php // `MyBlock.php` public static function mutateData(array $data): array { return ['foo' => 'bar']; } ``` ```blade {{--- `my-block.blade.php` --}} @dump($foo) // 'bar' ``` ### Preload data In some cases, you might want to preload some data for your blocks before mutating the data and then rendering it. This is something you can do on a block type/class level: ```php /** * Hook used to mass-preload related data to reduce the number of DB queries. * For instance, to load model objects/data from their IDs * * @param (array{ * type: string, * data: array, * })[] $blocks - The array of blocks' data for the given page and the given block type */ public static function preloadRelatedData(Page $page, array &$blocks): void ``` Note that your preload logic is run once per block type/class. It helps avoid N+1 query problems. You get a mutable reference to an array of block render data that you can mutate with the data you preloaded. That being said, do keep in mind that you're working with references, you will need to throw a few `&` around to properly change your data. It can be useful, for instance, when you want to preload related models based on an array of IDs. For instance: ```php use App\Models\SomeModel; use Z3d0X\FilamentFabricator\Helpers; // [...] /** * @param (array{ * type: string, * data: array{ * title: string, * items: array{ * title: string, * ref: int, * }[] * }, * })[] $blocks - The array of blocks' data for the given page and the given block type */ #[\Override] public static function preloadRelatedData(Page $page, array &$blocks): void { Helpers::preloadRelatedModels( blocks: $blocks, property: 'items', subProperty: 'ref', modelClass: SomeModel::class, ); // now $blocks[0]['data']['items'][0]['ref'] is the related instance of SomeModel } ``` ## Page Builder Underneath the hood `PageBuilder` is just a Filament's [Builder](https://filamentphp.com/docs/3.x/forms/fields/builder) field. Like other filament fields this field also has methods that can be used to modify it. You may configure it like this: ```php use Z3d0X\FilamentFabricator\Forms\Components\PageBuilder; PageBuilder::configureUsing(function (PageBuilder $builder) { $builder->collapsible(); //You can use any method supported by the Builder field }); ``` ### Block Picker Styles In addition to [customizations available in Filament's Builder](https://filamentphp.com/docs/3.x/forms/fields/builder#customizing-the-block-picker) `PageBuilder`, also includes a new method `blockPickerStyle()`. Currently there are two styles available: - `BlockPickerStyle::Dropdown` (default) - `BlockPickerStyle::Modal` ```php use Z3d0X\FilamentFabricator\Enums\BlockPickerStyle; use Z3d0X\FilamentFabricator\Forms\Components\PageBuilder; PageBuilder::configureUsing(function (PageBuilder $builder) { $builder->blockPickerStyle(BlockPickerStyle::Modal); }); ``` an alternative one-liner way of changing block picker style is using `blockPickerStyle()` method when registering the `FilamentFabricatorPlugin` in your Panel provider: ```php use Z3d0X\FilamentFabricator\Enums\BlockPickerStyle; use Z3d0X\FilamentFabricator\FilamentFabricatorPlugin; //.. public function panel(Panel $panel): Panel { return $panel // ... ->plugins([ FilamentFabricatorPlugin::make() ->blockPickerStyle(BlockPickerStyle::Modal), ]); } ``` > Pro Tip 💡: `BlockPickerStyle::Modal` works best when icons are assigned to blocks. https://filamentphp.com/docs/3.x/forms/fields/builder#setting-a-blocks-icon ## Page Resource ### Customize Navigation You may use the following methods in the `boot()` of a ServiceProvider to customize the navigation item of `PageResource` ```php use Z3d0X\FilamentFabricator\Resources\PageResource; PageResource::navigationGroup('Blog'); PageResource::navigationSort(1); PageResource::navigationIcon('heroicon-o-cube'); ``` ### Authorization To enforce policies, after generating a policy, you would need to register `\Z3d0X\FilamentFabricator\Models\Page` to use that policy in the `AuthServiceProvider`. ```php PagePolicy::class, ]; //... } ``` > If you are using [Shield](https://filamentphp.com/plugins/bezhansalleh-shield) just register the `PagePolicy` generated by it ## Caching By default, routes will be cached in a lazy manner. That means that a page needs to be hit before it's cached. If you so choose, you can also force all pages to be cached by running the following command: ```bash php artisan filament-fabricator:clear-routes-cache ``` By running the following command instead, you'll ensure the data is fresh before it's cached: ```bash php artisan filament-fabricator:clear-routes-cache --refresh ``` ## Configuration ### Auto-routing By default, your pages' routing will be done automatically for you so you don't have to worry about it. To do that the package registers a fallback route which, when hit, will render your page. If you want manual control over how your pages are rendered, you can disable this by setting the `routing.enabled` config option in your config file to `false`. ### Route prefix If you so desire, you can add a prefix to be used in all your pages' routes. This is used in conjunction with auto-routing. For instance: If a page has a slug `page-1`, and the prefix is set to `/pages`, then you'll access that page at the URL `/pages/page-1`. > **Warning:** When changing the route prefix in the config, you'll want to run the `php artisan filament-fabricator:clear-routes-cache --refresh` command ### Hooking the route cache into Laravel's lifecycle By default routes are properly cached, cleared, and refreshed whenever you would expect it to. This is achieved by hooking into the following core commands: - `cache:clear` -> clear routes cache - `config:cache` -> refresh routes cache - `config:clear` -> clear routes cache - `optimize` -> refresh routes cache - `optimize:clear` -> clear routes cache - `route:clear` -> clear routes cache If you don't want this behavior, you can opt out of it by setting the `hook-to-commands` config option to `false` in your config file. ## Compatibility | Fabricator | Filament | PHP | |------|----------|--------| | [1.x](https://github.com/z3d0x/filament-fabricator/tree/1.x) | ^2.0 | ^8.0 | | [2.x](https://github.com/z3d0x/filament-fabricator/tree/2.x) | ^3.0 | ^8.1 | | [3.x](https://github.com/z3d0x/filament-fabricator/tree/3.x) | ^4.0 | ^8.2 | ================================================ FILE: phpstan-baseline.neon ================================================ ================================================ FILE: phpstan.neon.dist ================================================ includes: - vendor/larastan/larastan/extension.neon - vendor/nesbot/carbon/extension.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon - vendor/phpstan/phpstan-deprecation-rules/rules.neon parameters: level: 5 paths: - src - config - database - routes tmpDir: build/phpstan treatPhpDocTypesAsCertain: false ================================================ FILE: phpunit.xml.dist ================================================ ./src ./routes tests/ ./src ================================================ FILE: pint.json ================================================ { "preset": "laravel", "rules": { "blank_line_before_statement": true, "concat_space": { "spacing": "one" }, "method_argument_space": true, "single_trait_insert_per_statement": true } } ================================================ FILE: rector.php ================================================ withPaths([ __DIR__ . '/bootstrap', __DIR__ . '/config', __DIR__ . '/routes', __DIR__ . '/src', __DIR__ . '/tests', ]) ->withCache() ->withParallel() ->withRules([ AddOverrideAttributeToOverriddenMethodsRector::class, TypedPropertyFromStrictConstructorRector::class, ]) ->withSkip([ // Only add classes that give false positives here ClosureToArrowFunctionRector::class, DisallowedEmptyRuleFixerRector::class, RemoveAlwaysTrueIfConditionRector::class, RemoveUnreachableStatementRector::class, SimplifyEmptyCheckOnEmptyArrayRector::class, SimplifyUselessVariableRector::class, ]) ->withPhpSets() ->withPreparedSets( deadCode: true, codeQuality: true, earlyReturn: true, ) ->withSetProviders(LaravelSetProvider::class) ->withComposerBased(laravel: true, phpunit: true) ->withSets([ LaravelLevelSetList::UP_TO_LARAVEL_110, ]); ================================================ FILE: resources/lang/ar/page-resource.php ================================================ [ 'blocks' => 'الوحدات', 'layout' => 'المخطط', 'page' => 'صفحة', 'pages' => 'صفحات', 'parent' => 'الرئيسي', 'slug' => 'الرابط المميز', 'title' => 'العنوان', 'url' => 'الرابط', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'الرابط المميز لا يمكن أن يبدأ أو ينتهي بشرطة مائلة.', ], 'actions' => [ 'save' => 'حفظ', 'visit' => 'زيارة', ], ]; ================================================ FILE: resources/lang/en/page-resource.php ================================================ [ 'blocks' => 'Blocks', 'layout' => 'Layout', 'page' => 'Page', 'pages' => 'Pages', 'parent' => 'Parent', 'slug' => 'Slug', 'title' => 'Title', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'The slug cannot start or end with a slash.', ], 'actions' => [ 'save' => 'Save', 'visit' => 'Visit', ], ]; ================================================ FILE: resources/lang/fr/page-resource.php ================================================ [ 'blocks' => 'Blocs', 'layout' => 'Mise en page', 'page' => 'Page', 'pages' => 'Pages', 'parent' => 'Parent', 'slug' => 'Slug', 'title' => 'Titre', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'Le slug ne peut pas commencer ou se terminer par un slash.', ], 'actions' => [ 'save' => 'Enregistrer', 'visit' => 'Visiter', ], ]; ================================================ FILE: resources/lang/id/page-resource.php ================================================ [ 'blocks' => 'Blok', 'layout' => 'Tata Letak', 'page' => 'Halaman', 'pages' => 'Halaman-halaman', 'parent' => 'Induk', 'slug' => 'Slug', 'title' => 'Judul', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'Slug tidak boleh diawali atau diakhiri dengan garis miring.', ], 'actions' => [ 'save' => 'Simpan', 'visit' => 'Kunjungi', ], ]; ================================================ FILE: resources/lang/nl/page-resource.php ================================================ [ 'blocks' => 'Blokken', 'layout' => 'Ontwerp', 'page' => 'Pagina', 'pages' => 'Pagina\'s', 'parent' => 'Bovenliggend', 'slug' => 'Slug', 'title' => 'Titel', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'De slug mag niet beginnen of eindigen met een schuine streep.', ], 'actions' => [ 'save' => 'Opslaan', 'visit' => 'Bezoeken', ], ]; ================================================ FILE: resources/lang/pl/page-resource.php ================================================ [ 'blocks' => 'Bloki', 'layout' => 'Układ', 'page' => 'Strona', 'pages' => 'Strony', 'parent' => 'Element nadrzędny', 'slug' => 'Slug', 'title' => 'Tytuł', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'Slug nie może zaczynać się ani kończyć znakiem „/”.', ], 'actions' => [ 'save' => 'Zapisz', 'visit' => 'Odwiedź', ], ]; ================================================ FILE: resources/lang/ru/page-resource.php ================================================ [ 'blocks' => 'Блоки', 'layout' => 'Макет', 'page' => 'Страница', 'pages' => 'Страницы', 'parent' => 'Родительская страница', 'slug' => 'Ссылка', 'title' => 'Название', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'Ссылка не может начинаться или заканчиваться косой чертой.', ], 'actions' => [ 'save' => 'Сохранить', 'visit' => 'Посетить', ], ]; ================================================ FILE: resources/lang/tr/page-resource.php ================================================ [ 'blocks' => 'Bloklar', 'layout' => 'Düzen', 'page' => 'Sayfa', 'pages' => 'Sayfalar', 'parent' => 'Üst', 'slug' => 'Kısa Ad', 'title' => 'Başlık', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'Kısa ad bir eğik çizgi ile başlayamaz veya bitirilemez.', ], 'actions' => [ 'save' => 'Kaydet', 'visit' => 'Siteyi Aç', ], ]; ================================================ FILE: resources/lang/uk/page-resource.php ================================================ [ 'blocks' => 'Блоки', 'layout' => 'Макет', 'page' => 'Сторінка', 'pages' => 'Сторінки', 'parent' => 'Батьківська сторінка', 'slug' => 'Посилання', 'title' => 'Назва', 'url' => 'URL', ], 'errors' => [ 'slug_starts_or_ends_with_slash' => 'Посилання не може починатися або закінчуватися косою межею.', ], 'actions' => [ 'save' => 'Зберегти', 'visit' => 'Відвідати', ], ]; ================================================ FILE: resources/views/components/forms/components/page-builder/dropdown-block-picker.blade.php ================================================ @props([ 'action', 'afterItem' => null, 'blocks', 'columns' => null, 'key', 'statePath', 'trigger', 'width' => null, ]) class(['fi-fo-builder-block-picker']) }} > {{ $trigger }}
@foreach ($blocks as $block) @php $wireClickActionArguments = ['block' => $block->getName()]; if (filled($afterItem)) { $wireClickActionArguments['afterItem'] = $afterItem; } $wireClickActionArguments = \Illuminate\Support\Js::from($wireClickActionArguments); $wireClickAction = "mountAction('{$action->getName()}', {$wireClickActionArguments}, { schemaComponent: '{$key}' })"; @endphp {{ $block->getLabel() }} @endforeach
================================================ FILE: resources/views/components/forms/components/page-builder/modal-block-picker.blade.php ================================================ @props([ 'action', 'afterItem' => null, 'blocks', 'columns' => null, 'key', 'statePath', 'trigger', 'width' => null, ]) class(['fi-fo-builder-block-picker']) }} >
{{ $trigger }}
@foreach ($blocks as $block) @php $wireClickActionArguments = ['block' => $block->getName()]; if (filled($afterItem)) { $wireClickActionArguments['afterItem'] = $afterItem; } $wireClickActionArguments = \Illuminate\Support\Js::from($wireClickActionArguments); $wireClickAction = "mountAction('{$action->getName()}', {$wireClickActionArguments}, { schemaComponent: '{$key}' })"; @endphp @endforeach
================================================ FILE: resources/views/components/forms/components/page-builder.blade.php ================================================ @php use Filament\Actions\Action; use Z3d0X\FilamentFabricator\Enums\BlockPickerStyle; use Illuminate\Support\MessageBag; use Illuminate\Support\ViewErrorBag; $containers = $getChildComponentContainers(); $blockPickerBlocks = $getBlockPickerBlocks(); $blockPickerColumns = $getBlockPickerColumns(); $blockPickerWidth = $getBlockPickerWidth(); $blockPickerStyle = $getBlockPickerStyle(); $addAction = $getAction($getAddActionName()); $addBetweenAction = $getAction($getAddBetweenActionName()); $cloneAction = $getAction($getCloneActionName()); $collapseAllAction = $getAction($getCollapseAllActionName()); $expandAllAction = $getAction($getExpandAllActionName()); $deleteAction = $getAction($getDeleteActionName()); $moveDownAction = $getAction($getMoveDownActionName()); $moveUpAction = $getAction($getMoveUpActionName()); $reorderAction = $getAction($getReorderActionName()); $extraItemActions = $getExtraItemActions(); $isAddable = $isAddable(); $isCloneable = $isCloneable(); $isCollapsible = $isCollapsible(); $isDeletable = $isDeletable(); $isReorderableWithButtons = $isReorderableWithButtons(); $isReorderableWithDragAndDrop = $isReorderableWithDragAndDrop(); $key = $getKey(); $statePath = $getStatePath(); @endphp
merge($getExtraAttributes(), escape: false) ->class(['fi-fo-builder grid gap-y-4']) }} > @if ($isCollapsible && ($collapseAllAction->isVisible() || $expandAllAction->isVisible()))
count($containers) < 2, ]) > @if ($collapseAllAction->isVisible()) {{ $collapseAllAction }} @endif @if ($expandAllAction->isVisible()) {{ $expandAllAction }} @endif
@endif @if (count($containers))
    @php $hasBlockLabels = $hasBlockLabels(); $hasBlockNumbers = $hasBlockNumbers(); @endphp @foreach ($containers as $uuid => $item) @php $visibleExtraItemActions = array_filter( $extraItemActions, fn (Action $action): bool => $action(['item' => $uuid])->isVisible(), ); @endphp
  • @if ($isReorderableWithDragAndDrop || $isReorderableWithButtons || $hasBlockLabels || $isCloneable || $isDeletable || $isCollapsible || count($visibleExtraItemActions))
    @if ($isReorderableWithDragAndDrop || $isReorderableWithButtons)
      @if ($isReorderableWithDragAndDrop)
    • {{ $reorderAction }}
    • @endif @if ($isReorderableWithButtons)
    • {{ $moveUpAction(['item' => $uuid])->disabled($loop->first) }}
    • {{ $moveDownAction(['item' => $uuid])->disabled($loop->last) }}
    • @endif
    @endif @if ($hasBlockLabels)

    $isBlockLabelTruncated(), 'cursor-pointer select-none' => $isCollapsible, ]) > {{ $item->getParentComponent()->getLabel($item->getRawState(), $uuid) }} @if ($hasBlockNumbers) {{ $loop->iteration }} @endif

    @endif @if ($isCloneable || $isDeletable || $isCollapsible || count($visibleExtraItemActions))
      @foreach ($visibleExtraItemActions as $extraItemAction)
    • {{ $extraItemAction(['item' => $uuid]) }}
    • @endforeach @if ($isCloneable)
    • {{ $cloneAction(['item' => $uuid]) }}
    • @endif @if ($isDeletable)
    • {{ $deleteAction(['item' => $uuid]) }}
    • @endif @if ($isCollapsible)
    • {{ $getAction('collapse') }}
      {{ $getAction('expand') }}
    • @endif
    @endif
    @endif
    {{ $item }}
  • @if (! $loop->last) @if ($isAddable && $addBetweenAction->isVisible()) @elseif (filled($labelBetweenItems = $getLabelBetweenItems()))
  • {{ $labelBetweenItems }}
  • @endif @endif @endforeach
@endif @if ($isAddable) @if ($blockPickerStyle === BlockPickerStyle::Dropdown) {{ $addAction }} @elseif ($blockPickerStyle === BlockPickerStyle::Modal) {{ $addAction }} @endif @endif
================================================ FILE: resources/views/components/layouts/base.blade.php ================================================ @props([ 'page', 'title' => null, 'dir' => 'ltr', ]) @use(Z3d0X\FilamentFabricator\View\LayoutRenderHook) {{ \Filament\Support\Facades\FilamentView::renderHook(LayoutRenderHook::HEAD_START) }} @foreach (\Z3d0X\FilamentFabricator\Facades\FilamentFabricator::getMeta() as $tag) {{ $tag }} @endforeach @if ($favicon = \Z3d0X\FilamentFabricator\Facades\FilamentFabricator::getFavicon()) @endif {{ $title ? "{$title} - " : null }} {{ config('app.name') }} @foreach (\Z3d0X\FilamentFabricator\Facades\FilamentFabricator::getStyles() as $name => $path) @if (\Illuminate\Support\Str::of($path)->startsWith('<')) {!! $path !!} @else @endif @endforeach {{ \Filament\Support\Facades\FilamentView::renderHook(LayoutRenderHook::HEAD_END) }} {{ \Filament\Support\Facades\FilamentView::renderHook(LayoutRenderHook::BODY_START) }} {{ $slot }} {{ \Filament\Support\Facades\FilamentView::renderHook(LayoutRenderHook::SCRIPTS_START) }} @foreach (\Z3d0X\FilamentFabricator\Facades\FilamentFabricator::getScripts() as $name => $path) @if (\Illuminate\Support\Str::of($path)->startsWith('<')) {!! $path !!} @else @endif @endforeach @stack('scripts') {{ \Filament\Support\Facades\FilamentView::renderHook(LayoutRenderHook::SCRIPTS_END) }} {{ \Filament\Support\Facades\FilamentView::renderHook(LayoutRenderHook::BODY_END) }} ================================================ FILE: resources/views/components/page-blocks.blade.php ================================================ @aware(['page']) @props(['blocks' => []]) @php $groups = \Z3d0X\FilamentFabricator\Helpers::arrayRefsGroupBy($blocks, 'type'); foreach ($groups as $blockType => &$group) { /** * @var class-string<\Z3d0X\FilamentFabricator\PageBlocks\PageBlock> $blockClass */ $blockClass = \Z3d0X\FilamentFabricator\Facades\FilamentFabricator::getPageBlockFromName($blockType); if (!empty($blockClass) && $page !== null) { $blockClass::preloadRelatedData($page, $group); } } @endphp @foreach ($blocks as $blockData) @php $pageBlock = \Z3d0X\FilamentFabricator\Facades\FilamentFabricator::getPageBlockFromName($blockData['type']); @endphp @isset($pageBlock) @endisset @endforeach ================================================ FILE: resources/views/preview.blade.php ================================================ @props(['page', 'component']) ================================================ FILE: resources/views/tests/fixtures/blade-wrapper.blade.php ================================================
{{ $this->form }}
================================================ FILE: routes/web.php ================================================ prefix(FilamentFabricator::getRoutingPrefix()) ->group(function () { Route::get('/{filamentFabricatorPage?}', PageController::class) ->where('filamentFabricatorPage', '.*') ->fallback(); }); } ================================================ FILE: src/Commands/Aliases/MakeLayoutCommand.php ================================================ option('refresh'); /** * @var array $pages */ $pages = FilamentFabricator::getPageModel()::query() ->whereNull('parent_id') ->with('allChildren') ->get(); foreach ($pages as $page) { $this->clearPageCache($page, $shouldRefresh); if ($shouldRefresh) { $this->pageRoutesService->updateUrlsOf($page); } } return static::SUCCESS; } protected function clearPageCache(PageContract $page, bool $shouldRefresh = false) { $this->pageRoutesService->removeUrlsOf($page); $argSets = $page->getAllUrlCacheKeysArgs(); foreach ($argSets as $args) { $key = $page->getUrlCacheKey($args); Cache::forget($key); if ($shouldRefresh) { // Caches the URL before returning it /* $noop = */ $page->getUrl($args); } } $childPages = $page->allChildren; if (filled($childPages)) { foreach ($childPages as $childPage) { $this->clearPageCache($childPage, $shouldRefresh); } } } } ================================================ FILE: src/Commands/MakeLayoutCommand.php ================================================ argument('name') ?? text( label: 'What is the layout name?', placeholder: 'DefaultLayout', required: true, )) ->trim('/') ->trim('\\') ->trim(' ') ->replace('/', '\\'); $layoutClass = (string) Str::of($layout)->afterLast('\\'); $layoutNamespace = Str::of($layout)->contains('\\') ? (string) Str::of($layout)->beforeLast('\\') : ''; $shortName = Str::of($layout) ->beforeLast('Layout') ->explode('\\') ->map(fn ($segment) => Str::kebab($segment)) ->implode('.'); $view = Str::of($layout) ->beforeLast('Layout') ->prepend('components\\filament-fabricator\\layouts\\') ->explode('\\') ->map(fn ($segment) => Str::kebab($segment)) ->implode('.'); $path = app_path( (string) Str::of($layout) ->prepend('Filament\\Fabricator\\Layouts\\') ->replace('\\', '/') ->append('.php'), ); $viewPath = resource_path( (string) Str::of($view) ->replace('.', '/') ->prepend('views/') ->append('.blade.php'), ); $files = [$path, $viewPath]; if (! $this->option('force') && $this->checkForCollision($files)) { return static::INVALID; } $this->copyStubToApp('Layout', $path, [ 'class' => $layoutClass, 'namespace' => 'App\\Filament\\Fabricator\\Layouts' . ($layoutNamespace !== '' ? "\\{$layoutNamespace}" : ''), 'shortName' => $shortName, ]); $this->copyStubToApp('LayoutView', $viewPath); $this->info("Successfully created {$layout}!"); return static::SUCCESS; } } ================================================ FILE: src/Commands/MakePageBlockCommand.php ================================================ argument('name') ?? text( label: 'What is the block name?', placeholder: 'HeroBlock', required: true, )) ->trim('/') ->trim('\\') ->trim(' ') ->replace('/', '\\'); $pageBlockClass = (string) Str::of($pageBlock)->afterLast('\\'); $pageBlockNamespace = Str::of($pageBlock)->contains('\\') ? (string) Str::of($pageBlock)->beforeLast('\\') : ''; $shortName = Str::of($pageBlock) ->beforeLast('Block') ->explode('\\') ->map(fn ($segment) => Str::kebab($segment)) ->implode('.'); $view = Str::of($pageBlock) ->beforeLast('Block') ->prepend('components\\filament-fabricator\\page-blocks\\') ->explode('\\') ->map(fn ($segment) => Str::kebab($segment)) ->implode('.'); $path = app_path( (string) Str::of($pageBlock) ->prepend('Filament\\Fabricator\\PageBlocks\\') ->replace('\\', '/') ->append('.php'), ); $viewPath = resource_path( (string) Str::of($view) ->replace('.', '/') ->prepend('views/') ->append('.blade.php'), ); $files = [$path, $viewPath]; if (! $this->option('force') && $this->checkForCollision($files)) { return static::INVALID; } $this->copyStubToApp('PageBlock', $path, [ 'class' => $pageBlockClass, 'namespace' => 'App\\Filament\\Fabricator\\PageBlocks' . ($pageBlockNamespace !== '' ? "\\{$pageBlockNamespace}" : ''), 'shortName' => $shortName, ]); $this->copyStubToApp('PageBlockView', $viewPath); $this->info("Successfully created {$pageBlock}!"); return static::SUCCESS; } } ================================================ FILE: src/Enums/BlockPickerStyle.php ================================================ getPageModel() * @method static ?string getRoutingPrefix() * @method static array getPageUrls() * @method static ?string getPageUrlFromId(int $id, bool $prefixSlash = false) * * @see FilamentFabricatorManager */ class FilamentFabricator extends Facade { protected static function getFacadeAccessor() { return 'filament-fabricator'; } } ================================================ FILE: src/FilamentFabricatorManager.php ================================================ > */ protected Collection $pageBlocks; /** @var Collection> */ protected Collection $layouts; protected array $schemaSlot = []; protected array $meta = []; protected array $scripts = []; protected array $styles = []; protected ?string $favicon = 'favicon.ico'; protected array $pageUrls = []; /** * @note It's only separated to not cause a major version change. * In the next major release, feel free to make it a constructor promoted property */ protected PageRoutesService $routesService; public function __construct(?PageRoutesService $routesService = null) { $this->routesService = $routesService ?? resolve(PageRoutesService::class); /** @var Collection> */ $pageBlocks = collect([]); /** @var Collection> */ $layouts = collect([]); $this->pageBlocks = $pageBlocks; $this->layouts = $layouts; } /** * @param class-string $class * @param class-string|class-string $baseClass */ public function registerComponent(string $class, string $baseClass): void { match ($baseClass) { Layout::class => static::registerLayout($class), PageBlock::class => static::registerPageBlock($class), default => throw new Exception('Invalid class type'), }; } /** @param class-string $layout */ public function registerLayout(string $layout): void { if (! is_subclass_of($layout, Layout::class)) { throw new InvalidArgumentException("{$layout} must extend " . Layout::class); } $this->layouts->put($layout::getName(), $layout); } /** @param class-string $pageBlock */ public function registerPageBlock(string $pageBlock): void { if (! is_subclass_of($pageBlock, PageBlock::class)) { throw new InvalidArgumentException("{$pageBlock} must extend " . PageBlock::class); } $this->pageBlocks->put($pageBlock::getName(), $pageBlock); } public function registerSchemaSlot(string $name, array|Closure $schema): void { $this->schemaSlot[$name] = $schema; } public function pushMeta(array $meta): void { $this->meta = array_merge($this->meta, $meta); } public function registerScripts(array $scripts): void { $this->scripts = array_merge($this->scripts, $scripts); } public function registerStyles(array $styles): void { $this->styles = array_merge($this->styles, $styles); } public function favicon(string $favicon): void { $this->favicon = $favicon; } /** * @return class-string|null */ public function getLayoutFromName(string $layoutName): ?string { return $this->layouts->get($layoutName); } /** * @return class-string|null */ public function getPageBlockFromName(string $name): ?string { return $this->pageBlocks->get($name); } /** * Get the list of registered layout labels/names * * @return string[] */ public function getLayouts(): array { return $this->layouts->map(fn ($layout) => $layout::getLabel())->toArray(); } public function getDefaultLayoutName(): ?string { return $this->layouts->keys()->first(); } /** * @return Block[] */ public function getPageBlocks(): array { return $this->pageBlocks->map(fn ($block) => $block::getBlockSchema())->toArray(); } public function getPageBlocksRaw(): array { return $this->pageBlocks->toArray(); } public function getSchemaSlot(string $name): array|Closure { return $this->schemaSlot[$name] ?? []; } public function getMeta(): array { return array_unique($this->meta); } public function getScripts(): array { return $this->scripts; } public function getStyles(): array { return $this->styles; } public function getFavicon(): ?string { return $this->favicon; } /** @return class-string */ public function getPageModel(): string { return config('filament-fabricator.page-model') ?? Page::class; } public function getRoutingPrefix(): ?string { $prefix = config('filament-fabricator.routing.prefix'); if (is_null($prefix)) { return null; } $prefix = Str::start($prefix, '/'); if ($prefix === '/') { return $prefix; } return rtrim($prefix, '/'); } /** * @return string[] */ public function getPageUrls(): array { return $this->routesService->getAllUrls(); } public function getPageUrlFromId(int|string $id, array $args = []): ?string { /** @var (PageContract&Model)|null $page */ $page = $this->getPageModel()::query()->find($id); return $page?->getUrl($args); } } ================================================ FILE: src/FilamentFabricatorPlugin.php ================================================ resources(array_filter([ config('filament-fabricator.page-resource'), ])); if (! $panel->hasPlugin(FilamentPeekPlugin::ID)) { // Automatically register FilamentPeekPlugin if it is not already registered $panel->plugin(FilamentPeekPlugin::make()); } } public function boot(Panel $panel): void { // } public function blockPickerStyle(?BlockPickerStyle $style): static { $this->blockPickerStyle = $style; return $this; } public function getBlockPickerStyle(): ?BlockPickerStyle { return $this->blockPickerStyle; } public static function get(): static { /** @var static $plugin */ $plugin = filament(app(static::class)->getId()); return $plugin; } } ================================================ FILE: src/FilamentFabricatorServiceProvider.php ================================================ name(FilamentFabricatorManager::ID) ->hasConfigFile() ->hasMigrations( 'create_pages_table', 'fix_slug_unique_constraint_on_pages_table', ) ->hasRoute('web') ->hasViews() ->hasTranslations() ->hasCommands($this->getCommands()) ->hasInstallCommand(function (InstallCommand $installCommand) { $installCommand ->startWith(fn (InstallCommand $installCommand) => $installCommand->call('filament:upgrade')) ->publishConfigFile() ->publishMigrations() ->askToRunMigrations() ->askToStarRepoOnGitHub('z3d0x/filament-fabricator'); }); } protected function getCommands(): array { $commands = [ MakeLayoutCommand::class, MakePageBlockCommand::class, ClearRoutesCacheCommand::class, ]; $aliases = []; foreach ($commands as $command) { $class = 'Z3d0X\\FilamentFabricator\\Commands\\Aliases\\' . class_basename($command); if (! class_exists($class)) { continue; } $aliases[] = $class; } return array_merge($commands, $aliases); } public function packageRegistered(): void { parent::packageRegistered(); $this->app->singleton('filament-fabricator', function () { return resolve(FilamentFabricatorManager::class); }); } public function bootingPackage(): void { if (! $this->app->runningInConsole() || $this->app->runningUnitTests()) { Route::bind('filamentFabricatorPage', function ($value) { /** * @var PageRoutesService $routesService */ $routesService = resolve(PageRoutesService::class); return $routesService->findPageOrFail($value); }); $this->registerComponentsFromDirectory( Layout::class, config('filament-fabricator.layouts.register'), config('filament-fabricator.layouts.path'), config('filament-fabricator.layouts.namespace') ); $this->registerComponentsFromDirectory( PageBlock::class, config('filament-fabricator.page-blocks.register'), config('filament-fabricator.page-blocks.path'), config('filament-fabricator.page-blocks.namespace') ); } } public function packageBooted() { parent::packageBooted(); FilamentFabricator::getPageModel()::observe(PageRoutesObserver::class); if ((bool) config('filament-fabricator.hook-to-commands')) { Event::listen(CommandFinished::class, OptimizeWithLaravel::class); } } /** * @template T of (class-string|class-string) * * @param T $baseClass * @param T[] $register - The components to register taken from the user's config file */ protected function registerComponentsFromDirectory(string $baseClass, array $register, ?string $directory, ?string $namespace): void { if (blank($directory) || blank($namespace)) { return; } $filesystem = app(Filesystem::class); if ((! $filesystem->exists($directory)) && (! Str::of($directory)->contains('*'))) { return; } $namespace = Str::of($namespace); collect($filesystem->allFiles($directory)) ->lazy() ->map(function (SplFileInfo $file) use ($namespace): string { /** * @var ?string $variableNamespace */ $variableNamespace = $namespace->contains('*') ? str_ireplace( ['\\' . $namespace->before('*'), $namespace->after('*')], ['', ''], Str::of($file->getPath()) ->after(base_path()) ->replace(['/'], ['\\']), ) : null; return $namespace ->append('\\', $file->getRelativePathname()) ->when($variableNamespace, fn ($namespace) => $namespace->replace('*', $variableNamespace)) ->replace(['/', '.php'], ['\\', '']) ->toString(); }) ->concat($register) ->filter(fn (string $class): bool => is_subclass_of($class, $baseClass) && (! (new ReflectionClass($class))->isAbstract())) ->each(fn (string $class) => FilamentFabricator::registerComponent($class, $baseClass)) ->all(); } } ================================================ FILE: src/Forms/Components/PageBuilder.php ================================================ blocks(FilamentFabricator::getPageBlocks()); $this->mutateDehydratedStateUsing(static function (?array $state): array { if (! is_array($state)) { return []; } $registerPageBlockNames = array_keys(FilamentFabricator::getPageBlocksRaw()); return collect($state) ->filter(fn (array $block) => in_array($block['type'], $registerPageBlockNames, true)) ->values() ->toArray(); }); $blockPickerStyle = FilamentFabricatorPlugin::get()->getBlockPickerStyle(); if (! is_null($blockPickerStyle)) { $this->blockPickerStyle($blockPickerStyle); } } public function blockPickerStyle(BlockPickerStyle $style): static { if ($style === BlockPickerStyle::Modal) { $this->blockPickerColumns(3); } $this->blockPickerStyle = $style; return $this; } public function getBlockPickerStyle(): BlockPickerStyle { return $this->blockPickerStyle; } } ================================================ FILE: src/Helpers.php ================================================ , * }[] $blocks * @param class-string $modelClass * @param null|Closure(Builder):Builder $editQuery */ public static function preloadRelatedModels( array &$blocks, string $property, string $modelClass, ?string $subProperty = null, ?Closure $editQuery = null, string $primaryKeyColumn = 'id', ): void { $editQuery ??= fn (Builder $builder) => $builder; $targetsSubProperty = $subProperty !== null; $ids = collect($blocks) ->lazy() ->map(function ($block) use ($targetsSubProperty, $property, $subProperty) { $collection = collect($block['data'][$property]); if ($targetsSubProperty) { $collection = $collection->pluck($subProperty); } return $collection->all(); }) ->flatten() ->filter() ->unique() ->toArray(); $query = $modelClass::query() ->whereIn($primaryKeyColumn, $ids); $query = $editQuery($query); /** * @var TModel[] $models */ $models = $query->get(); $models = collect($models)->groupBy($primaryKeyColumn); foreach ($blocks as &$block) { if ($targetsSubProperty) { foreach ($block['data'][$property] as &$item) { $rawData = $item[$subProperty]; $item[$subProperty] = is_array($rawData) ? array_map(fn ($key) => data_get($models, (string) $key)->first(), $rawData) : data_get($models, (string) $rawData)->first(); } } else { $rawData = $block['data'][$property]; $block['data'][$property] = is_array($rawData) ? array_map(fn ($key) => data_get($models, (string) $key)->first(), $rawData) : data_get($models, (string) $rawData)->first(); } } } } ================================================ FILE: src/Http/Controllers/PageController.php ================================================ findPageOrFail('/'); } /** @var ?class-string $layout */ $layout = FilamentFabricator::getLayoutFromName($filamentFabricatorPage->layout); if (! isset($layout)) { throw new Exception("Filament Fabricator: Layout \"{$filamentFabricatorPage->layout}\" not found"); } /** @var string $component */ $component = $layout::getComponent(); return Blade::render( <<<'BLADE' BLADE, ['component' => $component, 'page' => $filamentFabricatorPage] ); } } ================================================ FILE: src/Layouts/Layout.php ================================================ shouldHandleEvent($event)) { return; } if ($this->shouldRefresh($event)) { $this->refresh(); } else { $this->clear(); } } public function shouldHandleEvent(CommandFinished $event) { return $event->exitCode === Command::SUCCESS && in_array($event->command, static::COMMANDS); } public function shouldRefresh(CommandFinished $event) { return in_array($event->command, static::REFRESH_COMMANDS); } public function refresh() { $this->callCommand([ '--refresh' => true, ]); } public function clear() { $this->callCommand(); } public function callCommand(array $params = []) { Artisan::call(ClearRoutesCacheCommand::class, $params); } } ================================================ FILE: src/Models/Concerns/HandlesPageUrls.php ================================================ $args */ public function getUrlCacheKey(array $args = []): string { // $keyArgs = collect($this->getDefaultUrlArgs())->merge($args)->all(); $id = $this->id; return "filament-fabricator::page-url--$id"; } /** * Get the URL determined by this entity and the provided arguments * * @param array $args */ public function getUrl(array $args = []): string { $cacheKey = $this->getUrlCacheKey($args); // NOTE: Users must run the command that clears the routes cache if the routing prefix ever changes return Cache::rememberForever($cacheKey, function () use ($args) { /** * @var ?Page $parent */ $parent = $this->parent; // If there's no parent page, then the "parent" URI is just the routing prefix. $parentUri = is_null($parent) ? (FilamentFabricator::getRoutingPrefix() ?? '/') : $parent->getUrl($args); // Every URI in cache has a leading slash, this ensures it's // present even if the prefix doesn't have it set explicitly $parentUri = Str::start($parentUri, '/'); // This page's part of the URL (i.e. its URI) is defined as the slug. // For the same reasons as above, we need to add a leading slash. $selfUri = $this->slug; $selfUri = Str::start($selfUri, '/'); // If the parent URI is the root, then we have nothing to glue on. // Therefore the page's URL is simply its URI. // This avoids having two consecutive slashes. if ($parentUri === '/') { return $selfUri; } // Remove any trailing slash in the parent URI since // every URIs we'll use has a leading slash. // This avoids having two consecutive slashes. $parentUri = rtrim($parentUri, '/'); return "{$parentUri}{$selfUri}"; }); } /** * Get all the available argument sets for the available cache keys * * @return array[] */ public function getAllUrlCacheKeysArgs(): array { // By default, the entire list of available URL cache keys // is simply a list containing the default one since we can't // magically infer all the possible state for the library user's customizations. return [ $this->getDefaultUrlCacheArgs(), ]; } /** * Get all the available URLs for this entity * * @return string[] */ public function getAllUrls(): array { return array_map([$this, 'getUrl'], $this->getAllUrlCacheKeysArgs()); } /** * Get all the cache keys for the available URLs for this entity * * @return string[] */ public function getAllUrlCacheKeys(): array { return array_map([$this, 'getUrlCacheKey'], $this->getAllUrlCacheKeysArgs()); } } ================================================ FILE: src/Models/Contracts/HasPageUrls.php ================================================ $children * @property-read Collection $allChildren * @property-read Carbon $created_at * @property-read Carbon $updated_at */ interface Page extends HasPageUrls { public function parent(): BelongsTo; public function children(): HasMany; public function allChildren(): HasMany; /** @return Builder */ public static function query(); } ================================================ FILE: src/Models/Page.php ================================================ table)) { $this->setTable(config('filament-fabricator.table_name', 'pages')); } parent::__construct($attributes); } protected $guarded = []; public function parent(): BelongsTo { return $this->belongsTo(static::class, 'parent_id'); } public function children(): HasMany { return $this->hasMany(static::class, 'parent_id'); } public function allChildren(): HasMany { return $this->children() ->select('id', 'slug', 'title', 'parent_id') ->with('allChildren:id,slug,title,parent_id'); } protected function casts(): array { return array_merge(parent::casts(), [ 'blocks' => 'array', 'parent_id' => 'integer', ]); } } ================================================ FILE: src/Observers/PageRoutesObserver.php ================================================ pageRoutesService->updateUrlsOf($page); } /** * Handle the Page "updated" event. */ public function updated(PageContract&Model $page): void { // If the parent_id has changed, and if the relationship has already been loaded // then after an update we might not read the right parent. That's why we always // load it on update, this ensures we clear the old URLs properly (they were cached) // and set the new ones properly (we have the right parent to do so). if ($page->wasChanged('parent_id')) { $page->load('parent'); } $this->pageRoutesService->updateUrlsOf($page); } /** * Handle the Page "deleting" event. */ public function deleting(PageContract&Model $page): void { // We do the logic in `deleting` instead of `deleted` since we need access to the object // both in memory and in database (e.g. to load relationship data). // Before properly deleting it, remove its URLs from // all the mappings and caches. $this->pageRoutesService->removeUrlsOf($page); // Doubly-linked list style deletion: // - Re-attache the given page children to the parent of the given page // - Promote the pages to a "root page" (i.e. page with no parent) if the given page had no parent // Only load one level of children since they're the ones that will be re-attached $page->load('children'); $children = $page->children; foreach ($children as $childPage) { /** * @var Model|PageContract $childPage */ // We use `?? null` followed by `?: null` to go around the cast to integer // and make sure we have `null` instead of `0` when there's no parent. $parentId = $page->parent_id ?? null; $parentId = $parentId ?: null; // Using update, instead of associate or dissociate, we trigger DB events (which we need) $childPage->update([ 'parent_id' => $parentId, ]); } } /** * Handle the Page "deleted" event. */ public function deleted(PageContract&Model $page): void { // Since `deleting` is called before `deleted`, and // since everything is handled there, do nothing. } /** * Handle the Page "restored" event. */ public function restored(PageContract&Model $page): void { $this->pageRoutesService->updateUrlsOf($page); } /** * Handle the Page "force deleted" event. */ public function forceDeleting(PageContract&Model $page): void { // You always go through `deleting` before going through `forceDeleting`. // Since everything is properly handled in `deleting`, do nothing here. } /** * Handle the Page "force deleted" event. */ public function forceDeleted(PageContract&Model $page): void { // You always go through `deleted` before going through `forceDeleted`. // Since everything is properly handled in `deleted`, do nothing here. } } ================================================ FILE: src/PageBlocks/PageBlock.php ================================================ , * })[] $blocks - The array of blocks' data for the given page and the given block type */ public static function preloadRelatedData(Page $page, array &$blocks): void {} } ================================================ FILE: src/Resources/PageResource/Pages/Concerns/HasPreviewModal.php ================================================ data['layout'] ?? null; if (empty($layoutName)) { return []; } $layout = FilamentFabricator::getLayoutFromName($layoutName); if (empty($layout)) { throw new Exception("Filament Fabricator: Layout \"{$layoutName}\" not found"); } /** @var string $component */ $component = $layout::getComponent(); $data['component'] = $component; return $data; } } ================================================ FILE: src/Resources/PageResource/Pages/CreatePage.php ================================================ visible(config('filament-fabricator.enable-view-page')), DeleteAction::make(), Action::make('visit') ->label(__('filament-fabricator::page-resource.actions.visit')) ->url(function () { /** @var PageContract&Model $page */ $page = $this->getRecord(); return FilamentFabricator::getPageUrlFromId($page->id); }) ->icon('heroicon-o-arrow-top-right-on-square') ->openUrlInNewTab() ->color('success') ->visible(config('filament-fabricator.routing.enabled')), Action::make('save') ->action('save') ->label(__('filament-fabricator::page-resource.actions.save')), ]; } } ================================================ FILE: src/Resources/PageResource/Pages/ListPages.php ================================================ label(__('filament-fabricator::page-resource.actions.visit')) ->url(function () { /** @var PageContract&Model $page */ $page = $this->getRecord(); return FilamentFabricator::getPageUrlFromId($page->id); }) ->icon('heroicon-o-arrow-top-right-on-square') ->openUrlInNewTab() ->color('success') ->visible(config('filament-fabricator.routing.enabled')), ]; } } ================================================ FILE: src/Resources/PageResource.php ================================================ columns(3) ->components([ Group::make() ->schema([ Group::make()->schema(FilamentFabricator::getSchemaSlot(ResourceSchemaSlot::BLOCKS_BEFORE)), PageBuilder::make('blocks') ->label(__('filament-fabricator::page-resource.labels.blocks')), Group::make()->schema(FilamentFabricator::getSchemaSlot(ResourceSchemaSlot::BLOCKS_AFTER)), ]) ->columnSpan(2), Group::make() ->columnSpan(1) ->schema([ Group::make()->schema(FilamentFabricator::getSchemaSlot(ResourceSchemaSlot::SIDEBAR_BEFORE)), Section::make() ->schema([ TextEntry::make('page_url') ->label(__('filament-fabricator::page-resource.labels.url')) ->visible(fn (?PageContract $record) => config('filament-fabricator.routing.enabled') && filled($record)) ->state(fn (?PageContract $record) => FilamentFabricator::getPageUrlFromId($record?->id)), TextInput::make('title') ->label(__('filament-fabricator::page-resource.labels.title')) ->afterStateUpdated(function (Get $get, Set $set, ?string $state, ?PageContract $record) { if (! $get('is_slug_changed_manually') && filled($state) && blank($record)) { $set('slug', Str::slug($state, language: config('app.locale', 'en'))); } }) ->debounce('500ms') ->required(), Hidden::make('is_slug_changed_manually') ->default(false) ->dehydrated(false), TextInput::make('slug') ->label(__('filament-fabricator::page-resource.labels.slug')) ->unique(ignoreRecord: true, modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('parent_id', $get('parent_id'))) ->afterStateUpdated(function (Set $set) { $set('is_slug_changed_manually', true); }) ->rule(function ($state) { return function (string $attribute, $value, Closure $fail) use ($state) { if ($state !== '/' && (Str::startsWith($value, '/') || Str::endsWith($value, '/'))) { $fail(__('filament-fabricator::page-resource.errors.slug_starts_or_ends_with_slash')); } }; }) ->required(), Select::make('layout') ->label(__('filament-fabricator::page-resource.labels.layout')) ->options(FilamentFabricator::getLayouts()) ->default(fn () => FilamentFabricator::getDefaultLayoutName()) ->live() ->required(), Select::make('parent_id') ->label(__('filament-fabricator::page-resource.labels.parent')) ->searchable() ->preload() ->reactive() ->suffixAction( fn ($get, $context) => Action::make($context . '-parent') ->icon('heroicon-o-arrow-top-right-on-square') ->url(fn () => PageResource::getUrl($context, ['record' => $get('parent_id')])) ->openUrlInNewTab() ->visible(fn () => filled($get('parent_id'))) ) ->relationship( 'parent', 'title', function (Builder $query, ?PageContract $record) { if (filled($record)) { $query->where('id', '!=', $record->id); } } ), ]), Group::make()->schema(FilamentFabricator::getSchemaSlot(ResourceSchemaSlot::SIDEBAR_AFTER)), ]), ]); } public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('title') ->label(__('filament-fabricator::page-resource.labels.title')) ->searchable() ->sortable(), TextColumn::make('url') ->label(__('filament-fabricator::page-resource.labels.url')) ->toggleable() ->getStateUsing(fn (?PageContract $record) => FilamentFabricator::getPageUrlFromId($record->id) ?: null) ->url(fn (?PageContract $record) => FilamentFabricator::getPageUrlFromId($record->id) ?: null, true) ->visible(config('filament-fabricator.routing.enabled')), TextColumn::make('layout') ->label(__('filament-fabricator::page-resource.labels.layout')) ->badge() ->toggleable() ->sortable(), TextColumn::make('parent.title') ->label(__('filament-fabricator::page-resource.labels.parent')) ->toggleable(isToggledHiddenByDefault: true) ->formatStateUsing(fn ($state) => $state ?? '-') ->url(fn (?PageContract $record) => filled($record->parent_id) ? PageResource::getUrl('edit', ['record' => $record->parent_id]) : null), ]) ->filters([ SelectFilter::make('layout') ->label(__('filament-fabricator::page-resource.labels.layout')) ->options(FilamentFabricator::getLayouts()), ]) ->recordActions([ ViewAction::make() ->visible(config('filament-fabricator.enable-view-page')), EditAction::make(), Action::make('visit') ->label(__('filament-fabricator::page-resource.actions.visit')) ->url(fn (?PageContract $record) => FilamentFabricator::getPageUrlFromId($record->id) ?: null) ->icon('heroicon-o-arrow-top-right-on-square') ->openUrlInNewTab() ->color('success') ->visible(config('filament-fabricator.routing.enabled')), ]) ->toolbarActions([]); } public static function getModelLabel(): string { return __('filament-fabricator::page-resource.labels.page'); } public static function getPluralModelLabel(): string { return __('filament-fabricator::page-resource.labels.pages'); } public static function getPages(): array { return array_filter([ 'index' => ListPages::route('/'), 'create' => CreatePage::route('/create'), 'view' => config('filament-fabricator.enable-view-page') ? ViewPage::route('/{record}') : null, 'edit' => EditPage::route('/{record}/edit'), ]); } } ================================================ FILE: src/Services/PageRoutesService.php ================================================ ID) mapping based on the user provided URI. // The mapping expect a URI that starts with a / // thus we "normalize" the URI by ensuring it starts with one. // Not doing so would result in a false negative. $mapping = $this->getUriToIdMapping(); $uri = Str::start($uri, '/'); return $mapping[$uri] ?? -1; } /** * Get an instance of your Page model from a URI, or NULL if none matches * * @return null|(Page&Model) */ public function getPageFromUri(string $uri): ?Page { $id = $this->getPageIdFromUri($uri); // We know the getPageIdFromUri uses -1 as a "sentinel" value // for when the page is not found, so return null in those cases if ($id === -1) { return null; } /** @var null|(Page&Model) */ return FilamentFabricator::getPageModel()::find($id); } /** * Update the cached URLs for the given page (as well as all its descendants') */ public function updateUrlsOf(Page&Model $page): void { // We mutate the mapping without events to ensure we don't have "concurrent" // modifications of the same mapping. This allows us to skip the use of locks // in an environment where only unrelated pages can be modified by separate // users at the same time, which is a responsibility the library users // should enforce themselves. FilamentFabricator::getPageModel()::withoutEvents(function () use ($page) { $mapping = $this->getUriToIdMapping(); $this->updateUrlsAndDescendantsOf($page, $mapping); $this->replaceUriToIdMapping($mapping); }); } /** * Remove the cached URLs for the given page */ public function removeUrlsOf(Page $page): void { // First remove the entries from the (ID -> URI) mapping $idToUrlsMapping = $this->getIdToUrisMapping(); // NOTE: We should never be here without the page being // in the URLs mapping. Remove this once the // lifecycle issue has been dealt with $urls = $idToUrlsMapping[$page->id] ?? []; $idToUrlsMapping[$page->id] = null; unset($idToUrlsMapping[$page->id]); $this->replaceIdToUriMapping($idToUrlsMapping); // Then remove the entries from the (URI -> ID) mapping $uriToIdMapping = $this->getUriToIdMapping(); foreach ($urls as $uri) { unset($uriToIdMapping[$uri]); } $this->replaceUriToIdMapping($uriToIdMapping); // Finally, clear the page's local caches of its own URL. // This means that Page::getAllUrls() and such will now compute // fresh values. $this->forgetPageLocalCache($page); } /** * Get an instance of your Page model from a URI, or throw if there is none */ public function findPageOrFail(string $uri): Page&Model { $id = $this->getPageIdFromUri($uri); // If the page doesn't exists, we know getPageIdFromUri // will return -1. Thus, findOrFail will fail as expected. /** @var Page&Model */ return FilamentFabricator::getPageModel()::findOrFail($id); } /** * Get the list of all the registered URLs * * @return string[] */ public function getAllUrls(): array { $uriToIdMapping = $this->getUriToIdMapping(); // $uriToIdMapping is an associative array that maps URIs to IDs. // Thus, the list of URLs is the keys of that array. // Since PHP handles keys very weirdly when using array_keys, // we simply get its array_values to have a truly regular array // instead of an associative array where the keys are all numbers // but possibly non-sorted. /* @phpstan-ignore arrayValues.list (This ensures the keys in the array are numerical and sorted) */ return array_values(array_keys($uriToIdMapping)); } /** * Get the URI -> ID mapping * * @return array */ protected function getUriToIdMapping(): array { // The mapping will be cached for most requests. // The very first person hitting the cache when it's not readily available // will sadly have to recompute the whole thing. return Cache::rememberForever(static::URI_TO_ID_MAPPING, function () { // Even though we technically have 2 separate caches // we want them to not really be independent. // Here we ensure our initial state depends on the other // cache's initial state. $idsToUrisMapping = $this->getIdToUrisMapping(); $uriToIdMapping = []; // We simply "reverse" the one-to-many mapping to a many-to-one foreach ($idsToUrisMapping as $id => $uris) { foreach ($uris as $uri) { $uriToIdMapping[$uri] = $id; } } return $uriToIdMapping; }); } /** * Get the ID -> URI[] mapping * * @return array */ protected function getIdToUrisMapping(): array { // The mapping will be cached for most requests. // The very first person hitting the cache when it's not readily available // will sadly have to recompute the whole thing. // This could be a critical section and bottleneck depending on the use cases. // Any optimization to this can greatly improve the entire package's performances // in one fell swoop. return Cache::rememberForever(static::ID_TO_URI_MAPPING, function () { $mapping = FilamentFabricator::getPageModel()::query() ->with('parent') ->get() ->toBase() ->mapWithKeys(function (Page $page): array { // @phpstan-ignore-line // Note that this also has the benefits of computing // the page's local caches. return [$page->id => $page->getAllUrls()]; }) ->all(); return $mapping; }); } /** * Get the cached URIs for the given page * * @return string[] */ protected function getUrisForPage(Page $page): array { $mapping = $this->getIdToUrisMapping(); return $mapping[$page->id] ?? []; } /** * Update routine for the given page * * @param array $uriToIdMapping - The URI -> ID mapping (as a reference, to be modified in-place) * @return void */ protected function updateUrlsAndDescendantsOf(Page&Model $page, array &$uriToIdMapping) { // First ensure consistency by removing any trace of the old URLs // for the given page. Whether local or in the URI to ID mapping. $this->unsetOldUrlsOf($page, $uriToIdMapping); // These URLs will always be fresh since we unset the old ones just above $urls = $page->getAllUrls(); foreach ($urls as $uri) { $id = $uriToIdMapping[$uri] ?? -1; // If while iterating the fresh URLs we encounter one // that is already mapped to the right page ID // then there's nothing to do for this URL // and thus continue onward with the next one. if ($id === $page->id) { continue; } // Otherwise, we have a URI that already exists // and is mapped to the wrong ID, or it wasn't // in the mapping yet. In both cases we just need // to add it to the mapping at the correct spot. $uriToIdMapping[$uri] = $page->id; } // Since we're recursing down the tree, we preload the relationships // once, and traverse down the tree. This helps with performances. // TODO: Make it work with loadMissing instead of load to reduce the number of useless DB queries $page->load(['allChildren']); foreach ($page->allChildren as $childPage) { /** * @var Page&Model $childPage */ // A change in a parent page will always result // in a change to its descendant. As such, // we need to recompute everything that's // a descendant of this page. $this->updateUrlsAndDescendantsOf($childPage, $uriToIdMapping); } } /** * Remove old URLs of the given page from the cached mappings * * @param array $uriToIdMapping - The URI -> ID mapping (as a reference, to be modified in-place) * @return void */ protected function unsetOldUrlsOf(Page $page, array &$uriToIdMapping) { // When we're hitting this path, caches haven't been invalidated yet. // Thus, we don't need to query the mappings to get the old URLs. $oldUrlSet = collect($page->getAllUrls())->lazy()->sort()->all(); // Once we're done collecting the previous URLs, and since we want // to unset ALL old URLs for this given page, we might as well // forget its local caches here. $this->forgetPageLocalCache($page); // Since we just forgot the page's local caches, this doesn't // return the old set of URLs, but instead computes and caches // the new URLs based on the page's currently loaded data. $newUrlSet = collect($page->getAllUrls())->lazy()->sort()->all(); // The old URLs are those that are present in the $oldUrlSet // but are not present in $newUrlSet. Hence the use of array_diff // whose role is to return exactly that. Also note we sorted the arrays // in order to make sure the diff algorithm has every chances to be // optimal in performance. $oldUrls = array_diff($oldUrlSet, $newUrlSet); // Simply go through the list of old URLs and remove them from the mapping. // This is one of the reasons we pass it by reference. foreach ($oldUrls as $oldUrl) { unset($uriToIdMapping[$oldUrl]); } $idToUrlsMapping = $this->getIdToUrisMapping(); $idToUrlsMapping[$page->id] = $newUrlSet; $this->replaceIdToUriMapping($idToUrlsMapping); } /** * Forget all URL caches tied to the page (cf. Page::getAllUrlCacheKeys) */ protected function forgetPageLocalCache(Page $page) { // The page local caches are simply those behind the // URL cache keys. Compute the keys, forget the caches. $cacheKeys = $page->getAllUrlCacheKeys(); foreach ($cacheKeys as $cacheKey) { Cache::forget($cacheKey); } } /** * Completely replaced the cached ID -> URI[] mapping * * @param array $idToUriMapping */ protected function replaceIdToUriMapping(array $idToUriMapping): void { if (empty($idToUriMapping)) { // If the new mapping is empty, that means we've been // cleaning the last entries. Therefore we must // forget the cached data to properly clear it out // and also allow proper cache invalidation Cache::forget(static::ID_TO_URI_MAPPING); } else { // Replace the ID -> URI[] mapping with the given one. // This is done "atomically" with regards to the cache. // Note that concurrent read and writes can result in lost updates. // And thus in an invalid state. Cache::forever(static::ID_TO_URI_MAPPING, $idToUriMapping); } } /** * Completely replace the cached URI -> ID mapping * * @param array $uriToIdMapping */ protected function replaceUriToIdMapping(array $uriToIdMapping): void { if (empty($uriToIdMapping)) { // If the new mapping is empty, that means we've been // cleaning the last entries. Therefore we must // forget the cached data to properly clear it out // and also allow proper cache invalidation Cache::forget(static::URI_TO_ID_MAPPING); } else { // Replace the URI -> ID mapping with the given one. // This is done "atomically" with regards to the cache. // Note that concurrent read and writes can result in lost updates. // And thus in an invalid state. Cache::forever(static::URI_TO_ID_MAPPING, $uriToIdMapping); } } } ================================================ FILE: src/View/LayoutRenderHook.php ================================================ title"> {{-- Header Here --}} {{-- Footer Here --}} ================================================ FILE: stubs/PageBlock.stub ================================================ schema([ // ]); } public static function mutateData(array $data): array { return $data; } } ================================================ FILE: stubs/PageBlockView.stub ================================================ @aware(['page'])
//
================================================ FILE: tests/Commands/ClearRoutesCacheCommand.test.php ================================================ toBeInstanceOf(ClearRoutesCacheCommand::class); }); it('clears all route caches', function () { /** * @var PageRoutesService $service */ $service = resolve(PageRoutesService::class); /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $child */ $child = Page::create([ 'layout' => 'default', 'title' => 'My child page', 'slug' => 'my-child-page', 'blocks' => [], 'parent_id' => $page->id, ]); $service->getAllUrls(); // ensure everything is cached beforehand artisan('filament-fabricator:clear-routes-cache') ->assertSuccessful(); expect(Cache::get('filament-fabricator::PageRoutesService::uri-to-id'))->toBeEmpty(); expect(Cache::get('filament-fabricator::PageRoutesService::id-to-uri'))->toBeEmpty(); $cacheKeys = [...$page->getAllUrlCacheKeys(), ...$child->getAllUrlCacheKeys()]; expect($cacheKeys)->not->toBeEmpty(); expect( collect($cacheKeys) ->every(fn (string $cacheKey) => ! Cache::has($cacheKey)) )->toBeTrue(); }); it('refreshes the cache properly', function (string $flag, string $newPrefix) { /** * @var PageRoutesService $service */ $service = resolve(PageRoutesService::class); /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $child */ $child = Page::create([ 'layout' => 'default', 'title' => 'My child page', 'slug' => 'my-child-page', 'blocks' => [], 'parent_id' => $page->id, ]); $urls = collect([...$page->getAllUrls(), ...$child->getAllUrls()])->sort()->toArray(); $prevUTI = Cache::get('filament-fabricator::PageRoutesService::uri-to-id'); $prevUTI = collect($prevUTI)->sort()->toArray(); $prevITU = Cache::get('filament-fabricator::PageRoutesService::id-to-uri'); $prevITU = collect($prevITU)->sort()->toArray(); Config::set('filament-fabricator.routing.prefix', $newPrefix); artisan('filament-fabricator:clear-routes-cache', [ $flag => true, ]) ->assertSuccessful(); $newUrls = collect([...$page->getAllUrls(), ...$child->getAllUrls()])->sort()->toArray(); expect($newUrls)->not->toEqualCanonicalizing($urls); expect($newUrls)->not->toBeEmpty(); expect( collect($newUrls) ->every(fn (string $url) => str_starts_with($url, "/$newPrefix")) )->toBeTrue(); $newUTI = Cache::get('filament-fabricator::PageRoutesService::uri-to-id'); $newUTI = collect($newUTI)->sort()->toArray(); expect($newUTI)->not->toEqual($prevUTI); expect($newUTI)->not->toBeEmpty(); expect( collect($newUTI) ->keys() ->every(fn (string $uri) => str_starts_with($uri, "/$newPrefix")) ); $newITU = Cache::get('filament-fabricator::PageRoutesService::id-to-uri'); $newITU = collect($newITU)->sort()->toArray(); expect($newITU)->not->toEqual($prevITU); expect($newITU)->not->toBeEmpty(); expect( collect($newITU) ->values() ->flatten() ->every(fn (string $uri) => str_starts_with($uri, "/$newPrefix")) ); })->with([ ['--refresh', 'newprefix'], ['-R', 'np'], ]); }); ================================================ FILE: tests/ExampleTest.php ================================================ toBeTrue(); }); ================================================ FILE: tests/Fixtures/PageBuilderTestComponent.php ================================================ msgBag = new MessageBag; $this->errors = new ViewErrorBag; $this->errors->put('default', $this->msgBag); } public function form(Schema $form): Schema { return $form->schema([ PageBuilder::make('blocks') ->blocks([]), ])->statePath('data'); } public function getErrorBag() { return $this->errors->getBag('default'); } public function getViewErrorBag() { return $this->errors; } public function mount(): void { $this->form->fill(); } public function save() { $this->form->getState(); } public function render() { return view('filament-fabricator::tests.fixtures.blade-wrapper'); } } ================================================ FILE: tests/Forms/Components/PageBuilder.test.php ================================================ id('test') ->plugins([ FilamentFabricatorPlugin::make(), ]), ); }); it('renders without throwing an exception', function () { // TODO: Make the test run livewire(PageBuilderTestComponent::class) ->fillForm([ 'data' => [ ['title' => 'Test Item'], // make sure at least one item exists ], ]) ->assertSeeHtml('class="fi-fo-builder-item') ->assertSchemaExists('blocks'); })->skip('Proper Livewire unit testing isn\'t possible atm'); }); ================================================ FILE: tests/Observers/PageRoutesObserver.test.php ================================================ delete(); }); describe('#created($page)', function () { it('properly adds all the page\'s URLs to the mapping', function () { $beforeUrls = FilamentFabricator::getPageUrls(); /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); $afterUrls = $sortUrls(FilamentFabricator::getPageUrls()); expect($afterUrls)->not->toEqual($beforeUrls); $pageUrls = $sortUrls($page->getAllUrls()); expect($afterUrls)->toEqual($pageUrls); }); it('properly works on child pages', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $page */ $child = Page::create([ 'layout' => 'default', 'title' => 'My stuff', 'slug' => 'my-stuff', 'blocks' => [], 'parent_id' => $page->id, ]); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); $allUrls = FilamentFabricator::getPageUrls(); $allUrls = $sortUrls($allUrls); $fromPages = $sortUrls([ ...$page->getAllUrls(), ...$child->getAllUrls(), ]); $expectedUrls = $sortUrls([ '/my-slug', '/my-slug/my-stuff', ]); expect($allUrls)->toEqual($expectedUrls); expect($fromPages)->toEqual($expectedUrls); /** * @var Page $page */ $descendant = Page::create([ 'layout' => 'default', 'title' => 'Abc xyz', 'slug' => 'abc-xyz', 'blocks' => [], 'parent_id' => $child->id, ]); $allUrls = FilamentFabricator::getPageUrls(); $allUrls = $sortUrls($allUrls); $fromPages = $sortUrls([ ...$page->getAllUrls(), ...$child->getAllUrls(), ...$descendant->getAllUrls(), ]); $expectedUrls = $sortUrls([ '/my-slug', '/my-slug/my-stuff', '/my-slug/my-stuff/abc-xyz', ]); expect($allUrls)->toEqual($expectedUrls); expect($fromPages)->toEqual($expectedUrls); }); }); describe('#updated($page)', function () { it('removes the old URLs from the mapping', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); $oldUrls = $page->getAllUrls(); $page->slug = 'not-my-slug'; $page->save(); $allUrls = FilamentFabricator::getPageUrls(); expect($allUrls)->not->toContain(...$oldUrls); }); it('adds the new URLs to the mapping', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); $page->slug = 'not-my-slug'; $page->save(); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); $expected = $sortUrls(['/not-my-slug']); $newUrls = $sortUrls($page->getAllUrls()); $allUrls = $sortUrls(FilamentFabricator::getPageUrls()); expect($allUrls)->toEqual($expected); expect($newUrls)->toEqual($expected); }); it('properly updates all child (and descendant) routes', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); $child1 = Page::create([ 'layout' => 'default', 'title' => 'My child 1', 'slug' => 'child-1', 'blocks' => [], 'parent_id' => $page->id, ]); $child2 = Page::create([ 'layout' => 'default', 'title' => 'My child 2', 'slug' => 'child-2', 'blocks' => [], 'parent_id' => $page->id, ]); $child3 = Page::create([ 'layout' => 'default', 'title' => 'My child 3', 'slug' => 'child-3', 'blocks' => [], 'parent_id' => $page->id, ]); $childOfChild = Page::create([ 'layout' => 'default', 'title' => 'Subchild 1', 'slug' => 'subchild-1', 'blocks' => [], 'parent_id' => $child2->id, ]); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); /** * @var Page[] $descendants */ $descendants = [$child1, $child2, $child3, $childOfChild]; $pages = [$page, ...$descendants]; $oldUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); $page->slug = 'not-my-slug'; $page->save(); foreach ($descendants as $descendant) { $descendant->refresh(); } $newUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); expect($newUrlSets)->not->toEqual($oldUrlSets); $allUrls = $sortUrls(FilamentFabricator::getPageUrls()); $fromPages = $sortUrls(collect($pages)->flatMap(fn (Page $page) => $page->getAllUrls())->toArray()); $expectedUrls = $sortUrls([ '/not-my-slug', '/not-my-slug/child-1', '/not-my-slug/child-2', '/not-my-slug/child-3', '/not-my-slug/child-2/subchild-1', ]); expect($allUrls)->toEqual($expectedUrls); expect($fromPages)->toEqual($expectedUrls); $child2->slug = 'not-child-2-xyz'; $child2->save(); foreach ($descendants as $descendant) { $descendant->refresh(); } $allUrls = $sortUrls(FilamentFabricator::getPageUrls()); $fromPages = $sortUrls(collect($pages)->flatMap(fn (Page $page) => $page->getAllUrls())->toArray()); $expectedUrls = $sortUrls([ '/not-my-slug', '/not-my-slug/child-1', '/not-my-slug/not-child-2-xyz', '/not-my-slug/child-3', '/not-my-slug/not-child-2-xyz/subchild-1', ]); expect($allUrls)->toEqual($expectedUrls); expect($fromPages)->toEqual($expectedUrls); }); it('properly updates itself and descendants when changing which page is the parent (BelongsTo#associate and BelongsTo#dissociate)', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $child1 */ $child1 = Page::create([ 'layout' => 'default', 'title' => 'My child 1', 'slug' => 'child-1', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $child2 */ $child2 = Page::create([ 'layout' => 'default', 'title' => 'My child 2', 'slug' => 'child-2', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $child3 */ $child3 = Page::create([ 'layout' => 'default', 'title' => 'My child 3', 'slug' => 'child-3', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $childOfChild */ $childOfChild = Page::create([ 'layout' => 'default', 'title' => 'Subchild 1', 'slug' => 'subchild-1', 'blocks' => [], 'parent_id' => $child2->id, ]); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); /** * @var Page[] $descendants */ $descendants = [$child1, $child2, $child3, $childOfChild]; $pages = [$page, ...$descendants]; $oldUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); $child2->parent()->associate($child1); $child2->save(); $child3->parent()->dissociate(); $child3->save(); foreach ($descendants as $descendant) { $descendant->refresh(); } $newUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); $fromPages = $sortUrls(collect($pages)->flatMap(fn (Page $page) => $page->getAllUrls())->toArray()); expect($newUrlSets)->not->toEqual($oldUrlSets); $allUrls = $sortUrls(FilamentFabricator::getPageUrls()); $expectedUrls = $sortUrls([ '/my-slug', '/my-slug/child-1', '/my-slug/child-1/child-2', '/child-3', '/my-slug/child-1/child-2/subchild-1', ]); expect($allUrls)->toEqual($expectedUrls); expect($fromPages)->toEqual($expectedUrls); }); it('properly updates itself and descendants when changing which page is the parent (Model#update)', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $child1 */ $child1 = Page::create([ 'layout' => 'default', 'title' => 'My child 1', 'slug' => 'child-1', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $child2 */ $child2 = Page::create([ 'layout' => 'default', 'title' => 'My child 2', 'slug' => 'child-2', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $child3 */ $child3 = Page::create([ 'layout' => 'default', 'title' => 'My child 3', 'slug' => 'child-3', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $childOfChild */ $childOfChild = Page::create([ 'layout' => 'default', 'title' => 'Subchild 1', 'slug' => 'subchild-1', 'blocks' => [], 'parent_id' => $child2->id, ]); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); /** * @var Page[] $descendants */ $descendants = [$child1, $child2, $child3, $childOfChild]; $pages = [$page, ...$descendants]; $oldUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); $child2->update([ 'parent_id' => $child1->id, ]); $child3->update([ 'parent_id' => null, ]); foreach ($descendants as $descendant) { $descendant->refresh(); } $newUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); $fromPages = $sortUrls(collect($pages)->flatMap(fn (Page $page) => $page->getAllUrls())->toArray()); expect($newUrlSets)->not->toEqual($oldUrlSets); $allUrls = $sortUrls(FilamentFabricator::getPageUrls()); $expectedUrls = $sortUrls([ '/my-slug', '/my-slug/child-1', '/my-slug/child-1/child-2', '/child-3', '/my-slug/child-1/child-2/subchild-1', ]); expect($allUrls)->toEqual($expectedUrls); expect($fromPages)->toEqual($expectedUrls); }); it('properly updates itself and descendants when changing which page is the parent (manual change and Model#save)', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $child1 */ $child1 = Page::create([ 'layout' => 'default', 'title' => 'My child 1', 'slug' => 'child-1', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $child2 */ $child2 = Page::create([ 'layout' => 'default', 'title' => 'My child 2', 'slug' => 'child-2', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $child3 */ $child3 = Page::create([ 'layout' => 'default', 'title' => 'My child 3', 'slug' => 'child-3', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $childOfChild */ $childOfChild = Page::create([ 'layout' => 'default', 'title' => 'Subchild 1', 'slug' => 'subchild-1', 'blocks' => [], 'parent_id' => $child2->id, ]); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); $descendants = [$child1, $child2, $child3, $childOfChild]; $pages = [$page, ...$descendants]; $oldUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); $child2->parent_id = $child1->id; $child2->save(); $child3->parent_id = null; $child3->save(); foreach ($descendants as $descendant) { $descendant->refresh(); } $newUrlSets = array_map(fn (Page $page) => $page->getAllUrls(), $descendants); $fromPages = $sortUrls(collect($pages)->flatMap(fn (Page $page) => $page->getAllUrls())->toArray()); expect($newUrlSets)->not->toEqual($oldUrlSets); $allUrls = FilamentFabricator::getPageUrls(); $allUrls = $sortUrls($allUrls); $expectedUrls = $sortUrls([ '/my-slug', '/my-slug/child-1', '/my-slug/child-1/child-2', '/child-3', '/my-slug/child-1/child-2/subchild-1', ]); expect($allUrls)->toEqual($expectedUrls); expect($fromPages)->toEqual($expectedUrls); }); }); describe('#deleting($page)', function () { it('removes the page\'s URLs from the mapping', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); $beforeUrls = FilamentFabricator::getPageUrls(); $page->delete(); $afterUrls = FilamentFabricator::getPageUrls(); expect($afterUrls)->not->toEqual($beforeUrls); expect($afterUrls)->toBeEmpty(); }); it('sets the childrens\' parent to null if $page had no parent', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $child */ $child = Page::create([ 'layout' => 'default', 'title' => 'My child page', 'slug' => 'my-child-page', 'blocks' => [], 'parent_id' => $page->id, ]); $page->delete(); $child->refresh(); expect($child->parent_id)->toBeNull(); $urls = FilamentFabricator::getPageUrls(); $expected = ['/my-child-page']; expect($urls)->toEqual($expected); expect($child->getAllUrls())->toEqual($expected); }); it('attaches the children to $page\'s parent if it had one', function () { /** * @var Page $page */ $page = Page::create([ 'layout' => 'default', 'title' => 'My title', 'slug' => 'my-slug', 'blocks' => [], 'parent_id' => null, ]); /** * @var Page $child */ $child = Page::create([ 'layout' => 'default', 'title' => 'My child page', 'slug' => 'my-child-page', 'blocks' => [], 'parent_id' => $page->id, ]); /** * @var Page $descendant */ $descendant = Page::create([ 'layout' => 'default', 'title' => 'My sub page', 'slug' => 'my-sub-page', 'blocks' => [], 'parent_id' => $child->id, ]); $sortUrls = fn (array $urls) => collect($urls) ->sort() ->values() ->toArray(); $child->delete(); $descendant->refresh(); $page->refresh(); expect($descendant->parent_id)->toBe($page->id); $urls = $sortUrls(FilamentFabricator::getPageUrls()); $expected = $sortUrls([ '/my-slug', '/my-slug/my-sub-page', ]); $fromPages = $sortUrls(collect([$page, $descendant])->flatMap(fn (Page $page) => $page->getAllUrls())->toArray()); expect($urls)->toEqual($expected); expect($fromPages)->toEqual($expected); }); }); }); ================================================ FILE: tests/Pest.php ================================================ set('app.key', 'base64:' . base64_encode(random_bytes(32))); } }); uses(TestCase::class)->in(__DIR__)->beforeEach(function () { $this->withSession([]); }); ================================================ FILE: tests/TestCase.php ================================================ 'Z3d0X\\FilamentFabricator\\Database\\Factories\\' . class_basename($modelName) . 'Factory' ); } protected function getPackageProviders($app) { return [ LivewireServiceProvider::class, FilamentServiceProvider::class, FormsServiceProvider::class, SupportServiceProvider::class, BladeIconsServiceProvider::class, BladeHeroiconsServiceProvider::class, FilamentFabricatorServiceProvider::class, ]; } public function getEnvironmentSetUp($app) { config()->set('database.default', 'testing'); $migration = include __DIR__ . '/../database/migrations/create_pages_table.php.stub'; $migration->up(); $migration = include __DIR__ . '/../database/migrations/fix_slug_unique_constraint_on_pages_table.php.stub'; $migration->up(); } }