Add PSR HTTP Message Interfaces and Dependencies

- Implemented StreamInterface, UploadedFileInterface, and UriInterface as per PSR standards.
- Added getallheaders function to retrieve HTTP headers in a compatible manner.
- Included LICENSE files for ralouphie/getallheaders and symfony/deprecation-contracts.
- Introduced function for triggering deprecation notices in Symfony.
This commit is contained in:
2025-12-28 12:44:00 +01:00
parent cf600ae727
commit cd264483f8
410 changed files with 60841 additions and 16 deletions

View File

@@ -0,0 +1,49 @@
# Contributing to WP OOP Plugin Lib
Thank you for your interest in contributing to this library! At this point, it is still in an early development stage, but especially because of that feedback is much appreciated!
Just two general guidelines:
* All contributors are expected to follow the [WordPress Code of Conduct](https://make.wordpress.org/handbook/community-code-of-conduct/).
* All contributors who submit a pull request are agreeing to release their contribution under the [GPLv2+ license](https://github.com/felixarntz/wp-oop-plugin-lib/blob/main/LICENSE).
## Providing feedback
If you're already using the library in a WordPress plugin, or you've started experimenting with it, you may run into limitations, or you simply may have questions on how a certain part of the infrastructure is supposed to work. You may run into a bug, or you may think about another WordPress API that you would like to see covered by this library. In any case, please let me know by [opening an issue](https://github.com/felixarntz/wp-oop-plugin-lib/issues/new/choose)!
## Sharing your use-cases
I'd love to learn how this library is being used by the WordPress ecosystem! Not only out of pure curiosity, but also because it allows me to improve it to potentially cater for those use-cases better. If you're already using the library in an open-source project, please share a link to the repository on [this issue](https://github.com/felixarntz/wp-oop-plugin-lib/issues/1)!
## Contributing code
Pull requests are welcome! For little fixes, feel free to go right ahead and open one. For new features or larger enhancements, I'd encourage you to open an issue first where we can scope and discuss the change. Though of course in any case feel free to jump right into writing code! You can do so by [forking this repository](https://github.com/felixarntz/wp-oop-plugin-lib/fork) and later opening a pull request with your changes.
### Guidelines for contributing code
If you're interested in contributing code, please consider the following guidelines and best practices:
* All code must follow the [WordPress Coding Standards and best practices](https://developer.wordpress.org/coding-standards/), including documentation. They are enforced via the project's PHP_CodeSniffer configuration.
* All code must be backward-compatible with WordPress 6.0 and PHP 7.2.
* All code must pass the automated PHP code quality requirements via the project's PHPMD and PHPStan configuration.
* All functional code changes should be accompanied by PHPUnit tests.
### Getting started with writing code
For the linting tools to work, you'll need to have [Composer](https://getcomposer.org/) installed on your machine. In order to make use of the built-in development environment including the ability to run the PHPUnit tests, you'll also need [Docker](https://www.docker.com/) and [Node.js](https://nodejs.org/).
The following linting commands are available:
* `composer lint`: Checks the code with [PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer/).
* `composer format`: Automatically fixes code problems detected by PHPCodeSniffer, where possible.
* `composer phpmd`: Checks the code with [PHPMD](https://github.com/phpmd/phpmd).
* `composer phpstan`: Checks the code with [PHPStan](https://github.com/phpstan/phpstan).
The following commands allow running PHPUnit tests using the built-in environment:
* `npm run test-php`: Runs the PHPUnit tests for a regular (single) WordPress site.
* `npm run test-php-multisite`: Runs the PHPUnit tests for a WordPress multisite.
The project comes with a demo WordPress plugin file `wp-oop-plugin-lib.php`, which does nothing but autoload the library's classes. If you need to quickly test some WordPress or library code, feel free to temporarily modify the file in your development environment. You can access the built-in environment with that demo plugin active using [wp-env](https://www.npmjs.com/package/@wordpress/env):
* `npm run wp-env start`: Starts the environment (typically available at `http://localhost:8888/`).
* `npm run wp-env stop`: Stops the environment.

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,49 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Abstract_Capability
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Contracts\Capability;
/**
* Base class representing a WordPress capability.
*
* @since 0.1.0
*/
abstract class Abstract_Capability implements Capability {
/**
* Capability key.
*
* @since 0.1.0
* @var string
*/
private $key;
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $key Capability key.
*/
public function __construct( string $key ) {
$this->key = $key;
}
/**
* Gets the capability key / slug.
*
* @since 0.1.0
*
* @return string Capability key.
*/
public function get_key(): string {
return $this->key;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Base_Capability
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities;
/**
* Class representing a WordPress base capability.
*
* A base capability can be granted to a user role, or based on other base capabilities.
*
* @since 0.1.0
*/
class Base_Capability extends Abstract_Capability {
/**
* Required base capabilities needed to grant this capability.
*
* If empty list, it means this base capability must be granted directly on individual user roles.
*
* @since 0.1.0
* @var string[]
*/
private $required_caps;
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $key Capability key.
* @param string[] $required_caps Optional. Required base capabilities needed to grant this capability. An empty
* array means this base capability must be granted directly on individual user
* roles. Default empty array.
*/
public function __construct( string $key, array $required_caps = array() ) {
parent::__construct( $key );
$this->required_caps = $required_caps;
}
/**
* Sets the required base capabilities from the user's role(s) to grant this base capability dynamically.
*
* While WordPress's built-in capabilities are stored in the database on their relevant roles, this approach is
* usually sub optimal for plugins and can lead to out of sync data. Instead, granting custom capabilities
* depending on one or more base capabilities controls custom capabilities programmatically.
*
* If the capability should instead be granted on a specific user role, an empty array can be provided to skip
* granting the capability based on any built-in capabilities.
*
* @since 0.1.0
*
* @param string[] $required_caps Required base capabilities needed to grant this capability. An empty array means
* this base capability must be granted directly on individual user roles. Default
* empty array.
*/
public function set_required_caps( array $required_caps ): void {
$this->required_caps = $required_caps;
}
/**
* Gets the required base capabilities from the user's role(s) to grant this capability.
*
* An empty array means the base capability should be granted directly on individual user roles.
*
* @since 0.1.0
*
* @return string[] Required capabilities needed to grant this capability, or empty array to indicate that this
* base capability must be granted directly on individual user roles.
*/
public function get_required_caps(): array {
return $this->required_caps;
}
}

View File

@@ -0,0 +1,202 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Container
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities;
use ArrayAccess;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Contracts\Capability;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Invalid_Type_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception;
/**
* Class for a capability container.
*
* @since 0.1.0
*/
class Capability_Container implements Container, ArrayAccess {
/**
* Capabilities stored in the container.
*
* @since 0.1.0
* @var array<string, callable>
*/
private $capabilities = array();
/**
* Capability instances already created.
*
* @since 0.1.0
* @var array<string, Capability>
*/
private $instances = array();
/**
* Checks if a capability for the given key exists in the container.
*
* @since 0.1.0
*
* @param string $key Capability key.
* @return bool True if the capability exists in the container, false otherwise.
*/
public function has( string $key ): bool {
return isset( $this->capabilities[ $key ] );
}
/**
* Gets the capability for the given key from the container.
*
* @since 0.1.0
*
* @param string $key Capability key.
* @return Capability Capability for the given key.
*
* @throws Not_Found_Exception Thrown when option with given key is not found.
* @throws Invalid_Type_Exception Thrown when option with given key has invalid type.
*/
public function get( string $key ) {
if ( ! isset( $this->capabilities[ $key ] ) ) {
throw new Not_Found_Exception(
esc_html(
sprintf(
/* translators: %s: capability key */
__( 'Capability with key %s was not found in container', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
if ( ! isset( $this->instances[ $key ] ) ) {
$instance = $this->capabilities[ $key ]( $this );
if ( ! $instance instanceof Capability ) {
throw new Invalid_Type_Exception(
esc_html(
sprintf(
/* translators: %s: capability key */
__( 'Capability with key %s is not of type Capability', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
$this->instances[ $key ] = $instance;
}
return $this->instances[ $key ];
}
/**
* Sets the given capability under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Capability key.
* @param callable $creator Capability creator closure.
*/
public function set( string $key, callable $creator ): void {
$this->capabilities[ $key ] = $creator;
unset( $this->instances[ $key ] );
}
/**
* Sets a capability using the given required capabilities under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Capability key.
* @param string[]|callable $required_caps Array with required base capabilities if this is a base capability,
* or callback function to dynamically determine the required base
* capabilities if this is a meta capability.
*/
public function set_by_args( string $key, $required_caps ): void {
$this->set(
$key,
function () use ( $key, $required_caps ) {
if ( is_callable( $required_caps ) ) {
return new Meta_Capability( $key, $required_caps );
}
return new Base_Capability( $key, $required_caps );
}
);
}
/**
* Unsets the capability under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Capability key.
*/
public function unset( string $key ): void {
unset( $this->capabilities[ $key ], $this->instances[ $key ] );
}
/**
* Gets all keys in the container.
*
* @since 0.1.0
*
* @return string[] List of keys.
*/
public function get_keys(): array {
return array_keys( $this->capabilities );
}
/**
* Checks if a capability for the given key exists in the container.
*
* @since 0.1.0
*
* @param mixed $key Capability key.
* @return bool True if the capability exists in the container, false otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetExists( $key ) {
return $this->has( $key );
}
/**
* Gets the capability for the given key from the container.
*
* @since 0.1.0
*
* @param mixed $key Capability key.
* @return Capability Capability for the given key.
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return $this->get( $key );
}
/**
* Sets the given capability under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Capability key.
* @param mixed $value Capability creator closure.
*/
#[\ReturnTypeWillChange]
public function offsetSet( $key, $value ) {
$this->set( $key, $value );
}
/**
* Unsets the capability under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Capability key.
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $key ) {
$this->unset( $key );
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Controller
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception;
/**
* Class for controlling how to grant a specific set of capabilities.
*
* This is useful for allowing to customize how capabilities are granted, e.g. by triggering a WordPress action hook
* and passing an instance of the class to it.
*
* @since 0.1.0
*/
class Capability_Controller {
/**
* Capability container.
*
* @since 0.1.0
* @var Capability_Container
*/
private $container;
/**
* Constructor.
*
* @since 0.1.0
*
* @param Capability_Container $container Container with the capabilities that this controller instance should be
* able to control.
*/
public function __construct( Capability_Container $container ) {
$this->container = $container;
}
/**
* Grants the given capability dynamically depending on required base capabilities from the user's role(s).
*
* While WordPress's built-in capabilities are stored in the database on their relevant roles, this approach is
* usually sub optimal for plugins and can lead to out of sync data. Instead, granting custom capabilities
* depending on one or more base capabilities controls custom capabilities programmatically.
*
* If the capability should instead be granted on a specific user role, an empty array can be provided to skip
* granting the capability based on any built-in capabilities.
*
* @since 0.1.0
*
* @param string $cap Base capability to grant. Must be part of the plugin capabilities in this
* controller.
* @param string[] $required_caps Required capabilities needed to grant this capability. An empty array means this
* is a base capability and must be granted directly on individual user roles.
* Default empty array.
*
* @throws Not_Found_Exception Thrown when the capability is not found in the container or is not a base capability.
*/
public function grant_cap_for_base_caps( string $cap, array $required_caps ): void {
$capability = $this->container->get( $cap );
if ( ! $capability instanceof Base_Capability ) {
throw new Not_Found_Exception(
sprintf(
/* translators: %s: capability key */
esc_html__( 'Capability %s must be a base capability to grant it based on other capabilities.', 'wp-oop-plugin-lib' ), // phpcs:ignore Generic.Files.LineLength.TooLong
esc_html( $cap )
)
);
}
$capability->set_required_caps( $required_caps );
}
/**
* Grants the given meta capability dynamically depending on a callback function.
*
* @since 0.1.0
*
* @param string $cap Meta capability to grant. Must be part of the plugin capabilities in this
* controller.
* @param callable $map_callback Callback function to determine the required base capabilities needed to grant this
* meta capability. The function receives the user ID and any additional parameters
* passed alongside the capability check and must return an array.
*
* @throws Not_Found_Exception Thrown when the capability is not found in the container or is not a meta capability.
*/
public function set_meta_map_callback( string $cap, callable $map_callback ): void {
$capability = $this->container->get( $cap );
if ( ! $capability instanceof Meta_Capability ) {
throw new Not_Found_Exception(
sprintf(
/* translators: %s: capability key */
esc_html__( 'Capability %s must be a meta capability to set a map callback.', 'wp-oop-plugin-lib' ), // phpcs:ignore Generic.Files.LineLength.TooLong
esc_html( $cap )
)
);
}
$capability->set_map_callback( $map_callback );
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Filters
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Hooks;
/**
* Class that adds filters to dynamically grant base capabilities and meta capabilities.
*
* @since 0.1.0
*/
class Capability_Filters implements With_Hooks {
/**
* Capability container.
*
* @since 0.1.0
* @var Capability_Container
*/
private $container;
/**
* Constructor.
*
* @since 0.1.0
*
* @param Capability_Container $container Container with the capabilities that filters should be added for.
*/
public function __construct( Capability_Container $container ) {
$this->container = $container;
}
/**
* Adds relevant WordPress hooks.
*
* @since 0.1.0
*/
public function add_hooks(): void {
add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), 10, 2 );
add_filter( 'map_meta_cap', array( $this, 'filter_map_meta_cap' ), 10, 4 );
}
/**
* Gets the map of dynamic capabilities and which base capabilities they should map to.
*
* @since 0.1.0
*
* @return array<string, string[]> Map of `$cap => $required_caps` pairs.
*/
public function get_required_base_caps_map(): array {
$keys = $this->container->get_keys();
$caps_map = array();
foreach ( $keys as $key ) {
$capability = $this->container->get( $key );
if ( ! $capability instanceof Base_Capability ) {
continue;
}
$required_caps = $capability->get_required_caps();
if ( ! $required_caps ) { // Skip capabilities that aren't dynamic.
continue;
}
$caps_map[ $key ] = $required_caps;
}
return $caps_map;
}
/**
* Gets the map of meta capabilities and their map callbacks.
*
* @since 0.1.0
*
* @return array<string, callable> Map of `$cap => $map_callback` pairs.
*/
public function get_meta_map_callbacks_map(): array {
$keys = $this->container->get_keys();
$callbacks_map = array();
foreach ( $keys as $key ) {
$capability = $this->container->get( $key );
if ( ! $capability instanceof Meta_Capability ) {
continue;
}
$callbacks_map[ $key ] = $capability->get_map_callback();
}
return $callbacks_map;
}
/**
* Filters a user's capabilities, granting dynamic capabilities based on existing base capabilities.
*
* This should be used as a callback for the {@see 'user_has_cap'} filter.
*
* @since 0.1.0
*
* @param array<string, bool> $allcaps Array of key/value pairs where keys represent a capability name and boolean
* values represent whether the user has that capability.
* @param string[] $caps Required primitive capabilities for the requested capability.
* @return array<string, bool> Filtered $allcaps, including dynamically granted custom capabilities.
*/
public function filter_user_has_cap( array $allcaps, array $caps ): array {
// Bail early if not checking for any of the relevant capabilities.
$relevant_caps = array_filter(
$caps,
function ( string $cap ) {
return $this->container->has( $cap );
}
);
if ( count( $relevant_caps ) === 0 ) {
return $allcaps;
}
$required_base_caps_map = $this->get_required_base_caps_map();
foreach ( $required_base_caps_map as $cap => $required_caps ) {
$grant = true;
foreach ( $required_caps as $required_cap ) {
if ( ! isset( $allcaps[ $required_cap ] ) || ! $allcaps[ $required_cap ] ) {
$grant = false;
break;
}
}
$allcaps[ $cap ] = $grant;
}
return $allcaps;
}
/**
* Filters the mapping of a meta capability to one or more base capabilities.
*
* This should be used as a callback for the {@see 'map_meta_cap'} filter.
*
* @since 0.1.0
*
* @param string[] $caps Primitive capabilities required of the user.
* @param string $cap Capability being checked.
* @param int $user_id User ID.
* @param mixed[] $args Additional arguments passed alongside the capability check.
* @return string[] Filtered $caps, potentially altered by the relevant map callback.
*/
public function filter_map_meta_cap( array $caps, string $cap, int $user_id, array $args ): array {
// Bail early if not checking for any of the relevant capabilities.
if ( ! $this->container->has( $cap ) ) {
return $caps;
}
$meta_map_callbacks_map = $this->get_meta_map_callbacks_map();
if ( ! isset( $meta_map_callbacks_map[ $cap ] ) ) {
return $caps;
}
$map_callback = $meta_map_callbacks_map[ $cap ];
$required_caps = $map_callback( $user_id, ...$args );
if ( ! is_array( $required_caps ) || ! $required_caps ) { // Prevent invalid return values.
return $caps;
}
return $required_caps;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Contracts\Capability
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Contracts;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Key;
/**
* Interface for a WordPress capability.
*
* @since 0.1.0
*/
interface Capability extends With_Key {
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Meta_Capability
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities;
/**
* Class representing a WordPress meta capability.
*
* A meta capability is a capability that is mapped to one or more base capabilities based on dynamic logic.
*
* @since 0.1.0
*/
class Meta_Capability extends Abstract_Capability {
/**
* Callback function to determine the required base capabilities needed to grant this meta capability.
*
* @since 0.1.0
* @var callable
*/
private $map_callback;
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $key Capability key.
* @param ?callable $map_callback Optional. Callback function to determine the required base capabilities needed to
* grant this meta capability. function receives the user ID and any additional
* parameters passed alongside the capability check and must return an array.
* Default null.
*/
public function __construct( string $key, ?callable $map_callback = null ) {
parent::__construct( $key );
$this->map_callback = $map_callback ?? function () {
return array();
};
}
/**
* Sets the callback function to determine the required base capabilities needed to grant this meta capability.
*
* @since 0.1.0
*
* @param callable $map_callback Callback function to determine the required base capabilities needed to grant this
* meta capability. The function receives the user ID and any additional parameters
* passed alongside the capability check and must return an array.
*/
public function set_map_callback( callable $map_callback ): void {
$this->map_callback = $map_callback;
}
/**
* Gets the callback function to determine the required base capabilities needed to grant this meta capability.
*
* @since 0.1.0
*
* @return callable Callback function to determine the required base capabilities needed to grant this meta
* capability. The function receives the user ID and any additional parameters passed alongside
* the capability check and must return an array.
*/
public function get_map_callback(): callable {
return $this->map_callback;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Array_Key_Value_Repository
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
/**
* Class for a repository that stores keys and values in an array.
*
* @since 0.1.0
*/
class Array_Key_Value_Repository implements Key_Value_Repository {
/**
* The items in the repository.
*
* @since 0.1.0
* @var array<string, mixed>
*/
private $items = array();
/**
* Constructor.
*
* @since 0.1.0
*
* @param array<string, mixed> $initial_items Optional. Initial keys and values. Default empty array.
*/
public function __construct( array $initial_items = array() ) {
$this->items = $initial_items;
}
/**
* Checks whether a value for the given key exists in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool True if a value for the key exists, false otherwise.
*/
public function exists( string $key ): bool {
return isset( $this->items[ $key ] );
}
/**
* Gets the value for a given key from the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param mixed $default Optional. Value to return if no value exists for the key. Default null.
* @return mixed Value for the key, or the default if no value exists.
*/
public function get( string $key, $default = null ) {
return isset( $this->items[ $key ] ) ? $this->items[ $key ] : $default;
}
/**
* Updates the value for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param mixed $value New value to set for the key.
* @return bool True on success, false on failure.
*/
public function update( string $key, $value ): bool {
$this->items[ $key ] = $value;
return true;
}
/**
* Deletes the data for a given key from the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool True on success, false on failure.
*/
public function delete( string $key ): bool {
if ( ! isset( $this->items[ $key ] ) ) {
return false;
}
unset( $this->items[ $key ] );
return true;
}
}

View File

@@ -0,0 +1,172 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Array_Registry
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
use ArrayAccess;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Registry;
/**
* Class for a registry that registers arguments on an associative array.
*
* This can be used to provide a consistent registration API around data structures which in WordPress can only be
* added via filters.
*
* @since 0.1.0
*/
class Array_Registry implements Registry, Arrayable, ArrayAccess {
/**
* The registered items.
*
* @since 0.1.0
* @var array<string, object>
*/
private $items = array();
/**
* Constructor.
*
* @since 0.1.0
*
* @param array<string, array<string, mixed>> $initial_items Optional. Initial array of registered items. Default
* empty array.
*/
public function __construct( array $initial_items = array() ) {
$this->items = array_map(
static function ( $item ) {
return (object) $item;
},
$initial_items
);
}
/**
* Registers an item with the given key and arguments.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param array<string, mixed> $args Item registration arguments.
* @return bool True on success, false on failure.
*/
public function register( string $key, array $args ): bool {
$this->items[ $key ] = (object) $args;
return true;
}
/**
* Checks whether an item with the given key is registered.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool True if the item is registered, false otherwise.
*/
public function is_registered( string $key ): bool {
return isset( $this->items[ $key ] );
}
/**
* Gets the registered item for the given key from the registry.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return object|null The registered item definition, or `null` if not registered.
*/
public function get_registered( string $key ) {
if ( ! isset( $this->items[ $key ] ) ) {
return null;
}
return $this->items[ $key ];
}
/**
* Gets all items from the registry.
*
* @since 0.1.0
*
* @return array<string, object> Associative array of keys and their item definitions, or empty array if nothing is
* registered. This is effectively the array representation of the registry.
*/
public function get_all_registered(): array {
return $this->items;
}
/**
* Returns the array representation of the registry.
*
* @since 0.1.0
*
* @return array<string, mixed> Array representation.
*/
public function to_array(): array {
return array_map(
static function ( $item ) {
if ( $item instanceof Arrayable ) {
return $item->to_array();
}
return (array) $item;
},
$this->items
);
}
/**
* Checks if an item for the given key is registered.
*
* @since 0.1.0
*
* @param mixed $key Item key.
* @return bool True if the item is registered, false otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetExists( $key ) {
return $this->is_registered( $key );
}
/**
* Gets the item for the given key from the container.
*
* @since 0.1.0
*
* @param mixed $key Item key.
* @return object|null The registered item definition, or `null` if not registered.
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return $this->get_registered( $key );
}
/**
* Registers an item with the given key and arguments.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param array<string, mixed> $args Item registration arguments.
*/
#[\ReturnTypeWillChange]
public function offsetSet( $key, $args ) {
$this->register( $key, $args );
}
/**
* Magic unset method. Does nothing at this time, as , registries do not allow unregistering items.
*
* @since 0.1.0
*
* @param mixed $key Item key.
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $key ) {
// Empty method body.
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for something that can return its array representation.
*
* @since 0.1.0
*/
interface Arrayable {
/**
* Returns the array representation.
*
* @since 0.1.0
*
* @return mixed[] Array representation.
*/
public function to_array(): array;
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Collection
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* Interface for a collection.
*
* @since 0.1.0
*/
interface Collection extends IteratorAggregate, Countable {
/**
* Returns an iterator for the collection.
*
* @since 0.1.0
*
* @return Traversable Collection iterator.
*/
public function getIterator(): Traversable; /* @phpstan-ignore-line */
/**
* Returns the size of the collection.
*
* @since 0.1.0
*
* @return int Collection size.
*/
public function count(): int;
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for a container with read and write access.
*
* @since 0.1.0
*/
interface Container extends Container_Readonly {
/**
* Sets the given entry under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Key.
* @param callable $creator Entry creator closure.
*/
public function set( string $key, callable $creator ): void;
/**
* Unsets the entry under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Key.
*/
public function unset( string $key ): void;
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container_Readonly
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception;
/**
* Interface for a container with readonly access.
*
* @since 0.1.0
*/
interface Container_Readonly {
/**
* Checks if an entry for the given key exists in the container.
*
* @since 0.1.0
*
* @param string $key Key.
* @return bool True if the entry exists in the container, false otherwise.
*/
public function has( string $key ): bool;
/**
* Gets the entry for the given key from the container.
*
* @since 0.1.0
*
* @param string $key Key.
* @return mixed Entry for the given key.
*
* @throws Not_Found_Exception Thrown when entry with given key is not found.
*/
public function get( string $key );
/**
* Gets all keys in the container.
*
* @since 0.1.0
*
* @return string[] List of keys.
*/
public function get_keys(): array;
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Hook_Registrar
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for a class that adds the relevant hook to register items.
*
* @since 0.1.0
*/
interface Hook_Registrar {
/**
* Adds a callback that registers the items to the relevant hook.
*
* The callback receives a registry instance as the sole parameter, allowing to call the
* {@see Registry::register()} method.
*
* @since 0.1.0
*
* @param callable $register_callback Callback to register the items.
*/
public function add_register_callback( callable $register_callback ): void;
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for a key-value pair.
*
* @since 0.1.0
*/
interface Key_Value extends With_Key {
/**
* Checks whether the item has a value set.
*
* @since 0.1.0
*
* @return bool True if a value is set, false otherwise.
*/
public function has_value(): bool;
/**
* Gets the value for the item.
*
* @since 0.1.0
*
* @return mixed Value for the item.
*/
public function get_value();
/**
* Updates the value for the item.
*
* @since 0.1.0
*
* @param mixed $value New value to set for the item.
* @return bool True on success, false on failure.
*/
public function update_value( $value ): bool;
/**
* Deletes the data for the item.
*
* @since 0.1.0
*
* @return bool True on success, false on failure.
*/
public function delete_value(): bool;
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for a repository for key-value pairs.
*
* @since 0.1.0
*/
interface Key_Value_Repository {
/**
* Checks whether a value for the given key exists in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool True if a value for the key exists, false otherwise.
*/
public function exists( string $key ): bool;
/**
* Gets the value for a given key from the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param mixed $default Optional. Value to return if no value exists for the key. Default null.
* @return mixed Value for the key, or the default if no value exists.
*/
public function get( string $key, $default = null );
/**
* Updates the value for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param mixed $value New value to set for the key.
* @return bool True on success, false on failure.
*/
public function update( string $key, $value ): bool;
/**
* Deletes the data for a given key from the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool True on success, false on failure.
*/
public function delete( string $key ): bool;
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Registry
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for a registry of items.
*
* @since 0.1.0
*/
interface Registry {
/**
* Registers an item with the given key and arguments.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param array<string, mixed> $args Item registration arguments.
* @return bool True on success, false on failure.
*/
public function register( string $key, array $args ): bool;
/**
* Checks whether an item with the given key is registered.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool True if the item is registered, false otherwise.
*/
public function is_registered( string $key ): bool;
/**
* Gets the registered item for the given key from the registry.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return object|null The registered item definition, or `null` if not registered.
*/
public function get_registered( string $key );
/**
* Gets all items from the registry.
*
* @since 0.1.0
*
* @return array<string, mixed> Associative array of keys and their item definitions, or empty array if nothing is
* registered.
*/
public function get_all_registered(): array;
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Capabilities
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for a class that can check for capabilities.
*
* @since 0.1.0
*/
interface With_Capabilities {
/**
* Checks whether the entity has the given capability.
*
* @since 0.1.0
*
* @param string $cap Capability name.
* @param mixed ...$args Optional further parameters, typically starting with an entity ID.
* @return bool True if the entity has the given capability false otherwise.
*/
public function has_cap( string $cap, ...$args ): bool;
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Hooks
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for a class that includes WordPress hooks (actions and/or filters).
*
* @since 0.1.0
*/
interface With_Hooks {
/**
* Adds relevant WordPress hooks.
*
* @since 0.1.0
*/
public function add_hooks(): void;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Key
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for an item that has a key (a string identifier unique for the kind of item).
*
* @since 0.1.0
*/
interface With_Key {
/**
* Gets the key of the item.
*
* @since 0.1.0
*
* @return string Item key.
*/
public function get_key(): string;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Registration_Args
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts;
/**
* Interface for an item with registration arguments.
*
* @since 0.1.0
*/
interface With_Registration_Args {
/**
* Gets the registration arguments for the item.
*
* @since 0.1.0
*
* @return array<string, mixed> Item registration arguments.
*/
public function get_registration_args(): array;
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Capabilities;
/**
* Class representing the current user.
*
* @since 0.1.0
*/
class Current_User implements With_Capabilities {
/**
* Gets the current user ID.
*
* @since 0.1.0
*
* @return int The current user ID, or `0` if no user is signed in.
*/
public function get_id(): int {
return get_current_user_id();
}
/**
* Sets the current user to the one with the given ID.
*
* @since 0.1.0
*
* @param int $id User ID.
*/
public function set( int $id ): void {
wp_set_current_user( $id );
}
/**
* Checks whether current user is logged in.
*
* @since 0.1.0
*
* @return bool True if the user is logged in, false otherwise.
*/
public function is_logged_in(): bool {
return is_user_logged_in();
}
/**
* Checks whether the current user has the given capability.
*
* @since 0.1.0
*
* @param string $cap Capability name.
* @param mixed ...$args Optional further parameters, typically starting with an entity ID.
* @return bool True if the user has the given capability false otherwise.
*/
public function has_cap( string $cap, ...$args ): bool {
return wp_get_current_user()->has_cap( $cap, ...$args );
}
/**
* Creates a cryptographic token tied to the given action and the current user session.
*
* @since 0.1.0
*
* @param string $action Action to add context to the nonce.
* @return string The token.
*/
public function create_nonce( string $action ): string {
return wp_create_nonce( $action );
}
/**
* Verifies that the given security nonce is correct for the given action and the current user session.
*
* @since 0.1.0
*
* @param string $nonce Nonce value to verify.
* @param string $action Action context for the nonce.
* @return bool True if the nonce is valid, false otherwise.
*/
public function verify_nonce( string $nonce, string $action ): bool {
return (bool) wp_verify_nonce( $nonce, $action );
}
/**
* Checks whether the current user is a super admin.
*
* By default, super admins have access to all capabilities, unless explicitly denied to everyone.
*
* @since 0.1.0
*
* @return bool True if the user is a super admin, false otherwise.
*/
public function is_super_admin(): bool {
return is_super_admin();
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Invalid_Type_Exception
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception;
use RuntimeException;
/**
* Exception class for when a value has an invalid type.
*
* @since 0.1.0
*/
class Invalid_Type_Exception extends RuntimeException {
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception;
use RuntimeException;
/**
* Exception class for when a value for a key is not found.
*
* @since 0.1.0
*/
class Not_Found_Exception extends RuntimeException {
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\WP_Error_Exception
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception;
use RuntimeException;
use WP_Error;
/**
* Exception class equivalent to a WP_Error object.
*
* @since 0.1.0
*/
class WP_Error_Exception extends RuntimeException {
/**
* Text based error code.
*
* @since 0.1.0
* @var string
*/
private $error_code = '';
/**
* Gets the text based error code.
*
* @since 0.1.0
*
* @return string Text based error code.
*/
public function get_error_code(): string {
if ( ! $this->error_code ) {
// Fall back to using error message as error code.
return preg_replace(
'/[^a-z0-9_]+/',
'',
str_replace( array( ' ', '-' ), '_', strtolower( $this->getMessage() ) )
);
}
return $this->error_code;
}
/**
* Sets the text based error code.
*
* @since 0.1.0
*
* @param string $error_code Text based error code.
*/
public function set_error_code( string $error_code ): void {
$this->error_code = $error_code;
}
/**
* Creates a new WP_Error exception based on error code, and error message.
*
* @since 0.1.0
*
* @param string $error_code Text based error code.
* @param string $message Error message.
* @return WP_Error_Exception New exception instance.
*/
public static function create( string $error_code, string $message ): WP_Error_Exception {
$instance = new self( $message );
$instance->set_error_code( $error_code );
return $instance;
}
/**
* Creates a new WP_Error exception from the given WP_Error object.
*
* @since 0.1.0
*
* @param WP_Error $error WP_Error object.
* @return WP_Error_Exception New exception instance.
*/
public static function from_wp_error( WP_Error $error ): WP_Error_Exception {
return self::create( $error->get_error_code(), $error->get_error_message() );
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Generic_Key_Value
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
/**
* Class representing a generic key-value pair.
*
* Should typically not be used directly, but rather through a more specific class extending it.
*
* @since 0.1.0
*/
class Generic_Key_Value implements Key_Value {
/**
* Repository used for the item.
*
* @since 0.1.0
* @var Key_Value_Repository
*/
protected $repository;
/**
* Item key.
*
* @since 0.1.0
* @var string
*/
protected $key;
/**
* Item default value.
*
* @since 0.1.0
* @var mixed
*/
protected $default_value;
/**
* Constructor.
*
* @since 0.1.0
*
* @param Key_Value_Repository $repository Repository used for the item.
* @param string $key Item key.
* @param mixed $default_value Optional. Default value for the item if not set in the repository.
* If null, it will be ignored. Default null.
*/
public function __construct( Key_Value_Repository $repository, string $key, $default_value = null ) {
$this->repository = $repository;
$this->key = $key;
$this->default_value = $default_value;
}
/**
* Checks whether the item has a value set.
*
* @since 0.1.0
*
* @return bool True if a value is set, false otherwise.
*/
public function has_value(): bool {
return $this->repository->exists( $this->key );
}
/**
* Gets the value for the item.
*
* @since 0.1.0
*
* @return mixed Value for the item.
*/
public function get_value() {
// Pass default value if set.
if ( isset( $this->default_value ) ) {
return $this->repository->get( $this->key, $this->default_value );
}
return $this->repository->get( $this->key );
}
/**
* Updates the value for the item.
*
* @since 0.1.0
*
* @param mixed $value New value to set for the item.
* @return bool True on success, false on failure.
*/
public function update_value( $value ): bool {
return $this->repository->update( $this->key, $value );
}
/**
* Deletes the data for the item.
*
* @since 0.1.0
*
* @return bool True on success, false on failure.
*/
public function delete_value(): bool {
return $this->repository->delete( $this->key );
}
/**
* Gets the key of the item.
*
* @since 0.1.0
*
* @return string Item key.
*/
public function get_key(): string {
return $this->key;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Input
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
/**
* Read-only class for filtering input, effectively for access of immutable input data.
*
* @since 0.1.0
*/
class Input {
/**
* Map of input type to superglobal array.
*
* For use as fallback only.
*
* @since 0.1.0
* @var array<int, array<string, mixed>>
*/
private $fallback_map;
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
// Fallback map for environments where filter_input may not work with ENV or SERVER types.
$this->fallback_map = array(
INPUT_ENV => $_ENV,
INPUT_SERVER => $_SERVER, // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage
);
}
/**
* Gets a specific external variable by name and optionally filters it.
*
* @since 0.1.0
*
* @link https://php.net/manual/en/function.filter-input.php
*
* @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.
* @param string $variable_name Name of a variable to get.
* @param int $filter Optional. The ID of the filter to apply. The manual page lists the available
* filters.
* @param mixed $options Optional. Associative array of options or bitwise disjunction of flags. If filter
* accepts options, flags can be provided in "flags" field of array.
* @return mixed Value of the requested variable on success, false if the filter fails, null if the $variable_name
* variable is not set. If the flag FILTER_NULL_ON_FAILURE is used, it returns false if the variable
* is not set and null if the filter fails.
*/
public function filter( $type, $variable_name, $filter = FILTER_DEFAULT, $options = 0 ) {
/* @phpstan-ignore-next-line */
$value = filter_input( $type, $variable_name, $filter, $options );
/*
* Fallback for environments where filter_input may not work with specific types.
* This is only used for affected input types and if the value is not set.
*/
if (
isset( $this->fallback_map[ $type ] )
&& in_array( $value, array( null, false ), true )
&& array_key_exists( $variable_name, $this->fallback_map[ $type ] )
) {
return filter_var( $this->fallback_map[ $type ][ $variable_name ], $filter, $options );
}
return $value;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Mutable_Input
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
/**
* Read-only class for filtering mutable input, effectively for superglobal access.
*
* It is recommended to use the regular Input class for most input handling, while this class is useful for e.g. unit
* tests where you want to mock input values.
*
* @since 0.1.0
*/
class Mutable_Input extends Input {
/**
* Gets a specific external variable by name and optionally filters it.
*
* @since 0.1.0
*
* @link https://php.net/manual/en/function.filter-input.php
*
* @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.
* @param string $variable_name Name of a variable to get.
* @param int $filter Optional. The ID of the filter to apply. The manual page lists the available
* filters.
* @param mixed $options Optional. Associative array of options or bitwise disjunction of flags. If filter
* accepts options, flags can be provided in "flags" field of array.
* @return mixed Value of the requested variable on success, false if the filter fails, null if the $variable_name
* variable is not set. If the flag FILTER_NULL_ON_FAILURE is used, it returns false if the variable
* is not set and null if the filter fails.
*/
public function filter( $type, $variable_name, $filter = FILTER_DEFAULT, $options = 0 ) {
switch ( $type ) {
case INPUT_GET:
// phpcs:ignore WordPress.Security.NonceVerification
$superglobal = $_GET;
break;
case INPUT_POST:
// phpcs:ignore WordPress.Security.NonceVerification
$superglobal = $_POST;
break;
case INPUT_SERVER:
$superglobal = $_SERVER;
break;
case INPUT_COOKIE:
$superglobal = $_COOKIE;
break;
case INPUT_ENV:
$superglobal = $_ENV;
break;
default:
return null;
}
if ( ! isset( $superglobal[ $variable_name ] ) ) {
return null;
}
return filter_var( $superglobal[ $variable_name ], $filter, $options );
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Network_Env
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
/**
* Read-only class containing utilities for the network environment.
*
* @since 0.1.0
*/
class Network_Env {
/**
* Checks whether this WordPress installation is a multisite installation.
*
* @since 0.1.0
*
* @return bool True if a multisite installation, false otherwise.
*/
public function is_multisite(): bool {
return is_multisite();
}
/**
* Returns the network ID.
*
* @since 0.1.0
*
* @return int The network ID.
*/
public function id(): int {
return get_current_network_id();
}
/**
* Returns the network URL, i.e. relative to the home page.
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string The site URL.
*/
public function url( string $relative_path = '/' ): string {
$url = network_home_url( $relative_path );
/*
* In Multisite, network_home_url() returns a URL with a trailing slash, even if the path is empty.
* This is inconsistent with home_url(), so we fix that here.
*/
if ( '' === $relative_path ) {
$url = untrailingslashit( $url );
}
return $url;
}
/**
* Returns the network's WordPress URL, in which WordPress core is installed.
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string The WordPress URL.
*/
public function wp_url( string $relative_path = '/' ): string {
$url = network_site_url( $relative_path );
/*
* In Multisite, network_site_url() returns a URL with a trailing slash, even if the path is empty.
* This is inconsistent with site_url(), so we fix that here.
*/
if ( '' === $relative_path ) {
$url = untrailingslashit( $url );
}
return $url;
}
/**
* Returns the network admin URL (typically the 'wp-admin/network' directory within the WordPress URL).
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string The admin URL.
*/
public function admin_url( string $relative_path = '/' ): string {
return network_admin_url( $relative_path );
}
/**
* Returns the active plugins for the network.
*
* @since 0.1.0
*
* @return string[] List of plugin basenames, relative to the plugins directory.
*/
public function get_active_plugins(): array {
if ( ! $this->is_multisite() ) {
return array();
}
$active_plugins = (array) get_site_option( 'active_sitewide_plugins', array() );
$active_plugins = array_keys( $active_plugins );
sort( $active_plugins );
return $active_plugins;
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Network_Runner
*
* @since n.e.x.t
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
/**
* Class for running logic across the network.
*
* @since n.e.x.t
*/
class Network_Runner {
/**
* Network environment.
*
* @since n.e.x.t
* @var Network_Env
*/
private $network_env;
/**
* Constructor.
*
* @since n.e.x.t
*
* @param Network_Env $network_env Network environment.
*/
public function __construct( Network_Env $network_env ) {
$this->network_env = $network_env;
}
/**
* Runs a given callback for all sites in the network or for sites matching certain criteria.
*
* If not in a Multisite environment, the callback will simply be invoked once for the current site.
*
* @since n.e.x.t
*
* @param callable $callback Callback function to run for each site. It must be parameter-less
* and return a boolean for whether it completed successfully or not.
* @param array<string, mixed> $site_query_args Optional. Additional arguments for querying sites.
* @return bool True if the callback successfully ran for all relevant sites, false otherwise.
*/
public function run_for_sites( callable $callback, array $site_query_args = array() ): bool {
if ( ! $this->network_env->is_multisite() ) {
return (bool) call_user_func( $callback );
}
$site_query_args = wp_parse_args(
$site_query_args,
array( 'number' => 100 )
);
$site_query_args['fields'] = 'ids';
$site_ids = get_sites( $site_query_args );
// Iterate through the site and store for which ones the callback ran successfully.
$success_ids = array();
foreach ( $site_ids as $site_id ) {
switch_to_blog( $site_id );
if ( call_user_func( $callback ) ) {
$success_ids[] = $site_id;
}
restore_current_blog();
}
return count( $site_ids ) === count( $success_ids );
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Plugin_Env
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
/**
* Read-only class containing utilities for the plugin environment.
*
* @since 0.1.0
*/
class Plugin_Env {
/**
* Absolute path of the plugin main file.
*
* @since 0.1.0
* @var string
*/
private $main_file;
/**
* Current plugin version number.
*
* @since 0.1.0
* @var string
*/
private $version;
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $main_file Absolute path to the plugin main file.
* @param string $version Current plugin version number.
*/
public function __construct( string $main_file, string $version ) {
$this->main_file = $main_file;
$this->version = $version;
}
/**
* Returns the absolute path to the plugin main file.
*
* @since 0.1.0
*
* @return string Absolute path to the plugin main file.
*/
public function main_file(): string {
return $this->main_file;
}
/**
* Returns the current plugin version number.
*
* @since 0.1.0
*
* @return string Current plugin version number.
*/
public function version(): string {
return $this->version;
}
/**
* Returns the plugin basename.
*
* @since 0.1.0
*
* @return string Plugin basename.
*/
public function basename(): string {
return plugin_basename( $this->main_file );
}
/**
* Returns the absolute path for a relative path to the plugin directory.
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string Absolute path.
*/
public function path( string $relative_path = '/' ): string {
return plugin_dir_path( $this->main_file ) . ltrim( $relative_path, '/' );
}
/**
* Returns the full URL for a path relative to the plugin directory.
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string Full URL.
*/
public function url( string $relative_path = '/' ): string {
return plugin_dir_url( $this->main_file ) . ltrim( $relative_path, '/' );
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Service_Container
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
use ArrayAccess;
use Closure;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception;
/**
* Class for a service container.
*
* @since 0.1.0
*/
class Service_Container implements Container, ArrayAccess {
/**
* Services stored in the container.
*
* @since 0.1.0
* @var array<string, callable>
*/
private $services = array();
/**
* Service instances already created.
*
* @since 0.1.0
* @var array<string, object>
*/
private $instances = array();
/**
* Listener callbacks.
*
* These callbacks are attached to a specific service and called whenever that service is resolved.
*
* @since 0.1.0
* @var array<string, callable[]>
*/
private $listener_callbacks = array();
/**
* Checks if a service for the given key exists in the container.
*
* @since 0.1.0
*
* @param string $key Service key.
* @return bool True if the service exists in the container, false otherwise.
*/
public function has( string $key ): bool {
return $this->is_bound( $key );
}
/**
* Gets the service for the given key from the container.
*
* @since 0.1.0
*
* @param string $key Service key.
* @return object Service for the given key.
*
* @throws Not_Found_Exception Thrown when service with given key is not found.
*/
public function get( string $key ) {
if ( ! isset( $this->services[ $key ] ) ) {
throw new Not_Found_Exception(
esc_html(
sprintf(
/* translators: %s: service key */
__( 'Service with key %s was not found in container', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
return $this->resolve( $key );
}
/**
* Sets the given service under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Service key.
* @param callable $creator Service creator closure.
*/
public function set( string $key, callable $creator ): void {
$this->bind( $key, $creator );
}
/**
* Unsets the service under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Service key.
*/
public function unset( string $key ): void {
unset( $this->services[ $key ], $this->instances[ $key ] );
}
/**
* Gets all keys in the container.
*
* @since 0.1.0
*
* @return string[] List of keys.
*/
public function get_keys(): array {
return array_keys( $this->services );
}
/**
* Adds a listener callback for a service in the container.
*
* The callback will be called whenever that service is resolved, whether it is for the initial resolve, or after a
* subsequent change. The callback will receive the latest service instance and a reference to the container as
* parameters.
*
* @since 0.1.0
*
* @param string $key Service key.
* @param Closure $callback Listener callback.
*/
public function listen( string $key, Closure $callback ): void {
if ( ! isset( $this->listener_callbacks[ $key ] ) ) {
$this->listener_callbacks[ $key ] = array();
}
$this->listener_callbacks[ $key ][] = $callback;
}
/**
* Checks if a service for the given key exists in the container.
*
* @since 0.1.0
*
* @param string $key Service key.
* @return bool True if the service exists in the container, false otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetExists( $key ) {
return $this->has( $key );
}
/**
* Gets the service for the given key from the container.
*
* @since 0.1.0
*
* @param string $key Service key.
* @return mixed Service for the given key.
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return $this->get( $key );
}
/**
* Sets the given service under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Service key.
* @param mixed $value Service creator closure.
*/
#[\ReturnTypeWillChange]
public function offsetSet( $key, $value ) {
$this->set( $key, $value );
}
/**
* Unsets the service under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Service key.
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $key ) {
$this->unset( $key );
}
/**
* Binds the given service creator closure under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Service key.
* @param Closure $service_creator Service creator closure.
*/
private function bind( string $key, Closure $service_creator ): void {
$this->services[ $key ] = $service_creator;
// If the service was already resolved, update it.
if ( $this->is_resolved( $key ) ) {
$this->drop_stale_instances( $key );
// If there are listeners attached, resolve immediately so that dependents do not get stale.
// Otherwise, it only needs to be resolved once explicitly retrieved.
if ( isset( $this->listener_callbacks[ $key ] ) ) {
$this->resolve( $key );
}
}
}
/**
* Resolves the service for the given key and returns the resolved instance.
*
* If the service was already resolved, it will simply return the existing instance.
*
* @since 0.1.0
*
* @param string $key Service key.
* @return object Service resolved for the given key.
*/
private function resolve( string $key ) {
if ( isset( $this->instances[ $key ] ) ) {
return $this->instances[ $key ];
}
$this->instances[ $key ] = $this->services[ $key ]( $this );
$this->fire_listener_callbacks( $key );
return $this->instances[ $key ];
}
/**
* Checks whether the service under the given key has been bound.
*
* @since 0.1.0
*
* @param string $key Service key.
* @return bool True if the service has been bound, false otherwise.
*/
private function is_bound( string $key ): bool {
return isset( $this->services[ $key ] ) || isset( $this->instances[ $key ] );
}
/**
* Checks whether the service under the given key has already been resolved.
*
* @since 0.1.0
*
* @param string $key Service key.
* @return bool True if the service has been resolved, false otherwise.
*/
private function is_resolved( string $key ): bool {
return isset( $this->instances[ $key ] );
}
/**
* Drops the instances under the given key.
*
* This method can be used to force the {@see Service_Container::resolve()} method to re-resolve.
*
* @since 0.1.0
*
* @param string $key Service key.
*/
private function drop_stale_instances( string $key ): void {
unset( $this->instances[ $key ] );
}
/**
* Fires all listener callbacks under the given key, if any are set.
*
* @since 0.1.0
*
* @param string $key Service key.
*/
private function fire_listener_callbacks( string $key ): void {
if ( ! isset( $this->listener_callbacks[ $key ] ) ) {
return;
}
foreach ( $this->listener_callbacks[ $key ] as $callback ) {
$callback( $this->instances[ $key ], $this );
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Site_Env
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General;
/**
* Read-only class containing utilities for the site environment.
*
* @since 0.1.0
*/
class Site_Env {
/**
* Returns the site ID.
*
* @since 0.1.0
*
* @return int The site ID.
*/
public function id(): int {
return get_current_blog_id();
}
/**
* Returns information about the site.
*
* @since 0.1.0
* @see get_bloginfo()
*
* @param string $field The site field to retrieve.
* @return string The site field value.
*/
public function info( string $field ): string {
return (string) get_bloginfo( $field );
}
/**
* Returns the site URL, i.e. relative to the home page.
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string The site URL.
*/
public function url( string $relative_path = '/' ): string {
return home_url( $relative_path );
}
/**
* Returns the site's WordPress URL, in which WordPress core is installed.
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string The WordPress URL.
*/
public function wp_url( string $relative_path = '/' ): string {
return site_url( $relative_path );
}
/**
* Returns the site admin URL (typically the 'wp-admin' directory within the WordPress URL).
*
* @since 0.1.0
*
* @param string $relative_path Optional. Relative path. Default '/'.
* @return string The admin URL.
*/
public function admin_url( string $relative_path = '/' ): string {
return admin_url( $relative_path );
}
/**
* Returns the active plugins for the site.
*
* Does not include network-activated plugins (relevant for multisite installations).
*
* @since 0.1.0
*
* @return string[] List of plugin basenames, relative to the plugins directory.
*/
public function get_active_plugins(): array {
$active_plugins = (array) get_option( 'active_plugins', array() );
$network_env = new Network_Env();
if ( ! $network_env->is_multisite() ) {
return $active_plugins;
}
return array_values( array_diff( $active_plugins, $network_env->get_active_plugins() ) );
}
/**
* Returns the active themes for the site.
*
* This is either just the active theme, or the active theme and the child theme if a child theme is active.
*
* @since 0.1.0
*
* @return string[] List of theme directories, relative to the themes directory.
*/
public function get_active_themes(): array {
$parent_theme = get_template();
$child_theme = get_stylesheet();
$themes = array();
if ( $child_theme !== $parent_theme ) {
$themes[] = $child_theme;
}
$themes[] = $parent_theme;
return $themes;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Trait Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Traits\Cast_Value_By_Type
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Traits;
/**
* Trait with a function to cast a value by a type.
*
* @since 0.1.0
*/
trait Cast_Value_By_Type {
/**
* Casts the given value into the given type identifier.
*
* @since 0.1.0
*
* @param mixed $value Value to cast.
* @param string $type Type identifier. Supported values are 'bool', 'int', 'float', 'string', 'array', and
* 'object'.
* @return mixed The cast value.
*/
protected function cast_value_by_type( $value, string $type ) {
switch ( $type ) {
case 'bool':
case 'boolean':
return (bool) $value;
case 'int':
case 'integer':
return (int) $value;
case 'double':
case 'float':
return (float) $value;
case 'string':
return (string) $value;
case 'array':
case 'object': // Objects are handled as associative arrays in WordPress.
if ( ! $value ) {
return array();
}
return (array) $value;
}
return $value;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Trait Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Traits\Maybe_Throw
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Traits;
use Exception;
/**
* Trait with a function to cast a value by a type.
*
* @since 0.1.0
*/
trait Maybe_Throw {
/**
* Calls a callback function that may throw exceptions, and catches exceptions unless `WP_DEBUG` is enabled.
*
* @since 0.1.0
*
* @param callable $callback Callback function.
* @param mixed[] $callback_args Parameters to pass to the callback function.
* @return bool True on success, or false if an exception was caught.
*
* @throws Exception Thrown based on the underlying callback, and only if `WP_DEBUG` is enabled.
*/
protected function maybe_throw( callable $callback, array $callback_args ): bool {
if ( WP_DEBUG ) {
call_user_func_array( $callback, $callback_args );
return true;
}
try {
call_user_func_array( $callback, $callback_args );
} catch ( Exception $e ) {
return false;
}
return true;
}
/**
* Calls a callback function that may return a `WP_Error`, and throws an exception only if `WP_DEBUG` is enabled.
*
* @since 0.1.0
*
* @param callable $callback Callback function.
* @param mixed[] $callback_args Parameters to pass to the callback function.
* @return bool True on success, or false if a `WP_Error` was returned.
*
* @throws Exception Thrown based on the underlying callback, and only if `WP_DEBUG` is enabled.
*/
protected function maybe_throw_wp_error( callable $callback, array $callback_args ): bool {
$result = call_user_func_array( $callback, $callback_args );
if ( is_wp_error( $result ) ) {
if ( WP_DEBUG ) {
throw new Exception( esc_html( $result->get_error_message() ) );
}
return false;
}
return true;
}
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts;
/**
* Interface for an HTTP request to another URL.
*
* @since 0.1.0
*/
interface Request {
const GET = 'GET';
const POST = 'POST';
const PUT = 'PUT';
const PATCH = 'PATCH';
const DELETE = 'DELETE';
const HEAD = 'HEAD';
const OPTIONS = 'OPTIONS';
const TRACE = 'TRACE';
/**
* Retrieves the URL to which the request should be sent.
*
* @since 0.1.0
*
* @return string The request URL.
*/
public function get_url(): string;
/**
* Retrieves the HTTP method to be used for the request.
*
* @since 0.1.0
*
* @return string The HTTP method for the request.
*/
public function get_method(): string;
/**
* Retrieves the data to be sent with the request.
*
* @since 0.1.0
*
* @return array<string, mixed> The request data, or an empty array. If the request method is not GET or HEAD, in
* case of an empty array the request body should be used instead.
*/
public function get_data(): array;
/**
* Retrieves the body to be sent with the request.
*
* A request may have either data or a body, but not both.
*
* @since 0.1.0
*
* @return string The request body, or an empty string. Only relevant if the request method is not GET or HEAD. In
* case of an empty string, the request data should be used instead.
*/
public function get_body(): string;
/**
* Retrieves the headers to be sent with the request.
*
* @since 0.1.0
*
* @return array<string, string> The request headers.
*/
public function get_headers(): array;
/**
* Retrieves the options to be used for sending the request.
*
* @since 0.1.0
*
* @return array<string, mixed> The request options.
*/
public function get_options(): array;
/**
* Adds a header to the request.
*
* @since 0.1.0
*
* @param string $name The header name.
* @param string $value The header value.
*/
public function add_header( string $name, string $value ): void;
/**
* Adds data to the request.
*
* @since 0.1.0
*
* @param string $name The name under which to send the data.
* @param mixed $value The value to send.
*/
public function add_data( string $name, $value ): void;
/**
* Adds an option to the request.
*
* @since 0.1.0
*
* @param string $name The option name.
* @param mixed $value The option value.
*/
public function add_option( string $name, $value ): void;
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler
*
* @since n.e.x.t
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Multiple_Requests_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
use InvalidArgumentException;
/**
* Interface for an HTTP request to another URL.
*
* @since n.e.x.t
*/
interface Request_Handler {
/**
* Sends an HTTP request and returns the response.
*
* @since n.e.x.t
*
* @param Request $request The request to send.
* @return Response The response received.
*
* @throws Request_Exception Thrown if the request fails.
*/
public function request( Request $request ): Response;
/**
* Sends multiple HTTP requests and returns the responses.
*
* The returned responses are in the same order / use the same keys as the requests.
*
* If any of the requests fail, a Multiple_Requests_Exception will be thrown. The exception will contain the
* responses of the requests that succeeded, and the exceptions of the requests that failed.
*
* @since n.e.x.t
*
* @param array<string|int, Request> $requests The requests to send.
* @return array<string|int, Response> The responses received.
*
* @throws Multiple_Requests_Exception Thrown if one or more requests fail. If any requests succeeded, their
* responses will be included in the exception.
* @throws InvalidArgumentException Thrown if an invalid request is provided.
*/
public function request_multiple( array $requests ): array;
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts;
/**
* Interface for an HTTP response from another URL.
*
* @since 0.1.0
*/
interface Response {
/**
* Retrieves the HTTP status code received with the response.
*
* @since 0.1.0
*
* @return int The 3-digit HTTP status code.
*/
public function get_status(): int;
/**
* Retrieves the data received with the response.
*
* @since 0.1.0
*
* @return array<string, mixed> The response data, or an empty array if it could not automatically be decoded. In
* this case, the raw response body should be used.
*/
public function get_data(): array;
/**
* Retrieves the body received with the response.
*
* @since 0.1.0
*
* @return string The raw response body. If response data could be automatically decoded, this should be empty, and
* the response data should be used instead.
*/
public function get_body(): string;
/**
* Retrieves the headers received with the response.
*
* @since 0.1.0
*
* @return array<string, string> The response headers.
*/
public function get_headers(): array;
/**
* Retrieves a specific header received with the response.
*
* @since 0.1.0
*
* @param string $name The name of the header to retrieve.
* @return string The value of the header, or an empty string if the header was not found.
*/
public function get_header( string $name ): string;
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Delete_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP DELETE request to another URL.
*
* @since 0.1.0
*/
class Delete_Request extends Generic_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::DELETE;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Multiple_Requests_Exception
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response;
/**
* Exception class for when one or more HTTP requests of multiple requests sent together fail.
*
* @since 0.1.0
*/
class Multiple_Requests_Exception extends Request_Exception {
/**
* The exceptions for the requests that failed.
*
* @since 0.1.0
* @var array<string|int, Request_Exception>
*/
private $request_exceptions;
/**
* The responses for the requests that succeeded.
*
* @since 0.1.0
* @var array<string|int, Response>
*/
private $successful_responses;
/**
* Constructor.
*
* @since 0.1.0
*
* @param array<string|int, Request_Exception> $request_exceptions The exceptions for the requests that failed.
* @param array<string|int, Response> $successful_responses Optional. The responses for the requests that
* succeeded. Default empty array.
*/
public function __construct( array $request_exceptions, array $successful_responses = array() ) {
if ( ! $successful_responses ) {
$message = __( 'All requests failed.', 'wp-oop-plugin-lib' );
} elseif ( count( $request_exceptions ) === 1 ) {
$message = sprintf(
/* translators: %d: overall number of requests */
__( 'One out of %d requests failed.', 'wp-oop-plugin-lib' ),
count( $request_exceptions ) + count( $successful_responses )
);
} else {
$message = sprintf(
/* translators: 1: number of failed requests, 2: overall number of requests */
__( '%1$d out of %2$d requests failed.', 'wp-oop-plugin-lib' ),
count( $request_exceptions ),
count( $request_exceptions ) + count( $successful_responses )
);
}
parent::__construct( esc_html( $message ) );
$this->request_exceptions = $request_exceptions;
$this->successful_responses = $successful_responses;
}
/**
* Checks whether any of the requests succeeded.
*
* @since 0.1.0
*
* @return bool True if any of the requests succeeded, false otherwise.
*/
public function has_successful_responses(): bool {
return ! empty( $this->successful_responses );
}
/**
* Checks whether a specific request failed.
*
* @since 0.1.0
*
* @param string|int $key The key of the request.
* @return bool True if the request failed, false otherwise.
*/
public function has_failed( $key ): bool {
return isset( $this->request_exceptions[ $key ] );
}
/**
* Checks whether a specific request succeeded.
*
* @since 0.1.0
*
* @param string|int $key The key of the request.
* @return bool True if the request succeeded, false otherwise.
*/
public function has_succeeded( $key ): bool {
return isset( $this->successful_responses[ $key ] );
}
/**
* Retrieves the exception for a specific request that failed.
*
* Before calling this method, you should check whether the request failed using the has_failed() method.
*
* @since 0.1.0
*
* @param string|int $key The key of the request.
* @return Request_Exception The exception for the request that failed.
*/
public function get_exception( $key ): Request_Exception {
return $this->request_exceptions[ $key ];
}
/**
* Retrieves the response for a specific request that succeeded.
*
* Before calling this method, you should check whether the request succeeded using the has_succeeded() method.
*
* @since 0.1.0
*
* @param string|int $key The key of the request.
* @return Response The response for the request that succeeded.
*/
public function get_response( $key ): Response {
return $this->successful_responses[ $key ];
}
/**
* Retrieves the exceptions for the requests that failed.
*
* @since 0.1.0
*
* @return array<string|int, Request_Exception> The exceptions for the requests that failed.
*/
public function get_individual_exceptions(): array {
return $this->request_exceptions;
}
/**
* Retrieves the responses for the requests that succeeded.
*
* @since 0.1.0
*
* @return array<string|int, Response> The responses for the requests that succeeded.
*/
public function get_successful_responses(): array {
return $this->successful_responses;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception;
use RuntimeException;
/**
* Exception class for when an HTTP request fails.
*
* @since 0.1.0
*/
class Request_Exception extends RuntimeException {
}

View File

@@ -0,0 +1,299 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Generic_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Traits\Sanitize_Headers;
/**
* Class for a generic HTTP request to another URL.
*
* @since 0.1.0
*/
class Generic_Request implements Request {
use Sanitize_Headers;
/**
* The URL to which the request should be sent.
*
* @since 0.1.0
* @var string
*/
private $url;
/**
* The HTTP method to be used for the request.
*
* @since 0.1.0
* @var string
*/
private $method;
/**
* The data to be sent with the request.
*
* This is alternative to the body property, and should be used if the data will be sent as form data (e.g. for a
* POST request) or as query parameters (e.g. for a GET request).
*
* @since 0.1.0
* @var array<string, mixed>
*/
private $data;
/**
* The body to be sent with the request.
*
* This is alternative to the data property and should only be used if the data is not suitable for form data or
* query parameters.
*
* @since 0.1.0
* @var string
*/
private $body;
/**
* The headers to be sent with the request.
*
* @since 0.1.0
* @var array<string, string>
*/
private $headers;
/**
* Additional options for the request.
*
* @since 0.1.0
* @var array<string, mixed>
*/
private $options;
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
if ( $data && isset( $args['body'] ) && $args['body'] ) {
_doing_it_wrong(
__METHOD__,
// phpcs:ignore Generic.Files.LineLength.TooLong
esc_html__( 'Both a request data array and a request body were provided, but only one of them is allowed.', 'wp-oop-plugin-lib' ),
''
);
unset( $args['body'] );
}
$args = $this->sanitize_args( $args, __METHOD__ );
$this->url = $url;
$this->method = $args['method'] ?? Request::GET;
$this->data = $data;
$this->body = $args['body'] ?? '';
$this->headers = isset( $args['headers'] ) ? $this->sanitize_headers( $args['headers'] ) : array();
unset( $args['method'], $args['body'], $args['headers'] );
$this->options = $args;
}
/**
* Retrieves the URL to which the request should be sent.
*
* @since 0.1.0
*
* @return string The request URL.
*/
public function get_url(): string {
return $this->url;
}
/**
* Retrieves the HTTP method to be used for the request.
*
* @since 0.1.0
*
* @return string The HTTP method for the request.
*/
public function get_method(): string {
return $this->method;
}
/**
* Retrieves the data to be sent with the request.
*
* @since 0.1.0
*
* @return array<string, mixed> The request data, or an empty array. If the request method is not GET or HEAD, in
* case of an empty array the request body should be used instead.
*/
public function get_data(): array {
return $this->data;
}
/**
* Retrieves the body to be sent with the request.
*
* A request may have either data or a body, but not both.
*
* @since 0.1.0
*
* @return string The request body, or an empty string. Only relevant if the request method is not GET or HEAD. In
* case of an empty string, the request data should be used instead.
*/
public function get_body(): string {
return $this->body;
}
/**
* Retrieves the headers to be sent with the request.
*
* @since 0.1.0
*
* @return array<string, string> The request headers.
*/
public function get_headers(): array {
return $this->headers;
}
/**
* Retrieves the options to be used for sending the request.
*
* @since 0.1.0
*
* @return array<string, mixed> The request options.
*/
public function get_options(): array {
return $this->options;
}
/**
* Adds a header to the request.
*
* @since 0.1.0
*
* @param string $name The header name.
* @param string $value The header value.
*/
public function add_header( string $name, string $value ): void {
$this->headers[ $name ] = $value;
}
/**
* Adds data to the request.
*
* This is only possible if no hard-coded request body was provided for the request.
*
* @since 0.1.0
*
* @param string $name The name under which to send the data.
* @param mixed $value The value to send.
*/
public function add_data( string $name, $value ): void {
if ( $this->body ) {
_doing_it_wrong(
__METHOD__,
esc_html__( 'Data cannot be added to a request that already has a body.', 'wp-oop-plugin-lib' ),
''
);
return;
}
$this->data[ $name ] = $value;
}
/**
* Adds an option to the request.
*
* @since 0.1.0
*
* @param string $name The option name.
* @param mixed $value The option value.
*/
public function add_option( string $name, $value ): void {
$this->options[ $name ] = $value;
}
/**
* Sanitizes the provided arguments.
*
* For any invalid arguments, PHP warnings may be triggered, and they will be stripped.
*
* @since 0.1.0
*
* @param array<string, mixed> $args Request arguments, including but not limited to options.
* @param string $method PHP class method to reference in potential PHP warnings.
* @return array<string, mixed> The sanitized arguments.
*/
private function sanitize_args( array $args, string $method ): array {
if ( isset( $args['method'] ) && ! $this->is_valid_method( $args['method'] ) ) {
_doing_it_wrong(
esc_html( $method ),
esc_html(
sprintf(
/* translators: %s: invalid method string */
__( 'The value %s is not a valid HTTP request method.', 'wp-oop-plugin-lib' ),
(string) $args['method']
)
),
''
);
unset( $args['method'] );
}
if ( isset( $args['body'] ) && ! is_string( $args['body'] ) ) {
_doing_it_wrong(
esc_html( $method ),
esc_html__( 'The request body must be a string.', 'wp-oop-plugin-lib' ),
''
);
unset( $args['body'] );
}
if (
isset( $args['headers'] ) &&
( ! is_array( $args['headers'] ) || ( $args['headers'] && wp_is_numeric_array( $args['headers'] ) ) )
) {
_doing_it_wrong(
esc_html( $method ),
esc_html__( 'The request headers must be an associative array.', 'wp-oop-plugin-lib' ),
''
);
unset( $args['headers'] );
}
return $args;
}
/**
* Checks whether the given request method is valid.
*
* @since 0.1.0
*
* @param string $method The request method to check.
* @return bool True if the request method is valid, false otherwise.
*/
private function is_valid_method( string $method ): bool {
return in_array(
$method,
array(
Request::DELETE,
Request::GET,
Request::PATCH,
Request::POST,
Request::PUT,
),
true
);
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Generic_Response
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response;
use WpOrg\Requests\Utility\CaseInsensitiveDictionary;
/**
* Class for a generic HTTP response from another URL.
*
* @since 0.1.0
*/
class Generic_Response implements Response {
/**
* The HTTP status code received with the response.
*
* @since 0.1.0
* @var int
*/
private $status;
/**
* The body received with the response.
*
* @since 0.1.0
* @var string
*/
private $body;
/**
* The headers received with the response.
*
* @since 0.1.0
* @var CaseInsensitiveDictionary
*/
private $headers;
/**
* Constructor.
*
* @since 0.1.0
*
* @param int $status The HTTP status code received with the response.
* @param string $body The body received with the response.
* @param array<string, string> $headers The headers received with the response.
*/
public function __construct( int $status, string $body, array $headers ) {
// Prior to WordPress 6.2, this class had a different name.
if ( ! class_exists( CaseInsensitiveDictionary::class ) ) {
class_alias( 'Requests_Utility_CaseInsensitiveDictionary', CaseInsensitiveDictionary::class );
}
$this->status = $status;
$this->body = $body;
$this->headers = new CaseInsensitiveDictionary( $headers );
}
/**
* Retrieves the HTTP status code received with the response.
*
* @since 0.1.0
*
* @return int The 3-digit HTTP status code.
*/
public function get_status(): int {
return $this->status;
}
/**
* Retrieves the data received with the response.
*
* @since 0.1.0
*
* @return array<string, mixed> The response data, or an empty array if it could not automatically be decoded. In
* this case, the raw response body should be used.
*/
public function get_data(): array {
// This is a generic response, so we don't know how to decode the body.
return array();
}
/**
* Retrieves the body received with the response.
*
* @since 0.1.0
*
* @return string The raw response body. If response data could be automatically decoded, this should be empty, and
* the response data should be used instead.
*/
public function get_body(): string {
return $this->body;
}
/**
* Retrieves the headers received with the response.
*
* @since 0.1.0
*
* @return array<string, string> The response headers.
*/
public function get_headers(): array {
return $this->headers->getAll();
}
/**
* Retrieves a specific header received with the response.
*
* @since 0.1.0
*
* @param string $name The name of the header to retrieve.
* @return string The value of the header, or an empty string if the header was not found.
*/
public function get_header( string $name ): string {
if ( ! isset( $this->headers[ $name ] ) ) {
return '';
}
return $this->headers[ $name ];
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Get_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP GET request to another URL.
*
* @since 0.1.0
*/
class Get_Request extends Generic_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::GET;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,432 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\HTTP
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Multiple_Requests_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Traits\Sanitize_Headers;
use InvalidArgumentException;
use WP_HTTP_Proxy;
use WpOrg\Requests\Exception as Requests_Exception;
use WpOrg\Requests\Proxy\Http as Requests_Proxy_HTTP;
use WpOrg\Requests\Requests;
use WpOrg\Requests\Response as Requests_Response;
use WpOrg\Requests\Utility\CaseInsensitiveDictionary;
/**
* Class for sending HTTP requests and processing responses.
*
* @since 0.1.0
*/
class HTTP implements Request_Handler {
use Sanitize_Headers;
/**
* Default options to use for all requests.
*
* @since 0.1.0
* @var array<string, mixed>
*/
private $default_options;
/**
* Constructor.
*
* @since 0.1.0
*
* @param array<string, mixed> $default_options Optional. Default options to use for all requests. Default empty
* array.
*/
public function __construct( array $default_options = array() ) {
// Prior to WordPress 6.2, the Requests library was not using namespaces.
if ( ! class_exists( Requests_Exception::class ) ) {
class_alias( 'Requests_Exception', Requests_Exception::class );
}
if ( ! class_exists( Requests_Proxy_HTTP::class ) ) {
class_alias( 'Requests_Proxy_HTTP', Requests_Proxy_HTTP::class );
}
if ( ! class_exists( Requests_Response::class ) ) {
class_alias( 'Requests_Response', Requests_Response::class );
}
if ( ! class_exists( CaseInsensitiveDictionary::class ) ) {
class_alias( 'Requests_Utility_CaseInsensitiveDictionary', CaseInsensitiveDictionary::class );
}
// Remove potentially conflicting entries that are not actually options.
unset( $default_options['method'], $default_options['headers'], $default_options['body'] );
$this->default_options = $default_options;
}
/**
* Sends an HTTP request and returns the response.
*
* @since 0.1.0
*
* @param Request $request The request to send.
* @return Response The response received.
*
* @throws Request_Exception Thrown if the request fails.
*/
public function request( Request $request ): Response {
$headers = $request->get_headers();
$data = $request->get_data();
if ( ! $data ) {
$data = $request->get_body();
}
$args = wp_parse_args( $request->get_options(), $this->default_options );
$args['method'] = $request->get_method();
if ( $headers ) {
$args['headers'] = $headers;
}
if ( $data ) {
$args['body'] = $data;
}
$response = wp_remote_request( $request->get_url(), $args );
if ( is_wp_error( $response ) ) {
throw new Request_Exception( esc_html( $response->get_error_message() ) );
}
$status = (int) wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$headers = wp_remote_retrieve_headers( $response );
if ( $headers instanceof CaseInsensitiveDictionary ) {
$headers = $headers->getAll();
}
$headers = $this->sanitize_headers( $headers );
return $this->create_response( $status, $body, $headers );
}
/**
* Sends multiple HTTP requests and returns the responses.
*
* The returned responses are in the same order / use the same keys as the requests.
*
* If any of the requests fail, a Multiple_Requests_Exception will be thrown. The exception will contain the
* responses of the requests that succeeded, and the exceptions of the requests that failed.
*
* @since 0.1.0
*
* @param array<string|int, Request> $requests The requests to send.
* @return array<string|int, Response> The responses received.
*
* @throws Multiple_Requests_Exception Thrown if one or more requests fail. If any requests succeeded, their
* responses will be included in the exception.
* @throws InvalidArgumentException Thrown if an invalid request is provided.
*/
public function request_multiple( array $requests ): array {
// Ensure all values are Request objects.
foreach ( $requests as $request ) {
if ( ! $request instanceof Request ) {
throw new InvalidArgumentException(
esc_html__( 'Invalid request provided.', 'wp-oop-plugin-lib' )
);
}
}
$requests_args = array();
$responses = array();
foreach ( $requests as $key => $request ) {
// Assemble the options with WordPress defaults included.
$request_args = $this->build_request_args( $request );
// Allow short-circuiting requests, just like in WP_Http::request().
$pre_response = $this->run_wp_pre_http_request_filter( $request_args );
if ( null !== $pre_response ) {
$responses[ $key ] = $pre_response;
continue;
}
// Prepare the options for usage with the Requests library.
$request_args['options'] = $this->prepare_options_for_requests(
$request_args['options'],
$request_args['url'],
$request_args['type']
);
$requests_args[ $key ] = $request_args;
}
// If all requests were handled by the response pre filter, we don't actually need to send any requests.
if ( count( $requests_args ) > 0 ) {
// Similar to WP_Http::request(), avoid issues where mbstring.func_overload is enabled.
mbstring_binary_safe_encoding();
$responses = array_merge(
$responses,
Requests::request_multiple( $requests_args )
);
// See above.
reset_mbstring_encoding();
}
$successful = array();
$failed = array();
foreach ( $responses as $key => $response ) {
if ( $response instanceof Requests_Exception ) {
$failed[ $key ] = new Request_Exception( $response->getMessage() );
continue;
}
$status = (int) $response->status_code;
$body = $response->body;
$headers = $this->sanitize_headers( $response->headers->getAll() );
$successful[ $key ] = $this->create_response( $status, $body, $headers );
}
/*
* If any requests failed, throw a bulk exception.
* The successful responses will be included in the exception, so they can still be used if needed.
*/
if ( $failed ) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new Multiple_Requests_Exception( $failed, $successful );
}
// If this is reached, all requests succeeded.
return $successful;
}
/**
* Assembles the request arguments for the given request, to pass to the Requests library.
*
* @since 0.1.0
*
* @param Request $request The request to send.
* @return array<string, mixed> Request arguments.
*/
protected function build_request_args( Request $request ): array {
$headers = $request->get_headers();
$data = $request->get_data();
if ( ! $data ) {
$data = $request->get_body();
}
$request_args = array(
'url' => $request->get_url(),
'type' => $request->get_method(),
'options' => wp_parse_args( $request->get_options(), $this->default_options ),
);
if ( $headers ) {
$request_args['headers'] = $headers;
}
if ( $data ) {
$request_args['data'] = $data;
}
// Include the defaults from WP_Http::request(), since the Requests library does not include them.
$request_args['options'] = $this->merge_wp_default_options(
$request_args['options'],
$request_args['url'],
$request_args['type']
);
return $request_args;
}
/**
* Runs the WordPress 'pre_http_request' filter to allow short-circuiting requests.
*
* This is only used for multi requests, as single requests are handled by WP_Http::request() directly.
*
* When used in a multi request, the filter will be run for every request. For any request where it returns a value
* other than `false`, the request will not be actually sent and instead the data from the filter is used to create
* the response. If all requests within a multi request receive their response data in that way, no request is sent
* at all.
*
* @since 0.1.0
*
* @param array<string, mixed> $request_args Request arguments.
* @return Requests_Response|Requests_Exception|null Response object or exception based on the 'pre_http_request'
* filter data, or null if not filtered.
*/
private function run_wp_pre_http_request_filter( array $request_args ) {
$parsed_args = $request_args['options'];
$parsed_args['method'] = $request_args['type'];
$parsed_args['headers'] = $request_args['headers'] ?? array();
$parsed_args['cookies'] = $request_args['options']['cookies'] ?? array();
$parsed_args['body'] = $request_args['data'] ?? null;
// Allow short-circuiting requests, just like in WP_Http::request().
$pre = apply_filters( 'pre_http_request', false, $parsed_args, $request_args['url'] );
if ( false !== $pre ) {
if ( is_wp_error( $pre ) ) {
return new Requests_Exception( $pre->get_error_message(), 'pre_http_request' );
}
$response = new Requests_Response();
if ( $pre['response']['code'] ) {
$response->status_code = $pre['response']['code'];
}
if ( $pre['body'] ) {
$response->body = $pre['body'];
}
if ( $pre['headers'] ) {
foreach ( $pre['headers'] as $header_name => $header_value ) {
$response->headers[ $header_name ] = $header_value;
}
}
return $response;
}
return null;
}
/**
* Creates a response object based on the response data.
*
* @see https://www.rfc-editor.org/rfc/rfc1341.html#page-7
*
* @since 0.1.0
*
* @param int $status The HTTP status code received with the response.
* @param string $body The body received with the response.
* @param array<string, string> $headers The headers received with the response.
* @return Response The response object.
*/
private function create_response( int $status, string $body, array $headers ): Response {
if (
isset( $headers['content-type'] )
&& in_array( 'application/json', array_map( 'trim', explode( ';', $headers['content-type'] ) ), true )
) {
return new JSON_Response( $status, $body, $headers );
}
return new Generic_Response( $status, $body, $headers );
}
/**
* Populates the given options array with defaults.
*
* WordPress's API only allows making a single request at a time, while the Requests library allows making multiple
* requests. However, the Requests library does not include the WordPress defaults for requests, such as the default
* timeout. This method ensures that they include these defaults.
*
* Most of the code in this method is similar to code in WP_Http::request() in WordPress core.
*
* @since 0.1.0
*
* @param array<string, mixed> $options The options to prepare.
* @param string $url The request URL, only relevant as context for various filters.
* @param string $method The request method, relevant to determine some defaults.
* @return array<string, mixed> The prepared options, including WordPress defaults.
*/
private function merge_wp_default_options( array $options, string $url, string $method ): array {
$wp_user_agent = 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' );
$defaults = array(
'timeout' => apply_filters( 'http_request_timeout', 5, $url ),
'redirection' => apply_filters( 'http_request_redirection_count', 5, $url ),
'user-agent' => apply_filters( 'http_headers_useragent', $wp_user_agent, $url ),
'reject_unsafe_urls' => apply_filters( 'http_request_reject_unsafe_urls', false, $url ),
'blocking' => true,
'sslverify' => true,
'sslcertificates' => ABSPATH . WPINC . '/certificates/ca-bundle.crt',
'stream' => false,
'filename' => null,
'limit_response_size' => null,
);
if ( 'HEAD' === $method ) {
$defaults['redirection'] = 0;
}
if ( isset( $options['stream'] ) && $options['stream'] ) {
$defaults['filename'] = get_temp_dir() . basename( $url );
}
return wp_parse_args( $options, $defaults );
}
/**
* Prepares the options for a request directly via the Requests library.
*
* WordPress's API only allows making a single request at a time, while the Requests library allows making multiple
* requests. However, the Requests library uses different argument names, so this method prepares the WordPress
* options for usage with the Requests library.
*
* Most of the code in this method is similar to code in WP_Http::request() in WordPress core.
*
* @since 0.1.0
*
* @param array<string, mixed> $options The options to prepare.
* @param string $url The request URL, only relevant as context for various filters.
* @param string $method The request method, relevant to determine some defaults.
* @return array<string, mixed> The prepared options.
*/
private function prepare_options_for_requests( array $options, string $url, string $method ): array {
// Migrate WordPress options to Requests options.
$options = $this->migrate_wp_options_to_requests_options( $options );
// Enforce additional behavior similar to WordPress core.
if ( $options['filename'] ) {
$options['blocking'] = true;
}
if ( 'HEAD' !== $method && 'GET' !== $method ) {
$options['data_format'] = 'body';
}
$options['verify'] = apply_filters( 'https_ssl_verify', $options['verify'], $url );
// Add proxy settings if necessary, similar to WordPress core.
$proxy = new WP_HTTP_Proxy();
if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
$options['proxy'] = new Requests_Proxy_HTTP( $proxy->host() . ':' . $proxy->port() );
if ( $proxy->use_authentication() ) {
$options['proxy']->use_authentication = true;
$options['proxy']->user = $proxy->username();
$options['proxy']->pass = $proxy->password();
}
}
return $options;
}
/**
* Migrates WordPress options to Requests options.
*
* @since 0.1.0
*
* @param array<string, mixed> $options The options to migrate.
* @return array<string, mixed> The migrated options.
*/
private function migrate_wp_options_to_requests_options( array $options ): array {
if ( isset( $options['limit_response_size'] ) ) {
$options['max_bytes'] = $options['limit_response_size'];
}
if ( ! $options['redirection'] ) {
$options['follow_redirects'] = false;
} else {
$options['redirects'] = $options['redirection'];
}
if ( ! $options['sslverify'] ) {
$options['verify'] = false;
$options['verifyname'] = false;
} else {
$options['verify'] = $options['sslcertificates'];
}
$options['useragent'] = $options['user-agent'];
unset(
$options['limit_response_size'],
$options['redirection'],
$options['sslverify'],
$options['sslcertificates'],
$options['stream'], // This is irrelevant as the 'filename' presence alone handles it.
$options['user-agent']
);
return $options;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\JSON_Patch_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP PATCH request that sends JSON to another URL.
*
* @since 0.1.0
*/
class JSON_Patch_Request extends JSON_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::PATCH;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\JSON_Post_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP POST request that sends JSON to another URL.
*
* @since 0.1.0
*/
class JSON_Post_Request extends JSON_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::POST;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\JSON_Put_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP PUT request that sends JSON to another URL.
*
* @since 0.1.0
*/
class JSON_Put_Request extends JSON_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::PUT;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\JSON_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
/**
* Class for an HTTP request that sends JSON to another URL.
*
* @since 0.1.0
*/
class JSON_Request extends Generic_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
// If the body is provided directly, it must be JSON-encoded data.
if ( isset( $args['body'] ) && is_string( $args['body'] ) && $args['body'] ) {
json_decode( $args['body'], true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
_doing_it_wrong(
__METHOD__,
// phpcs:ignore Generic.Files.LineLength.TooLong
esc_html__( 'When providing the JSON request body directly, it must be a valid JSON string.', 'wp-oop-plugin-lib' ),
''
);
unset( $args['body'] );
}
}
// Ensure the Content-Type header is set to application/json, unless otherwise specified.
if ( ! isset( $args['headers'] ) ) {
$args['headers'] = array( 'Content-Type' => 'application/json' );
} elseif ( ! isset( $args['headers']['Content-Type'] ) && ! isset( $args['headers']['content-type'] ) ) {
$args['headers']['Content-Type'] = 'application/json';
}
parent::__construct( $url, $data, $args );
}
/**
* Retrieves the data to be sent with the request.
*
* @since 0.1.0
*
* @return array<string, mixed> The request data, or an empty array. If the request method is not GET or HEAD, in
* case of an empty array the request body should be used instead.
*/
public function get_data(): array {
// The data should be sent as JSON, so the body should be used instead.
return array();
}
/**
* Retrieves the body to be sent with the request.
*
* A request may have either data or a body, but not both.
*
* @since 0.1.0
*
* @return string The request body, or an empty string. Only relevant if the request method is not GET or HEAD. In
* case of an empty string, the request data should be used instead.
*/
public function get_body(): string {
$body = parent::get_body();
if ( $body ) {
return $body;
}
$data = parent::get_data();
if ( ! $data ) {
return '';
}
return wp_json_encode( $data );
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\JSON_Response
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
/**
* Class for a JSON HTTP response from another URL.
*
* @since 0.1.0
*/
class JSON_Response extends Generic_Response {
/**
* Data decoded from the JSON response body.
*
* @since 0.1.0
* @var array<string, mixed>|null
*/
private $data;
/**
* Retrieves the data received with the response.
*
* @since 0.1.0
*
* @return array<string, mixed> The response data, or an empty array if it could not automatically be decoded. In
* this case, the raw response body should be used.
*/
public function get_data(): array {
if ( ! isset( $this->data ) ) {
$this->data = json_decode( $this->get_body(), true );
if ( null === $this->data ) {
$this->data = array();
}
}
return $this->data;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Patch_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP PATCH request to another URL.
*
* @since 0.1.0
*/
class Patch_Request extends Generic_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::PATCH;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Post_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP POST request to another URL.
*
* @since 0.1.0
*/
class Post_Request extends Generic_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::POST;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Put_Request
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
/**
* Class for a HTTP PUT request to another URL.
*
* @since 0.1.0
*/
class Put_Request extends Generic_Request {
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $url The URL to which the request should be sent.
* @param array<string, mixed> $data Optional. The data to be sent with the request. Default empty array.
* @param array<string, mixed> $args Optional. Additional options for the request. See {@see WP_Http::request()}
* for possible options. Providing the 'body' key is only allowed if the data
* parameter is empty, and only as a string. Default empty array.
*/
public function __construct( string $url, array $data = array(), array $args = array() ) {
$args['method'] = Request::PUT;
parent::__construct( $url, $data, $args );
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Trait Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Traits\Sanitize_Headers
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Traits;
/**
* Trait with a function to sanitize headers into an associative array of strings.
*
* @since 0.1.0
*/
trait Sanitize_Headers {
/**
* Sanitizes the given headers associative array to make sure all values are strings.
*
* Multiple values for the same header will be concatenated with a comma.
*
* @since 0.1.0
*
* @param array<string, string|array<string>> $headers The headers to sanitize.
* @return array<string, string> The sanitized headers.
*/
protected function sanitize_headers( array $headers ): array {
$sanitized_headers = array();
foreach ( $headers as $name => $value ) {
if ( is_array( $value ) ) {
$sanitized_headers[ $name ] = implode( ', ', $value );
} else {
$sanitized_headers[ $name ] = $value;
}
}
return $sanitized_headers;
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Abstract_Entity_Key_Value
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\Entity_Key_Value;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\Entity_Key_Value_Repository;
/**
* Base class representing an key-value pair that is connected to an entity.
*
* Should typically not be used directly, but rather through a more specific class extending it.
*
* @since 0.1.0
*/
class Abstract_Entity_Key_Value implements Entity_Key_Value {
/**
* Repository used for the item.
*
* @since 0.1.0
* @var Entity_Key_Value_Repository
*/
protected $repository;
/**
* Item key.
*
* @since 0.1.0
* @var string
*/
protected $key;
/**
* Item default value.
*
* @since 0.1.0
* @var mixed
*/
protected $default_value;
/**
* Constructor.
*
* @since 0.1.0
*
* @param Entity_Key_Value_Repository $repository Repository used for the item.
* @param string $key Item key.
* @param mixed $default_value Optional. Default value for the item if not set in the
* repository. If null, it will be ignored. Default null.
*/
public function __construct( Entity_Key_Value_Repository $repository, string $key, $default_value = null ) {
$this->repository = $repository;
$this->key = $key;
$this->default_value = $default_value;
}
/**
* Checks whether the item has a value set in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return bool True if a value is set, false otherwise.
*/
public function has_value( int $entity_id ): bool {
return $this->repository->exists( $entity_id, $this->key );
}
/**
* Gets the value for the item in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return mixed Value for the item.
*/
public function get_value( int $entity_id ) {
// Pass default value if set.
if ( isset( $this->default_value ) ) {
return $this->repository->get( $entity_id, $this->key, $this->default_value );
}
return $this->repository->get( $entity_id, $this->key );
}
/**
* Updates the value for the item in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param mixed $value New value to set for the item.
* @return bool True on success, false on failure.
*/
public function update_value( int $entity_id, $value ): bool {
return $this->repository->update( $entity_id, $this->key, $value );
}
/**
* Deletes the data for the item in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return bool True on success, false on failure.
*/
public function delete_value( int $entity_id ): bool {
return $this->repository->delete( $entity_id, $this->key );
}
/**
* Gets the key of the item.
*
* @since 0.1.0
*
* @return string Item key.
*/
public function get_key(): string {
return $this->key;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\Entity_Key_Value
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts;
/**
* Interface for a key-value pair that is connected to an entity.
*
* @since 0.1.0
*/
interface Entity_Key_Value {
/**
* Checks whether the item has a value set in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return bool True if a value is set, false otherwise.
*/
public function has_value( int $entity_id ): bool;
/**
* Gets the value for the item in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return mixed Value for the item.
*/
public function get_value( int $entity_id );
/**
* Updates the value for the item in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param mixed $value New value to set for the item.
* @return bool True on success, false on failure.
*/
public function update_value( int $entity_id, $value ): bool;
/**
* Deletes the data for the item in the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return bool True on success, false on failure.
*/
public function delete_value( int $entity_id ): bool;
/**
* Gets the key of the item.
*
* @since 0.1.0
*
* @return string Item key.
*/
public function get_key(): string;
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\Entity_Key_Value_Repository
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts;
/**
* Interface for a repository for key-value pairs that are connected to an entity.
*
* @since 0.1.0
*/
interface Entity_Key_Value_Repository {
/**
* Checks whether a value for the given entity and key exists in the repository.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Item key.
* @return bool True if a value for the key exists, false otherwise.
*/
public function exists( int $entity_id, string $key ): bool;
/**
* Gets the value for a given entity and key from the repository.
*
* Always returns a single value.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Item key.
* @param mixed $default Optional. Value to return if no value exists for the key. Default null.
* @return mixed Value for the key, or the default if no value exists.
*/
public function get( int $entity_id, string $key, $default = null );
/**
* Updates the value for a given entity and key in the repository.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Item key.
* @param mixed $value New value to set for the key.
* @return bool True on success, false on failure.
*/
public function update( int $entity_id, string $key, $value ): bool;
/**
* Deletes the data for a given entity and key from the repository.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Item key.
* @return bool True on success, false on failure.
*/
public function delete( int $entity_id, string $key ): bool;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\With_Entity_ID
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts;
/**
* Interface for a class that is aware of a specific entity ID.
*
* @since 0.1.0
*/
interface With_Entity_ID {
/**
* Gets the entity ID.
*
* @since 0.1.0
*
* @return int The entity ID.
*/
public function get_entity_id(): int;
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\With_Single
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts;
/**
* Interface for a key-value pair repository with support for differentiating between entries with a single value vs
* with multiple values.
*
* @since 0.1.0
*/
interface With_Single {
/**
* Gets the 'single' config for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool Whether or not the item should be singleed.
*/
public function get_single( string $key ): bool;
/**
* Sets the 'single' config for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param bool $single Item single config.
*/
public function set_single( string $key, bool $single ): void;
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Entity_Aware_Meta_Container
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use ArrayAccess;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container_Readonly;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\With_Entity_ID;
/**
* Class for a meta container scoped to a specific entity.
*
* @since 0.1.0
*/
class Entity_Aware_Meta_Container implements Container_Readonly, ArrayAccess, With_Entity_ID {
/**
* The original meta container.
*
* @since 0.1.0
* @var Meta_Container
*/
private $wrapped_container;
/**
* ID of the entity to scope this instance to.
*
* @since 0.1.0
* @var int
*/
private $entity_id;
/**
* Item instances already created.
*
* @since 0.1.0
* @var array<string, Entity_Aware_Meta_Key>
*/
private $instances = array();
/**
* Constructor.
*
* @since 0.1.0
*
* @param Meta_Container $wrapped_container Underlying entity aware instance that this scoped instance
* should inherit from.
* @param int $entity_id ID of the entity to scope this instance to.
*/
public function __construct( Meta_Container $wrapped_container, int $entity_id ) {
$this->wrapped_container = $wrapped_container;
$this->entity_id = $entity_id;
}
/**
* Checks if a meta key for the given key exists in the container.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @return bool True if the meta key exists in the container, false otherwise.
*/
public function has( string $key ): bool {
return $this->wrapped_container->has( $key );
}
/**
* Gets the meta key for the given key from the container.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @return Entity_Aware_Meta_Key Meta key for the given key.
*
* @throws Not_Found_Exception Thrown when meta key with given key is not found.
*/
public function get( string $key ) {
if ( ! $this->wrapped_container->has( $key ) ) {
throw new Not_Found_Exception(
esc_html(
sprintf(
/* translators: %s: meta key */
__( 'Meta key with key %s was not found in container', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
if ( ! isset( $this->instances[ $key ] ) ) {
$this->instances[ $key ] = new Entity_Aware_Meta_Key(
$this->wrapped_container->get( $key ),
$this->entity_id
);
}
return $this->instances[ $key ];
}
/**
* Gets all keys in the container.
*
* @since 0.1.0
*
* @return string[] List of keys.
*/
public function get_keys(): array {
return $this->wrapped_container->get_keys();
}
/**
* Checks if a meta key for the given key exists in the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
* @return bool True if the meta key exists in the container, false otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetExists( $key ) {
return $this->has( $key );
}
/**
* Gets the meta key for the given key from the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
* @return mixed Meta key for the given key.
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return $this->get( $key );
}
/**
* Sets the given meta key under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
* @param mixed $value Item creator closure.
*/
#[\ReturnTypeWillChange]
public function offsetSet( $key, $value ) {
// Does nothing as this class is read-only.
}
/**
* Unsets the meta key under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $key ) {
// Does nothing as this class is read-only.
}
/**
* Gets the entity ID.
*
* @since 0.1.0
*
* @return int The entity ID.
*/
public function get_entity_id(): int {
return $this->entity_id;
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Entity_Aware_Meta_Key
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\With_Entity_ID;
/**
* Wrapper representing a WordPress meta key scoped to a specific entity.
*
* @since 0.1.0
*/
class Entity_Aware_Meta_Key implements With_Entity_ID, Key_Value {
/**
* Underlying, general entity aware instance.
*
* @since 0.1.0
* @var Meta_Key
*/
private $wrapped_meta;
/**
* ID of the entity to scope this instance to.
*
* @since 0.1.0
* @var int
*/
private $entity_id;
/**
* Constructor.
*
* @since 0.1.0
*
* @param Meta_Key $wrapped_meta Underlying entity aware instance that this scoped instance
* should inherit from.
* @param int $entity_id ID of the entity to scope this instance to.
*/
public function __construct( Meta_Key $wrapped_meta, int $entity_id ) {
$this->wrapped_meta = $wrapped_meta;
$this->entity_id = $entity_id;
}
/**
* Checks whether the item has a value set.
*
* @since 0.1.0
*
* @return bool True if a value is set, false otherwise.
*/
public function has_value(): bool {
return $this->wrapped_meta->has_value( $this->entity_id );
}
/**
* Gets the value for the item.
*
* @since 0.1.0
*
* @return mixed Value for the item.
*/
public function get_value() {
return $this->wrapped_meta->get_value( $this->entity_id );
}
/**
* Updates the value for the item.
*
* @since 0.1.0
*
* @param mixed $value New value to set for the item.
* @return bool True on success, false on failure.
*/
public function update_value( $value ): bool {
return $this->wrapped_meta->update_value( $this->entity_id, $value );
}
/**
* Deletes the data for the item.
*
* @since 0.1.0
*
* @return bool True on success, false on failure.
*/
public function delete_value(): bool {
return $this->wrapped_meta->delete_value( $this->entity_id );
}
/**
* Gets the key of the item.
*
* @since 0.1.0
*
* @return string Option key.
*/
public function get_key(): string {
return $this->wrapped_meta->get_key();
}
/**
* Gets the entity ID.
*
* @since 0.1.0
*
* @return int The entity ID.
*/
public function get_entity_id(): int {
return $this->entity_id;
}
}

View File

@@ -0,0 +1,211 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Container
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use ArrayAccess;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Invalid_Type_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\Entity_Key_Value_Repository;
/**
* Class for a meta container.
*
* @since 0.1.0
*/
class Meta_Container implements Container, ArrayAccess {
/**
* Meta keys stored in the container.
*
* @since 0.1.0
* @var array<string, callable>
*/
private $meta_keys = array();
/**
* Meta key instances already created.
*
* @since 0.1.0
* @var array<string, Meta_Key>
*/
private $instances = array();
/**
* Checks if a meta key for the given key exists in the container.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @return bool True if the meta key exists in the container, false otherwise.
*/
public function has( string $key ): bool {
return isset( $this->meta_keys[ $key ] );
}
/**
* Gets the meta key for the given key from the container.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @return Meta_Key Meta key for the given key.
*
* @throws Not_Found_Exception Thrown when meta key with given key is not found.
* @throws Invalid_Type_Exception Thrown when meta key with given key has invalid type.
*/
public function get( string $key ) {
if ( ! isset( $this->meta_keys[ $key ] ) ) {
throw new Not_Found_Exception(
esc_html(
sprintf(
/* translators: %s: meta key */
__( 'Meta key with key %s was not found in container', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
if ( ! isset( $this->instances[ $key ] ) ) {
$instance = $this->meta_keys[ $key ]( $this );
if ( ! $instance instanceof Meta_Key ) {
throw new Invalid_Type_Exception(
esc_html(
sprintf(
/* translators: %s: meta key */
__( 'Meta key with key %s is not of type Meta_Key', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
$this->instances[ $key ] = $instance;
}
return $this->instances[ $key ];
}
/**
* Sets the given meta key under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @param callable $creator Meta key creator closure.
*/
public function set( string $key, callable $creator ): void {
$this->meta_keys[ $key ] = $creator;
unset( $this->instances[ $key ] );
}
/**
* Sets a meta key using the given repository and arguments under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @param Entity_Key_Value_Repository $repository Repository used for the meta key.
* @param array<string, mixed> $registration_args Optional. Meta key registration arguments. Default empty
* array.
*/
public function set_by_args( string $key, Entity_Key_Value_Repository $repository, array $registration_args = array() ): void { // phpcs:ignore Generic.Files.LineLength.TooLong
$this->set(
$key,
function () use ( $repository, $key, $registration_args ) {
return new Meta_Key( $repository, $key, $registration_args );
}
);
}
/**
* Unsets the meta key under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Meta key.
*/
public function unset( string $key ): void {
unset( $this->meta_keys[ $key ], $this->instances[ $key ] );
}
/**
* Gets all keys in the container.
*
* @since 0.1.0
*
* @return string[] List of keys.
*/
public function get_keys(): array {
return array_keys( $this->meta_keys );
}
/**
* Checks if a meta key for the given key exists in the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
* @return bool True if the meta key exists in the container, false otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetExists( $key ) {
return $this->has( $key );
}
/**
* Gets the meta key for the given key from the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
* @return Meta_Key Meta key for the given key.
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return $this->get( $key );
}
/**
* Sets the given meta key under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
* @param mixed $value Meta key creator closure.
*/
#[\ReturnTypeWillChange]
public function offsetSet( $key, $value ) {
$this->set( $key, $value );
}
/**
* Unsets the meta key under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Meta key.
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $key ) {
$this->unset( $key );
}
/**
* Creates an instance similar to this container, but scoped to the given entity.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return Entity_Aware_Meta_Container New container scoped to the object.
*/
public function create_entity_aware( int $entity_id ): Entity_Aware_Meta_Container {
return new Entity_Aware_Meta_Container( $this, $entity_id );
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Hook_Registrar
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Hook_Registrar;
/**
* Class that adds the relevant hook to register WordPress metadata.
*
* @since 0.1.0
*/
class Meta_Hook_Registrar implements Hook_Registrar {
/**
* WordPress metadata registry instance.
*
* @since 0.1.0
* @var Meta_Registry
*/
private $registry;
/**
* Constructor.
*
* @param Meta_Registry $registry WordPress metadata registry instance.
*/
public function __construct( Meta_Registry $registry ) {
$this->registry = $registry;
}
/**
* Adds a callback that registers the metadata to the relevant hook.
*
* The callback receives a registry instance as the sole parameter, allowing to call the
* {@see Meta_Registry::register()} method.
*
* @since 0.1.0
*
* @param callable $register_callback Callback to register the metadata.
*/
public function add_register_callback( callable $register_callback ): void {
add_action(
'init',
function () use ( $register_callback ) {
$register_callback( $this->registry );
}
);
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Key
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Registration_Args;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Traits\Cast_Value_By_Type;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\Entity_Key_Value_Repository;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\With_Single;
/**
* Class representing a WordPress meta key.
*
* @since 0.1.0
*/
class Meta_Key extends Abstract_Entity_Key_Value implements With_Registration_Args {
use Cast_Value_By_Type;
/**
* Meta key registration arguments.
*
* @since 0.1.0
* @var array<string, mixed>
*/
protected $registration_args;
/**
* Constructor.
*
* @since 0.1.0
*
* @param Entity_Key_Value_Repository $repository Repository used for the meta key.
* @param string $key Meta key.
* @param array<string, mixed> $registration_args Optional. Meta key registration arguments. Default empty
* array.
*/
public function __construct( Entity_Key_Value_Repository $repository, string $key, array $registration_args = array() ) { // phpcs:ignore Generic.Files.LineLength.TooLong
// Extract default value from registration arguments if passed.
$default = null;
if ( isset( $registration_args['default'] ) ) {
$default = $registration_args['default'];
}
// Set 'single' value from registration arguments if passed.
if ( $repository instanceof With_Single && isset( $registration_args['single'] ) ) {
$repository->set_single( $key, (bool) $registration_args['single'] );
}
parent::__construct( $repository, $key, $default );
$this->registration_args = $registration_args;
}
/**
* Checks whether the meta key has a value set for the given entity ID.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return bool True if a value is set, false otherwise.
*/
public function has_value( int $entity_id ): bool {
return parent::has_value( $entity_id );
}
/**
* Gets the value for the meta key for the given entity ID.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return mixed Value for the meta key.
*/
public function get_value( int $entity_id ) {
$value = parent::get_value( $entity_id );
/*
* By default, meta keys are assumed to have a single value. This may be overwritten via meta registration by
* setting the 'single' argument to `false`. In this case the value is always an array, and the individual
* items should be type-casted as needed.
*/
if ( isset( $this->registration_args['single'] ) && ! $this->registration_args['single'] ) {
$value = (array) $value;
if ( isset( $this->registration_args['type'] ) ) {
$value = array_map(
function ( $item ) {
return $this->cast_value_by_type( $item, $this->registration_args['type'] );
},
$value
);
}
return $value;
}
// Otherwise, this is a single value that should be type-casted as needed.
if ( isset( $this->registration_args['type'] ) ) {
return $this->cast_value_by_type( $value, $this->registration_args['type'] );
}
return $value;
}
/**
* Updates the value for the meta key for the given entity ID.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param mixed $value New value to set for the meta key.
* @return bool True on success, false on failure.
*/
public function update_value( int $entity_id, $value ): bool {
return parent::update_value( $entity_id, $value );
}
/**
* Deletes the data for the meta key for the given entity ID.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return bool True on success, false on failure.
*/
public function delete_value( int $entity_id ): bool {
return parent::delete_value( $entity_id );
}
/**
* Gets the meta key.
*
* @since 0.1.0
*
* @return string Meta key.
*/
public function get_key(): string {
return parent::get_key();
}
/**
* Gets the registration arguments for the meta key.
*
* @since 0.1.0
*
* @return array<string, mixed> Meta key registration arguments.
*/
public function get_registration_args(): array {
return $this->registration_args;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Registry
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Registry;
/**
* Class for a registry of WordPress metadata.
*
* @since 0.1.0
*/
class Meta_Registry implements Registry {
/**
* Object type.
*
* @since 0.1.0
* @var string
*/
protected $object_type;
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $object_type Object type.
*/
public function __construct( string $object_type ) {
$this->object_type = $object_type;
}
/**
* Registers a metadata item with the given key and arguments.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @param array<string, mixed> $args Meta key registration arguments.
* @return bool True on success, false on failure.
*/
public function register( string $key, array $args ): bool {
return register_meta( $this->object_type, $key, $args );
}
/**
* Checks whether a metadata item with the given key is registered.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @return bool True if the metadata item is registered, false otherwise.
*/
public function is_registered( string $key ): bool {
return registered_meta_key_exists( $this->object_type, $key );
}
/**
* Gets the registered metadata item for the given key from the registry.
*
* @since 0.1.0
*
* @param string $key Meta key.
* @return object|null The registered metadata definition, or `null` if not registered.
*/
public function get_registered( string $key ) {
$registered = get_registered_meta_keys( $this->object_type );
if ( ! isset( $registered[ $key ] ) ) {
return null;
}
return (object) $registered[ $key ];
}
/**
* Gets all metadata items from the registry.
*
* @since 0.1.0
*
* @return array<string, mixed> Associative array of keys and their metadata definitions, or empty array if nothing
* is registered.
*/
public function get_all_registered(): array {
return get_registered_meta_keys( $this->object_type );
}
}

View File

@@ -0,0 +1,194 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Repository
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\Entity_Key_Value_Repository;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Contracts\With_Single;
/**
* Class for a repository of WordPress metadata.
*
* @since 0.1.0
*/
class Meta_Repository implements Entity_Key_Value_Repository, With_Single {
/**
* Object type.
*
* @since 0.1.0
* @var string
*/
protected $object_type;
/**
* Single config as $key => $single pairs.
*
* @since 0.1.0
* @var array<string, bool>
*/
private $single_config = array();
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $object_type Object type.
*/
public function __construct( string $object_type ) {
$this->object_type = $object_type;
}
/**
* Checks whether a value for the given entity and meta key exists in the database.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Meta key.
* @return bool True if a value for the meta key exists, false otherwise.
*/
public function exists( int $entity_id, string $key ): bool {
return metadata_exists( $this->object_type, $entity_id, $key );
}
/**
* Gets the value for a given entity and meta key from the database.
*
* Always returns a single value.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Meta key.
* @param mixed $default Optional. Value to return if no value exists for the meta key. Default null.
* @return mixed Value for the meta key, or the default if no value exists.
*/
public function get( int $entity_id, string $key, $default = null ) {
if ( ! metadata_exists( $this->object_type, $entity_id, $key ) ) {
// If not single, ensure the default is within an array.
if ( ! $this->get_single( $key ) ) {
if ( null !== $default ) {
return array( $default );
}
return array();
}
return $default;
}
return get_metadata( $this->object_type, $entity_id, $key, $this->get_single( $key ) );
}
/**
* Updates the value for a given entity and meta key in the database.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Meta key.
* @param mixed $value New value to set for the meta key.
* @return bool True on success, false on failure.
*/
public function update( int $entity_id, string $key, $value ): bool {
/*
* If multiple values, delete the original ones first and then add the new ones individually, but only if the
* passed value is an indexed (not associative) array.
* There is only one caveat with this, but that is an edge-case: If the individual values of a multi-value meta
* key are themselves indexed arrays, this can lead to unexpected behavior with this implementation. A
* workaround would be to wrap them in another array before passing them to this method.
*/
if ( ! $this->get_single( $key ) && wp_is_numeric_array( $value ) ) {
delete_metadata( $this->object_type, $entity_id, $key );
foreach ( $value as $single_value ) {
add_metadata( $this->object_type, $entity_id, $key, $single_value );
}
return true;
}
return (bool) update_metadata( $this->object_type, $entity_id, $key, $value );
}
/**
* Deletes the data for a given entity and meta key from the database.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @param string $key Meta key.
* @return bool True on success, false on failure.
*/
public function delete( int $entity_id, string $key ): bool {
return (bool) delete_metadata( $this->object_type, $entity_id, $key );
}
/**
* Deletes all data for the given entity from the database.
*
* @since 0.1.0
*
* @param int $entity_id Entity ID.
* @return bool True on success, false on failure.
*/
public function delete_all( int $entity_id ): bool {
global $wpdb;
$table_name = $this->object_type . 'meta';
$table_col = $this->object_type . '_id';
$meta_ids = $wpdb->get_col(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT meta_id FROM {$wpdb->$table_name} WHERE $table_col = %d ",
$entity_id
)
);
foreach ( $meta_ids as $mid ) {
delete_metadata_by_mid( $this->object_type, $mid );
}
return true;
}
/**
* Updates the metadata caches for the given entity IDs.
*
* @since 0.1.0
*
* @param int[] $entity_ids Entity IDs.
* @return bool True on success, or false on failure.
*/
public function prime_caches( array $entity_ids ): bool {
return (bool) update_meta_cache( $this->object_type, $entity_ids );
}
/**
* Gets the 'single' config for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool Whether or not the item has a single value.
*/
public function get_single( string $key ): bool {
// The default value is true.
return $this->single_config[ $key ] ?? true;
}
/**
* Sets the 'single' config for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param bool $single Item 'single' config.
*/
public function set_single( string $key, bool $single ): void {
$this->single_config[ $key ] = $single;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Interface Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Contracts\With_Autoload_Config
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Contracts;
/**
* Interface for a key-value pair repository with autoload support.
*
* @since 0.1.0
*/
interface With_Autoload_Config {
/**
* Gets the autoload config for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @return bool|null Whether or not the item should be autoloaded, or null if not specified.
*/
public function get_autoload_config( string $key );
/**
* Sets the autoload config for a given key in the repository.
*
* @since 0.1.0
*
* @param string $key Item key.
* @param bool $autoload Item autoload config.
*/
public function set_autoload_config( string $key, bool $autoload ): void;
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Registration_Args;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Generic_Key_Value;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Traits\Cast_Value_By_Type;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Contracts\With_Autoload_Config;
/**
* Class representing a WordPress option.
*
* @since 0.1.0
*/
class Option extends Generic_Key_Value implements With_Registration_Args {
use Cast_Value_By_Type;
/**
* Option registration arguments.
*
* @since 0.1.0
* @var array<string, mixed>
*/
protected $registration_args;
/**
* Constructor.
*
* @since 0.1.0
*
* @param Key_Value_Repository $repository Repository used for the option.
* @param string $key Option key.
* @param array<string, mixed> $registration_args Optional. Option registration arguments. Default empty array.
*/
public function __construct( Key_Value_Repository $repository, string $key, array $registration_args = array() ) {
// Extract default value from registration arguments if passed.
$default = $registration_args['default'] ?? null;
// Set autoload value from registration arguments if passed.
if ( $repository instanceof With_Autoload_Config && isset( $registration_args['autoload'] ) ) {
$repository->set_autoload_config( $key, (bool) $registration_args['autoload'] );
}
// Unset autoload value in registration arguments, since it is not used by WordPress.
unset( $registration_args['autoload'] );
parent::__construct( $repository, $key, $default );
$this->registration_args = $registration_args;
}
/**
* Checks whether the option has a value set.
*
* @since 0.1.0
*
* @return bool True if a value is set, false otherwise.
*/
public function has_value(): bool {
return parent::has_value();
}
/**
* Gets the value for the option.
*
* @since 0.1.0
*
* @return mixed Value for the option.
*/
public function get_value() {
$value = parent::get_value();
if ( isset( $this->registration_args['type'] ) ) {
return $this->cast_value_by_type( $value, $this->registration_args['type'] );
}
return $value;
}
/**
* Updates the value for the option.
*
* @since 0.1.0
*
* @param mixed $value New value to set for the option.
* @return bool True on success, false on failure.
*/
public function update_value( $value ): bool {
return parent::update_value( $value );
}
/**
* Deletes the data for the option.
*
* @since 0.1.0
*
* @return bool True on success, false on failure.
*/
public function delete_value(): bool {
return parent::delete_value();
}
/**
* Gets the key of the option.
*
* @since 0.1.0
*
* @return string Option key.
*/
public function get_key(): string {
return parent::get_key();
}
/**
* Gets the registration arguments for the option.
*
* @since 0.1.0
*
* @return array<string, mixed> Option registration arguments.
*/
public function get_registration_args(): array {
return $this->registration_args;
}
}

View File

@@ -0,0 +1,198 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Container
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options;
use ArrayAccess;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Invalid_Type_Exception;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Exception\Not_Found_Exception;
/**
* Class for an option container.
*
* @since 0.1.0
*/
class Option_Container implements Container, ArrayAccess {
/**
* Options stored in the container.
*
* @since 0.1.0
* @var array<string, callable>
*/
private $options = array();
/**
* Option instances already created.
*
* @since 0.1.0
* @var array<string, Option>
*/
private $instances = array();
/**
* Checks if an option for the given key exists in the container.
*
* @since 0.1.0
*
* @param string $key Option key.
* @return bool True if the option exists in the container, false otherwise.
*/
public function has( string $key ): bool {
return isset( $this->options[ $key ] );
}
/**
* Gets the option for the given key from the container.
*
* @since 0.1.0
*
* @param string $key Option key.
* @return Option Option for the given key.
*
* @throws Not_Found_Exception Thrown when option with given key is not found.
* @throws Invalid_Type_Exception Thrown when option with given key has invalid type.
*/
public function get( string $key ) {
if ( ! isset( $this->options[ $key ] ) ) {
throw new Not_Found_Exception(
esc_html(
sprintf(
/* translators: %s: option key */
__( 'Option with key %s was not found in container', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
if ( ! isset( $this->instances[ $key ] ) ) {
$instance = $this->options[ $key ]( $this );
if ( ! $instance instanceof Option ) {
throw new Invalid_Type_Exception(
esc_html(
sprintf(
/* translators: %s: option key */
__( 'Option with key %s is not of type Option', 'wp-oop-plugin-lib' ),
$key
)
)
);
}
$this->instances[ $key ] = $instance;
}
return $this->instances[ $key ];
}
/**
* Sets the given option under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Option key.
* @param callable $creator Option creator closure.
*/
public function set( string $key, callable $creator ): void {
$this->options[ $key ] = $creator;
unset( $this->instances[ $key ] );
}
/**
* Sets an option using the given repository and arguments under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Option key.
* @param Key_Value_Repository $repository Repository used for the option.
* @param array<string, mixed> $registration_args Optional. Option registration arguments. Default empty array.
*/
public function set_by_args( string $key, Key_Value_Repository $repository, array $registration_args = array() ): void { // phpcs:ignore Generic.Files.LineLength.TooLong
$this->set(
$key,
function () use ( $repository, $key, $registration_args ) {
return new Option( $repository, $key, $registration_args );
}
);
}
/**
* Unsets the option under the given key in the container.
*
* @since 0.1.0
*
* @param string $key Option key.
*/
public function unset( string $key ): void {
unset( $this->options[ $key ], $this->instances[ $key ] );
}
/**
* Gets all keys in the container.
*
* @since 0.1.0
*
* @return string[] List of keys.
*/
public function get_keys(): array {
return array_keys( $this->options );
}
/**
* Checks if an option for the given key exists in the container.
*
* @since 0.1.0
*
* @param mixed $key Option key.
* @return bool True if the option exists in the container, false otherwise.
*/
#[\ReturnTypeWillChange]
public function offsetExists( $key ) {
return $this->has( $key );
}
/**
* Gets the option for the given key from the container.
*
* @since 0.1.0
*
* @param mixed $key Option key.
* @return Option Option for the given key.
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return $this->get( $key );
}
/**
* Sets the given option under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Option key.
* @param mixed $value Option creator closure.
*/
#[\ReturnTypeWillChange]
public function offsetSet( $key, $value ) {
$this->set( $key, $value );
}
/**
* Unsets the option under the given key in the container.
*
* @since 0.1.0
*
* @param mixed $key Option key.
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $key ) {
$this->unset( $key );
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Hook_Registrar
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Hook_Registrar;
/**
* Class that adds the relevant hook to register WordPress options.
*
* @since 0.1.0
*/
class Option_Hook_Registrar implements Hook_Registrar {
/**
* WordPress option registry instance.
*
* @since 0.1.0
* @var Option_Registry
*/
private $registry;
/**
* Constructor.
*
* @param Option_Registry $registry WordPress option registry instance.
*/
public function __construct( Option_Registry $registry ) {
$this->registry = $registry;
}
/**
* Adds a callback that registers the options to the relevant hook.
*
* The callback receives a registry instance as the sole parameter, allowing to call the
* {@see Option_Registry::register()} method.
*
* @since 0.1.0
*
* @param callable $register_callback Callback to register the options.
*/
public function add_register_callback( callable $register_callback ): void {
add_action(
'init',
function () use ( $register_callback ) {
$register_callback( $this->registry );
}
);
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Registry
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Registry;
/**
* Class for a registry of WordPress options.
*
* @since 0.1.0
*/
class Option_Registry implements Registry {
/**
* Default option group to use.
*
* @since 0.1.0
* @var string
*/
private $default_group;
/**
* Constructor.
*
* @since 0.1.0
*
* @param string $default_group Default option group to use.
*/
public function __construct( string $default_group ) {
$this->default_group = $default_group;
}
/**
* Registers an option with the given key and arguments.
*
* If the arguments include a 'group' key, that value will be used as the option group as used by WordPress core.
* Otherwise, the default group will be used.
*
* @since 0.1.0
*
* @param string $key Option key.
* @param array<string, mixed> $args Option registration arguments.
* @return bool True on success, false on failure.
*/
public function register( string $key, array $args ): bool {
// Use provided group, or default group otherwise.
$group = $args['group'] ?? $this->default_group;
register_setting( $group, $key, $args );
return true;
}
/**
* Checks whether an option with the given key is registered.
*
* @since 0.1.0
*
* @param string $key Option key.
* @return bool True if the option is registered, false otherwise.
*/
public function is_registered( string $key ): bool {
$registered = get_registered_settings();
return isset( $registered[ $key ] );
}
/**
* Gets the registered option for the given key from the registry.
*
* @since 0.1.0
*
* @param string $key Option key.
* @return object|null The registered option definition, or `null` if not registered.
*/
public function get_registered( string $key ) {
$registered = get_registered_settings();
return $registered[ $key ] ?? null;
}
/**
* Gets all options from the registry.
*
* @since 0.1.0
*
* @return array<string, mixed> Associative array of keys and their option definitions, or empty array if nothing
* is registered.
*/
public function get_all_registered(): array {
return get_registered_settings();
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Class Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository
*
* @since 0.1.0
* @package wp-oop-plugin-lib
*/
namespace Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Contracts\With_Autoload_Config;
/**
* Class for a repository of WordPress options.
*
* @since 0.1.0
*/
class Option_Repository implements Key_Value_Repository, With_Autoload_Config {
/**
* Autoload config as $key => $autoload pairs.
*
* @since 0.1.0
* @var array<string, bool>
*/
private $autoload_config = array();
/**
* Checks whether a value for the given option exists in the database.
*
* @since 0.1.0
*
* @param string $key Option key.
* @return bool True if a value for the option exists, false otherwise.
*/
public function exists( string $key ): bool {
$value = get_option( $key, null );
return null !== $value;
}
/**
* Gets the value for a given option from the database.
*
* @since 0.1.0
*
* @param string $key Option key.
* @param mixed $default Optional. Value to return if no value exists for the option. Default null.
* @return mixed Value for the option, or the default if no value exists.
*/
public function get( string $key, $default = null ) {
return get_option( $key, $default );
}
/**
* Updates the value for a given option in the database.
*
* @since 0.1.0
*
* @param string $key Option key.
* @param mixed $value New value to set for the option.
* @return bool True on success, false on failure.
*/
public function update( string $key, $value ): bool {
$autoload = $this->get_autoload_config( $key );
// Warn if no autoload config is set.
if ( null === $autoload ) {
$message = __( 'Updating an option without having an autoload value specified is discouraged.', 'wp-oop-plugin-lib' ); // phpcs:ignore Generic.Files.LineLength.TooLong
$message .= ' ' . sprintf(
/* translators: 1: Method name, 2: Argument name, 3: Method name */
__( 'Use the %1$s method or pass the "%2$s" argument to the %3$s to specify an autoload value.', 'wp-oop-plugin-lib' ), // phpcs:ignore Generic.Files.LineLength.TooLong
__CLASS__ . '::set_autoload_config()',
'autoload',
Option::class . '::__construct()'
);
_doing_it_wrong(
__METHOD__,
esc_html( $message ),
''
);
}
return (bool) update_option( $key, $value, $autoload );
}
/**
* Deletes the data for a given option from the database.
*
* @since 0.1.0
*
* @param string $key Option key.
* @return bool True on success, false on failure.
*/
public function delete( string $key ): bool {
return (bool) delete_option( $key );
}
/**
* Gets the autoload config for a given option in the database.
*
* @since 0.1.0
*
* @param string $key Option key.
* @return bool|null Whether or not the item should be autoloaded, or null if not specified.
*/
public function get_autoload_config( string $key ) {
// The default value is true.
return $this->autoload_config[ $key ] ?? null;
}
/**
* Sets the autoload config for a given option in the database.
*
* @since 0.1.0
*
* @param string $key Option key.
* @param bool $autoload Option autoload config.
*/
public function set_autoload_config( string $key, bool $autoload ): void {
$this->autoload_config[ $key ] = $autoload;
}
}