├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .php-cs-fixer.dist
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── bin
└── wrangle
├── captainhook.json
├── cliff.toml
├── composer.json
├── composer.lock
├── phive.xml
├── phpstan.neon.dist
├── phpunit.xml
├── rector.php
├── scripts
├── bump.sh
├── php_cs_fixer.sh
└── test_setup.sh
├── src
├── Auth
│ ├── Token.php
│ └── TokenInterface.php
├── Commands
│ ├── JobQueueCommand.php
│ └── Wrangle
│ │ └── RunQueueCommand.php
├── Controllers
│ ├── AbstractController.php
│ └── ControllerInterface.php
├── Core
│ ├── Assets.php
│ ├── CoreHooks.php
│ ├── Loader.php
│ ├── PluginOrThemeable.php
│ ├── Router.php
│ └── RouterException.php
├── Database
│ ├── Connection.php
│ ├── MigrationInitialiser.php
│ ├── Migrations.php
│ ├── Migrations
│ │ ├── 20201018145438_create_auth_tokens_table.php
│ │ ├── 20210112174503_create_queued_jobs_table.php
│ │ ├── 20210122110424_update_queued_jobs_table.php
│ │ ├── 20221203173957_add_queue_name_to_queued_jobs_table.php
│ │ ├── 20230320094337_create_events_table.php
│ │ └── 20230614065259_add_soft_delete_to_auth_table.php
│ └── phinx.default.yml
├── Enums
│ └── AcfEnum.php
├── Fields
│ └── FieldGroupInterface.php
├── Hooks
│ ├── HookInterface.php
│ ├── HookIsSet.php
│ ├── JobCommandHook.php
│ └── TemplateHook.php
├── Http
│ ├── ArraysRequestInput.php
│ ├── Handlers
│ │ ├── AjaxHandler.php
│ │ ├── CustomRouteHandler.php
│ │ ├── HandlerInterface.php
│ │ ├── HasMiddleware.php
│ │ ├── RestHandler.php
│ │ └── TemplateHandler.php
│ ├── ResponseFactory.php
│ ├── ServerRequest.php
│ ├── Shutdown.php
│ └── WPRest
│ │ └── ServerRequest.php
├── Jobs
│ ├── JobInterface.php
│ ├── Queue.php
│ └── Queueable.php
├── Log
│ ├── LogEventHandler.php
│ └── LogHandlerType.php
├── Middleware
│ └── TestOnlyMiddleware.php
├── Models
│ ├── Comment.php
│ ├── CustomPostable.php
│ ├── Event.php
│ ├── MenuItem.php
│ ├── Option.php
│ ├── Post.php
│ ├── PostMeta.php
│ ├── PostSugar.php
│ ├── PostTypeInterface.php
│ ├── QueuedJob.php
│ ├── User.php
│ ├── UserMeta.php
│ ├── UserSugar.php
│ └── WPMenuItem.php
├── Registry
│ └── RegistryInterface.php
├── Router
│ ├── AltoRouter.php
│ └── Strategy
│ │ ├── AjaxPrivateStrategy.php
│ │ ├── AjaxPublicStrategy.php
│ │ ├── CustomStrategy.php
│ │ ├── RestStrategy.php
│ │ ├── StrategyException.php
│ │ ├── StrategyFactory.php
│ │ └── StrategyInterface.php
├── View
│ ├── Blade
│ │ ├── Blade.php
│ │ ├── Directives.php
│ │ └── Local.php
│ ├── BladeView.php
│ ├── GetsDirectory.php
│ ├── LegacyPlatesView.php
│ ├── Plates
│ │ ├── CaptainHook
│ │ │ └── Lint.php
│ │ ├── Engine.php
│ │ ├── Exception
│ │ │ ├── FuncException.php
│ │ │ ├── HydrateTemplateException.php
│ │ │ ├── PlatesException.php
│ │ │ ├── RenderTemplateException.php
│ │ │ ├── StackException.php
│ │ │ └── TemplateErrorException.php
│ │ ├── Extension.php
│ │ ├── Extension
│ │ │ ├── Data
│ │ │ │ ├── DataExtension.php
│ │ │ │ └── data.php
│ │ │ ├── ExtensionInterface.php
│ │ │ ├── Folders
│ │ │ │ ├── FoldersExtension.php
│ │ │ │ └── folders.php
│ │ │ ├── LayoutSections
│ │ │ │ ├── DefaultLayoutRenderTemplate.php
│ │ │ │ ├── LayoutRenderTemplate.php
│ │ │ │ ├── LayoutSectionsExtension.php
│ │ │ │ ├── Sections.php
│ │ │ │ └── layout-sections.php
│ │ │ ├── Path
│ │ │ │ ├── PathExtension.php
│ │ │ │ ├── ResolvePathArgs.php
│ │ │ │ └── path.php
│ │ │ └── RenderContext
│ │ │ │ ├── FuncArgs.php
│ │ │ │ ├── RenderContext.php
│ │ │ │ ├── RenderContextExtension.php
│ │ │ │ ├── func.php
│ │ │ │ └── render-context.php
│ │ ├── MagicResolver.php
│ │ ├── PlatesExtension.php
│ │ ├── RenderTemplate.php
│ │ ├── RenderTemplate
│ │ │ ├── ComposeRenderTemplate.php
│ │ │ ├── FileSystemRenderTemplate.php
│ │ │ ├── MapContentRenderTemplate.php
│ │ │ ├── MockRenderTemplate.php
│ │ │ ├── PhpRenderTemplate.php
│ │ │ ├── RenderTemplateDecorator.php
│ │ │ ├── StaticFileRenderTemplate.php
│ │ │ └── ValidatePathRenderTemplate.php
│ │ ├── Template.php
│ │ ├── Template
│ │ │ └── match.php
│ │ ├── TemplateReference.php
│ │ ├── Util
│ │ │ ├── Container.php
│ │ │ └── util.php
│ │ └── Validator.php
│ ├── PlatesView.php
│ ├── TwigAdhocFunction.php
│ ├── TwigExtension.php
│ ├── TwigView.php
│ └── ViewInterface.php
└── helpers.php
├── stubs
├── db.php
└── stan-constants.php
├── tests
├── Integration
│ ├── Auth
│ │ └── TokenTest.php
│ ├── ContainerHelpersTest.php
│ ├── Core
│ │ ├── DistAssetsTest.php
│ │ ├── LoaderTest.php
│ │ ├── StaticAssetsTest.php
│ │ └── hooks_test.yaml
│ ├── Hooks
│ │ └── HookIsSetTest.php
│ ├── Jobs
│ │ └── QueueTest.php
│ └── Router
│ │ └── StrategyFactoryTest.php
├── Pest.php
├── Unit
│ └── ExampleTest.php
├── bootstrap.php
└── test-config.php
└── wp-cli.yml
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Release
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | with:
12 | fetch-depth: 0
13 | - name: Generate a changelog
14 | uses: orhun/git-cliff-action@v2
15 | id: changelog
16 | with:
17 | args: --latest --strip all
18 | env:
19 | OUTPUT: CHANGES.md
20 | - name: Set env
21 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
22 | - name: Install Hub
23 | run: sudo apt-get update && sudo apt-get install -y hub
24 | - name: Create a Release
25 | run: hub release create -m "${{ env.RELEASE_VERSION }}" -m "${{ steps.changelog.outputs.content }}" "${{ env.RELEASE_VERSION }}"
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | node_modules
3 | .php_cs.cache
4 | .php-cs-fixer.cache
5 | phpstan.neon
6 | .phpunit.result.cache
7 | tools
8 | wp-test
9 | tests/coverage
10 | phinx.yml
11 | logs
12 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist:
--------------------------------------------------------------------------------
1 | in(__DIR__ . '/src')
5 | ;
6 |
7 | $config = new PhpCsFixer\Config();
8 |
9 | return $config->setRules([
10 | '@Symfony' => true,
11 | 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
12 | 'blank_line_after_opening_tag' => false,
13 | 'phpdoc_no_package' => false,
14 | 'phpdoc_to_comment' => false,
15 | 'concat_space' => ['spacing' => 'one'],
16 | 'binary_operator_spaces' => ['default' => 'align'],
17 | 'increment_style' => ['style' => 'post'],
18 | 'no_leading_import_slash' => false,
19 | 'blank_line_between_import_groups' => false,
20 | 'global_namespace_import' => ['import_classes' => true],
21 | ])
22 | ->setFinder($finder)
23 | ;
24 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "bmewburn.vscode-intelephense-client",
4 | "breezelin.phpstan",
5 | "sonarsource.sonarlint-vscode",
6 | "emeraldwalk.runonsave"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "phpstan.configuration": "phpstan.neon",
3 | "phpstan.level": "config",
4 | "emeraldwalk.runonsave": {
5 | "commands": [
6 | {
7 | "match": "(?
2 |
3 | # Forme WordPress Framework
4 |
5 | Forme is an MVC framework for WordPress. This is the core Forme Framework library, which is used by the Plugin, Theme and CodeGen components.
6 |
7 | [Click here for Documentation](https://formewp.github.io)
8 |
9 | ## Development
10 |
11 | For development run `phive install --force-accept-unsigned` followed by `composer install`.
12 |
13 | Tools are in `./tools` rather than `./vendor/bin`
14 |
15 | You also need [git cliff](https://github.com/orhun/git-cliff) for generating changelogs and [pcov](https://github.com/krakjoe/pcov) to generate coverage stats for infection to measure against.
16 |
17 | The useful ones are set up as composer scripts. Tests should run automatically on commit.
18 |
19 | ```sh
20 | composer test # run pest
21 | composer test:setup # set up WP installation for integration testing
22 | composer stan # run phpstan on src
23 | composer rector:check # rector dry run on src
24 | composer rector:fix # rector on src
25 | composer cs:check # php cs fixer dry run on src
26 | composer cs:fix # php cs fixer on src
27 | composer changelog # run git cliff
28 | composer hooks # install git hooks (will run on composer install automatically)
29 | composer bump:version # bump to the next patch version - can also take argument "minor" or "major"
30 | composer infection # run infection on src
31 | composer infection:log # run infection on src and log to infection.html
32 | ```
33 |
--------------------------------------------------------------------------------
/bin/wrangle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add($command);
25 | }
26 | }
27 | $application->run();
28 |
--------------------------------------------------------------------------------
/captainhook.json:
--------------------------------------------------------------------------------
1 | {
2 | "commit-msg": {
3 | "enabled": true,
4 | "actions": [
5 | {
6 | "action": "\\Ramsey\\CaptainHook\\ValidateConventionalCommit",
7 | "options": {
8 | "config": {
9 | "types": [
10 | "feat",
11 | "fix",
12 | "docs",
13 | "style",
14 | "refactor",
15 | "perf",
16 | "test",
17 | "chore",
18 | "dev"
19 | ],
20 | "typeCase": "lower",
21 | "scopeCase": "lower"
22 | }
23 | }
24 | }
25 | ]
26 | },
27 | "pre-push": {
28 | "enabled": false,
29 | "actions": []
30 | },
31 | "pre-commit": {
32 | "enabled": true,
33 | "actions": [
34 | {
35 | "action": "\\CaptainHook\\App\\Hook\\PHP\\Action\\Linting",
36 | "options": []
37 | },
38 | {
39 | "action": "composer test",
40 | "options": []
41 | },
42 | {
43 | "action": "./scripts/php_cs_fixer.sh",
44 | "options": []
45 | }
46 | ]
47 | },
48 | "prepare-commit-msg": {
49 | "enabled": true,
50 | "actions": [
51 | {
52 | "action": "\\Ramsey\\CaptainHook\\PrepareConventionalCommit",
53 | "options": {
54 | "config": {
55 | "types": [
56 | "feat",
57 | "fix",
58 | "docs",
59 | "style",
60 | "refactor",
61 | "perf",
62 | "test",
63 | "chore",
64 | "dev"
65 | ],
66 | "typeCase": "lower",
67 | "scopeCase": "lower"
68 | }
69 | }
70 | }
71 | ]
72 | },
73 | "post-commit": {
74 | "enabled": false,
75 | "actions": []
76 | },
77 | "post-merge": {
78 | "enabled": false,
79 | "actions": []
80 | },
81 | "post-checkout": {
82 | "enabled": false,
83 | "actions": []
84 | },
85 | "post-rewrite": {
86 | "enabled": false,
87 | "actions": []
88 | },
89 | "post-change": {
90 | "enabled": false,
91 | "actions": []
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | # configuration file for git-cliff (0.1.0)
2 |
3 | [changelog]
4 | # changelog header
5 | header = """
6 | # Changelog\n
7 | All notable changes to this project will be documented in this file.\n
8 | """
9 | # template for the changelog body
10 | # https://tera.netlify.app/docs/#introduction
11 | body = """
12 | {% if version %}\
13 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
14 | {% else %}\
15 | ## [unreleased]
16 | {% endif %}\
17 | {% for group, commits in commits | group_by(attribute="group") %}
18 | ### {{ group | upper_first }}
19 | {% for commit in commits %}
20 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
21 | {% endfor %}
22 | {% endfor %}\n
23 | """
24 | # remove the leading and trailing whitespace from the template
25 | trim = true
26 | # changelog footer
27 | footer = """
28 |
29 | """
30 |
31 | [git]
32 | # parse the commits based on https://www.conventionalcommits.org
33 | conventional_commits = true
34 | # filter out the commits that are not conventional
35 | filter_unconventional = true
36 | # regex for parsing and grouping commits
37 | commit_parsers = [
38 | {message = "^feat", group = "Features"},
39 | {message = "^fix", group = "Bug Fixes"},
40 | {message = "^doc", group = "Documentation"},
41 | {message = "^dev", group = "Dev Worflow"},
42 | {message = "^perf", group = "Performance"},
43 | {message = "^refactor", group = "Refactor"},
44 | {message = "^style", group = "Styling"},
45 | {message = "^test", group = "Testing"},
46 | {message = "^chore\\(release\\): prepare for", skip = true},
47 | {message = "^chore", group = "Miscellaneous Tasks"},
48 | {body = ".*security", group = "Security"},
49 | ]
50 | # filter out the commits that are not matched by commit parsers
51 | filter_commits = false
52 | # glob pattern for matching git tags
53 | tag_pattern = "v[0-9]*"
54 | # regex for skipping tags
55 | skip_tags = "v0.1.0-beta.1"
56 | # regex for ignoring tags
57 | ignore_tags = ""
58 | # sort the tags chronologically
59 | date_order = false
60 | # sort the commits inside sections by oldest/newest order
61 | sort_commits = "oldest"
62 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "forme/framework",
3 | "description": "An MVC framework for WordPress.",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Moussa Clarke",
8 | "email": "moussaclarke@gmail.com"
9 | }
10 | ],
11 | "require": {
12 | "php": "^8.1",
13 | "cakephp/core": "^5.0",
14 | "http-interop/response-sender": "^1.0",
15 | "illuminate/config": "^10.0 || ^11.0",
16 | "illuminate/database": "^10.0 || ^11.0",
17 | "illuminate/view": "^10.0 || ^11.0",
18 | "laminas/laminas-diactoros": "^3.0",
19 | "laravel/prompts": "^0.3.5",
20 | "league/plates": "^3.5",
21 | "log1x/sage-directives": "^2.0",
22 | "monolog/monolog": "^2.9 || ^3.3",
23 | "papertower/wp-rest-api-psr7": "^0.8.0",
24 | "php-di/php-di": "^7.0.2",
25 | "ramsey/uuid": "^4.7.4",
26 | "rareloop/psr7-server-request-extension": "^2.1",
27 | "relay/relay": "^3.0",
28 | "robmorgan/phinx": "^0.16",
29 | "spatie/enum": "^3.13",
30 | "symfony/string": "^6.4 || ^7.2",
31 | "symfony/yaml": "^6.4 || ^7.2",
32 | "twig/twig": "^3.14",
33 | "vlucas/phpdotenv": "^5.5"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "Forme\\Framework\\": "src/"
38 | },
39 | "files": [
40 | "src/helpers.php",
41 | "src/View/Plates/Template/match.php",
42 | "src/View/Plates/Extension/Data/data.php",
43 | "src/View/Plates/Extension/Path/path.php",
44 | "src/View/Plates/Extension/RenderContext/func.php",
45 | "src/View/Plates/Extension/RenderContext/render-context.php",
46 | "src/View/Plates/Extension/LayoutSections/layout-sections.php",
47 | "src/View/Plates/Extension/Folders/folders.php",
48 | "src/View/Plates/Util/util.php",
49 | "src/Commands/Wrangle/RunQueueCommand.php"
50 | ]
51 | },
52 | "require-dev": {
53 | "fakerphp/faker": "^1.23",
54 | "filp/whoops": "^2.15.2",
55 | "mockery/mockery": "^1.6.2",
56 | "nunomaduro/mock-final-classes": "dev-master",
57 | "pestphp/pest": "^3.0",
58 | "php-stubs/acf-pro-stubs": "^6.0.6",
59 | "php-stubs/woocommerce-stubs": "^9.5",
60 | "php-stubs/wp-cli-stubs": "^2.8",
61 | "phpstan/phpstan": "^2.1.6",
62 | "ramsey/conventional-commits": "^1.5",
63 | "rector/rector": "^2.0",
64 | "symfony/var-dumper": "^7.2",
65 | "szepeviktor/phpstan-wordpress": "^2.0"
66 | },
67 | "repositories": [
68 | {
69 | "type": "vcs",
70 | "url": "https://github.com/moussaclarke/wp-pest-integration-test-setup.git"
71 | }
72 | ],
73 | "scripts": {
74 | "test": "./tools/pest",
75 | "test:setup": "./scripts/test_setup.sh",
76 | "stan": "./tools/phpstan",
77 | "rector:check": "./tools/rector process src --dry-run",
78 | "rector:fix": "./tools/rector process src",
79 | "cs:check": "./tools/php-cs-fixer fix --config ./.php-cs-fixer.dist --dry-run --diff",
80 | "cs:fix": "./tools/php-cs-fixer fix --config ./.php-cs-fixer.dist --diff",
81 | "changelog": "git cliff -o CHANGELOG.md",
82 | "hooks": "./tools/captainhook install -f",
83 | "post-install-cmd": "@hooks",
84 | "bump:version": "./scripts/bump.sh",
85 | "infection": "./tools/infection -s",
86 | "infection:log": "./tools/infection --logger-html=infection.html"
87 | },
88 | "config": {
89 | "bin-dir": "tools",
90 | "sort-packages": true,
91 | "allow-plugins": {
92 | "pestphp/pest-plugin": true,
93 | "composer/installers": true
94 | }
95 | },
96 | "minimum-stability": "dev",
97 | "prefer-stable": true,
98 | "bin": [
99 | "bin/wrangle"
100 | ]
101 | }
102 |
--------------------------------------------------------------------------------
/phive.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | # Copy to phpstan.neon and edit according to your specific setup
2 | includes:
3 | - vendor/szepeviktor/phpstan-wordpress/extension.neon
4 | parameters:
5 | level: 6
6 | bootstrapFiles:
7 | - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php
8 | - vendor/php-stubs/acf-pro-stubs/acf-pro-stubs.php
9 | - vendor/php-stubs/wp-cli-stubs/wp-cli-stubs.php
10 | - stubs/stan-constants.php
11 | dynamicConstantNames:
12 | - WP_ENV
13 | - DB_COLLATE
14 | ignoreErrors:
15 | - "#Unsafe usage of new static#"
16 | -
17 | message: '#^Undefined variable: \$this$#'
18 | path: tests/*
19 | checkMissingIterableValueType: false
20 | paths:
21 | - ./src
22 | - ./tests
23 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | ./tests/Unit/
10 |
11 |
12 | ./tests/Integration/
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
11 | __DIR__ . '/src',
12 | ]);
13 |
14 | $rectorConfig->bootstrapFiles([
15 | __DIR__ . '/vendor/php-stubs/wordpress-stubs/wordpress-stubs.php',
16 | ]);
17 |
18 | // define sets of rules
19 | $rectorConfig->sets([
20 | LevelSetList::UP_TO_PHP_80,
21 | SetList::CODE_QUALITY,
22 | SetList::CODING_STYLE,
23 | SetList::DEAD_CODE,
24 | ]);
25 |
26 | $rectorConfig->skip([
27 | // skip deprecated classes
28 | __DIR__ . '/src/Core/TemplateHandler.php',
29 | __DIR__ . '/src/Core/BootstrapNavWalker.php',
30 | __DIR__ . '/src/Core/LegacyBootstrapNavWalker.php',
31 | __DIR__ . '/src/Core/ViewProvider.php',
32 | __DIR__ . '/src/Core/View.php',
33 | // skip var annotation rule since it flags where we use it to coerce variable types
34 | RemoveNonExistingVarAnnotationRector::class,
35 | ]);
36 | };
37 |
--------------------------------------------------------------------------------
/scripts/bump.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #Get the highest tag number
4 | CURRENT_VERSION=`git describe --abbrev=0 --tags`
5 | # remove the v from the front
6 | CURRENT_VERSION=${CURRENT_VERSION#v}
7 | # set it to 0.0.0 if it's empty
8 | CURRENT_VERSION=${CURRENT_VERSION:-'0.0.0'}
9 |
10 | #Get number parts
11 | MAJOR="${CURRENT_VERSION%%.*}"; CURRENT_VERSION="${CURRENT_VERSION#*.}"
12 | MINOR="${CURRENT_VERSION%%.*}"; CURRENT_VERSION="${CURRENT_VERSION#*.}"
13 | PATCH="${CURRENT_VERSION%%.*}"; CURRENT_VERSION="${CURRENT_VERSION#*.}"
14 |
15 | #Increase version number depending on the scope argument, default to patch
16 | case $1 in
17 | major) MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0;;
18 | minor) MINOR=$((MINOR+1)); PATCH=0;;
19 | patch) PATCH=$((PATCH+1)); ;;
20 | *) PATCH=$((PATCH+1)); ;;
21 | esac
22 |
23 | #Get current hash and see if it already has a tag
24 | GIT_COMMIT=`git rev-parse HEAD`
25 | HAS_TAG=`git describe --contains $GIT_COMMIT 2>/dev/null`
26 |
27 | #Create new tag
28 | NEW_VERSION="$MAJOR.$MINOR.$PATCH"
29 |
30 | # Only tag if no tag already
31 | if [ -z "$HAS_TAG" ]; then
32 | echo "Updating version to $NEW_VERSION"
33 | # if git cliff is installed Generate a changelog
34 | if command -v git-cliff &> /dev/null
35 | then
36 | echo "Generating changelog with git cliff"
37 | git cliff -o CHANGELOG.md -t $NEW_VERSION
38 | git add CHANGELOG.md
39 | else
40 | echo "We couldn't find git cliff so skipping changelog. You can install it with 'brew install git-cliff' or 'cargo install git-cliff'"
41 | echo "Then run 'git cliff --init' in the project directory"
42 | fi
43 | echo "Committing new version changes and pushing"
44 | git commit -a -m "chore: bump version"
45 | git push
46 | echo "Tagging with v$NEW_VERSION and pushing"
47 | git tag "v$NEW_VERSION"
48 | git push --tags
49 | else
50 | echo "There is already a tag on the current commit - abandoning bump"
51 | exit 1
52 | fi
53 |
--------------------------------------------------------------------------------
/scripts/php_cs_fixer.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "php-cs-fixer pre commit hook start"
4 |
5 | PHP_CS_FIXER="./tools/php-cs-fixer"
6 | HAS_PHP_CS_FIXER=false
7 |
8 | if command -v $PHP_CS_FIXER &> /dev/null
9 | then
10 | HAS_PHP_CS_FIXER=true
11 | fi
12 |
13 | # exclude plate files
14 | if $HAS_PHP_CS_FIXER; then
15 | FILES=`git diff --cached --name-only --diff-filter=AM | grep '\.php' | sed '/plate/d'`
16 | if [ -z "$FILES" ]
17 | then
18 | echo "No php files needing cs checks in this commit."
19 | else
20 | $PHP_CS_FIXER fix --verbose --config=.php-cs-fixer.dist ${FILES}
21 | git add ${FILES}
22 | fi
23 | else
24 | echo ""
25 | echo "Please install php-cs-fixer before committing, e.g.:"
26 | echo ""
27 | echo "phive install php-cs-fixer"
28 | echo ""
29 | exit 1
30 | fi
31 |
32 | echo "php-cs-fixer pre commit hook finish"
33 |
--------------------------------------------------------------------------------
/scripts/test_setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # check if wp-test directory exists first and if so then abort
4 | if [ -d "wp-test" ]; then
5 | echo "The wp-test directory already exists"
6 | exit 0
7 | fi
8 |
9 | # also make sure that wp cli is installed
10 | WPCLI_EXISTS='command -v wp'
11 | if ! $WPCLI_EXISTS &> /dev/null
12 | then
13 | echo "You need to install wp-cli globally to run the configuration script"
14 | exit 0
15 | fi
16 |
17 | # install forme base feeding via composer create-project with --no-script so it doesn't run all the scripts
18 | composer create-project forme/base wp-test --no-scripts
19 |
20 | # then run composer install-wordpress
21 | cd wp-test
22 | composer install-wordpress
23 | cd ..
24 |
25 | pwd=`pwd`
26 |
27 | # then run wp config create manually setting dbname, dbuser, dbpass and dbprefix
28 | wp config create --dbname=$(pwd)"/wp-test/testing" --dbuser=dbuser --dbpass=password --dbprefix=wptests_ --skip-check
29 |
30 | # wp config set constants WP_ENV, FORME_PRIVATE_ROOT, DB_DIR, DB_FILE, DISABLE_WP_CRON
31 | wp config set WP_ENV testing
32 | wp config set FORME_PRIVATE_ROOT $(pwd)"/"
33 | wp config set DB_DIR $(pwd)"/wp-test/"
34 | wp config set DB_FILE testing.sqlite3
35 | wp config set DISABLE_WP_CRON true --raw
36 |
37 | # requires & wrap forme_private_root in if statements
38 | file="wp-test/public/wp-config.php"
39 | # if sed --version succceds then this is probably a linux system
40 | if sed --version >/dev/null 2>&1; then
41 | sed -i '/\/\* That'\''s all, stop editing\! Happy publishing\. \*\//a\
42 | \
43 | require_once FORME_PRIVATE_ROOT.'"'"'/vendor/autoload.php'"'"';\
44 | ' $file
45 |
46 | sed -i '/define( '\''FORME_PRIVATE_ROOT'\''/i\
47 | \
48 | if (!defined('"'"'FORME_PRIVATE_ROOT'"'"')) {\
49 | ' $file
50 |
51 | sed -i '/define( '\''FORME_PRIVATE_ROOT'\''/a\
52 | }\
53 | ' $file
54 | # otherwise this is probably a mac, we need to add '' because of ancient sed
55 | else
56 | sed -i '' '/\/\* That'\''s all, stop editing\! Happy publishing\. \*\//a\
57 | \
58 | require_once FORME_PRIVATE_ROOT.'"'"'/vendor/autoload.php'"'"';\
59 | ' $file
60 |
61 | sed -i '' '/define( '\''FORME_PRIVATE_ROOT'\''/i\
62 | \
63 | if (!defined('"'"'FORME_PRIVATE_ROOT'"'"')) {\
64 | ' $file
65 |
66 | sed -i '' '/define( '\''FORME_PRIVATE_ROOT'\''/a\
67 | }\
68 | ' $file
69 | fi
70 | echo "Success: Require autoload and test bootstrap files"
71 |
72 | # run composer init-dot-env
73 | cd wp-test
74 | composer init-dot-env
75 | cd ..
76 |
77 | # copy over db.php files
78 | cp stubs/db.php wp-test/public/wp-content/db.php
79 |
80 | # wp core install
81 | wp core install --url="http://localhost:8000" --title="Test Site" --admin_user="admin" --admin_password="p455w0rd" --admin_email="moussaclarke@gmail.com"
82 |
--------------------------------------------------------------------------------
/src/Auth/Token.php:
--------------------------------------------------------------------------------
1 | getFromDatabase($name);
19 | if ($result === null) {
20 | $result = $this->create($name, $expire);
21 | }
22 |
23 | return $result->token;
24 | }
25 |
26 | public function validate(string $token, string $name): bool
27 | {
28 | $result = $this->getFromDatabase($name);
29 |
30 | return $result && $result->token === $token;
31 | }
32 |
33 | public function destroy(string $name): void
34 | {
35 | Capsule::table('forme_auth_tokens')
36 | ->where('name', '=', $name)
37 | ->where('deleted_at', '=', null)
38 | ->update(['deleted_at' => Carbon::now()->format(self::DATE_FORMAT)]);
39 | }
40 |
41 | public function expires(string $name): ?Carbon
42 | {
43 | $result = $this->getFromDatabase($name);
44 |
45 | return $result ? Carbon::parse($result->expiry) : null;
46 | }
47 |
48 | /**
49 | * @return Model|object|static|null
50 | */
51 | private function getFromDatabase(string $name)
52 | {
53 | $this->purge();
54 |
55 | return Capsule::table('forme_auth_tokens')
56 | ->where('name', '=', $name)
57 | ->where('deleted_at', '=', null)
58 | ->first();
59 | }
60 |
61 | /**
62 | * @return Model|object|static|null
63 | */
64 | private function create(string $name, string $expire)
65 | {
66 | $token = Uuid::uuid4();
67 | $expiry = strtotime($expire, time());
68 | $dt = Carbon::now();
69 | $dt->setTimestamp($expiry);
70 |
71 | $id = Capsule::table('forme_auth_tokens')->insertGetId(
72 | ['name' => $name, 'token' => $token, 'expiry' => $dt->format(self::DATE_FORMAT)]
73 | );
74 |
75 | return Capsule::table('forme_auth_tokens')
76 | ->where('id', '=', $id)
77 | ->first();
78 | }
79 |
80 | /**
81 | * Purge expired tokens.
82 | */
83 | private function purge(): void
84 | {
85 | $dt = Carbon::now();
86 | Capsule::table('forme_auth_tokens')
87 | ->where('expiry', '<=', $dt->format(self::DATE_FORMAT))
88 | ->where('deleted_at', '=', null)
89 | ->update(['deleted_at' => $dt->format(self::DATE_FORMAT)]);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Auth/TokenInterface.php:
--------------------------------------------------------------------------------
1 | container = getContainer();
25 | $this->queue = $this->container->get(Queue::class);
26 | }
27 |
28 | /**
29 | * Spec: It should
30 | * run the next job in the queue
31 | * return feedback response.
32 | */
33 | public function __invoke(array $args = []): void
34 | {
35 | $response = $this->queue->next($args[0] ?? null);
36 | WP_CLI::success($response);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Commands/Wrangle/RunQueueCommand.php:
--------------------------------------------------------------------------------
1 | addArgument(name: 'name', mode: InputArgument::OPTIONAL, description: 'The queue name');
22 | $this->setDescription('Run the next job in the queue.');
23 | $this->setHelp("This command will run the next job in the queue. You can specify a queue name, otherwise it will run the default queue.");
24 | }
25 |
26 | public function execute(InputInterface $input, OutputInterface $output): int
27 | {
28 | $queueName = $input->getArgument('name') ?? null;
29 |
30 | $response = getInstance(Queue::class)->next($queueName);
31 | info($response);
32 |
33 | return Command::SUCCESS;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Controllers/AbstractController.php:
--------------------------------------------------------------------------------
1 | __invoke($request);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Controllers/ControllerInterface.php:
--------------------------------------------------------------------------------
1 | template->maybeAdd();
20 | $this->command->maybeAdd();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Core/Loader.php:
--------------------------------------------------------------------------------
1 | container = getContainer();
23 | }
24 |
25 | /**
26 | * Adds an action to be registered with WordPress.
27 | */
28 | public function addAction(string $actionName, string $class, string $method, int $priority = 10, int $numberOfArgs = 1): void
29 | {
30 | $this->actions[] = $this->convert($actionName, $class, $method, $priority, $numberOfArgs);
31 | }
32 |
33 | /**
34 | * Adds a filter to be registered with WordPress.
35 | */
36 | public function addFilter(string $filterName, string $class, string $method, int $priority = 10, int $numberOfArgs = 1): void
37 | {
38 | $this->filters[] = $this->convert($filterName, $class, $method, $priority, $numberOfArgs);
39 | }
40 |
41 | /**
42 | * Register all the actions and filters within a suitably formatted config yaml.
43 | */
44 | public function addConfig(string $yaml): void
45 | {
46 | $config = Yaml::parse($yaml);
47 | if (isset($config['actions'])) {
48 | foreach ($config['actions'] as $action) {
49 | $method = $action['method'] ?? 'default';
50 | $priority = $action['priority'] ?? 10;
51 | $arguments = $action['arguments'] ?? 1;
52 | $this->addAction($action['hook'], $action['class'], $method, $priority, $arguments);
53 | }
54 | }
55 |
56 | if (isset($config['filters'])) {
57 | foreach ($config['filters'] as $filter) {
58 | $method = $filter['method'] ?? 'default';
59 | $priority = $filter['priority'] ?? 10;
60 | $arguments = $filter['arguments'] ?? 1;
61 | $this->addFilter($filter['hook'], $filter['class'], $method, $priority, $arguments);
62 | }
63 | }
64 | }
65 |
66 | private function convert(string $hook, string $class, string $method, int $priority, int $numberOfArgs): array
67 | {
68 | if ($method === 'default') {
69 | // for backwards compatibility we need to check if the the class has register method
70 | $rc = new ReflectionClass($class);
71 | $method = $rc->hasMethod('register') ? 'register' : '__invoke';
72 | }
73 |
74 | return [
75 | 'hook' => $hook,
76 | 'resolvedCallable' => $method === '__invoke' ? $this->container->get($class) : [$this->container->get($class), $method],
77 | 'priority' => $priority,
78 | 'numberOfArgs' => $numberOfArgs,
79 | ];
80 | }
81 |
82 | /**
83 | * Register user and core hooks with WordPress.
84 | */
85 | public function run(): void
86 | {
87 | foreach ($this->filters as $hook) {
88 | add_filter($hook['hook'], $hook['resolvedCallable'], $hook['priority'], $hook['numberOfArgs']);
89 | }
90 |
91 | foreach ($this->actions as $hook) {
92 | add_action($hook['hook'], $hook['resolvedCallable'], $hook['priority'], $hook['numberOfArgs']);
93 | }
94 |
95 | $coreHooks = $this->container->get(CoreHooks::class);
96 | $coreHooks->load();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Core/PluginOrThemeable.php:
--------------------------------------------------------------------------------
1 | getFileName() ?: '';
26 | }
27 |
28 | /**
29 | * Resolve plugin path
30 | * This is fairly brittle, we are assuming standard structure
31 | * And that the child class extending this is always in plugin-dir/app/Core.
32 | */
33 | protected static function getPluginPath(): string
34 | {
35 | $realClassDir = dirname(static::getFileName(), 3);
36 | $baseName = basename($realClassDir);
37 |
38 | return realpath(WP_PLUGIN_DIR . '/' . $baseName) ?: '';
39 | }
40 |
41 | /**
42 | * Resolve theme path.
43 | */
44 | protected static function getThemePath(): string
45 | {
46 | return realpath(get_template_directory()) ?: '';
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Core/Router.php:
--------------------------------------------------------------------------------
1 | get(StrategyFactory::class)->get($type);
25 | // if the handler is a class we should get it via the container otherwise it won't autowire
26 | if (is_string($handler) && class_exists($handler)) {
27 | $handler = method_exists($handler, 'handle') ? [$handler, 'handle'] : $container->get($handler);
28 | }
29 |
30 | if (is_array($handler) && class_exists($handler[0])) {
31 | $handler[0] = $container->get($handler[0]);
32 | }
33 |
34 | if (!is_callable($handler)) {
35 | throw new RouterException('Not a callable handler');
36 | }
37 |
38 | static::$currentHandler = $strategy->convert($route, $handler, $method);
39 |
40 | return new self();
41 | }
42 |
43 | public static function get(string $route, callable|string|array $handler, string $type = 'custom'): static
44 | {
45 | return self::map($route, $handler, $type, 'GET');
46 | }
47 |
48 | public static function post(string $route, callable|string|array $handler, string $type = 'custom'): static
49 | {
50 | return self::map($route, $handler, $type, 'POST');
51 | }
52 |
53 | public static function put(string $route, callable|string|array $handler, string $type = 'custom'): static
54 | {
55 | return self::map($route, $handler, $type, 'PUT');
56 | }
57 |
58 | public static function patch(string $route, callable|string|array $handler, string $type = 'custom'): static
59 | {
60 | return self::map($route, $handler, $type, 'PATCH');
61 | }
62 |
63 | public static function delete(string $route, callable|string|array $handler, string $type = 'custom'): static
64 | {
65 | return self::map($route, $handler, $type, 'DELETE');
66 | }
67 |
68 | public static function addMiddleware(MiddlewareInterface|string $value): static
69 | {
70 | static::$currentHandler->addMiddleware($value);
71 |
72 | return new self();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Core/RouterException.php:
--------------------------------------------------------------------------------
1 | 'sqlite',
21 | 'database' => DB_DIR . DB_FILE,
22 | ];
23 | } else {
24 | $args = [
25 | 'driver' => 'mysql',
26 | 'host' => DB_HOST,
27 | 'database' => DB_NAME,
28 | 'username' => DB_USER,
29 | 'password' => DB_PASSWORD,
30 | 'charset' => DB_CHARSET,
31 | ];
32 | if (DB_COLLATE) {
33 | $args['collation'] = DB_COLLATE;
34 | }
35 | }
36 | $args['prefix'] = $this->getPrefix();
37 |
38 | $this->capsule->addConnection($args);
39 |
40 | // Make this Capsule instance available globally via static methods
41 | $this->capsule->setAsGlobal();
42 |
43 | // Setup the Eloquent ORM
44 | $this->capsule->bootEloquent();
45 | }
46 |
47 | protected function getPrefix(): string
48 | {
49 | global $wpdb;
50 | if ($wpdb) {
51 | return $wpdb->prefix ?: 'wp_';
52 | } else {
53 | $configLocation = ABSPATH . 'wp-config.php';
54 |
55 | return configExtract('table_prefix', $configLocation) ?: 'wp_';
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Database/MigrationInitialiser.php:
--------------------------------------------------------------------------------
1 | defaultConfigLocation = realpath(__DIR__ . '/phinx.default.yml');
16 | }
17 |
18 | /**
19 | * Creates a phinx file if it doesn't exist yet.
20 | */
21 | public function maybeCreate(string $configLocation): void
22 | {
23 | if (!file_exists($configLocation)) {
24 | // grab the default
25 | $defaultConfig = Yaml::parse(file_get_contents($this->defaultConfigLocation));
26 | // save the config
27 | $yaml = Yaml::dump($defaultConfig);
28 | file_put_contents($configLocation, $yaml);
29 | $this->logger->info('Creating phinx file at ' . $configLocation);
30 | }
31 | }
32 |
33 | public function setEnvironments(string $configLocation): void
34 | {
35 | global $wpdb;
36 |
37 | $phinxConfig = Yaml::parse(file_get_contents($configLocation));
38 | $originalConfig = $phinxConfig;
39 | // make sure the default environment matches the set environment, if wp_env not set then assume development
40 | $defaultEnvironment = $phinxConfig['environments']['default_environment'];
41 | if ($defaultEnvironment != WP_ENV) {
42 | $phinxConfig['environments']['default_environment'] = WP_ENV ?: 'development';
43 | $defaultEnvironment = $phinxConfig['environments']['default_environment'];
44 | $this->logger->info('Setting the Phinx db environment to ' . $defaultEnvironment);
45 | }
46 |
47 | // if db credentials do not match, let's set them - host/port//name/user/pass/charset
48 | $dbCredentials = $phinxConfig['environments'][$defaultEnvironment];
49 | if (
50 | ($dbCredentials['host'] != DB_HOST && $dbCredentials['host'] . ':' . $dbCredentials['port'] != DB_HOST) ||
51 | $dbCredentials['name'] != DB_NAME ||
52 | $dbCredentials['user'] != DB_USER ||
53 | $dbCredentials['pass'] != DB_PASSWORD ||
54 | $dbCredentials['charset'] != DB_CHARSET
55 | ) {
56 | $hostPort = explode(':', DB_HOST);
57 | $dbCredentials['host'] = $hostPort[0];
58 | $dbCredentials['port'] = $hostPort[1] ?? 3306;
59 | $dbCredentials['name'] = DB_NAME;
60 | $dbCredentials['user'] = DB_USER;
61 | $dbCredentials['pass'] = DB_PASSWORD;
62 | $dbCredentials['charset'] = DB_CHARSET;
63 | $phinxConfig['environments'][$defaultEnvironment] = $dbCredentials;
64 | $this->logger->info('Updating the db credentials for Phinx');
65 | }
66 |
67 | if (WP_ENV === 'testing' && (defined('USE_MYSQL') && USE_MYSQL)) {
68 | $phinxConfig['environments']['testing']['adapter'] = 'mysql';
69 | }
70 |
71 | // add db_prefix
72 | $dbPrefix = $wpdb->prefix ?: 'wp_';
73 | foreach (array_keys($phinxConfig['environments']) as $key) {
74 | if (!str_starts_with($key, 'default_')) {
75 | $phinxConfig['environments'][$key]['table_prefix'] = $dbPrefix;
76 | }
77 | }
78 |
79 | $this->logger->info('Adding db prefix ' . $dbPrefix);
80 | // save to phinx.yml if things have changed
81 | if ($phinxConfig !== $originalConfig) {
82 | $yaml = Yaml::dump($phinxConfig);
83 | file_put_contents($configLocation, $yaml);
84 | $this->logger->info('phinx.yml saved after env changes');
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Database/Migrations.php:
--------------------------------------------------------------------------------
1 | phinxConfigLocation = FORME_PRIVATE_ROOT . 'phinx.yml';
31 | $this->phinxApplication = new TextWrapper($phinxApplication, ['configuration' => $this->phinxConfigLocation]);
32 | }
33 |
34 | public function migrate(): array
35 | {
36 | $messages = [];
37 | // this should run on plugin/theme install or on upgrade
38 | // create phinx.yml if it doesn't exist yet
39 | $this->initialiser->maybeCreate($this->phinxConfigLocation);
40 | // set the db environments
41 | $this->initialiser->setEnvironments($this->phinxConfigLocation);
42 | // grab the current config and save before changes to check for diff
43 | $phinxConfig = Yaml::parse(file_get_contents($this->phinxConfigLocation));
44 | $originalConfig = $phinxConfig;
45 | // make sure this framework directory _and_ the plugin/theme migration directories are in
46 | // adding plugins/themes at different points should be ok because older migrations will still run
47 | $migrationPaths = $phinxConfig['paths']['migrations'];
48 | if (!is_array($migrationPaths)) {
49 | // assume this has just been created, so let's blank it
50 | $migrationPaths = [];
51 | $this->logger->info('Initialising migration paths');
52 | }
53 |
54 | // add plugin/theme paths if not in
55 | if (self::isPlugin() && !in_array(self::getPluginPath() . self::DB_DIR, $migrationPaths)) {
56 | $migrationPaths[] = self::getPluginPath() . self::DB_DIR;
57 | $this->logger->info('Adding plugin migration path');
58 | } elseif (!self::isPlugin() && !in_array(self::getThemePath() . self::DB_DIR, $migrationPaths)) {
59 | $migrationPaths[] = self::getThemePath() . self::DB_DIR;
60 | $this->logger->info('Adding theme migration path');
61 | }
62 |
63 | // add framework path if not in
64 | if (!in_array(__DIR__ . '/Migrations', $migrationPaths)) {
65 | $migrationPaths[] = __DIR__ . '/Migrations';
66 | $this->logger->info('Adding framework migration path');
67 | }
68 |
69 | $phinxConfig['paths']['migrations'] = $migrationPaths;
70 | // save to phinx.yml if things have changed
71 | if ($phinxConfig !== $originalConfig) {
72 | $yaml = Yaml::dump($phinxConfig);
73 | file_put_contents($this->phinxConfigLocation, $yaml);
74 | $messages[] = ['type' => 'success', 'text' => 'Forme phinx.yml saved, check ' . $this->phinxConfigLocation];
75 | $this->logger->info('phinx.yml saved after path changes');
76 | }
77 |
78 | $this->logger->info('Migration path config completed');
79 | // run migrations
80 | $output = $this->phinxApplication->getMigrate(WP_ENV ?: 'development');
81 | $this->logger->info($output);
82 | $messages[] = ['type' => 'success', 'text' => 'Forme Migration completed, check server logs for more info'];
83 |
84 | return $messages;
85 | }
86 |
87 | /**
88 | * CAREFUL!!! This method rolls back all forme migrations and thereby borks your database state. You probably only want this in a testing environment.
89 | */
90 | public function rollback(): array
91 | {
92 | $messages = [];
93 | // run migrations
94 | $output = $this->phinxApplication->getRollback(WP_ENV ?: 'testing', 0);
95 | $this->logger->info($output);
96 | $messages[] = ['type' => 'success', 'text' => 'Forme Rollback completed, check server logs for more info'];
97 |
98 | return $messages;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Database/Migrations/20201018145438_create_auth_tokens_table.php:
--------------------------------------------------------------------------------
1 | table('forme_auth_tokens');
23 | $table->addColumn('name', 'string')
24 | ->addColumn('token', 'string')
25 | ->addColumn('expiry', 'datetime')
26 | ->create();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Database/Migrations/20210112174503_create_queued_jobs_table.php:
--------------------------------------------------------------------------------
1 | table('forme_queued_jobs');
28 | $table->addColumn('class', 'string')
29 | ->addColumn('arguments', 'text')
30 | ->addColumn('started_at', 'datetime', ['null' => true])
31 | ->addColumn('completed_at', 'datetime', ['null' => true])
32 | ->addColumn('created_at', 'datetime')
33 | ->addColumn('updated_at', 'datetime')
34 | ->create();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Database/Migrations/20210122110424_update_queued_jobs_table.php:
--------------------------------------------------------------------------------
1 | table('forme_queued_jobs');
27 | $table
28 | ->addColumn('scheduled_for', 'datetime')
29 | ->addColumn('frequency', 'string', ['null' => true])
30 | ->update();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Database/Migrations/20221203173957_add_queue_name_to_queued_jobs_table.php:
--------------------------------------------------------------------------------
1 | table('forme_queued_jobs');
27 | $table
28 | ->addColumn('queue_name', 'string', ['null' => true])
29 | ->update();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Database/Migrations/20230320094337_create_events_table.php:
--------------------------------------------------------------------------------
1 | table('forme_events', ['signed' => false]);
18 | $table->addColumn('type', 'string')
19 | ->addColumn('payload', 'text', ['limit' => MysqlAdapter::TEXT_LONG])
20 | ->addColumn('created_at', 'datetime')
21 | ->create();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Database/Migrations/20230614065259_add_soft_delete_to_auth_table.php:
--------------------------------------------------------------------------------
1 | table('forme_auth_tokens');
11 | $table->addColumn('deleted_at', 'datetime', ['null' => true]);
12 | $table->save();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Database/phinx.default.yml:
--------------------------------------------------------------------------------
1 | paths:
2 | migrations: "%%PHINX_CONFIG_DIR%%/db/migrations"
3 | seeds: "%%PHINX_CONFIG_DIR%%/db/seeds"
4 | environments:
5 | default_migration_table: phinxlog
6 | default_environment: development
7 | production:
8 | {
9 | adapter: mysql,
10 | host: localhost,
11 | name: production_db,
12 | user: root,
13 | pass: "",
14 | port: 3306,
15 | charset: utf8,
16 | }
17 | development:
18 | {
19 | adapter: mysql,
20 | host: localhost,
21 | name: development_db,
22 | user: root,
23 | pass: "",
24 | port: 3306,
25 | charset: utf8,
26 | }
27 | testing:
28 | {
29 | adapter: sqlite,
30 | host: localhost,
31 | name: testing_db,
32 | user: root,
33 | pass: "",
34 | port: 3306,
35 | charset: utf8,
36 | }
37 | version_order: creation
38 |
--------------------------------------------------------------------------------
/src/Enums/AcfEnum.php:
--------------------------------------------------------------------------------
1 | fieldNames[$this->value];
21 | }
22 |
23 | /**
24 | * @return int|string
25 | *
26 | * @throws UnknownEnumProperty
27 | */
28 | public function __get(string $name)
29 | {
30 | return match ($name) {
31 | 'name' => $this->acfName(),
32 | 'key' => $this->value,
33 | 'value' => $this->value,
34 | 'label' => $this->label,
35 | default => throw new UnknownEnumProperty($name),
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Fields/FieldGroupInterface.php:
--------------------------------------------------------------------------------
1 | callbacks, function ($filter) use ($callable) {
17 | $filter = array_values($filter);
18 | $filter = $filter[0];
19 | if (is_array($filter['function']) && isset($filter['function'][0])) {
20 | return str_contains($filter['function'][0]::class, $callable);
21 | } elseif (is_string($filter['function'])) {
22 | return str_contains($filter['function'], $callable);
23 | } elseif ($filter['function'] instanceof $callable || $filter['function'] == $callable) {
24 | return true;
25 | }
26 | });
27 | if ($alreadySet !== []) {
28 | return true;
29 | }
30 | }
31 |
32 | return false;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Hooks/JobCommandHook.php:
--------------------------------------------------------------------------------
1 | input()[$offset]);
11 | }
12 |
13 | public function offsetGet(mixed $offset): mixed
14 | {
15 | return $this->input()[$offset] ?? null;
16 | }
17 |
18 | public function offsetSet(mixed $offset, mixed $value): void
19 | {
20 | if (is_null($offset)) {
21 | $this->input()[] = $value;
22 | } else {
23 | $this->input()[$offset] = $value;
24 | }
25 | }
26 |
27 | public function offsetUnset(mixed $offset): void
28 | {
29 | unset($this->input()[$offset]);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Http/Handlers/AjaxHandler.php:
--------------------------------------------------------------------------------
1 | handler = $handler;
23 | }
24 |
25 | public function __invoke(): void
26 | {
27 | $request = \Forme\request();
28 |
29 | $controller = $this->handler;
30 |
31 | // we pass the response closure into the dispatcher
32 | $responseFunc = function ($request, $handler) use ($controller) {
33 | $response = call_user_func($controller, $request);
34 |
35 | return ResponseFactory::create($response);
36 | };
37 | $response = $this->dispatchMiddleware($request, $responseFunc);
38 | $this->shutdown->shutdown($response);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Http/Handlers/CustomRouteHandler.php:
--------------------------------------------------------------------------------
1 | router = $factory->make(AltoRouter::class);
33 | }
34 |
35 | public function __invoke(): void
36 | {
37 | if (!$this->matched) {
38 | $route = $this->router->match();
39 | if ($route && isset($route['target'])) {
40 | $this->matched = true;
41 | // We call this here because things like admin bar rely on this hook
42 | // but TODO: test for side-effects?
43 | do_action('template_redirect');
44 | $request = \Forme\request();
45 | if (isset($route['params'])) {
46 | $request = $request->withQueryParams($route['params'] + $request->getQueryParams());
47 | }
48 |
49 | // we pass the response closure into the dispatcher
50 | $responseFunc = function ($request, $handler) use ($route) {
51 | $response = call_user_func($route['target'], $request);
52 |
53 | return ResponseFactory::create($response);
54 | };
55 | $response = $this->dispatchMiddleware($request, $responseFunc);
56 | $this->shutdown->shutdown($response);
57 | }
58 | }
59 | }
60 |
61 | /**
62 | * @param string $route A string to match (ex: 'myfoo')
63 | * @param callable $callback A function to run, examples:
64 | * Routes::map('myfoo', 'my_callback_function');
65 | * Routes::map('mybaq', array($my_class, 'method'));
66 | * Routes::map('myqux', function() {
67 | * //stuff goes here
68 | * });
69 | */
70 | public function map(string $route, callable $callback, string $method = 'GET', string $args = ''): void
71 | {
72 | $route = $this->convertRoute($route);
73 | $this->router->map($method, trailingslashit($route), $callback, $args);
74 | $this->router->map($method, untrailingslashit($route), $callback, $args);
75 | }
76 |
77 | /**
78 | * @return string A string in a format for AltoRouter
79 | * ex: [:my_param]
80 | */
81 | private function convertRoute(string $routeString): string
82 | {
83 | if (strpos($routeString, '[') > -1) {
84 | return $routeString;
85 | }
86 |
87 | $routeString = preg_replace('#(:)\w+#', '/[$0]', $routeString);
88 | $routeString = str_replace('[[', '[', $routeString);
89 | $routeString = str_replace(']]', ']', $routeString);
90 | $routeString = str_replace('[/:', '[:', $routeString);
91 | $routeString = str_replace('//[', '/[', $routeString);
92 | if (str_starts_with($routeString, '/')) {
93 | $routeString = substr($routeString, 1);
94 | }
95 |
96 | return $routeString;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Http/Handlers/HandlerInterface.php:
--------------------------------------------------------------------------------
1 | middlewareQueue[] = $middleware;
19 | }
20 |
21 | public function getMiddlewareQueue(): array
22 | {
23 | return $this->middlewareQueue;
24 | }
25 |
26 | public function addMiddlewareQueue(array $queue): void
27 | {
28 | $this->middlewareQueue = array_merge($this->middlewareQueue, $queue);
29 | }
30 |
31 | public function dispatchMiddleware(ServerRequestInterface $request, callable $responseFunc): ResponseInterface
32 | {
33 | $queue = array_merge($this->middlewareQueue ?: [], [$responseFunc]);
34 | $resolver = fn ($entry) => is_string($entry) ? \Forme\getInstance($entry) : $entry;
35 | $relay = new Relay($queue, $resolver);
36 |
37 | return $relay->handle($request);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Http/Handlers/RestHandler.php:
--------------------------------------------------------------------------------
1 | handler = $handler;
26 | }
27 |
28 | public function __invoke(WP_REST_Request $request): ResponseInterface
29 | {
30 | // convert from WP_REST into PSR7
31 | $request = ServerRequest::fromRequest($request);
32 |
33 | $restHandler = $this->handler;
34 |
35 | // we pass the response closure into the dispatcher
36 | $responseFunc = function ($request, $handler) use ($restHandler) {
37 | $response = call_user_func($restHandler, $request);
38 |
39 | return ResponseFactory::create($response);
40 | };
41 | $response = $this->dispatchMiddleware($request, $responseFunc);
42 |
43 | $this->shutdown->shutdown($response);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Http/Handlers/TemplateHandler.php:
--------------------------------------------------------------------------------
1 | getControllerClassFromTemplate($template);
33 |
34 | return $this->handleRequest($controller);
35 | }
36 |
37 | private function getControllerClassFromTemplate(string $template): string
38 | {
39 | $controllerName = (new UnicodeString(basename($template, '.php')))->camel()->title()->toString();
40 | // if the name doesn't end with Controller then add 'Controller'
41 | $controllerName .= str_ends_with($controllerName, 'Controller') ? '' : 'Controller';
42 | // Classes can't start with a number so we have to special case the behaviour here
43 | if ($controllerName === '404Controller') {
44 | $controllerName = 'Error' . $controllerName;
45 | }
46 |
47 | return $this->getNameSpace($template) . '\\' . $controllerName;
48 | }
49 |
50 | /** @return null */
51 | private function handleRequest(string $controllerName)
52 | {
53 | if (!class_exists($controllerName)) {
54 | return null;
55 | }
56 |
57 | /** @var ControllerInterface */
58 | $controller = $this->container->get($controllerName);
59 | $this->addMiddlewareQueue($controller->getMiddlewareQueue());
60 |
61 | $postId = get_queried_object_id();
62 |
63 | // acf sugar
64 | if (function_exists('acf')) {
65 | $fields = get_fields($postId);
66 | $options = get_fields('options');
67 | }
68 |
69 | $request = \Forme\request();
70 | $request = $request->withParsedBody([
71 | 'postId' => $postId,
72 | 'fields' => $fields ? arrayKeysToCamelCase($fields) : [],
73 | 'options' => $options ? arrayKeysToCamelCase($options) : [],
74 | ]);
75 |
76 | // we pass the response closure into the dispatcher
77 | $responseFunc = function ($request, $handler) use ($controller) {
78 | $response = $controller->handle($request);
79 |
80 | return ResponseFactory::create($response);
81 | };
82 | $response = $this->dispatchMiddleware($request, $responseFunc);
83 | $this->shutdown->shutdown($response);
84 | }
85 |
86 | /**
87 | * Extract the namespace from file contents.
88 | */
89 | private function getNameSpace(string $file): ?string
90 | {
91 | $fileContents = file_get_contents($file);
92 | if (preg_match('#^namespace\s+(.+?);$#sm', $fileContents, $m)) {
93 | return $m[1];
94 | }
95 |
96 | return null;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Http/ResponseFactory.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class ServerRequest extends DiactorosServerRequest implements ArrayAccess
16 | {
17 | use InteractsWithInput;
18 | use InteractsWithUri;
19 | use ArraysRequestInput;
20 |
21 | public static function fromRequest(ServerRequestInterface $request): ServerRequestInterface
22 | {
23 | $parsedBody = $request->getParsedBody();
24 |
25 | // Is this a JSON request?
26 | if (stripos($request->getHeaderLine('Content-Type'), 'application/json') !== false) {
27 | $parsedBody = @json_decode($request->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
28 | }
29 |
30 | return new static(
31 | $request->getServerParams(),
32 | $request->getUploadedFiles(),
33 | $request->getUri(),
34 | $request->getMethod(),
35 | $request->getBody(),
36 | $request->getHeaders(),
37 | $request->getCookieParams(),
38 | $request->getQueryParams(),
39 | $parsedBody,
40 | $request->getProtocolVersion()
41 | );
42 | }
43 |
44 | public function ajax(): bool
45 | {
46 | if (!$this->hasHeader('X-Requested-With')) {
47 | return false;
48 | }
49 |
50 | return $this->getHeader('X-Requested-With')[0] === 'XMLHttpRequest';
51 | }
52 |
53 | public function getMethod(): string
54 | {
55 | return strtoupper(parent::getMethod());
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Http/Shutdown.php:
--------------------------------------------------------------------------------
1 | send_headers();
22 |
23 | // If we're handling a WordPressController response at this point then WordPress will already have
24 | // sent headers as it happens earlier in the lifecycle. For this scenario we need to do a bit more
25 | // work to make sure that duplicate headers are not sent back.
26 | send($this->removeSentHeadersAndMoveIntoResponse($response));
27 | }
28 |
29 | exit();
30 | }
31 |
32 | private function removeSentHeadersAndMoveIntoResponse(ResponseInterface $response): ResponseInterface
33 | {
34 | // 1. Format the previously sent headers into an array of [key, value]
35 | // 2. Remove all headers from the output that we find
36 | // 3. Filter out any headers that would clash with those already in the response
37 | $headersToAdd = collect(headers_list())->map(function ($header) {
38 | $parts = explode(':', $header, 2);
39 | header_remove($parts[0]);
40 |
41 | return $parts;
42 | })->filter(fn ($header) => strtolower($header[0]) != 'content-type');
43 |
44 | // Add the previously sent headers into the response
45 | // Note: You can't mutate a response so we need to use the reduce to end up with a response
46 | // object with all the correct headers
47 | /** @var ResponseInterface */
48 | $responseToSend = collect($headersToAdd)->reduce(fn (ResponseInterface $newResponse, array $header) => $newResponse->withAddedHeader($header[0], $header[1]), $response);
49 |
50 | return $responseToSend;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Http/WPRest/ServerRequest.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class ServerRequest extends WPRestServerRequest implements ServerRequestInterface, ArrayAccess
16 | {
17 | use InteractsWithInput;
18 | use InteractsWithUri;
19 | use ArraysRequestInput;
20 | }
21 |
--------------------------------------------------------------------------------
/src/Jobs/JobInterface.php:
--------------------------------------------------------------------------------
1 | getQueue();
15 | $queue->dispatch([
16 | 'class' => $this::class,
17 | 'arguments' => $args,
18 | 'queue_name' => $queueName,
19 | ]);
20 | }
21 |
22 | public function schedule(array $args = []): void
23 | {
24 | $queue = $this->getQueue();
25 | $args['class'] = $this::class;
26 | $queue->schedule($args);
27 | }
28 |
29 | public function start(array $args = []): void
30 | {
31 | $queue = $this->getQueue();
32 | $args['class'] = $this::class;
33 | $queue->start($args);
34 | }
35 |
36 | public function stop(array $args = [], ?string $queueName = null): void
37 | {
38 | $queue = $this->getQueue();
39 | $queue->stop(['class' => $this::class, 'queue_name' => $queueName, 'arguments' => $args]);
40 | }
41 |
42 | protected function getQueue(): Queue
43 | {
44 | if (property_exists($this, 'queue') && is_a($this->queue, Queue::class)) {
45 | return $this->queue;
46 | }
47 |
48 | if (property_exists($this, 'container') && is_a($this->container, ContainerInterface::class)) {
49 | return $this->container->get(Queue::class);
50 | }
51 |
52 | if (function_exists('app')) {
53 | $reflection = new ReflectionFunction('app');
54 | $returnType = $reflection->getReturnType();
55 | if ($returnType && is_a((string) $returnType, ContainerInterface::class, true)) {
56 | return app()->get(Queue::class);
57 | }
58 | }
59 |
60 | if (function_exists('Forme\getInstance') && defined('FORME_PRIVATE_ROOT')) {
61 | return \Forme\getInstance(Queue::class);
62 | }
63 |
64 | throw new RuntimeException('Unable to instantiate queue, either provide a suitable container or inject the queue directly into the job class constructor as $queue');
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Log/LogEventHandler.php:
--------------------------------------------------------------------------------
1 | 'log', 'payload' => $record]);
17 | } catch (Exception $e) {
18 | // noop - table might not exist, or mysql connection might be unavailable, but we don't really want to handle that here
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Log/LogHandlerType.php:
--------------------------------------------------------------------------------
1 | 'Not Allowed outside of test environment'], 401);
20 | } else {
21 | return $handler->handle($request);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Models/Comment.php:
--------------------------------------------------------------------------------
1 | hasOne(\Forme\Framework\Models\Post::class);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Models/CustomPostable.php:
--------------------------------------------------------------------------------
1 | snake()->toString();
30 | }
31 |
32 | static::addGlobalScope('postType', fn (Builder $query) => $query->where('post_type', '=', $postType));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Models/Event.php:
--------------------------------------------------------------------------------
1 |
35 | */
36 | protected $casts = [
37 | 'payload' => 'array',
38 | ];
39 | }
40 |
--------------------------------------------------------------------------------
/src/Models/MenuItem.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | public function childItems(): Collection
25 | {
26 | $type = $this->menuType;
27 |
28 | $collection = self::with('meta')->whereHas('meta', function (Builder $query) {
29 | $query->where('meta_key', '_menu_item_menu_item_parent')->where('meta_value', (string) $this->ID);
30 | })
31 | ->orderBy('menu_order')
32 | ->get();
33 |
34 | return self::attachWPMenuItemData($collection, $type);
35 | }
36 |
37 | public function getUrlAttribute(): string
38 | {
39 | return $this->wordPressMenuItem->url;
40 | }
41 |
42 | /**
43 | * @return ?Collection
44 | */
45 | public static function getByMenuType(string $type): ?Collection
46 | {
47 | $indexedItems = self::getIndexedWPNavMenuItems($type);
48 |
49 | // bail if null
50 | if (!$indexedItems) {
51 | return null;
52 | }
53 |
54 | // convert to an array of ids
55 | $itemIds = array_keys($indexedItems);
56 |
57 | $collection = self::with('meta')->whereHas('meta', function (Builder $query) {
58 | $query->where('meta_key', '_menu_item_menu_item_parent')->where('meta_value', '0');
59 | })
60 | ->whereIn('ID', $itemIds)
61 | ->orderBy('menu_order')
62 | ->get();
63 |
64 | return self::attachWPMenuItemData($collection, $type);
65 | }
66 |
67 | /**
68 | * @param Collection $collection
69 | *
70 | * @return Collection
71 | */
72 | private static function attachWPMenuItemData(Collection $collection, string $type): ?Collection
73 | {
74 | $indexedItems = self::getIndexedWPNavMenuItems($type);
75 |
76 | // bail if null
77 | if (!$indexedItems) {
78 | return null;
79 | }
80 |
81 | return $collection->map(function (MenuItem $item) use ($indexedItems, $type) {
82 | $item->menuType = $type;
83 | /** @var WPMenuItem|WP_Post */
84 | $WPMenuItem = $indexedItems[$item->ID];
85 | $item->title = $WPMenuItem->title;
86 | $item->url = $WPMenuItem->url;
87 | $item->classes = $WPMenuItem->classes;
88 | $item->target = $WPMenuItem->target;
89 | $item->description = $WPMenuItem->description;
90 | $item->wordPressMenuItem = $WPMenuItem;
91 |
92 | return $item;
93 | });
94 | }
95 |
96 | /**
97 | * @return array
98 | */
99 | private static function getIndexedWPNavMenuItems(string $type): ?array
100 | {
101 | // Get the nav menu based on the requested menu.
102 | $menu = wp_get_nav_menu_object($type);
103 |
104 | // Get the nav menu based on the theme_location.
105 | $locations = get_nav_menu_locations();
106 | if (!$menu && isset($locations[$type])) {
107 | $menu = wp_get_nav_menu_object($locations[$type]);
108 | }
109 |
110 | /*
111 | * If no menu was found: bail.
112 | */
113 | if (!$menu) {
114 | return null;
115 | }
116 |
117 | // If the menu exists, get its items.
118 | $items = wp_get_nav_menu_items($menu->term_id, ['update_post_term_cache' => false]) ?: [];
119 |
120 | $indexedItems = [];
121 | foreach ($items as $item) {
122 | $indexedItems[$item->ID] = $item;
123 | }
124 |
125 | array_walk($items, function ($item) use ($indexedItems) {
126 | $indexedItems[$item->ID] = $item;
127 | });
128 |
129 | return $indexedItems;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/Models/Option.php:
--------------------------------------------------------------------------------
1 | option_id;
25 | }
26 |
27 | public function getNameAttribute(): string
28 | {
29 | return $this->option_name;
30 | }
31 |
32 | public function setNameAttribute(string $value): void
33 | {
34 | $this->option_name = $value;
35 | }
36 |
37 | public function setValueAttribute(string $value): void
38 | {
39 | $this->option_value = $value;
40 | }
41 |
42 | public function getValueAttribute(): string
43 | {
44 | return $this->option_value;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Models/Post.php:
--------------------------------------------------------------------------------
1 | $children
26 | */
27 | class Post extends Model
28 | {
29 | use PostSugar;
30 |
31 | protected $table = 'posts';
32 |
33 | protected $primaryKey = 'ID';
34 |
35 | /**
36 | * @var string
37 | */
38 | public const CREATED_AT = 'post_date';
39 |
40 | /**
41 | * @var string
42 | */
43 | public const UPDATED_AT = 'post_modified';
44 |
45 | /**
46 | * Filter by post type.
47 | */
48 | public function scopeType(Builder $query, string $type = 'post'): Builder
49 | {
50 | return $query->where('post_type', '=', $type);
51 | }
52 |
53 | /**
54 | * Filter by post status.
55 | */
56 | public function scopeStatus(Builder $query, string $status = 'publish'): Builder
57 | {
58 | return $query->where('post_status', '=', $status);
59 | }
60 |
61 | /**
62 | * Filter by post author.
63 | */
64 | public function scopeAuthor(Builder $query, ?string $author = null): ?Builder
65 | {
66 | if ($author) {
67 | return $query->where('post_author', '=', $author);
68 | } else {
69 | return null;
70 | }
71 | }
72 |
73 | /**
74 | * Get comments from the post.
75 | */
76 | public function comments(): HasMany
77 | {
78 | return $this->hasMany(Comment::class, 'comment_post_ID');
79 | }
80 |
81 | /**
82 | * Get meta fields from the post.
83 | */
84 | public function meta(): HasMany
85 | {
86 | return $this->hasMany(PostMeta::class, 'post_id');
87 | }
88 |
89 | /**
90 | * @param string $key
91 | *
92 | * @return mixed
93 | */
94 | public function __get($key)
95 | {
96 | // if key ends with "_meta" return the meta value
97 | if (str_ends_with($key, '_meta')) {
98 | return $this->meta->firstWhere('meta_key', substr($key, 0, -5))?->meta_value;
99 | }
100 |
101 | return parent::__get($key);
102 | }
103 |
104 | public function generateSlug(): void
105 | {
106 | $slug = wp_unique_post_slug(
107 | sanitize_title($this->post_title),
108 | $this->ID,
109 | $this->post_status,
110 | $this->post_type,
111 | $this->post_parent
112 | );
113 |
114 | $this->post_name = $slug;
115 | }
116 |
117 | public function save(array $options = [])
118 | {
119 | if (!$this->post_name) {
120 | $this->generateSlug();
121 | }
122 |
123 | return parent::save($options);
124 | }
125 |
126 | /**
127 | * @return Collection
128 | */
129 | public function getChildrenAttribute(): Collection
130 | {
131 | return self::where('post_parent', $this->ID)
132 | ->where('post_status', 'publish')
133 | ->get();
134 | }
135 |
136 | public function hasChildren(): bool
137 | {
138 | return $this->children->isNotEmpty();
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Models/PostMeta.php:
--------------------------------------------------------------------------------
1 | belongsTo(Post::class, 'post_id', 'ID');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Models/PostSugar.php:
--------------------------------------------------------------------------------
1 | ID);
25 | }
26 |
27 | public function getTitleAttribute(): string
28 | {
29 | return $this->post_title;
30 | }
31 |
32 | public function setTitleAttribute(string $value): void
33 | {
34 | $this->post_title = $value;
35 | }
36 |
37 | public function getContentAttribute(): string
38 | {
39 | return $this->post_content;
40 | }
41 |
42 | public function setContentAttribute(string $value): void
43 | {
44 | $this->post_content = $value;
45 | }
46 |
47 | public function getExcerptAttribute(): string
48 | {
49 | return $this->post_excerpt;
50 | }
51 |
52 | public function setExcerptAttribute(string $value): void
53 | {
54 | $this->post_excerpt = $value;
55 | }
56 |
57 | public function getSlugAttribute(): string
58 | {
59 | return $this->post_name;
60 | }
61 |
62 | public function setSlugAttribute(string $value): void
63 | {
64 | $this->post_name = $value;
65 | }
66 |
67 | public function getTypeAttribute(): string
68 | {
69 | return $this->post_type;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Models/PostTypeInterface.php:
--------------------------------------------------------------------------------
1 | 'datetime',
14 | 'started_at' => 'datetime',
15 | 'completed_at' => 'datetime',
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/src/Models/User.php:
--------------------------------------------------------------------------------
1 | hasMany(UserMeta::class, 'user_id');
25 | }
26 |
27 | /**
28 | * @param string $key
29 | *
30 | * @return mixed
31 | */
32 | public function __get($key)
33 | {
34 | // if key ends with "_meta" return the meta value
35 | if (str_ends_with($key, '_meta')) {
36 | return $this->meta->firstWhere('meta_key', substr($key, 0, -5))?->meta_value;
37 | }
38 |
39 | return parent::__get($key);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Models/UserMeta.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class, 'user_id', 'ID');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Models/UserSugar.php:
--------------------------------------------------------------------------------
1 | user_url;
27 | }
28 |
29 | public function setUrlAttribute(string $value): void
30 | {
31 | $this->user_url = $value;
32 | }
33 |
34 | public function getLoginAttribute(): string
35 | {
36 | return $this->user_login;
37 | }
38 |
39 | public function setLoginAttribute(string $value): void
40 | {
41 | $this->user_login = $value;
42 | }
43 |
44 | public function getNicenameAttribute(): string
45 | {
46 | return $this->user_nicename;
47 | }
48 |
49 | public function setNicenameAttribute(string $value): void
50 | {
51 | $this->user_nicename = $value;
52 | }
53 |
54 | public function getEmailAttribute(): string
55 | {
56 | return $this->user_email;
57 | }
58 |
59 | public function setEmailAttribute(string $value): void
60 | {
61 | $this->user_email = $value;
62 | }
63 |
64 | public function getActivationKeyAttribute(): string
65 | {
66 | return $this->user_activation_key;
67 | }
68 |
69 | public function setActivationKeyAttribute(string $value): void
70 | {
71 | $this->user_activation_key = $value;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Models/WPMenuItem.php:
--------------------------------------------------------------------------------
1 | container->make(AjaxHandler::class);
25 | $ajaxHandler->setHandler($handler);
26 | add_action('wp_ajax_' . $route, $ajaxHandler);
27 |
28 | return $ajaxHandler;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Router/Strategy/AjaxPublicStrategy.php:
--------------------------------------------------------------------------------
1 | container->make(AjaxHandler::class);
25 | $ajaxHandler->setHandler($handler);
26 |
27 | add_action('wp_ajax_nopriv_' . $route, $ajaxHandler);
28 |
29 | return $ajaxHandler;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Router/Strategy/CustomStrategy.php:
--------------------------------------------------------------------------------
1 | container->make(CustomRouteHandler::class);
25 | $customRouteHandler->map($route, $handler, $method ?? 'GET');
26 | add_action('wp_loaded', $customRouteHandler);
27 |
28 | return $customRouteHandler;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Router/Strategy/RestStrategy.php:
--------------------------------------------------------------------------------
1 | container->make(RestHandler::class);
25 | $restHandler->setHandler($handler);
26 | add_action('rest_api_init', function () use ($method, $restHandler, $route) {
27 | $routeBase = substr($route, strpos($route, '/', 1));
28 | $namespace = str_replace($routeBase, '', $route);
29 | register_rest_route($namespace, $routeBase, [
30 | 'methods' => $method ?? 'GET',
31 | 'callback' => $restHandler,
32 | 'permission_callback' => '__return_true',
33 | ]);
34 | });
35 |
36 | return $restHandler;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Router/Strategy/StrategyException.php:
--------------------------------------------------------------------------------
1 | camel()->title()->toString() . 'Strategy';
24 |
25 | return $this->container->get($class);
26 | } else {
27 | throw new StrategyException('Undefined routing type');
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Router/Strategy/StrategyInterface.php:
--------------------------------------------------------------------------------
1 | container = $container ?: new Container();
35 |
36 | $this->setupContainer((array) $viewPaths, $cachePath);
37 | (new ViewServiceProvider($this->container))->register();
38 |
39 | $this->factory = $this->container->get('view');
40 | $this->compiler = $this->container->get('blade.compiler');
41 | }
42 |
43 | public function render(string $view, array $data = [], array $mergeData = []): string
44 | {
45 | return $this->make($view, $data, $mergeData)->render();
46 | }
47 |
48 | public function make($view, $data = [], $mergeData = []): View
49 | {
50 | return $this->factory->make($view, $data, $mergeData);
51 | }
52 |
53 | public function compiler(): BladeCompiler
54 | {
55 | return $this->compiler;
56 | }
57 |
58 | public function directive(string $name, callable $handler): void
59 | {
60 | $this->compiler->directive($name, $handler);
61 | }
62 |
63 | public function if(string $name, callable $callback): void
64 | {
65 | $this->compiler->if($name, $callback);
66 | }
67 |
68 | public function exists($view): bool
69 | {
70 | return $this->factory->exists($view);
71 | }
72 |
73 | public function file($path, $data = [], $mergeData = []): View
74 | {
75 | return $this->factory->file($path, $data, $mergeData);
76 | }
77 |
78 | public function share($key, $value = null)
79 | {
80 | return $this->factory->share($key, $value);
81 | }
82 |
83 | public function composer($views, $callback): array
84 | {
85 | return $this->factory->composer($views, $callback);
86 | }
87 |
88 | public function creator($views, $callback): array
89 | {
90 | return $this->factory->creator($views, $callback);
91 | }
92 |
93 | public function addNamespace($namespace, $hints): self
94 | {
95 | $this->factory->addNamespace($namespace, $hints);
96 |
97 | return $this;
98 | }
99 |
100 | public function replaceNamespace($namespace, $hints): self
101 | {
102 | $this->factory->replaceNamespace($namespace, $hints);
103 |
104 | return $this;
105 | }
106 |
107 | public function __call(string $method, array $params): mixed
108 | {
109 | return call_user_func_array([$this->factory, $method], $params);
110 | }
111 |
112 | protected function setupContainer(array $viewPaths, string $cachePath): void
113 | {
114 | $this->container->bindIf('files', fn () => new Filesystem(), true);
115 |
116 | $this->container->bindIf('events', fn () => new Dispatcher(), true);
117 |
118 | $this->container->bindIf('config', fn () => new Config([
119 | 'view.paths' => $viewPaths,
120 | 'view.compiled' => $cachePath,
121 | ]), true);
122 |
123 | Facade::setFacadeApplication($this->container);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/View/Blade/Directives.php:
--------------------------------------------------------------------------------
1 | getFileName());
22 |
23 | if (file_exists($directiveSet = $dir . '/Directives/' . $name . '.php')) {
24 | return require_once $directiveSet;
25 | }
26 |
27 | return null;
28 | }
29 |
30 | public function collect(): Collection
31 | {
32 | return collect($this->directives)
33 | ->flatMap(function ($directive) {
34 | if ($directive === 'ACF' && !function_exists('acf')) {
35 | return null;
36 | }
37 |
38 | if ($directive === 'Local') {
39 | return require_once __DIR__ . '/Local.php';
40 | }
41 |
42 | return $this->get($directive);
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/View/Blade/Local.php:
--------------------------------------------------------------------------------
1 | fn ($name = "''") => '',
6 | 'languageatrributes' => fn ($doctype = "'html'") => '',
7 | ];
8 |
--------------------------------------------------------------------------------
/src/View/BladeView.php:
--------------------------------------------------------------------------------
1 | view = new Blade($this->getDir() . '/../../views', FORME_PRIVATE_ROOT . 'view-cache');
18 | $directives->collect()
19 | ->each(function ($directive, $function) {
20 | $this->view->directive($function, $directive);
21 | });
22 | }
23 |
24 | public function render(string $template, array $context = []): string
25 | {
26 | return $this->view->render($template, $context);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/View/GetsDirectory.php:
--------------------------------------------------------------------------------
1 | getFileName();
12 |
13 | return dirname($filename);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/View/LegacyPlatesView.php:
--------------------------------------------------------------------------------
1 | view = Engine::create($this->getDir() . self::RELATIVE_VIEW_DIR, 'plate.php');
20 | }
21 |
22 | public function render(string $template, array $context = []): string
23 | {
24 | $template = MagicResolver::resolve($template);
25 |
26 | return $this->view->render($template, $context);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/View/Plates/CaptainHook/Lint.php:
--------------------------------------------------------------------------------
1 | getIndexOperator()->getStagedFilesOfType('php', ['A', 'C', 'M']);
28 | $changedPlateFiles = array_filter($changedPlateFiles, fn ($file) => $this->isPlatesFile($file));
29 |
30 | $directory = dirname($config->getPath());
31 |
32 | $failedFiles = [];
33 | $messages = [];
34 |
35 | foreach ($changedPlateFiles as $file) {
36 | $prefix = IOUtil::PREFIX_OK;
37 | if ($this->hasValidationErrors($directory . '/' . $file)) {
38 | $prefix = IOUtil::PREFIX_FAIL;
39 | $failedFiles[] = $directory . '/' . $file;
40 | }
41 | $messages[] = $prefix . ' ' . $file;
42 | }
43 |
44 | $io->write(['', '', implode(PHP_EOL, $messages), ''], true, IO::VERBOSE);
45 |
46 | $failedFilesCount = count($failedFiles);
47 |
48 | if ($failedFilesCount > 0) {
49 | throw new ActionFailed('Linting failed: View template errors in ' . $failedFilesCount . ' file(s)' . PHP_EOL . PHP_EOL . implode(PHP_EOL, $failedFiles));
50 | }
51 | }
52 |
53 | /**
54 | * Lint a php file.
55 | */
56 | protected function hasValidationErrors(string $file): bool
57 | {
58 | try {
59 | Validator::validateFile($file);
60 |
61 | return false;
62 | } catch (TemplateErrorException $e) {
63 | return true;
64 | }
65 | }
66 |
67 | protected function isPlatesFile(string $file): bool
68 | {
69 | $filename = pathinfo($file, PATHINFO_FILENAME);
70 | $extension = pathinfo($file, PATHINFO_EXTENSION);
71 |
72 | // handle both .php and .phtml files
73 | if ($extension === 'php' || $extension === 'phtml') {
74 | return str_ends_with($filename, 'plate');
75 | }
76 |
77 | return false;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/View/Plates/Engine.php:
--------------------------------------------------------------------------------
1 | container = $container ?: Util\Container::create(['engine_methods' => []]);
13 | }
14 |
15 | /** Create a configured engine and set the base dir and extension optionally */
16 | public static function create($base_dir, $ext = null)
17 | {
18 | return self::createWithConfig(array_filter([
19 | 'base_dir' => $base_dir,
20 | 'ext' => $ext,
21 | ]));
22 | }
23 |
24 | /** Create a configured engine and pass in an array to configure after extension registration */
25 | public static function createWithConfig(array $config = [])
26 | {
27 | $plates = new self();
28 |
29 | $plates->register(new PlatesExtension());
30 | $plates->register(new Extension\Data\DataExtension());
31 | $plates->register(new Extension\Path\PathExtension());
32 | $plates->register(new Extension\RenderContext\RenderContextExtension());
33 | $plates->register(new Extension\LayoutSections\LayoutSectionsExtension());
34 | $plates->register(new Extension\Folders\FoldersExtension());
35 |
36 | $plates->addConfig($config);
37 |
38 | return $plates;
39 | }
40 |
41 | public function render(string $template, array $data = [], array $attributes = []): string
42 | {
43 | $template = MagicResolver::resolve($template);
44 |
45 | return $this->container->get('renderTemplate')->renderTemplate(new Template(
46 | $template,
47 | $data,
48 | $attributes
49 | ));
50 | }
51 |
52 | public function addMethods(array $methods)
53 | {
54 | $this->container->merge('engine_methods', $methods);
55 | }
56 |
57 | public function __call($method, array $args)
58 | {
59 | $methods = $this->container->get('engine_methods');
60 | if (isset($methods[$method])) {
61 | return $methods[$method]($this, ...$args);
62 | }
63 |
64 | throw new \BadMethodCallException(sprintf('No method %s found for engine.', $method));
65 | }
66 |
67 | public function register(Extension $extension)
68 | {
69 | $extension->register($this);
70 | }
71 |
72 | public function getContainer()
73 | {
74 | return $this->container;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/View/Plates/Exception/FuncException.php:
--------------------------------------------------------------------------------
1 | getContainer();
13 | $c->add('data.globals', []);
14 | $c->add('data.template_data', []);
15 |
16 | $plates->defineConfig(['merge_parent_data' => true]);
17 | $plates->pushComposers(fn ($c) => array_filter([
18 | 'data.addGlobals' => $c->get('data.globals') ? addGlobalsCompose($c->get('data.globals')) : null,
19 | 'data.mergeParentData' => $c->get('config')['merge_parent_data'] ? mergeParentDataCompose() : null,
20 | 'data.perTemplateData' => $c->get('data.template_data') ? perTemplateDataCompose($c->get('data.template_data')) : null,
21 | ]));
22 |
23 | $plates->addMethods([
24 | 'addGlobals' => function (Plates\Engine $e, array $data) {
25 | $c = $e->getContainer();
26 | $c->merge('data.globals', $data);
27 | },
28 | 'addGlobal' => function (Plates\Engine $e, $name, $value) {
29 | $e->getContainer()->merge('data.globals', [$name => $value]);
30 | },
31 | 'addData' => function (Plates\Engine $e, $data, $name = null) {
32 | if (!$name) {
33 | return $e->addGlobals($data);
34 | }
35 |
36 | $template_data = $e->getContainer()->get('data.template_data');
37 | if (!isset($template_data[$name])) {
38 | $template_data[$name] = [];
39 | }
40 |
41 | $template_data[$name] = array_merge($template_data[$name], $data);
42 | $e->getContainer()->add('data.template_data', $template_data);
43 | },
44 | ]);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/Data/data.php:
--------------------------------------------------------------------------------
1 | $template->withData(array_merge($globals, $template->data));
10 | }
11 |
12 | function mergeParentDataCompose()
13 | {
14 | return fn (Template $template) => $template->parent !== null
15 | ? $template->withData(array_merge($template->parent()->data, $template->data))
16 | : $template;
17 | }
18 |
19 | function perTemplateDataCompose(array $template_data_map)
20 | {
21 | return function (Template $template) use ($template_data_map) {
22 | $name = $template->get('normalized_name', $template->name);
23 |
24 | return isset($template_data_map[$name])
25 | ? $template->withData(array_merge($template_data_map[$name], $template->data))
26 | : $template;
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/ExtensionInterface.php:
--------------------------------------------------------------------------------
1 | getContainer();
12 | $c->add('folders.folders', []);
13 | $c->wrapStack('path.resolvePath', fn ($stack, $c) => array_merge($stack, [
14 | 'folders' => foldersResolvePath(
15 | $c->get('folders.folders'),
16 | $c->get('config')['folder_separator'],
17 | $c->get('fileExists')
18 | ),
19 | ]));
20 | $c->wrapComposed('path.normalizeName', fn ($composed, $c) => array_merge($composed, [
21 | 'folders.stripFolders' => stripFoldersNormalizeName($c->get('folders.folders')),
22 | ]));
23 |
24 | $plates->defineConfig([
25 | 'folder_separator' => '::',
26 | ]);
27 | $plates->addMethods([
28 | 'addFolder' => function ($plates, $folder, $prefixes, $fallback = false) {
29 | $prefixes = is_string($prefixes) ? [$prefixes] : $prefixes;
30 | if ($fallback) {
31 | $prefixes[] = '';
32 | }
33 |
34 | $plates->getContainer()->merge('folders.folders', [
35 | $folder => [
36 | 'folder' => $folder,
37 | 'prefixes' => $prefixes,
38 | ],
39 | ]);
40 | },
41 | ]);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/Folders/folders.php:
--------------------------------------------------------------------------------
1 | path, $sep)) {
14 | return $next($args);
15 | }
16 |
17 | [$folder, $name] = explode($sep, $args->path);
18 | if (!isset($folders[$folder])) {
19 | return $next($args);
20 | }
21 |
22 | $folder_struct = $folders[$folder];
23 |
24 | foreach ($folder_struct['prefixes'] as $prefix) {
25 | $path = $next($args->withPath(
26 | Plates\Util\joinPath([$prefix, $name])
27 | ));
28 |
29 | // no need to check if file exists if we only have prefix
30 | if ((is_countable($folder_struct['prefixes']) ? count($folder_struct['prefixes']) : 0) == 1 || $file_exists($path)) {
31 | return $path;
32 | }
33 | }
34 |
35 | // none of the paths matched, just return what we have.
36 | return $path;
37 | };
38 | }
39 |
40 | function stripFoldersNormalizeName(array $folders, string $sep = '::'): Closure
41 | {
42 | return function ($name) use ($folders, $sep) {
43 | foreach ($folders as $folder) {
44 | foreach (array_filter($folder['prefixes']) as $prefix) {
45 | if (str_starts_with($name, $prefix)) {
46 | return $folder['folder'] . $sep . substr($name, strlen($prefix) + 1);
47 | }
48 | }
49 | }
50 |
51 | return $name;
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/LayoutSections/DefaultLayoutRenderTemplate.php:
--------------------------------------------------------------------------------
1 | parent || $template->get('no_layout')) {
15 | return $this->render->renderTemplate($template, $rt ?: $this);
16 | }
17 |
18 | $ref = $template->reference;
19 | $contents = $this->render->renderTemplate($template, $rt ?: $this);
20 |
21 | if ($ref()->get('layout')) {
22 | return $contents;
23 | }
24 |
25 | $layout = $ref()->fork($this->layout_path);
26 | $ref()->with('layout', $layout->reference);
27 |
28 | return $contents;
29 | }
30 |
31 | public static function factory($layout_path) {
32 | return fn(Plates\RenderTemplate $rt) => new self($rt, $layout_path);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/LayoutSections/LayoutRenderTemplate.php:
--------------------------------------------------------------------------------
1 | reference;
11 | $content = $this->render->renderTemplate($template, $rt ?: $this);
12 |
13 | $layout_ref = $ref()->get('layout');
14 | if (!$layout_ref) {
15 | return $content;
16 | }
17 |
18 | $layout = $layout_ref()->with('sections', $ref()->get('sections'));
19 | $layout->get('sections')->add('content', $content);
20 |
21 | return ($rt ?: $this)->renderTemplate($layout);
22 | }
23 |
24 | public static function factory() {
25 | return fn(Plates\RenderTemplate $render) => new self($render);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/LayoutSections/LayoutSectionsExtension.php:
--------------------------------------------------------------------------------
1 | getContainer();
13 |
14 | $c->wrap('renderTemplate.factories', function ($factories, $c) {
15 | $default_layout_path = $c->get('config')['default_layout_path'];
16 | if ($default_layout_path) {
17 | $factories[] = DefaultLayoutRenderTemplate::factory($default_layout_path);
18 | }
19 |
20 | $factories[] = LayoutRenderTemplate::factory();
21 |
22 | return $factories;
23 | });
24 |
25 | $plates->defineConfig(['default_layout_path' => null]);
26 | $plates->pushComposers(fn ($c) => ['layoutSections.sections' => sectionsCompose()]);
27 | $plates->addFuncs(function ($c) {
28 | $template_args = RenderContext\assertTemplateArgsFunc();
29 | $one_arg = RenderContext\assertArgsFunc(1);
30 |
31 | return [
32 | 'layout' => [layoutFunc(), $template_args],
33 | 'section' => [sectionFunc(), RenderContext\assertArgsFunc(1, 1)],
34 | 'start' => [startFunc(), $one_arg],
35 | 'push' => [startFunc(START_APPEND), $one_arg],
36 | 'unshift' => [startFunc(START_PREPEND), $one_arg],
37 | ];
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/LayoutSections/Sections.php:
--------------------------------------------------------------------------------
1 | sections[$name] = $content;
16 | }
17 |
18 | public function append($name, $content)
19 | {
20 | $this->sections[$name] = ($this->get($name) ?: '') . $content;
21 | }
22 |
23 | public function prepend($name, $content)
24 | {
25 | $this->sections[$name] = $content . ($this->get($name) ?: '');
26 | }
27 |
28 | public function clear($name)
29 | {
30 | unset($this->sections[$name]);
31 | }
32 |
33 | public function get($name)
34 | {
35 | return $this->sections[$name] ?? null;
36 | }
37 |
38 | public function merge(Sections $sections)
39 | {
40 | return new self(array_merge($this->sections, $sections->sections));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/LayoutSections/layout-sections.php:
--------------------------------------------------------------------------------
1 | $template->with('sections', $template->parent !== null ? $template->parent()->get('sections') : new Sections());
13 | }
14 |
15 | function layoutFunc()
16 | {
17 | return function (FuncArgs $args) {
18 | [$name, $data] = $args->args;
19 |
20 | $layout = $args->template()->fork($name, $data ?: []);
21 | $args->template()->with('layout', $layout->reference);
22 |
23 | return $layout;
24 | };
25 | }
26 |
27 | function sectionFunc()
28 | {
29 | return function (FuncArgs $args) {
30 | [$name, $else] = $args->args;
31 |
32 | $res = $args->template()->get('sections')->get($name);
33 | if ($res || !$else) {
34 | return $res;
35 | }
36 |
37 | return is_callable($else)
38 | ? Plates\Util\obWrap($else)
39 | : (string) $else;
40 | };
41 | }
42 |
43 | const START_APPEND = 0;
44 | const START_PREPEND = 1;
45 | const START_REPLACE = 2;
46 |
47 | /** Starts the output buffering for a section, update of 0 = replace, 1 = append, 2 = prepend */
48 | function startFunc($update = START_REPLACE)
49 | {
50 | return startBufferFunc(fn (FuncArgs $args) => function ($contents) use ($update, $args) {
51 | $name = $args->args[0];
52 | $sections = $args->template()->get('sections');
53 |
54 | if ($update === START_APPEND) {
55 | $sections->append($name, $contents);
56 | } elseif ($update === START_PREPEND) {
57 | $sections->prepend($name, $contents);
58 | } else {
59 | $sections->add($name, $contents);
60 | }
61 | });
62 | }
63 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/Path/PathExtension.php:
--------------------------------------------------------------------------------
1 | getContainer();
12 | $c->add('path.resolvePath.prefixes', fn ($c) => (array) ($c->get('config')['base_dir'] ?? []));
13 | $c->addComposed('path.normalizeName', fn ($c) => [
14 | 'path.stripExt' => stripExtNormalizeName(),
15 | 'path.stripPrefix' => stripPrefixNormalizeName($c->get('path.resolvePath.prefixes')),
16 | ]);
17 | $c->addStack('path.resolvePath', function ($c) {
18 | $config = $c->get('config');
19 | $prefixes = $c->get('path.resolvePath.prefixes');
20 |
21 | return array_filter([
22 | 'path.id' => idResolvePath(),
23 | 'path.prefix' => $prefixes ? prefixResolvePath($prefixes, $c->get('fileExists')) : null,
24 | 'path.ext' => isset($config['ext']) ? extResolvePath($config['ext']) : null,
25 | 'path.relative' => relativeResolvePath(),
26 | ]);
27 | });
28 | $plates->defineConfig([
29 | 'ext' => 'phtml',
30 | 'base_dir' => null,
31 | ]);
32 | $plates->pushComposers(fn ($c) => [
33 | 'path.normalizeName' => normalizeNameCompose($c->get('path.normalizeName')),
34 | 'path.resolvePath' => resolvePathCompose($c->get('path.resolvePath')),
35 | ]);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/Path/ResolvePathArgs.php:
--------------------------------------------------------------------------------
1 | context, $this->template);
16 | }
17 |
18 | public function withContext(array $context)
19 | {
20 | return new self($this->path, $context, $this->template);
21 | }
22 |
23 | public static function fromTemplate(Template $template)
24 | {
25 | return new self($template->name, [], clone $template);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/Path/path.php:
--------------------------------------------------------------------------------
1 | $template->with('path', $resolve_path(ResolvePathArgs::fromTemplate($template)));
10 | }
11 |
12 | function normalizeNameCompose(callable $normalize_name)
13 | {
14 | return fn (Plates\Template $template) => $template->with(
15 | 'normalized_name',
16 | Plates\Util\isPath($template->name) ? $normalize_name($template->get('path')) : $template->name
17 | );
18 | }
19 |
20 | function stripExtNormalizeName()
21 | {
22 | return function ($name) {
23 | $ext = pathinfo($name, PATHINFO_EXTENSION);
24 | if ($ext === '' || $ext === '0') {
25 | return $name;
26 | }
27 |
28 | return substr($name, 0, (strlen($ext) + 1) * -1); // +1 for the leading `.`
29 | };
30 | }
31 |
32 | function stripPrefixNormalizeName(array $prefixes)
33 | {
34 | $prefixes = array_filter($prefixes);
35 |
36 | return function ($name) use ($prefixes) {
37 | foreach ($prefixes as $prefix) {
38 | if (str_starts_with($name, $prefix . '/')) {
39 | return substr($name, strlen($prefix) + 1); // +1 for the trailing `/`
40 | }
41 | }
42 |
43 | return $name;
44 | };
45 | }
46 |
47 | /** appends an extension to the name */
48 | function extResolvePath($ext = 'phtml')
49 | {
50 | $full_ext = '.' . $ext;
51 | $ext_len = strlen($full_ext);
52 |
53 | return function (ResolvePathArgs $args, $next) use ($full_ext, $ext_len) {
54 | // ext is already there, just skip
55 | if (strrpos($args->path, $full_ext) === strlen($args->path) - $ext_len) {
56 | return $next($args);
57 | }
58 |
59 | return $next($args->withPath($args->path . $full_ext));
60 | };
61 | }
62 |
63 | function prefixResolvePath(array $prefixes, $file_exists = 'file_exists')
64 | {
65 | return function (ResolvePathArgs $args, $next) use ($prefixes, $file_exists) {
66 | if ($prefixes === []) {
67 | return $next($args);
68 | }
69 |
70 | foreach ($prefixes as $cur_prefix) {
71 | $path = Plates\Util\isAbsolutePath($args->path)
72 | ? $next($args)
73 | : $next($args->withPath(
74 | Plates\Util\joinPath([$cur_prefix, $args->path])
75 | ));
76 |
77 | // we have a match, let's return
78 | if ($file_exists($path)) {
79 | return $path;
80 | }
81 |
82 | // at this point, we need to try the next prefix, but before we do, let's strip the prefix
83 | // if there is one since this might a be a relative path
84 | $stripped_args = null;
85 | foreach ($prefixes as $prefix) {
86 | if (str_starts_with($path, $prefix)) {
87 | $stripped_args = $args->withPath(substr($path, strlen($prefix))); // remove the prefix
88 | break;
89 | }
90 | }
91 |
92 | // could not strip the prefix, so there's not point in continuing on
93 | if (!$stripped_args) {
94 | return $path;
95 | }
96 |
97 | $args = $stripped_args;
98 | }
99 |
100 | // at this point, none of the paths resolved into a valid path, let's just return the last one
101 | return $path;
102 | };
103 | }
104 |
105 | /** Figures out the path based off of the parent templates current path */
106 | function relativeResolvePath()
107 | {
108 | return function (ResolvePathArgs $args, $next) {
109 | $is_relative = Plates\Util\isRelativePath($args->path) && $args->template->parent;
110 |
111 | if (!$is_relative) {
112 | return $next($args); // nothing to do
113 | }
114 |
115 | $current_directory = dirname($args->template->parent()->get('path'));
116 |
117 | return $next($args->withPath(
118 | Plates\Util\joinPath([$current_directory, $args->path])
119 | ));
120 | };
121 | }
122 |
123 | function idResolvePath()
124 | {
125 | return fn (ResolvePathArgs $args, $next) => $args->path;
126 | }
127 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/RenderContext/FuncArgs.php:
--------------------------------------------------------------------------------
1 | ref->template;
16 | }
17 |
18 | public function withName($func_name)
19 | {
20 | return new self($this->render, $this->ref, $func_name, $this->args);
21 | }
22 |
23 | public function withArgs($args)
24 | {
25 | return new self($this->render, $this->ref, $this->func_name, $args);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/RenderContext/RenderContext.php:
--------------------------------------------------------------------------------
1 | func_stack = $func_stack ?: Plates\Util\stack([platesFunc()]);
18 | }
19 |
20 | public function __get($name)
21 | {
22 | if (!$this->func_stack) {
23 | throw new BadMethodCallException('Cannot access ' . $name . ' because no func stack has been setup.');
24 | }
25 |
26 | return $this->invokeFuncStack($name, []);
27 | }
28 |
29 | public function __set($name, $value)
30 | {
31 | throw new BadMethodCallException('Cannot set ' . $name . ' on this render context.');
32 | }
33 |
34 | public function __call($name, array $args)
35 | {
36 | if (!$this->func_stack) {
37 | throw new BadMethodCallException('Cannot call ' . $name . ' because no func stack has been setup.');
38 | }
39 |
40 | return $this->invokeFuncStack($name, $args);
41 | }
42 |
43 | public function __invoke(array $args = [])
44 | {
45 | if (!$this->func_stack) {
46 | throw new BadMethodCallException('Cannot invoke the render context because no func stack has been setup.');
47 | }
48 |
49 | return $this->invokeFuncStack('__invoke', $args);
50 | }
51 |
52 | private function invokeFuncStack($name, array $args)
53 | {
54 | return ($this->func_stack)(new FuncArgs(
55 | $this->render,
56 | $this->ref,
57 | $name,
58 | $args
59 | ));
60 | }
61 |
62 | public static function factory(callable $create_render, $func_stack = null)
63 | {
64 | return fn (Plates\TemplateReference $ref) => new self(
65 | $create_render(),
66 | $ref,
67 | $func_stack
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/RenderContext/RenderContextExtension.php:
--------------------------------------------------------------------------------
1 | getContainer();
13 | $c->addStack('renderContext.func', fn ($c) => [
14 | 'notFound' => notFoundFunc(),
15 | 'plates' => Plates\Util\stackGroup([
16 | splitByNameFunc($c->get('renderContext.func.funcs')),
17 | aliasNameFunc($c->get('renderContext.func.aliases')),
18 | ]),
19 | ]);
20 | $c->add('renderContext.func.aliases', [
21 | 'e' => 'escape',
22 | '__invoke' => 'escape',
23 | 'stop' => 'end',
24 | ]);
25 | $c->add('renderContext.func.funcs', function ($c) {
26 | $template_args = assertTemplateArgsFunc();
27 | $one_arg = assertArgsFunc(1);
28 | $config = $c->get('config');
29 |
30 | return [
31 | 'insert' => [insertFunc(), $template_args],
32 | 'render' => [insertFunc(), $template_args],
33 | 'escape' => [
34 | isset($config['escape_flags'], $config['escape_encoding'])
35 | ? escapeFunc($config['escape_flags'], $config['escape_encoding'])
36 | : escapeFunc(),
37 | $one_arg,
38 | ],
39 | 'data' => [templateDataFunc(), assertArgsFunc(0, 1)],
40 | 'name' => [accessTemplatePropFunc('name')],
41 | 'context' => [accessTemplatePropFunc('context')],
42 | 'component' => [componentFunc(), $template_args],
43 | 'slot' => [slotFunc(), $one_arg],
44 | 'end' => [endFunc()],
45 | ];
46 | });
47 | $c->add('include.bind', fn ($c) => renderContextBind());
48 | $c->add('renderContext.factory', fn ($c) => RenderContext::factory(
49 | fn () => $c->get('renderTemplate'),
50 | $c->get('renderContext.func')
51 | ));
52 |
53 | $plates->defineConfig([
54 | 'render_context_var_name' => 'v',
55 | 'escape_encoding' => null,
56 | 'escape_flags' => null,
57 | ]);
58 | $plates->pushComposers(fn ($c) => [
59 | 'renderContext.renderContext' => renderContextCompose(
60 | $c->get('renderContext.factory'),
61 | $c->get('config')['render_context_var_name']
62 | ),
63 | ]);
64 |
65 | $plates->addMethods([
66 | 'registerFunction' => function (Plates\Engine $e, $name, callable $func, callable $assert_args = null, $simple = true) {
67 | $c = $e->getContainer();
68 | $func = $simple ? wrapSimpleFunc($func) : $func;
69 |
70 | $c->wrap('renderContext.func.funcs', function ($funcs, $c) use ($name, $func, $assert_args) {
71 | $funcs[$name] = $assert_args ? [$assert_args, $func] : [$func];
72 |
73 | return $funcs;
74 | });
75 | },
76 | 'addFuncs' => function (Plates\Engine $e, callable $add_funcs, $simple = false) {
77 | $e->getContainer()->wrap('renderContext.func.funcs', function ($funcs, $c) use ($add_funcs, $simple) {
78 | $new_funcs = $simple
79 | ? array_map(wrapSimpleFunc::class, $add_funcs($c))
80 | : $add_funcs($c);
81 |
82 | return array_merge($funcs, $new_funcs);
83 | });
84 | },
85 | 'wrapFuncs' => function (Plates\Engine $e, callable $wrap_funcs) {
86 | $e->getContainer()->wrap('renderContext.func.funcs', $wrap_funcs);
87 | },
88 | ]);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/RenderContext/func.php:
--------------------------------------------------------------------------------
1 | template()->get('component_slot_data') !== null) {
15 | throw new FuncException('Cannot nest component func calls.');
16 | }
17 |
18 | $args->template()->with('component_slot_data', []);
19 |
20 | return function ($contents) use ($insert, $args) {
21 | [$name, $data] = $args->args;
22 |
23 | $data = array_merge(
24 | $data ?: [],
25 | ['slot' => $contents],
26 | $args->template()->get('component_slot_data')
27 | );
28 |
29 | $insert($args->withArgs([$name, $data]));
30 |
31 | $args->template()->with('component_slot_data', null);
32 | };
33 | });
34 | }
35 |
36 | function slotFunc()
37 | {
38 | return startBufferFunc(function (FuncArgs $args) {
39 | if ($args->template()->get('component_slot_data') === null) {
40 | throw new FuncException('Cannot call slot func outside of component definition.');
41 | }
42 |
43 | return function ($contents) use ($args) {
44 | $slot_data = $args->template()->get('component_slot_data');
45 | $slot_data[$args->args[0]] = $contents;
46 | $args->template()->with('component_slot_data', $slot_data);
47 | };
48 | });
49 | }
50 |
51 | function startBufferFunc(callable $create_callback)
52 | {
53 | return function (FuncArgs $args) use ($create_callback) {
54 | $buffer_stack = $args->template()->get('buffer_stack') ?: [];
55 |
56 | ob_start();
57 | $buffer_stack[] = [ob_get_level(), $create_callback($args)];
58 |
59 | $args->template()->with('buffer_stack', $buffer_stack);
60 | };
61 | }
62 |
63 | function endFunc()
64 | {
65 | return function (FuncArgs $args) {
66 | $buffer_stack = $args->template()->get('buffer_stack') ?: [];
67 | if ((is_countable($buffer_stack) ? count($buffer_stack) : 0) === 0) {
68 | throw new FuncException('Cannot end a section definition because no section has been started.');
69 | }
70 |
71 | [$ob_level, $callback] = array_pop($buffer_stack);
72 |
73 | if ($ob_level != ob_get_level()) {
74 | throw new FuncException('Output buffering level does not match when section was started.');
75 | }
76 |
77 | $contents = ob_get_clean();
78 |
79 | $callback($contents);
80 |
81 | $args->template()->with('buffer_stack', $buffer_stack);
82 | };
83 | }
84 |
85 | function insertFunc($echo = null)
86 | {
87 | $echo = $echo ?: Plates\Util\phpEcho();
88 |
89 | return function (FuncArgs $args) use ($echo) {
90 | [$name, $data] = $args->args;
91 | $name = MagicResolver::resolve($name);
92 | $child = $args->template()->fork($name, $data ?: []);
93 | $echo($args->render->renderTemplate($child));
94 | };
95 | }
96 |
97 | function templateDataFunc()
98 | {
99 | return function (FuncArgs $args) {
100 | [$data] = $args->args;
101 |
102 | return array_merge($args->template()->data, $data);
103 | };
104 | }
105 |
106 | /** Enables the backwards compatibility with the old extension functions */
107 | function wrapSimpleFunc(callable $func, $enable_bc = false)
108 | {
109 | return function (FuncArgs $args) use ($func, $enable_bc) {
110 | if ($enable_bc && is_array($func) && isset($func[0]) && $func[0] instanceof Plates\Extension\ExtensionInterface) {
111 | $func[0]->template = $args->template();
112 | }
113 |
114 | return $func(...$args->args);
115 | };
116 | }
117 |
118 | function accessTemplatePropFunc($prop)
119 | {
120 | return fn (FuncArgs $args) => $args->template()->{$prop};
121 | }
122 |
123 | function escapeFunc($flags = ENT_COMPAT | ENT_HTML401, $encoding = 'UTF-8')
124 | {
125 | return fn (FuncArgs $args) => htmlspecialchars($args->args[0], $flags, $encoding);
126 | }
127 |
128 | function assertArgsFunc($num_required, $num_default = 0)
129 | {
130 | return function (FuncArgs $args, $next) use ($num_required, $num_default) {
131 | if ((is_countable($args->args) ? count($args->args) : 0) < $num_required) {
132 | throw new FuncException(sprintf('Func %s has %s argument(s).', $args->func_name, $num_required));
133 | }
134 |
135 | if ((is_countable($args->args) ? count($args->args) : 0) >= $num_required + $num_default) {
136 | return $next($args);
137 | }
138 |
139 | $args = $args->withArgs(array_merge($args->args, array_fill(
140 | 0,
141 | $num_required + $num_default - (is_countable($args->args) ? count($args->args) : 0),
142 | null
143 | )));
144 |
145 | return $next($args);
146 | };
147 | }
148 |
149 | function assertTemplateArgsFunc()
150 | {
151 | return assertArgsFunc(1, 2);
152 | }
153 |
154 | /** Creates aliases for certain functions */
155 | function aliasNameFunc(array $aliases)
156 | {
157 | return function (FuncArgs $args, $next) use ($aliases) {
158 | if (!isset($aliases[$args->func_name])) {
159 | return $next($args);
160 | }
161 |
162 | while (isset($aliases[$args->func_name])) {
163 | $args = $args->withName($aliases[$args->func_name]);
164 | }
165 |
166 | return $next($args);
167 | };
168 | }
169 |
170 | /** Allows splitting of the handlers from the args name */
171 | function splitByNameFunc(array $handlers)
172 | {
173 | return function (FuncArgs $args, $next) use ($handlers) {
174 | $name = $args->func_name;
175 | if (isset($handlers[$name])) {
176 | $handler = Plates\Util\stackGroup($handlers[$name]);
177 |
178 | return $handler($args, $next);
179 | }
180 |
181 | return $next($args);
182 | };
183 | }
184 |
185 | function notFoundFunc()
186 | {
187 | return function (FuncArgs $args) {
188 | throw new FuncException('The function ' . $args->func_name . ' does not exist.');
189 | };
190 | }
191 |
--------------------------------------------------------------------------------
/src/View/Plates/Extension/RenderContext/render-context.php:
--------------------------------------------------------------------------------
1 | reference);
12 |
13 | return $template->withAddedData([
14 | $var_name => $render_context,
15 | ])->with('render_context', $render_context);
16 | };
17 | }
18 |
19 | function renderContextBind()
20 | {
21 | return fn (Closure $inc, Template $template) => $template->get('render_context')
22 | ? $inc->bindTo($template->get('render_context'))
23 | : $inc;
24 | }
25 |
--------------------------------------------------------------------------------
/src/View/Plates/MagicResolver.php:
--------------------------------------------------------------------------------
1 | getContainer();
10 |
11 | $c->add('config', [
12 | 'validate_paths' => true,
13 | 'php_extensions' => ['php', 'phtml'],
14 | 'image_extensions' => ['png', 'jpg'],
15 | ]);
16 | $c->addComposed('compose', fn () => []);
17 | $c->add('fileExists', fn ($c) => 'file_exists');
18 | $c->add('renderTemplate', function ($c) {
19 | $rt = new RenderTemplate\FileSystemRenderTemplate([
20 | [
21 | Template\matchExtensions($c->get('config')['php_extensions']),
22 | new RenderTemplate\PhpRenderTemplate($c->get('renderTemplate.bind')),
23 | ],
24 | [
25 | Template\matchExtensions($c->get('config')['image_extensions']),
26 | RenderTemplate\MapContentRenderTemplate::base64Encode(new RenderTemplate\StaticFileRenderTemplate()),
27 | ],
28 | [
29 | Template\matchStub(true),
30 | new RenderTemplate\StaticFileRenderTemplate(),
31 | ],
32 | ]);
33 | if ($c->get('config')['validate_paths']) {
34 | $rt = new RenderTemplate\ValidatePathRenderTemplate($rt, $c->get('fileExists'));
35 | }
36 |
37 | $rt = array_reduce($c->get('renderTemplate.factories'), fn ($rt, $create) => $create($rt), $rt);
38 | $rt = new RenderTemplate\ComposeRenderTemplate($rt, $c->get('compose'));
39 |
40 | return $rt;
41 | });
42 | $c->add('renderTemplate.bind', fn () => Util\id());
43 | $c->add('renderTemplate.factories', fn () => []);
44 |
45 | $plates->addMethods([
46 | 'pushComposers' => function (Engine $e, $def_composer) {
47 | $e->getContainer()->wrapComposed('compose', fn ($composed, $c) => array_merge($composed, $def_composer($c)));
48 | },
49 | 'unshiftComposers' => function (Engine $e, $def_composer) {
50 | $e->getContainer()->wrapComposed('compose', fn ($composed, $c) => array_merge($def_composer($c), $composed));
51 | },
52 | 'addConfig' => function (Engine $e, array $config) {
53 | $e->getContainer()->merge('config', $config);
54 | },
55 | /* merges in config values, but will defer to values already set in the config */
56 | 'defineConfig' => function (Engine $e, array $config_def) {
57 | $config = $e->getContainer()->get('config');
58 | $e->getContainer()->add('config', array_merge($config_def, $config));
59 | },
60 | ]);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/View/Plates/RenderTemplate.php:
--------------------------------------------------------------------------------
1 | compose = $compose;
14 | }
15 |
16 | public function renderTemplate(Plates\Template $template, ?Plates\RenderTemplate $rt = null) {
17 | return $this->render->renderTemplate(($this->compose)($template), $rt ?: $this);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/View/Plates/RenderTemplate/FileSystemRenderTemplate.php:
--------------------------------------------------------------------------------
1 | render_sets as [$match, $render]) {
15 | if ($match($template)) {
16 | return $render->renderTemplate($template, $rt ?: $this);
17 | }
18 | }
19 |
20 | throw new Plates\Exception\RenderTemplateException('No renderer was available for the template: ' . $template->name);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/View/Plates/RenderTemplate/MapContentRenderTemplate.php:
--------------------------------------------------------------------------------
1 | map_content = $map_content;
14 | }
15 |
16 | public function renderTemplate(Plates\Template $template, ?Plates\RenderTemplate $rt = null) {
17 | return ($this->map_content)($this->render->renderTemplate($template, $rt ?: $this));
18 | }
19 |
20 | public static function base64Encode(Plates\RenderTemplate $render) {
21 | return new self($render, 'base64_encode');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/View/Plates/RenderTemplate/MockRenderTemplate.php:
--------------------------------------------------------------------------------
1 | mocks[$template->name])) {
15 | throw new Plates\Exception\RenderTemplateException('Mock include does not exist for name: ' . $template->name);
16 | }
17 |
18 | return $this->mocks[$template->name]($template);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/View/Plates/RenderTemplate/PhpRenderTemplate.php:
--------------------------------------------------------------------------------
1 | bind = $bind;
14 | }
15 |
16 | public function renderTemplate(Plates\Template $template, ?Plates\RenderTemplate $render = null) {
17 | if (!defined('FORME_PLATES_VALIDATION') || FORME_PLATES_VALIDATION) {
18 | Validator::validate($template);
19 | }
20 | $inc = self::createInclude();
21 | $inc = $this->bind ? ($this->bind)($inc, $template) : $inc;
22 |
23 | return Plates\Util\obWrap(function() use ($inc, $template) {
24 | $inc($template->get('path'), $template->data);
25 | });
26 | }
27 |
28 | private static function createInclude() {
29 | return function() {
30 | $funcGetArg = func_get_arg(1);
31 | extract($funcGetArg);
32 | include func_get_arg(0);
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/View/Plates/RenderTemplate/RenderTemplateDecorator.php:
--------------------------------------------------------------------------------
1 | get('path');
17 | return ($this->get_contents)($path);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/View/Plates/RenderTemplate/ValidatePathRenderTemplate.php:
--------------------------------------------------------------------------------
1 | get('path');
15 | if (!$path || ($this->file_exists)($path)) {
16 | return $this->render->renderTemplate($template, $rt ?: null);
17 | }
18 |
19 | throw new Plates\Exception\RenderTemplateException('Template path ' . $path . ' is not a valid path for template: ' . $template->name);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/View/Plates/Template.php:
--------------------------------------------------------------------------------
1 | reference = ($ref ?: new TemplateReference)->update($this);
19 | }
20 |
21 | public function with($key, $value) {
22 | return $this->withAddedAttributes([$key => $value]);
23 | }
24 |
25 | public function get($key, $default = null) {
26 | return array_key_exists($key, $this->attributes)
27 | ? $this->attributes[$key]
28 | : $default;
29 | }
30 |
31 | /** Returns the deferenced parent template */
32 | public function parent() {
33 | return $this->parent !== null ? ($this->parent)() : null;
34 | }
35 |
36 | public function withName($name) {
37 | return new self($name, $this->data, $this->attributes, $this->reference, $this->parent);
38 | }
39 |
40 | public function withData(array $data) {
41 | return new self($this->name, $data, $this->attributes, $this->reference, $this->parent);
42 | }
43 |
44 | public function withAddedData(array $data) {
45 | return new self($this->name, array_merge($this->data, $data), $this->attributes, $this->reference, $this->parent);
46 | }
47 |
48 | public function withAttributes(array $attributes) {
49 | return new self($this->name, $this->data, $attributes, $this->reference, $this->parent);
50 | }
51 |
52 | public function withAddedAttributes(array $attributes) {
53 | return new self($this->name, $this->data, array_merge($this->attributes, $attributes), $this->reference, $this->parent);
54 | }
55 |
56 | public function __clone() {
57 | $this->reference = new TemplateReference();
58 | }
59 |
60 | /** Create a new template based off of this current one */
61 | public function fork($name, array $data = [], array $attributes = []) {
62 | return new self(
63 | $name,
64 | $data,
65 | $attributes,
66 | null,
67 | $this->reference
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/View/Plates/Template/match.php:
--------------------------------------------------------------------------------
1 | $match(pathinfo($path, PATHINFO_EXTENSION)));
10 | }
11 |
12 | function matchName($name)
13 | {
14 | return fn(Template $template) => $template->get('normalized_name', $template->name) == $name;
15 | }
16 |
17 | function matchExtensions(array $extensions)
18 | {
19 | return matchPathExtension(fn($ext) => in_array($ext, $extensions));
20 | }
21 |
22 | function matchAttribute($attribute, callable $match)
23 | {
24 | return fn(Template $template) => $match($template->get($attribute));
25 | }
26 |
27 | function matchStub($res)
28 | {
29 | return fn(Template $template) => $res;
30 | }
31 |
32 | function matchAny(array $matches)
33 | {
34 | return function (Template $template) use ($matches) {
35 | foreach ($matches as $match) {
36 | if ($match($template)) {
37 | return true;
38 | }
39 | }
40 |
41 | return false;
42 | };
43 | }
44 |
45 | function matchAll(array $matches)
46 | {
47 | return function (Template $template) use ($matches) {
48 | foreach ($matches as $match) {
49 | if (!$match($template)) {
50 | return false;
51 | }
52 | }
53 |
54 | return true;
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/src/View/Plates/TemplateReference.php:
--------------------------------------------------------------------------------
1 | template;
13 | }
14 |
15 | public function update(Template $template)
16 | {
17 | $this->template = $template;
18 |
19 | return $this;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/View/Plates/Util/Container.php:
--------------------------------------------------------------------------------
1 | $val) {
15 | $c->add($key, $val);
16 | }
17 |
18 | return $c;
19 | }
20 |
21 | public function add($id, $value)
22 | {
23 | if (array_key_exists($id, $this->cached)) {
24 | throw new \LogicException('Cannot add service after it has been frozen.');
25 | }
26 |
27 | $this->boxes[$id] = [$value, $value instanceof \Closure];
28 | }
29 |
30 | public function addComposed($id, callable $define_composers)
31 | {
32 | $this->add($id, fn ($c) => compose(...array_values($c->get($id . '.composers'))));
33 | $this->add($id . '.composers', $define_composers);
34 | }
35 |
36 | public function wrapComposed($id, callable $wrapped)
37 | {
38 | $this->wrap($id . '.composers', $wrapped);
39 | }
40 |
41 | public function addStack($id, callable $define_stack)
42 | {
43 | $this->add($id, fn ($c) => stack($c->get($id . '.stack')));
44 | $this->add($id . '.stack', $define_stack);
45 | }
46 |
47 | public function wrapStack($id, callable $wrapped)
48 | {
49 | $this->wrap($id . '.stack', $wrapped);
50 | }
51 |
52 | public function merge($id, array $values)
53 | {
54 | $old = $this->get($id);
55 | $this->add($id, array_merge($old, $values));
56 | }
57 |
58 | public function wrap($id, $wrapper)
59 | {
60 | if (!$this->has($id)) {
61 | throw new \LogicException('Cannot wrap service ' . $id . ' that does not exist.');
62 | }
63 |
64 | $box = $this->boxes[$id];
65 | $this->boxes[$id] = [fn ($c) => $wrapper($this->unbox($box, $c), $c), true];
66 | }
67 |
68 | public function get($id)
69 | {
70 | if (array_key_exists($id, $this->cached)) {
71 | return $this->cached[$id];
72 | }
73 |
74 | if (!$this->has($id)) {
75 | throw new \LogicException('Cannot retrieve service ' . $id . ' that does exist.');
76 | }
77 |
78 | $result = $this->unbox($this->boxes[$id], $this);
79 | if ($this->boxes[$id][1]) { // only cache services
80 | $this->cached[$id] = $result;
81 | }
82 |
83 | return $result;
84 | }
85 |
86 | public function has($id)
87 | {
88 | return array_key_exists($id, $this->boxes);
89 | }
90 |
91 | private function unbox($box, Container $c)
92 | {
93 | [$value, $is_factory] = $box;
94 | if (!$is_factory) {
95 | return $value;
96 | }
97 |
98 | return $value($c);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/View/Plates/Util/util.php:
--------------------------------------------------------------------------------
1 | $arg;
11 | }
12 |
13 | /** wraps a closure in output buffering and returns the buffered
14 | content. */
15 | function obWrap(callable $wrap)
16 | {
17 | $cur_level = ob_get_level();
18 |
19 | try {
20 | ob_start();
21 | $wrap();
22 |
23 | return ob_get_clean();
24 | } catch (\Exception|\Throwable $e) {
25 | }
26 |
27 | // clean the ob stack
28 | while (ob_get_level() > $cur_level) {
29 | ob_end_clean();
30 | }
31 |
32 | throw $e;
33 | }
34 |
35 | /** simple utility that wraps php echo which allows for stubbing out the
36 | echo func for testing */
37 | function phpEcho()
38 | {
39 | return function ($v) {
40 | echo $v;
41 | };
42 | }
43 |
44 | /** stack a set of functions into each other and returns the stacked func */
45 | function stack(array $funcs)
46 | {
47 | return array_reduce($funcs, fn ($next, $func) => function (...$args) use ($next, $func) {
48 | $args[] = $next;
49 |
50 | return $func(...$args);
51 | }, function () {
52 | throw new StackException('No handler was able to return a result.');
53 | });
54 | }
55 |
56 | function stackGroup(array $funcs)
57 | {
58 | $end_next = null;
59 | array_unshift($funcs, function (...$args) use (&$end_next) {
60 | return $end_next(...array_slice($args, 0, -1));
61 | });
62 | $next = stack($funcs);
63 |
64 | return function (...$args) use ($next, &$end_next) {
65 | $end_next = end($args);
66 |
67 | return $next(...array_slice($args, 0, -1));
68 | };
69 | }
70 |
71 | /** compose(f, g)(x) = f(g(x)) */
72 | function compose(...$funcs)
73 | {
74 | return pipe(...array_reverse($funcs));
75 | }
76 |
77 | /** pipe(f, g)(x) = g(f(x)) */
78 | function pipe(...$funcs)
79 | {
80 | return fn ($arg) => array_reduce($funcs, fn ($acc, $func) => $func($acc), $arg);
81 | }
82 |
83 | function joinPath(array $parts, $sep = DIRECTORY_SEPARATOR)
84 | {
85 | return array_reduce(array_filter($parts), function ($acc, $part) use ($sep) {
86 | if ($acc === null) {
87 | return rtrim($part, $sep);
88 | }
89 |
90 | return $acc . $sep . ltrim($part, $sep);
91 | }, null);
92 | }
93 |
94 | function isAbsolutePath($path)
95 | {
96 | return str_starts_with($path, '/');
97 | }
98 |
99 | function isRelativePath($path)
100 | {
101 | return str_starts_with($path, './') || str_starts_with($path, '../');
102 | }
103 |
104 | function isResourcePath($path)
105 | {
106 | return str_contains($path, '://');
107 | }
108 |
109 | function isPath($path)
110 | {
111 | return isAbsolutePath($path) || isRelativePath($path) || isResourcePath($path);
112 | }
113 |
114 | /** returns the debug type of an object as string for exception printing */
115 | function debugType($v)
116 | {
117 | if (is_object($v)) {
118 | return 'object ' . $v::class;
119 | }
120 |
121 | return gettype($v);
122 | }
123 |
124 | function spliceArrayAtKey(array $array, $key, array $values, $after = true)
125 | {
126 | $new_array = [];
127 | $spliced = false;
128 | foreach ($array as $array_key => $val) {
129 | if ($array_key == $key) {
130 | $spliced = true;
131 | if ($after) {
132 | $new_array[$array_key] = $val;
133 | $new_array = array_merge($new_array, $values);
134 | } else {
135 | $new_array = array_merge($new_array, $values);
136 | $new_array[$array_key] = $val;
137 | }
138 | } else {
139 | $new_array[$array_key] = $val;
140 | }
141 | }
142 |
143 | if (!$spliced) {
144 | throw new PlatesException('Could not find key ' . $key . ' in array.');
145 | }
146 |
147 | return $new_array;
148 | }
149 |
150 | function cachedFileExists(Psr\SimpleCache\CacheInterface $cache, $ttl = 3600, $file_exists = 'file_exists')
151 | {
152 | return function ($path) use ($cache, $ttl, $file_exists) {
153 | $key = 'League.Plates.file_exists.' . $path;
154 | $res = $cache->get($key);
155 | if (!$res) {
156 | $res = $file_exists($path);
157 | $cache->set($key, $res, $ttl);
158 | }
159 |
160 | return $res;
161 | };
162 | }
163 |
164 | /** Invokes the callable if the predicate returns true. Returns the value otherwise. */
165 | function when(callable $predicate, callable $fn)
166 | {
167 | return fn ($arg) => $predicate($arg) ? $fn($arg) : $arg;
168 | }
169 |
--------------------------------------------------------------------------------
/src/View/Plates/Validator.php:
--------------------------------------------------------------------------------
1 | '/\s*if\s*\(.*\)\s*\{.*\}/m',
14 | 'message' => 'Single line if statements are not allowed in Plates. Use a ternary or split up the statement.',
15 | ],
16 | [
17 | 'pattern' => '/\s*echo\s*/m',
18 | 'message' => 'Echo statements are not allowed in Plates. Use "=" instead.',
19 | ],
20 | [
21 | 'pattern' => '/(?=.*<\?php)(?!.*\?\>)/m',
22 | 'message' => 'Multiline php statements are not allowed in Plates. Split this up or extract the logic.',
23 | ],
24 | [
25 | 'pattern' => '/.*<\?php.*;\s*[^\s]+\?\>/m',
26 | 'message' => 'Multiple php statements on a single line are not allowed in Plates. Split this up or extract the logic.',
27 | ],
28 | [
29 | 'pattern' => '/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\s*=[^[>=<]]/m',
30 | 'message' => 'Variable assignments are not allowed in Plates. Move to a controller or extract into a helper function.',
31 | ],
32 | [
33 | 'pattern' => '/(if|foreach|switch|for|while)\s*\(.*\).*\n*.*\{/m',
34 | 'message' => 'Plates does not allow curly brackets in control structures. You must use short syntax.',
35 | ],
36 | ];
37 |
38 | public static function validate(Template $template): void
39 | {
40 | // get the contents of the template
41 | $path = $template->get('path');
42 |
43 | self::validateFile($path);
44 | }
45 |
46 | public static function validateFile(string $path): void
47 | {
48 | $contents = file_get_contents($path);
49 | array_map(function (array $rule) use ($contents, $path) {
50 | $errorLine = self::checkAndReturnLine($rule['pattern'], $contents);
51 | if ($errorLine) {
52 | throw new TemplateErrorException(message: $rule['message'], line: $errorLine, filename: $path);
53 | }
54 | }, self::RULES);
55 | }
56 |
57 | private static function checkAndReturnLine(string $pattern, string $content): ?int
58 | {
59 | $match = preg_match(pattern: $pattern, subject: $content, matches: $matches, flags: PREG_OFFSET_CAPTURE);
60 |
61 | if (!$match) {
62 | return null;
63 | }
64 |
65 | $characterPosition = (int) $matches[0][1];
66 |
67 | if ($characterPosition === 0) {
68 | return 1;
69 | }
70 |
71 | [$before] = str_split($content, $characterPosition);
72 |
73 | return strlen($before) - strlen(str_replace("\n", '', $before)) + 1;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/View/PlatesView.php:
--------------------------------------------------------------------------------
1 | view = Engine::create($this->getDir() . self::RELATIVE_VIEW_DIR, 'plate.php');
19 | }
20 |
21 | public function render(string $template, array $context = []): string
22 | {
23 | return $this->view->render($template, $context);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/View/TwigAdhocFunction.php:
--------------------------------------------------------------------------------
1 | wp_get_current_user(),
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/View/TwigView.php:
--------------------------------------------------------------------------------
1 | getDir() . '/../../views');
19 | $options = [];
20 | if (WP_ENV==='production') {
21 | $options['cache'] = FORME_PRIVATE_ROOT . 'view-cache';
22 | }
23 |
24 | $this->view = new Environment($loader, $options);
25 | $this->view->addExtension(getInstance(TwigExtension::class));
26 | }
27 |
28 | public function render(string $template, array $context = []): string
29 | {
30 | $template = str_replace('.', '/', $template);
31 |
32 | return $this->view->render($template . '.twig', $context);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/View/ViewInterface.php:
--------------------------------------------------------------------------------
1 | token= new Token();
9 | });
10 |
11 | it('returns a uuid token', function () {
12 | $result = $this->token->get('test');
13 | expect($result)->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/');
14 | });
15 |
16 | it('returns an existing token', function () {
17 | $initial = $this->token->get('test');
18 | $result = $this->token->get('test');
19 | expect($result)->toBe($initial);
20 | });
21 |
22 | it('validates a token', function () {
23 | $result = $this->token->get('test');
24 | expect($this->token->validate($result, 'test'))->toBe(true);
25 | expect($this->token->validate('invalid-garbage-token', 'test'))->toBe(false);
26 | });
27 |
28 | it('invalidates a token after default expiry', function () {
29 | $result = $this->token->get('test');
30 | // expire the token by setting it to 2 hours ago
31 | $expiry = strtotime('-2 hours', time());
32 | $dt = new DateTime();
33 | $dt->setTimestamp($expiry);
34 | Capsule::table('forme_auth_tokens')->where('name', '=', 'test')->update(['expiry' => $dt->format('Y-m-d H:i:s')]);
35 | expect($this->token->validate($result, 'test'))->toBe(false);
36 | });
37 |
38 | it('returns a new token after default expiry', function () {
39 | $result = $this->token->get('test');
40 | // expire the token by setting it to 2 hours ago
41 | $expiry = strtotime('-2 hours', time());
42 | $dt = new DateTime();
43 | $dt->setTimestamp($expiry);
44 | Capsule::table('forme_auth_tokens')->where('name', '=', 'test')->update(['expiry' => $dt->format('Y-m-d H:i:s')]);
45 | expect($this->token->get('test'))->not->toBe($result);
46 | });
47 |
48 | it('invalidates a token after custom expiry', function () {
49 | $result = $this->token->get('test', '+1 week');
50 | // expire the token by setting it to 1 week ago
51 | $expiry = strtotime('-1 week', time());
52 | $dt = new DateTime();
53 | $dt->setTimestamp($expiry);
54 | Capsule::table('forme_auth_tokens')->where('name', '=', 'test')->update(['expiry' => $dt->format('Y-m-d H:i:s')]);
55 | expect($this->token->validate($result, 'test'))->toBe(false);
56 | });
57 |
58 | it('returns a new token after custom expiry', function () {
59 | $result = $this->token->get('test', '+1 month');
60 | // expire the token by setting it to 1 month ago
61 | $expiry = strtotime('-1 month', time());
62 | $dt = new DateTime();
63 | $dt->setTimestamp($expiry);
64 | Capsule::table('forme_auth_tokens')->where('name', '=', 'test')->update(['expiry' => $dt->format('Y-m-d H:i:s')]);
65 | expect($this->token->get('test'))->not->toBe($result);
66 | });
67 |
68 | it('destroys all tokens', function () {
69 | $this->token->get('test');
70 | $this->token->destroy('test');
71 | expect(Capsule::table('forme_auth_tokens')->where('name', '=', 'test')->where('deleted_at', '=', null)->count())->toBe(0);
72 | });
73 |
74 | it('tells you when the token expires', function () {
75 | $this->token->get('test');
76 | expect($this->token->expires('test'))->not->toBe(null);
77 | expect($this->token->expires('test'))->toBeInstanceOf(Carbon::class);
78 | expect($this->token->expires('test')->getTimestamp())->toBeGreaterThan(time());
79 | });
80 |
--------------------------------------------------------------------------------
/tests/Integration/ContainerHelpersTest.php:
--------------------------------------------------------------------------------
1 | toBeInstanceOf(Container::class);
12 | });
13 |
14 | it('returns a logger instance', function () {
15 | expect(log())->toBeInstanceOf(Logger::class);
16 | });
17 |
18 | it('returns a singleton instance of a class', function () {
19 | $first = getInstance(stdClass::class);
20 | $first->foo = faker()->text();
21 | $second = getInstance(stdClass::class);
22 | expect($second->foo)->toBe($first->foo);
23 | });
24 |
25 | it('returns a new instance of a class', function () {
26 | $first = makeInstance(stdClass::class);
27 | $first->foo = faker()->text();
28 | $second = getInstance(stdClass::class);
29 | $second->foo = faker()->text();
30 | $third = makeInstance(stdClass::class);
31 | $third->foo = faker()->text();
32 | expect($second->foo)->not()->toBe($first->foo);
33 | expect($third->foo)->not()->toBe($first->foo);
34 | });
35 |
--------------------------------------------------------------------------------
/tests/Integration/Core/DistAssetsTest.php:
--------------------------------------------------------------------------------
1 | rootDir = dirname(__FILE__, 4) . '/wp-test' . THEME_DIR;
11 | // put a file and a manifest in assets/dist
12 | mkdir($this->rootDir . ASSETS_DIR, 0777, true);
13 | file_put_contents($this->rootDir . ASSETS_DIR . '/' . DUMMY_FILE, 'dummy');
14 | file_put_contents($this->rootDir
15 | . '/assets/dist/manifest.json', '{"assets/dist/' . DUMMY_FILE . '": "..' . THEME_DIR . ASSETS_DIR . '/' . DUMMY_FILE . '"}');
16 | });
17 |
18 | afterEach(function () {
19 | // remove the files in assets
20 | unlink($this->rootDir . ASSETS_DIR . '/' . DUMMY_FILE);
21 | unlink($this->rootDir . ASSETS_DIR . '/manifest.json');
22 | rmdir($this->rootDir . ASSETS_DIR);
23 | });
24 |
25 | test('time() returns the file time of the file', function () {
26 | expect(Assets::time(DUMMY_FILE))->toBe((string) filemtime($this->rootDir . '/assets/dist/dummy.txt'));
27 | });
28 |
29 | test('path() returns the absolute path of the file', function () {
30 | expect(Assets::path(DUMMY_FILE))->toBe($this->rootDir . '/assets/dist/dummy.txt');
31 | });
32 |
33 | test('uri() returns the uri of the file', function () {
34 | expect(Assets::uri(DUMMY_FILE))->toBe('../public/wp-content/themes/twentytwentyfour/assets/dist/dummy.txt');
35 | });
36 |
37 | test('distExists() returns true if dist folder exists', function () {
38 | expect(Assets::distExists())->toBe(true);
39 | });
40 |
41 | test('staticExists() returns false if no static folder exists', function () {
42 | expect(Assets::staticExists())->toBe(false);
43 | });
44 |
--------------------------------------------------------------------------------
/tests/Integration/Core/LoaderTest.php:
--------------------------------------------------------------------------------
1 | addAction('action_name', stdClass::class, 'method_name');
10 | // get actions by reflection
11 | $actions = (new ReflectionClass($loader))->getProperty('actions');
12 | $actions->setAccessible(true);
13 | $actions = $actions->getValue($loader);
14 | expect($actions)->toHaveLength(1);
15 | expect($actions[0]['hook'])->toBe('action_name');
16 | expect($actions[0]['resolvedCallable'][0])->toBeInstanceOf(stdClass::class);
17 | expect($actions[0]['resolvedCallable'][1])->toBe('method_name');
18 | expect($actions[0]['priority'])->toBe(10);
19 | expect($actions[0]['numberOfArgs'])->toBe(1);
20 | });
21 |
22 | test('adds an invokable action to be registered with WordPress', function () {
23 | $loader = new Loader();
24 | // mock invokeable class
25 | $class = new class() {
26 | public function __invoke(): string
27 | {
28 | return 'foo bar';
29 | }
30 | };
31 | $loader->addAction('action_name', get_class($class), '__invoke');
32 | // get actions by reflection
33 | $actions = (new ReflectionClass($loader))->getProperty('actions');
34 | $actions->setAccessible(true);
35 | $actions = $actions->getValue($loader);
36 | expect($actions)->toHaveLength(1);
37 | expect($actions[0]['hook'])->toBe('action_name');
38 | expect($actions[0]['resolvedCallable'])->toBeInstanceOf(get_class($class));
39 | expect($actions[0]['priority'])->toBe(10);
40 | expect($actions[0]['numberOfArgs'])->toBe(1);
41 | });
42 |
43 | test('adds a filter to be registered with WordPress', function () {
44 | $loader = new Loader();
45 | $loader->addFilter('filter_name', stdClass::class, 'method_name');
46 | // get filters by reflection
47 | $filters = (new ReflectionClass($loader))->getProperty('filters');
48 | $filters->setAccessible(true);
49 | $filters = $filters->getValue($loader);
50 | expect($filters)->toHaveLength(1);
51 | expect($filters[0]['hook'])->toBe('filter_name');
52 | expect($filters[0]['resolvedCallable'][0])->toBeInstanceOf(stdClass::class);
53 | expect($filters[0]['resolvedCallable'][1])->toBe('method_name');
54 | expect($filters[0]['priority'])->toBe(10);
55 | expect($filters[0]['numberOfArgs'])->toBe(1);
56 | });
57 |
58 | test('adds an invokable filter to be registered with WordPress', function () {
59 | $loader = new Loader();
60 | // mock invokeable class
61 | $class = new class() {
62 | public function __invoke(): string
63 | {
64 | return 'foo bar';
65 | }
66 | };
67 | $loader->addFilter('filter_name', get_class($class), '__invoke');
68 | // get filters by reflection
69 | $filters = (new ReflectionClass($loader))->getProperty('filters');
70 | $filters->setAccessible(true);
71 | $filters = $filters->getValue($loader);
72 | expect($filters)->toHaveLength(1);
73 | expect($filters[0]['hook'])->toBe('filter_name');
74 | expect($filters[0]['resolvedCallable'])->toBeInstanceOf(get_class($class));
75 | expect($filters[0]['priority'])->toBe(10);
76 | expect($filters[0]['numberOfArgs'])->toBe(1);
77 | });
78 |
79 | test('adds a priority to a filter or action', function () {
80 | $loader = new Loader();
81 | $loader->addFilter('filter_name', stdClass::class, 'method_name', 20);
82 | $loader->addAction('action_name', stdClass::class, 'method_name', 20);
83 | // get filters by reflection
84 | $filters = (new ReflectionClass($loader))->getProperty('filters');
85 | $filters->setAccessible(true);
86 | $filters = $filters->getValue($loader);
87 | expect($filters[0]['priority'])->toBe(20);
88 | // get actions by reflection
89 | $actions = (new ReflectionClass($loader))->getProperty('actions');
90 | $actions->setAccessible(true);
91 | $actions = $actions->getValue($loader);
92 | expect($actions[0]['priority'])->toBe(20);
93 | });
94 |
95 | test('adds a number of arguments to a filter or action', function () {
96 | $loader = new Loader();
97 | $loader->addFilter('filter_name', stdClass::class, 'method_name', 10, 2);
98 | $loader->addAction('action_name', stdClass::class, 'method_name', 10, 2);
99 | // get filters by reflection
100 | $filters = (new ReflectionClass($loader))->getProperty('filters');
101 | $filters->setAccessible(true);
102 | $filters = $filters->getValue($loader);
103 | expect($filters[0]['numberOfArgs'])->toBe(2);
104 | // get actions by reflection
105 | $actions = (new ReflectionClass($loader))->getProperty('actions');
106 | $actions->setAccessible(true);
107 | $actions = $actions->getValue($loader);
108 | expect($actions[0]['numberOfArgs'])->toBe(2);
109 | });
110 |
111 | test('adds a yaml config file of hooks to be registered with WordPress', function () {
112 | $loader = new Loader();
113 | $configString = file_get_contents(__DIR__ . '/hooks_test.yaml');
114 | $loader->addConfig($configString);
115 | // get actions by reflection
116 | $actions = (new ReflectionClass($loader))->getProperty('actions');
117 | $actions->setAccessible(true);
118 | $actions = $actions->getValue($loader);
119 | expect($actions)->toHaveLength(1);
120 | expect($actions[0]['hook'])->toBe('action_name');
121 | expect($actions[0]['resolvedCallable'][0])->toBeInstanceOf(stdClass::class);
122 | expect($actions[0]['resolvedCallable'][1])->toBe('method_name');
123 | expect($actions[0]['priority'])->toBe(10);
124 | expect($actions[0]['numberOfArgs'])->toBe(1);
125 | // get filters by reflection
126 | $filters = (new ReflectionClass($loader))->getProperty('filters');
127 | $filters->setAccessible(true);
128 | $filters = $filters->getValue($loader);
129 | expect($filters)->toHaveLength(1);
130 | expect($filters[0]['hook'])->toBe('filter_name');
131 | expect($filters[0]['resolvedCallable'][0])->toBeInstanceOf(stdClass::class);
132 | expect($filters[0]['resolvedCallable'][1])->toBe('method_name');
133 | expect($filters[0]['priority'])->toBe(1);
134 | expect($filters[0]['numberOfArgs'])->toBe(2);
135 | });
136 |
137 | test('run adds configured hooks to WordPress', function () {
138 | $loader = new Loader();
139 | $configString = file_get_contents(__DIR__ . '/hooks_test.yaml');
140 | $loader->addConfig($configString);
141 | $loader->run();
142 | expect(has_action('action_name'))->toBe(true);
143 | expect(has_filter('filter_name'))->toBe(true);
144 | });
145 |
--------------------------------------------------------------------------------
/tests/Integration/Core/StaticAssetsTest.php:
--------------------------------------------------------------------------------
1 | rootDir = dirname(__FILE__, 4) . '/wp-test/public/wp-content/themes/twentytwentyfour';
7 | // put a file in assets
8 | mkdir($this->rootDir . '/assets/static', 0777, true);
9 | file_put_contents($this->rootDir . '/assets/static/dummy.txt', 'dummy');
10 | });
11 |
12 | afterEach(function () {
13 | // remove the file in assets
14 | unlink($this->rootDir . '/assets/static/dummy.txt');
15 | rmdir($this->rootDir . '/assets/static');
16 | });
17 |
18 | test('time() returns the file time of the file', function () {
19 | expect(Assets::time('dummy.txt'))->toBe((string) filemtime($this->rootDir . '/assets/static/dummy.txt'));
20 | });
21 |
22 | test('path() returns the absolute path of the file', function () {
23 | expect(Assets::path('dummy.txt'))->toBe($this->rootDir . '/assets/static/dummy.txt');
24 | });
25 |
26 | test('uri() returns the uri of the file', function () {
27 | expect(Assets::uri('dummy.txt'))->toBe(get_template_directory_uri() . '/assets/static/dummy.txt');
28 | });
29 |
30 | test('distExists() returns false if no dist folder exists', function () {
31 | expect(Assets::distExists())->toBe(false);
32 | });
33 |
34 | test('staticExists() returns true if a static folder exists', function () {
35 | expect(Assets::staticExists())->toBe(true);
36 | });
37 |
--------------------------------------------------------------------------------
/tests/Integration/Core/hooks_test.yaml:
--------------------------------------------------------------------------------
1 | actions:
2 | - hook: action_name
3 | class: stdClass
4 | method: method_name
5 | filters:
6 | - hook: filter_name
7 | class: stdClass
8 | method: method_name
9 | priority: 1
10 | arguments: 2
11 |
--------------------------------------------------------------------------------
/tests/Integration/Hooks/HookIsSetTest.php:
--------------------------------------------------------------------------------
1 | toBe(true);
10 | });
11 |
12 | test('returns false if a class hook is not already set', function () {
13 | expect(HookIsSet::check('template_include', TemplateHandler::class))->toBe(false);
14 | });
15 |
--------------------------------------------------------------------------------
/tests/Integration/Router/StrategyFactoryTest.php:
--------------------------------------------------------------------------------
1 | factory = getContainer()->get(StrategyFactory::class);
10 | });
11 |
12 | test('successfully creates all the configured route types', function () {
13 | $types = $this->factory::TYPES;
14 | foreach ($types as $type) {
15 | $strategy = $this->factory->get($type);
16 | expect($strategy)->toBeInstanceOf(StrategyInterface::class);
17 | }
18 | });
19 |
20 | test('configured route types match the existing class files', function () {
21 | $strategyClassFiles = glob(__DIR__ . '/../../../src/Router/Strategy/*Strategy.php');
22 | $strategyClasses = array_map(function ($file) {
23 | return basename($file, '.php');
24 | }, $strategyClassFiles);
25 | $configuredTypes = $this->factory::TYPES;
26 | $configuredTypeClasses = array_map(function ($type) {
27 | return (new UnicodeString($type))->camel()->title()->toString() . 'Strategy';
28 | }, $configuredTypes);
29 | // check that each array contains the same values
30 | expect(array_diff($strategyClasses, $configuredTypeClasses))->toBeEmpty();
31 | });
32 |
--------------------------------------------------------------------------------
/tests/Pest.php:
--------------------------------------------------------------------------------
1 | beforeEach(function () {
5 | global $wp_filter;
6 | $wp_filter = [];
7 | migrate();
8 | })->in('Integration');
9 |
10 | uses()->afterEach(function () {
11 | rollback();
12 | })->in('Integration');
13 |
--------------------------------------------------------------------------------
/tests/Unit/ExampleTest.php:
--------------------------------------------------------------------------------
1 | toBe(4);
5 | });
6 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |