├── .editorconfig
├── src
├── Html
│ ├── Element
│ │ ├── H3WithClass.php
│ │ ├── DivWithClass.php
│ │ ├── SelectElement.php
│ │ └── ListElement.php
│ └── Element.php
├── Singleton.php
├── HookConstructorTo.php
├── HookInitTo.php
├── Db.php
├── HookAnnotation.php
├── Is.php
├── SiteInfo.php
├── helpers.php
├── Script.php
├── HookProxy.php
└── HookProxyListed.php
├── LICENSE
└── composer.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | eclint_block_comment_start = /*
13 | eclint_block_comment = *
14 | eclint_block_comment_end = */
15 |
--------------------------------------------------------------------------------
/src/Html/Element/H3WithClass.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress\Html\Element;
14 |
15 | use SzepeViktor\SentencePress\Html\Element;
16 |
17 | /**
18 | * Create an h3 element with classes.
19 | */
20 | class H3WithClass extends Element
21 | {
22 | public function __construct(string $classString, string $content = '') {
23 | parent::__construct('h3', ['class' => $classString], $content);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Html/Element/DivWithClass.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress\Html\Element;
14 |
15 | use SzepeViktor\SentencePress\Html\Element;
16 |
17 | /**
18 | * Create a div element with classes.
19 | */
20 | class DivWithClass extends Element
21 | {
22 | public function __construct(string $classString, string $content = '') {
23 | parent::__construct('div', ['class' => $classString], $content);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Singleton.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | /**
16 | * Singleton trait.
17 | */
18 | trait Singleton
19 | {
20 | /** @var self|null */
21 | private static $instance;
22 |
23 | public static function getInstance(): self
24 | {
25 | if (!self::$instance instanceof self) {
26 | self::$instance = new self;
27 | }
28 |
29 | return self::$instance;
30 | }
31 |
32 | private function __construct() {}
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Viktor Szépe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Html/Element/SelectElement.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress\Html\Element;
14 |
15 | use SzepeViktor\SentencePress\Html\Element;
16 |
17 | use function esc_html;
18 |
19 | /**
20 | * Create a select element with attributes from a list of option elements.
21 | */
22 | class SelectElement extends Element
23 | {
24 | /**
25 | * @param array $attributes HTML attributes of the select element.
26 | * @param array $options Option elements value=>raw_item.
27 | */
28 | public function __construct(array $attributes = [], array $options = [], string $currentValue = '') {
29 | parent::__construct('select', $attributes, \implode(\array_map(
30 | static function (string $optionValue, string $optionItem) use ($currentValue): string {
31 | return (new Element(
32 | 'option',
33 | \array_merge(
34 | ['value' => $optionValue],
35 | $optionValue === $currentValue ? ['selected' => null] : []
36 | ),
37 | esc_html($optionItem)
38 | ))->render();
39 | },
40 | \array_keys($options),
41 | $options
42 | )));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Html/Element/ListElement.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress\Html\Element;
14 |
15 | use SzepeViktor\SentencePress\Html\Element;
16 |
17 | /**
18 | * Create a list element with attributes from a list of children.
19 | */
20 | class ListElement extends Element
21 | {
22 | /**
23 | * @param array $attributes HTML attributes of the parent.
24 | * @param list $childrenContent Raw HTML content of children.
25 | * @param string $childTagName Name of children tags.
26 | */
27 | public function __construct(
28 | string $tagName = 'ul',
29 | array $attributes = [],
30 | array $childrenContent = [],
31 | string $childTagName = 'li'
32 | ) {
33 | parent::__construct($tagName, $attributes, \implode(array_map(
34 | static function (string $childContent) use ($childTagName): string {
35 | return \implode([
36 | self::LESS_THAN_SIGN,
37 | $childTagName,
38 | self::GREATER_THAN_SIGN,
39 | $childContent,
40 | self::LESS_THAN_SIGN,
41 | self::SOLIDUS,
42 | $childTagName,
43 | self::GREATER_THAN_SIGN,
44 | ]);
45 | },
46 | $childrenContent
47 | )));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "szepeviktor/sentencepress",
3 | "description": "OOP toolkit for daily tasks in WordPress development.",
4 | "license": "MIT",
5 | "keywords": [
6 | "tools",
7 | "oop",
8 | "wordpress"
9 | ],
10 | "require": {
11 | "php": "^7.4 || ^8.0"
12 | },
13 | "require-dev": {
14 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
15 | "johnbillion/wp-compat": "^1.1",
16 | "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1",
17 | "szepeviktor/phpstan-wordpress": "^2.0",
18 | "wp-coding-standards/wpcs": "3.1.0 as 2.3.0"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "SzepeViktor\\SentencePress\\": "src/"
23 | },
24 | "files": [
25 | "src/helpers.php"
26 | ]
27 | },
28 | "autoload-dev": {
29 | "psr-4": {
30 | "SzepeViktor\\SentencePress\\Tests\\": "tests/"
31 | }
32 | },
33 | "config": {
34 | "allow-plugins": {
35 | "dealerdirect/phpcodesniffer-composer-installer": true
36 | },
37 | "optimize-autoloader": true
38 | },
39 | "scripts": {
40 | "cs": [
41 | "@phpcs:set-php-version",
42 | "@phpcs:run"
43 | ],
44 | "phpcs:run": "phpcs -s --standard=PSR12NeutronRuleset --exclude=PEAR.Functions.FunctionCallSignature src/",
45 | "phpcs:set-php-version": "phpcs --config-set php_version 70400",
46 | "phpstan": "phpstan analyze -v",
47 | "syntax": "git ls-files --cached -z -- '*.php' | xargs -0 -L 1 -P 4 -- php -l",
48 | "test": [
49 | "@composer validate --strict",
50 | "@syntax",
51 | "@phpstan",
52 | "@cs"
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/HookConstructorTo.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use ReflectionClass;
16 |
17 | use function add_action;
18 |
19 | /**
20 | * Hook class constructor on to a specific action.
21 | *
22 | * Only actions are supported.
23 | * Example call with priority zero.
24 | *
25 | * HookConstructorTo::{'acf/init'}(MyClass::class, 0);
26 | */
27 | class HookConstructorTo
28 | {
29 | protected const DEFAULT_PRIORITY = 10;
30 |
31 | /**
32 | * Hook to the action in the method name.
33 | *
34 | * @param string $actionTag
35 | * @param array{0?: class-string, 1?: int} $arguments
36 | */
37 | public static function __callStatic(string $actionTag, array $arguments): void
38 | {
39 | if (! isset($arguments[0])) {
40 | throw new \ArgumentCountError('Class name must be supplied.');
41 | }
42 |
43 | /** @var class-string $class */
44 | $class = $arguments[0];
45 |
46 | $constructor = (new ReflectionClass($class))->getConstructor();
47 | if ($constructor === null) {
48 | throw new \ErrorException('The class must have a constructor defined.');
49 | }
50 |
51 | // Hook the constructor.
52 | add_action(
53 | $actionTag,
54 | static function () use ($class): void {
55 | // Pass hook parameters to constructor.
56 | $args = func_get_args();
57 | // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction
58 | new $class(...$args);
59 | },
60 | (int)($arguments[1] ?? self::DEFAULT_PRIORITY),
61 | $constructor->getNumberOfParameters()
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/HookInitTo.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use ReflectionClass;
16 |
17 | use function add_filter;
18 |
19 | /**
20 | * Hook init() method on to a specific action or filter.
21 | *
22 | * Example call with priority zero.
23 | *
24 | * HookInitTo::plugins_loaded(MyClass::class, 0);
25 | */
26 | class HookInitTo
27 | {
28 | protected const DEFAULT_PRIORITY = 10;
29 |
30 | /**
31 | * Hook to the action in the method name.
32 | *
33 | * @param string $actionTag
34 | * @param array{0?: class-string, 1?: int} $arguments
35 | *
36 | * @throws \ArgumentCountError
37 | * @throws \ReflectionException
38 | */
39 | public static function __callStatic(string $actionTag, array $arguments): void
40 | {
41 | if (! isset($arguments[0])) {
42 | throw new \ArgumentCountError('Class name must be supplied.');
43 | }
44 |
45 | /** @var class-string $class */
46 | $class = $arguments[0];
47 |
48 | $initMethod = (new ReflectionClass($class))->getMethod('init');
49 |
50 | // Hook 'init' method.
51 | add_filter(
52 | $actionTag,
53 | // phpcs:ignore NeutronStandard.Functions.TypeHint.NoReturnType
54 | static function () use ($class) {
55 | // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction
56 | $instance = new $class();
57 | // Pass hook parameters to init()
58 | $args = func_get_args();
59 |
60 | return $instance->init(...$args);
61 | },
62 | (int)($arguments[1] ?? self::DEFAULT_PRIORITY),
63 | $initMethod->getNumberOfParameters()
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Db.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | /**
16 | * Connect to global $wpdb instance from OOP code.
17 | *
18 | * Usage example.
19 | *
20 | * $db = new \Toolkit4WP\Db(); $db->prepare('...');
21 | *
22 | * @see https://www.php.net/manual/en/language.oop5.magic.php
23 | */
24 | class Db
25 | {
26 | /**
27 | * Get a property.
28 | *
29 | * @see https://codex.wordpress.org/Class_Reference/wpdb#Class_Variables
30 | * @param string $name
31 | * @return mixed
32 | */
33 | public function __get(string $name)
34 | {
35 | global $wpdb;
36 |
37 | return $wpdb->$name;
38 | }
39 |
40 | /**
41 | * Noop on set.
42 | *
43 | * @param string $name
44 | * @param mixed $propertyValue
45 | * @return void
46 | *
47 | * phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
48 | */
49 | public function __set(string $name, $propertyValue): void
50 | {
51 | }
52 |
53 | // phpcs:enable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
54 |
55 | /**
56 | * Execute a method.
57 | *
58 | * @see https://www.php.net/manual/en/language.oop5.overloading.php#object.call
59 | * @param string $name
60 | * @param list $arguments
61 | * @return mixed
62 | */
63 | public function __call(string $name, array $arguments)
64 | {
65 | global $wpdb;
66 |
67 | $callback = [$wpdb, $name];
68 | if (! \is_callable($callback)) {
69 | throw new \InvalidArgumentException(sprintf('Unknown wpdb method: %s', $name));
70 | }
71 |
72 | // phpcs:ignore NeutronStandard.Functions.DisallowCallUserFunc.CallUserFunc
73 | return \call_user_func_array($callback, $arguments);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/HookAnnotation.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use ReflectionClass;
16 | use ReflectionMethod;
17 |
18 | use function add_filter;
19 |
20 | /**
21 | * Implement hooking in method annotation.
22 | *
23 | * Format: @hook tag 10
24 | * @hook tag first
25 | * @hook tag last
26 | * @hook skip
27 | *
28 | * mindplay/annotations may be a better solution.
29 | *
30 | * @see https://github.com/szepeviktor/wordpress-website-lifecycle/blob/master/WordPress-hooks.md
31 | */
32 | trait HookAnnotation
33 | {
34 | protected function hookMethods(int $defaultPriority = 10): void
35 | {
36 | $classReflection = new ReflectionClass(self::class);
37 | // Look for hook tag in all public methods.
38 | foreach ($classReflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
39 | // Do not hook constructor, use HookConstructorTo.
40 | if ($method->isConstructor()) {
41 | continue;
42 | }
43 | $hookDetails = $this->getMetadata((string)$method->getDocComment(), $defaultPriority);
44 | if ($hookDetails === null) {
45 | continue;
46 | }
47 |
48 | add_filter(
49 | $hookDetails['tag'],
50 | [$this, $method->name],
51 | $hookDetails['priority'],
52 | $method->getNumberOfParameters()
53 | );
54 | }
55 | }
56 |
57 | /**
58 | * Read hook tag from docblock.
59 | *
60 | * @return array{tag: string, priority: int}|null
61 | */
62 | protected function getMetadata(string $docComment, int $defaultPriority): ?array
63 | {
64 | $matches = [];
65 | if (
66 | \preg_match(
67 | // @hook ( tag ) ( priority )
68 | '/^\s+\*\s+@hook\s+([\w\/._=-]+)(?:\s+(\d+|first|last))?\s*$/m',
69 | $docComment,
70 | $matches
71 | ) !== 1
72 | ) {
73 | return null;
74 | }
75 |
76 | if ($matches[1] === 'skip') {
77 | return null;
78 | }
79 |
80 | if (! isset($matches[2])) {
81 | return [
82 | 'tag' => $matches[1],
83 | 'priority' => $defaultPriority,
84 | ];
85 | }
86 |
87 | switch ($matches[2]) {
88 | case 'first':
89 | $priority = PHP_INT_MIN;
90 | break;
91 | case 'last':
92 | $priority = PHP_INT_MAX;
93 | break;
94 | default:
95 | $priority = (int)$matches[2];
96 | break;
97 | }
98 |
99 | return [
100 | 'tag' => $matches[1],
101 | 'priority' => $priority,
102 | ];
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/Is.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use WP_User;
16 |
17 | /**
18 | * Various request helpers.
19 | *
20 | * @see https://github.com/chesio/bc-security/blob/develop/classes/BlueChip/Security/Helpers/Is.php
21 | * X https://github.com/inpsyde/wp-context/blob/main/src/WpContext.php X
22 | */
23 | class Is
24 | {
25 | /**
26 | * Whether we are in a live environment.
27 | *
28 | * @return bool
29 | */
30 | public static function live(): bool
31 | {
32 | // Consider both production and staging environment as live.
33 | return \defined('WP_ENV') && \in_array(\WP_ENV, ['production', 'staging'], true);
34 | }
35 |
36 | /**
37 | * Whether given user is an administrator.
38 | *
39 | * @param \WP_User $user The given user.
40 | * @return bool
41 | */
42 | public static function admin(WP_User $user): bool
43 | {
44 | return \is_multisite() ? \user_can($user, 'manage_network') : \user_can($user, 'manage_options');
45 | }
46 |
47 | /**
48 | * Whether the current user is not logged in.
49 | *
50 | * @return bool
51 | */
52 | public static function anonymousUsers(): bool
53 | {
54 | return ! \is_user_logged_in();
55 | }
56 |
57 | /**
58 | * Whether the current user is a comment author.
59 | *
60 | * @return bool
61 | */
62 | public static function commentAuthor(): bool
63 | {
64 | // phpcs:ignore WordPress.VIP.RestrictedVariables.cache_constraints___COOKIE
65 | return isset($_COOKIE[sprintf('comment_author_%s', \COOKIEHASH)]);
66 | }
67 |
68 | /**
69 | * Whether current webserver interface is CLI.
70 | *
71 | * @return bool
72 | */
73 | public static function cli(): bool
74 | {
75 | return \php_sapi_name() === 'cli';
76 | }
77 |
78 | /**
79 | * Whether current request is of the given type.
80 | *
81 | * All of them are available even before 'muplugins_loaded' action,
82 | * exceptions are commented.
83 | *
84 | * @param string $type Type of request.
85 | * @return bool
86 | * phpcs:disable SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
87 | */
88 | public static function request(string $type): bool
89 | {
90 | // phpcs:disable Squiz.PHP.CommentedOutCode.Found
91 | switch ($type) {
92 | case 'installing':
93 | return \wp_installing();
94 | case 'index':
95 | return \wp_using_themes();
96 | case 'frontend':
97 | // Use !request('frontend') for admin pages.
98 | return (! \is_admin() || \wp_doing_ajax() ) && ! \wp_doing_cron();
99 | case 'admin':
100 | // Includes admin-ajax :(
101 | return \is_admin();
102 | case 'login':
103 | return isset($_SERVER['REQUEST_URI'])
104 | && \is_string($_SERVER['REQUEST_URI'])
105 | && \explode('?', $_SERVER['REQUEST_URI'])[0]
106 | === \wp_parse_url(\wp_login_url('', true), \PHP_URL_PATH);
107 | case 'async-upload':
108 | return isset($_SERVER['SCRIPT_FILENAME'])
109 | && sprintf('%swp-admin/async-upload.php', \ABSPATH) === $_SERVER['SCRIPT_FILENAME'];
110 | case 'preview': // in 'parse_query' action if (is_main_query())
111 | return \is_preview() || \is_customize_preview();
112 | case 'autosave': // After 'heartbeat_received', 500 action
113 | // Autosave post while editing and Heartbeat.
114 | return \defined('DOING_AUTOSAVE') && \DOING_AUTOSAVE === true;
115 | case 'rest': // After 'parse_request' action
116 | return \defined('REST_REQUEST') && \REST_REQUEST === true;
117 | case 'ajax':
118 | return \wp_doing_ajax();
119 | case 'xmlrpc':
120 | return \defined('XMLRPC_REQUEST') && \XMLRPC_REQUEST === true;
121 | case 'trackback': // In 'parse_query'
122 | return \is_trackback();
123 | case 'search': // In 'parse_query'
124 | return \is_search();
125 | case 'feed': // In 'parse_query'
126 | return \is_feed();
127 | case 'robots': // In 'parse_query'
128 | return \is_robots();
129 | case 'cron':
130 | return \wp_doing_cron();
131 | case 'wp-cli':
132 | return \defined('WP_CLI') && \WP_CLI === true;
133 | default:
134 | \_doing_it_wrong(__METHOD__, \esc_html(\sprintf('Unknown request type: %s', $type)), '0.1.0');
135 | return false;
136 | }
137 | // phpcs:enable
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/SiteInfo.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use WP_Filesystem_Base;
16 |
17 | use function trailingslashit;
18 |
19 | /**
20 | * Provide information on core paths and URLs.
21 | */
22 | class SiteInfo
23 | {
24 | /** @var array */
25 | protected $siteInfo = [];
26 |
27 | /**
28 | * Set paths and URLs.
29 | *
30 | * @see https://codex.wordpress.org/Determining_Plugin_and_Content_Directories
31 | */
32 | protected function init(): void
33 | {
34 | $uploadPathAndUrl = \wp_upload_dir();
35 | // phpcs:disable NeutronStandard.AssignAlign.DisallowAssignAlign.Aligned
36 | $this->siteInfo = [
37 | // Core
38 | 'site_path' => \ABSPATH,
39 | 'site_url' => \site_url(),
40 | 'home_path' => $this->getHomePath(),
41 | 'home_url' => \get_home_url(),
42 | 'includes_path' => sprintf('%s%s', \ABSPATH, \WPINC),
43 | 'includes_url' => \includes_url(),
44 |
45 | // Content
46 | 'content_path' => \WP_CONTENT_DIR,
47 | 'content_url' => \content_url(),
48 | 'uploads_path' => $uploadPathAndUrl['basedir'],
49 | 'uploads_url' => $uploadPathAndUrl['baseurl'],
50 |
51 | // Plugins
52 | 'plugins_path' => \WP_PLUGIN_DIR,
53 | 'plugins_url' => \plugins_url(),
54 | 'mu_plugins_path' => \WPMU_PLUGIN_DIR,
55 | 'mu_plugins_url' => \plugins_url(),
56 |
57 | // Themes
58 | 'themes_root_path' => \get_theme_root(),
59 | 'themes_root_url' => \get_theme_root_uri(),
60 | 'parent_theme_path' => \get_template_directory(),
61 | 'parent_theme_url' => \get_template_directory_uri(),
62 | 'child_theme_path' => \get_stylesheet_directory(),
63 | 'child_theme_url' => \get_stylesheet_directory_uri(),
64 | ];
65 | // phpcs:enable
66 | }
67 |
68 | /**
69 | * Public API.
70 | */
71 | public function getPath(string $name): string
72 | {
73 | return $this->getInfo($name, '_path');
74 | }
75 |
76 | /**
77 | * Public API.
78 | */
79 | public function getUrl(string $name): string
80 | {
81 | return $this->getInfo($name, '_url');
82 | }
83 |
84 | /**
85 | * Public API.
86 | */
87 | public function getUrlBasename(string $name): string
88 | {
89 | return \basename($this->getUrl($name));
90 | }
91 |
92 | /**
93 | * Public API.
94 | */
95 | public function usingChildTheme(): bool
96 | {
97 | $this->setInfo();
98 |
99 | return trailingslashit($this->siteInfo['parent_theme_path'])
100 | !== trailingslashit($this->siteInfo['child_theme_path']);
101 | }
102 |
103 | /**
104 | * Public API.
105 | */
106 | public function isUploadsWritable(): bool
107 | {
108 | // phpcs:disable Squiz.NamingConventions.ValidVariableName
109 | global $wp_filesystem;
110 | if (! $wp_filesystem instanceof WP_Filesystem_Base) {
111 | require_once sprintf('%swp-admin/includes/file.php', \ABSPATH);
112 | }
113 | assert($wp_filesystem instanceof WP_Filesystem_Base);
114 |
115 | $this->setInfo();
116 |
117 | $uploadsDir = trailingslashit($this->siteInfo['uploads_path']);
118 |
119 | return $wp_filesystem->exists($uploadsDir) && $wp_filesystem->is_writable($uploadsDir);
120 | // phpcs:enable
121 | }
122 |
123 | protected function setInfo(): void
124 | {
125 | if ($this->siteInfo !== []) {
126 | return;
127 | }
128 |
129 | if (! \did_action('init')) {
130 | throw new \LogicException('SiteInfo must be used in "init" action or later.');
131 | }
132 |
133 | $this->init();
134 | }
135 |
136 | protected function getInfo(string $name, string $suffix): string
137 | {
138 | $this->setInfo();
139 |
140 | $infoKey = sprintf('%s%s', $name, $suffix);
141 | if (! \array_key_exists($infoKey, $this->siteInfo)) {
142 | throw new \DomainException(sprintf('Unknown SiteInfo key: %s', $infoKey));
143 | }
144 |
145 | return trailingslashit($this->siteInfo[$infoKey]);
146 | }
147 |
148 | protected function getHomePath(): string
149 | {
150 | $homeUrl = \set_url_scheme(\get_option('home'), 'http');
151 | $siteUrl = \set_url_scheme(\get_option('siteurl'), 'http');
152 | if ($homeUrl !== '' && \strcasecmp($homeUrl, $siteUrl) !== 0) {
153 | $pos = \strripos(\ABSPATH, trailingslashit(\str_ireplace($homeUrl, '', $siteUrl)));
154 | if ($pos !== false) {
155 | return \substr(\ABSPATH, 0, $pos);
156 | }
157 | }
158 |
159 | return \ABSPATH;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/helpers.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use DOMDocument;
16 | use DOMElement;
17 |
18 | use function esc_url;
19 | use function get_template_directory_uri;
20 | use function wp_json_encode;
21 |
22 | /**
23 | * Return whether an array or a string is empty.
24 | *
25 | * Throw exception on int|float|bool|null|object|callable|resource
26 | *
27 | * @param mixed $thing
28 | */
29 | function isEmpty($thing): bool
30 | {
31 | if (\is_array($thing)) {
32 | return $thing === [];
33 | }
34 |
35 | if (\is_string($thing)) {
36 | return $thing === '';
37 | }
38 |
39 | throw new \InvalidArgumentException('Not a string nor an array.');
40 | }
41 |
42 | /**
43 | * Check whether a value is a non-empty array.
44 | *
45 | * @param mixed $thing Array to be tested.
46 | */
47 | function isNonEmptyArray($thing): bool
48 | {
49 | return \is_array($thing) && $thing !== [];
50 | }
51 |
52 | /**
53 | * @see https://html.spec.whatwg.org/multipage/introduction.html#syntax-errors
54 | */
55 | function htmlComment(string $comment): string
56 | {
57 | // Replace two dashes with an &mdash to be on the safe side.
58 | return \sprintf('', \str_replace('--', '—', $comment));
59 | }
60 |
61 | /**
62 | * @param mixed $condition
63 | */
64 | function ifPrint($condition, string $content): void
65 | {
66 | if (! $condition) {
67 | return;
68 | }
69 |
70 | // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found
71 | print $content;
72 | }
73 |
74 | function printAssetUri(string $path = ''): void
75 | {
76 | // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found
77 | print esc_url(
78 | \sprintf(
79 | '%s/assets%s',
80 | \dirname(get_template_directory_uri()),
81 | $path
82 | )
83 | );
84 | }
85 |
86 | /**
87 | * Prepare an SVG for inline display.
88 | *
89 | * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/SVG_In_HTML_Introduction
90 | *
91 | * @param string|false $path
92 | * @param array $attrs
93 | *
94 | * phpcs:disable Squiz.NamingConventions.ValidVariableName.NotCamelCaps,Squiz.NamingConventions.ValidFunctionName.NotCamelCaps
95 | */
96 | function get_inline_svg($path, array $attrs = [], int $ttl = 3600): string
97 | {
98 | $empty_svg = '';
99 |
100 | // phpcs:ignore PSR12NeutronRuleset.Strings.ConcatenationUsage.NotAllowed
101 | $cache_key = 'file-' . md5($path . wp_json_encode($attrs));
102 | $xml = wp_cache_get($cache_key, 'svg-contents');
103 | if ($xml !== false) {
104 | assert(is_string($xml));
105 |
106 | return $xml;
107 | }
108 |
109 | if ($path === false || ! file_exists($path)) {
110 | return $empty_svg;
111 | }
112 |
113 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
114 | $contents = file_get_contents($path);
115 | if ($contents === false || $contents === '') {
116 | return $empty_svg;
117 | }
118 |
119 | $document = new DOMDocument();
120 | $document->loadXML($contents);
121 |
122 | $svg_elems = $document->getElementsByTagName('svg');
123 | if ($svg_elems->length === 0) {
124 | return $empty_svg;
125 | }
126 |
127 | $svg_elem = $svg_elems->item(0);
128 | assert($svg_elem instanceof DOMElement, 'length === 0 makes sure we have element 0');
129 |
130 | // May cause duplicate ID error
131 | $svg_elem->removeAttribute('id');
132 |
133 | foreach ($attrs as $attr_name => $attr_value) {
134 | $svg_elem->setAttribute($attr_name, $attr_value);
135 | }
136 |
137 | // phpcs:disable Squiz.PHP.CommentedOutCode.Found
138 | // SVG version 1.1
139 | //$document->xmlVersion = '1.1';
140 | //$svg_elem->setAttribute('version', '1.1');
141 |
142 | // Handle the SVG as an image
143 | $svg_elem->setAttribute('role', 'img');
144 |
145 | $xml = $document->saveXML($svg_elem);
146 | assert(is_string($xml), 'saveXML returns false on failure');
147 |
148 | wp_cache_set($cache_key, $xml, 'svg-contents', $ttl);
149 |
150 | return $xml;
151 | }
152 |
153 | /**
154 | * Prepare an SVG from assets directory for inline display.
155 | *
156 | * @param array $attrs
157 | */
158 | function get_inline_svg_asset(string $filename, array $attrs = [], int $ttl = 3600): string
159 | {
160 | return get_inline_svg(sprintf('%s/assets/img/%s', get_template_directory(), $filename), $attrs, $ttl);
161 | }
162 |
163 | /**
164 | * Prepare an SVG from Media for inline display.
165 | *
166 | * @link https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/SVG_In_HTML_Introduction
167 | *
168 | * @param array $attrs
169 | */
170 | function get_inline_svg_media(int $attachment_id, array $attrs = [], int $ttl = 3600): string
171 | {
172 | return get_inline_svg(get_attached_file($attachment_id), $attrs, $ttl);
173 | }
174 |
175 | /**
176 | * Prepare an SVG as an icon.
177 | */
178 | function get_inline_svg_icon(string $filename, string $class_string = 'icon'): string
179 | {
180 | return get_inline_svg_asset(
181 | sprintf('icon/%s', $filename),
182 | [
183 | 'class' => $class_string,
184 | 'width' => '24',
185 | 'height' => '24',
186 | ]
187 | );
188 | }
189 |
--------------------------------------------------------------------------------
/src/Html/Element.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress\Html;
14 |
15 | use Traversable;
16 |
17 | use function esc_attr;
18 | use function esc_url;
19 | use function sanitize_key;
20 |
21 | /**
22 | * Create an HTML element with attributes.
23 | */
24 | class Element
25 | {
26 | public const SPACE = ' ';
27 |
28 | public const LESS_THAN_SIGN = '<';
29 |
30 | public const GREATER_THAN_SIGN = '>';
31 |
32 | public const SOLIDUS = '/';
33 |
34 | public const EQUALS_SIGN = '=';
35 |
36 | public const QUOTATION_MARK = '"';
37 |
38 | /** @see https://html.spec.whatwg.org/multipage/syntax.html#void-elements */
39 | public const VOIDS = [
40 | 'area', 'base', 'br', 'col', 'embed', 'hr', 'img',
41 | 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr',
42 | ];
43 |
44 | protected string $tagName;
45 |
46 | /** @var array */
47 | protected array $attributes;
48 |
49 | protected string $content;
50 |
51 | protected bool $isVoid;
52 |
53 | /**
54 | * Create an HTML element with pure PHP.
55 | *
56 | * @param array $attributes
57 | * @param string|\Traversable $content Raw HTML content.
58 | * @throws \Exception
59 | */
60 | public function __construct(string $tagName = 'div', array $attributes = [], $content = '')
61 | {
62 | $this->tagName = sanitize_key($tagName);
63 | $this->attributes = $attributes;
64 | $this->content = $content instanceof Traversable
65 | ? \implode(\iterator_to_array($content))
66 | : $content;
67 | $this->isVoid = \in_array($this->tagName, self::VOIDS, true);
68 |
69 | if ($this->isVoid && $this->content !== '') {
70 | throw new \Exception('Void HTML element with content.');
71 | }
72 | }
73 |
74 | /**
75 | * @param array{tag: string, attrs: array} $skeleton
76 | * @throws \Exception
77 | */
78 | public static function fromSkeleton(array $skeleton, string $content = ''): self
79 | {
80 | // @phpstan-ignore isset.offset,isset.offset
81 | if (! isset($skeleton['tag'], $skeleton['attrs'])) {
82 | throw new \Exception('Skeleton array needs tag and attrs elements.');
83 | }
84 |
85 | return new self($skeleton['tag'], $skeleton['attrs'], $content);
86 | }
87 |
88 | public function setContent(string $content): void
89 | {
90 | $this->content = $content;
91 | }
92 |
93 | public function getContent(): string
94 | {
95 | return $this->content;
96 | }
97 |
98 | /**
99 | * @param array $attributes
100 | */
101 | public function setAttributes(array $attributes): void
102 | {
103 | $this->attributes = $attributes;
104 | }
105 |
106 | /**
107 | * @return array
108 | */
109 | public function getAttributes(): array
110 | {
111 | return $this->attributes;
112 | }
113 |
114 | public function setAttribute(string $attributeName, string $attributeValue): void
115 | {
116 | $this->attributes[$attributeName] = $attributeValue;
117 | }
118 |
119 | public function setBooleanWhen(string $attribute, bool $condition): void
120 | {
121 | if (! $condition) {
122 | return;
123 | }
124 |
125 | $this->attributes[$attribute] = null;
126 | }
127 |
128 | public function render(): string
129 | {
130 | $attributeString = $this->getAttributeString();
131 |
132 | if ($attributeString !== '') {
133 | $attributeString = sprintf('%s%s', self::SPACE, $attributeString);
134 | }
135 |
136 | // Element.
137 | if ($this->isVoid) {
138 | return \implode([
139 | self::LESS_THAN_SIGN,
140 | $this->tagName,
141 | $attributeString,
142 | self::GREATER_THAN_SIGN,
143 | ]);
144 | }
145 |
146 | return \implode([
147 | self::LESS_THAN_SIGN,
148 | $this->tagName,
149 | $attributeString,
150 | self::GREATER_THAN_SIGN,
151 | $this->content,
152 | self::LESS_THAN_SIGN,
153 | self::SOLIDUS,
154 | $this->tagName,
155 | self::GREATER_THAN_SIGN,
156 | ]);
157 | }
158 |
159 | /**
160 | * Create an HTML attribute string from an array.
161 | */
162 | protected function getAttributeString(): string
163 | {
164 | $attributeStrings = [];
165 | foreach ($this->attributes as $attributeName => $attributeValue) {
166 | $attributeName = \preg_replace('/[^a-z0-9-]/', '', \strtolower($attributeName));
167 |
168 | // Boolean attributes.
169 | if ($attributeValue === null) {
170 | $attributeStrings[] = $attributeName;
171 | continue;
172 | }
173 |
174 | $attributeStrings[] = \implode([
175 | $attributeName,
176 | self::EQUALS_SIGN,
177 | self::QUOTATION_MARK,
178 | \in_array($attributeName, ['href', 'src'], true)
179 | ? esc_url($attributeValue)
180 | : esc_attr($attributeValue),
181 | self::QUOTATION_MARK,
182 | ]);
183 | }
184 |
185 | return \implode(self::SPACE, $attributeStrings);
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/Script.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use function add_filter;
16 | use function remove_filter;
17 | use function sanitize_title;
18 | use function wp_dequeue_script;
19 | use function wp_deregister_script;
20 | use function wp_enqueue_script;
21 | use function wp_parse_url;
22 | use function wp_register_script;
23 |
24 | /**
25 | * Handle a JavaScript resource.
26 | */
27 | class Script
28 | {
29 | /** @var string */
30 | protected $handle;
31 |
32 | /** @var string|false */
33 | protected $src;
34 |
35 | /** @var list */
36 | protected $deps;
37 |
38 | /** @var string|false|null */
39 | protected $ver;
40 |
41 | /** @var bool */
42 | protected $inFooter;
43 |
44 | /** @var bool */
45 | protected $registered;
46 |
47 | /** @var array */
48 | protected $attributes;
49 |
50 | /**
51 | * @param string $url Full URL of the script.
52 | */
53 | public function __construct(string $url)
54 | {
55 | $this->handle = sanitize_title(pathinfo((string)wp_parse_url($url, PHP_URL_PATH), PATHINFO_FILENAME));
56 | $this->src = $url;
57 | $this->deps = [];
58 | $this->ver = null;
59 | $this->inFooter = false;
60 | $this->registered = false;
61 | $this->attributes = [];
62 | }
63 |
64 | public static function aliasOf(string $handle): self
65 | {
66 | $script = new self($handle);
67 | $script->src = false;
68 |
69 | return $script;
70 | }
71 |
72 | public function setHandle(string $handle): self
73 | {
74 | $this->handle = $handle;
75 |
76 | return $this;
77 | }
78 |
79 | /**
80 | * @param list $deps
81 | */
82 | public function setDeps(array $deps): self
83 | {
84 | $this->deps = $deps;
85 |
86 | return $this;
87 | }
88 |
89 | public function setVer(string $ver): self
90 | {
91 | $this->ver = $ver;
92 |
93 | return $this;
94 | }
95 |
96 | public function setCoreVersion(): self
97 | {
98 | $this->ver = false;
99 |
100 | return $this;
101 | }
102 |
103 | public function removeVer(): self
104 | {
105 | $this->ver = null;
106 |
107 | return $this;
108 | }
109 |
110 | public function moveToFooter(): self
111 | {
112 | $this->inFooter = true;
113 |
114 | return $this;
115 | }
116 |
117 | public function register(): void
118 | {
119 | if ($this->registered) {
120 | return;
121 | }
122 |
123 | wp_register_script($this->handle, $this->src, $this->deps, $this->ver, $this->inFooter);
124 | $this->registered = true;
125 | }
126 |
127 | public function deregister(): void
128 | {
129 | if (! $this->registered) {
130 | return;
131 | }
132 |
133 | wp_deregister_script($this->handle);
134 | $this->registered = false;
135 | }
136 |
137 | public function enqueue(): void
138 | {
139 | if (! $this->registered) {
140 | $this->register();
141 | }
142 | if ($this->attributes !== []) {
143 | add_filter('script_loader_tag', [$this, 'modifyScriptElement'], 10, 2);
144 | }
145 | // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
146 | // @TODO if (! did_action('wp_enqueue_scripts')) doing_filter??? -> Exception
147 | wp_enqueue_script($this->handle);
148 | }
149 |
150 | public function dequeue(): void
151 | {
152 | if (! $this->registered) {
153 | return;
154 | }
155 |
156 | if ($this->attributes !== []) {
157 | remove_filter('script_loader_tag', [$this, 'modifyScriptElement'], 10);
158 | }
159 | wp_dequeue_script($this->handle);
160 | }
161 |
162 | public function modifyScriptElement(string $html, string $currentHandle): string
163 | {
164 | if ($currentHandle !== $this->handle) {
165 | return $html;
166 | }
167 |
168 | $attributes = array_reduce(
169 | $this->attributes,
170 | static function (array $attributes, string $attribute) use ($html): array {
171 | // Skip already present attributes
172 | if (preg_match(sprintf('#\s%s[\s>]#', preg_quote($attribute, '#')), $html)) {
173 | return $attributes;
174 | }
175 |
176 | $attributes[] = $attribute;
177 |
178 | return $attributes;
179 | },
180 | []
181 | );
182 |
183 | if ($attributes === []) {
184 | return $html;
185 | }
186 |
187 | return str_replace(' src=', sprintf(' %s src=', implode(' ', $attributes)), $html);
188 | }
189 |
190 | /**
191 | * @param non-empty-string $attributeString
192 | */
193 | public function addAttribute(string $attributeString): self
194 | {
195 | $this->attributes[] = $attributeString;
196 |
197 | return $this;
198 | }
199 |
200 | public function loadAsync(): self
201 | {
202 | return $this->addAttribute('async');
203 | }
204 |
205 | public function executeDefer(): self
206 | {
207 | return $this->addAttribute('defer');
208 | }
209 |
210 | public function executeAsModule(): self
211 | {
212 | return $this->addAttribute('type="module"');
213 | }
214 |
215 | public function executeNomodule(): self
216 | {
217 | return $this->addAttribute('nomodule');
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/HookProxy.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use Closure;
16 | use ReflectionClass;
17 | use ReflectionMethod;
18 |
19 | use function _wp_filter_build_unique_id;
20 | use function add_filter;
21 | use function remove_filter;
22 |
23 | /**
24 | * Implement lazy hooking.
25 | */
26 | trait HookProxy
27 | {
28 | use HookAnnotation;
29 |
30 | /** @var array */
31 | protected $callablesAdded;
32 |
33 | protected function lazyHookFunction(
34 | string $actionTag,
35 | callable $callback,
36 | int $priority,
37 | int $argumentCount,
38 | string $filePath
39 | ): void {
40 | add_filter(
41 | $actionTag,
42 | $this->generateClosureWithFileLoad($actionTag, $callback, $filePath),
43 | $priority,
44 | $argumentCount
45 | );
46 | }
47 |
48 | protected function lazyHookStaticMethod(
49 | string $actionTag,
50 | callable $callback,
51 | int $priority,
52 | int $argumentCount
53 | ): void {
54 | add_filter(
55 | $actionTag,
56 | $this->generateClosure($actionTag, $callback),
57 | $priority,
58 | $argumentCount
59 | );
60 | }
61 |
62 | protected function lazyHookMethod(
63 | string $actionTag,
64 | callable $callback,
65 | int $priority,
66 | int $argumentCount,
67 | ?callable $injector = null
68 | ): void {
69 | add_filter(
70 | $actionTag,
71 | $this->generateClosureWithInjector($actionTag, $callback, $injector),
72 | $priority,
73 | $argumentCount
74 | );
75 | }
76 |
77 | /**
78 | * This is not really lazy hooking as class must be loaded to use reflections.
79 | *
80 | * @param class-string $className
81 | */
82 | protected function lazyHookAllMethods(
83 | string $className,
84 | int $defaultPriority = 10,
85 | ?callable $injector = null
86 | ): void {
87 | $classReflection = new ReflectionClass($className);
88 | // Look for hook tag in all public methods.
89 | foreach ($classReflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
90 | // Do not hook constructor.
91 | if ($method->isConstructor()) {
92 | continue;
93 | }
94 | $hookDetails = $this->getMetadata((string)$method->getDocComment(), $defaultPriority);
95 | if ($hookDetails === null) {
96 | continue;
97 | }
98 |
99 | add_filter(
100 | $hookDetails['tag'],
101 | $this->generateClosureWithInjector($hookDetails['tag'], [$className, $method->name], $injector),
102 | $hookDetails['priority'],
103 | $method->getNumberOfParameters()
104 | );
105 | }
106 | }
107 |
108 | protected function unhook(
109 | string $actionTag,
110 | callable $callback,
111 | int $priority
112 | ): void {
113 | $id = $this->buildUniqueId($actionTag, $callback);
114 | if (! array_key_exists($id, $this->callablesAdded)) {
115 | return;
116 | }
117 |
118 | remove_filter(
119 | $actionTag,
120 | $this->callablesAdded[$id],
121 | $priority
122 | );
123 | unset($this->callablesAdded[$id]);
124 | }
125 |
126 | // phpcs:disable NeutronStandard.Functions.TypeHint.NoReturnType
127 |
128 | protected function generateClosure(string $actionTag, callable $callback): Closure
129 | {
130 | $id = $this->buildUniqueId($actionTag, $callback);
131 | $this->callablesAdded[$id] = static function (...$args) use ($callback) {
132 | return call_user_func_array($callback, $args);
133 | };
134 |
135 | return $this->callablesAdded[$id];
136 | }
137 |
138 | protected function generateClosureWithFileLoad(string $actionTag, callable $callback, string $filePath): Closure
139 | {
140 | $id = $this->buildUniqueId($actionTag, $callback);
141 | $this->callablesAdded[$id] = static function (...$args) use ($filePath, $callback) {
142 | require_once $filePath;
143 |
144 | return call_user_func_array($callback, $args);
145 | };
146 |
147 | return $this->callablesAdded[$id];
148 | }
149 |
150 | protected function generateClosureWithInjector(string $actionTag, callable $callback, ?callable $injector): Closure
151 | {
152 | if (! is_array($callback)) {
153 | throw new \InvalidArgumentException(
154 | sprintf('Callable is not an array: %s', var_export($callback, true))
155 | );
156 | }
157 |
158 | $id = $this->buildUniqueId($actionTag, $callback);
159 | $this->callablesAdded[$id] = $injector === null
160 | ? static function (...$args) use ($callback) {
161 | return call_user_func_array($callback, $args);
162 | }
163 | : static function (...$args) use ($injector, $callback) {
164 | $instance = call_user_func($injector, $callback[0]);
165 |
166 | return call_user_func_array([$instance, $callback[1]], $args);
167 | };
168 |
169 | return $this->callablesAdded[$id];
170 | }
171 |
172 | protected function buildUniqueId(string $actionTag, callable $callback): string
173 | {
174 | return sprintf('%s/%s', $actionTag, _wp_filter_build_unique_id('', $callback, 0));
175 | }
176 | }
177 | // @TODO Measurements: w/o OPcache, OPcache with file read, OPcache without file read
178 | // @TODO Add tests, remove_action, usage as filter with returned value,
179 | // one callable hooked to many action tags then removed
180 |
--------------------------------------------------------------------------------
/src/HookProxyListed.php:
--------------------------------------------------------------------------------
1 |
7 | * @license https://opensource.org/licenses/MIT MIT
8 | * @link https://github.com/szepeviktor/SentencePress
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace SzepeViktor\SentencePress;
14 |
15 | use Closure;
16 | use ReflectionClass;
17 | use ReflectionMethod;
18 |
19 | use function _wp_filter_build_unique_id;
20 | use function add_filter;
21 | use function current_filter;
22 | use function remove_filter;
23 |
24 | /**
25 | * Implement lazy hooking.
26 | * @phpstan-ignore trait.unused
27 | */
28 | trait HookProxyListed
29 | {
30 | use HookAnnotation;
31 |
32 | /** @var array> */
33 | protected $callablesAdded;
34 |
35 | /** @var list */
36 | protected $currentCallables;
37 |
38 | /**
39 | * @return mixed
40 | *
41 | * phpcs:disable NeutronStandard.Functions.TypeHint.NoReturnType
42 | */
43 | public function receiver(...$args)
44 | {
45 | // @FIXME Multiple (priority) hooking of receiver is not possible!
46 | $this->currentCallables = $this->callablesAdded[current_filter()];
47 |
48 | return call_user_func_array('TODO', $args);
49 | }
50 |
51 | protected function lazyHookFunction(
52 | string $actionTag,
53 | callable $callback,
54 | int $priority,
55 | int $argumentCount,
56 | string $filePath
57 | ): void {
58 | $this->callablesAdded[$actionTag][$priority] = [
59 | 'callback' => $callback,
60 | 'argumentCount' => $argumentCount,
61 | 'filePath' => $filePath,
62 | ];
63 | add_filter(
64 | $actionTag,
65 | [$this, 'receiver'],
66 | $priority,
67 | $argumentCount
68 | );
69 | }
70 |
71 | protected function lazyHookStaticMethod(
72 | string $actionTag,
73 | callable $callback,
74 | int $priority,
75 | int $argumentCount
76 | ): void {
77 | add_filter(
78 | $actionTag,
79 | $this->generateClosure($actionTag, $callback),
80 | $priority,
81 | $argumentCount
82 | );
83 | }
84 |
85 | protected function lazyHookMethod(
86 | string $actionTag,
87 | callable $callback,
88 | int $priority,
89 | int $argumentCount,
90 | ?callable $injector = null
91 | ): void {
92 | add_filter(
93 | $actionTag,
94 | $this->generateClosureWithInjector($actionTag, $callback, $injector),
95 | $priority,
96 | $argumentCount
97 | );
98 | }
99 |
100 | /**
101 | * This is not really lazy hooking as class must be loaded to use reflections.
102 | *
103 | * @param class-string $className
104 | */
105 | protected function lazyHookAllMethods(
106 | string $className,
107 | int $defaultPriority = 10,
108 | ?callable $injector = null
109 | ): void {
110 | $classReflection = new ReflectionClass($className);
111 | // Look for hook tag in all public methods.
112 | foreach ($classReflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
113 | // Do not hook constructor.
114 | if ($method->isConstructor()) {
115 | continue;
116 | }
117 | $hookDetails = $this->getMetadata((string)$method->getDocComment(), $defaultPriority);
118 | if ($hookDetails === null) {
119 | continue;
120 | }
121 |
122 | add_filter(
123 | $hookDetails['tag'],
124 | $this->generateClosureWithInjector($hookDetails['tag'], [$className, $method->name], $injector),
125 | $hookDetails['priority'],
126 | $method->getNumberOfParameters()
127 | );
128 | }
129 | }
130 |
131 | protected function unhook(
132 | string $actionTag,
133 | callable $callback,
134 | int $priority
135 | ): void {
136 | $id = $this->buildUniqueId($actionTag, $callback);
137 | if (! array_key_exists($id, $this->callablesAdded)) {
138 | return;
139 | }
140 |
141 | remove_filter(
142 | $actionTag,
143 | $this->callablesAdded[$id],
144 | $priority
145 | );
146 | unset($this->callablesAdded[$id]);
147 | }
148 |
149 | // phpcs:disable NeutronStandard.Functions.TypeHint.NoReturnType
150 |
151 | protected function generateClosure(string $actionTag, callable $callback): Closure
152 | {
153 | $id = $this->buildUniqueId($actionTag, $callback);
154 | $this->callablesAdded[$id] = static function (...$args) use ($callback) {
155 | return call_user_func_array($callback, $args);
156 | };
157 |
158 | return $this->callablesAdded[$id];
159 | }
160 |
161 | protected function generateClosureWithFileLoad(string $actionTag, callable $callback, string $filePath): Closure
162 | {
163 | $id = $this->buildUniqueId($actionTag, $callback);
164 | $this->callablesAdded[$id] = static function (...$args) use ($filePath, $callback) {
165 | require_once $filePath;
166 |
167 | return call_user_func_array($callback, $args);
168 | };
169 |
170 | return $this->callablesAdded[$id];
171 | }
172 |
173 | protected function generateClosureWithInjector(string $actionTag, callable $callback, ?callable $injector): Closure
174 | {
175 | if (! is_array($callback)) {
176 | throw new \InvalidArgumentException(
177 | sprintf('Callable is not an array: %s', var_export($callback, true))
178 | );
179 | }
180 |
181 | $id = $this->buildUniqueId($actionTag, $callback);
182 | $this->callablesAdded[$id] = $injector === null
183 | ? static function (...$args) use ($callback) {
184 | return call_user_func_array($callback, $args);
185 | }
186 | : static function (...$args) use ($injector, $callback) {
187 | $instance = call_user_func($injector, $callback[0]);
188 |
189 | return call_user_func_array([$instance, $callback[1]], $args);
190 | };
191 |
192 | return $this->callablesAdded[$id];
193 | }
194 |
195 | protected function buildUniqueId(callable $callback): string
196 | {
197 | return _wp_filter_build_unique_id('', $callback, 0);
198 | }
199 | }
200 | // TODO Measurements: w/o OPcache, OPcache with file read, OPcache without file read
201 | // TODO Add tests, remove_action, usage as filter with returned value,
202 | // one callable hooked to many action tags then removed
203 |
--------------------------------------------------------------------------------