diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2cd68eb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +### v0.2.0 + +* Add `Query::addScope` method for custom scopes +* Create `BootableTraits` trait +* Create `QueriesPosts` trait diff --git a/README.md b/README.md index f04dc98..0abdf52 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# WordPress Query Builder v0.1.0 +# WordPress Query Builder v0.2.0 > A fluent interface for creating WordPress Queries @@ -57,13 +57,15 @@ class FeaturedPostsQuery extends Query public function setup( Builder $builder ): Builder { // Setup a tax_query for posts with the 'featured' term. - $featured = [ - 'taxonomy' => 'featured', - 'fields' => 'slugs', - 'terms' => [ 'featured' ], + $tax_query = [ + [ + 'taxonomy' => 'featured', + 'fields' => 'slugs', + 'terms' => [ 'featured' ], + ], ]; - return $builder->taxonomy( $featured ); + return $builder->where( 'tax_query', $tax_query ); } } ``` @@ -90,6 +92,44 @@ $query = new Featured( $args ); $results = $query->limit( 3 )->get(); ``` +### Custom Scopes + +Custom scopes can be added to the global `Query` using the static `addScope` method. One of the simplest ways to add a scope is with a closure. + +```php +// Create a new scope with a closure. +Query::addScope( 'events', function( Builder $builder ) { + return $builder->where( 'post_type', 'event' ); +} ); + +// Call the scope when needed. +$results = Query::events()->limit( 3 ); +``` + +#### Custom Scope Classes + +Custom scope classes can be added to the global `Query`. The custom scope class will need to implement the `Scope` interface and contain the required `apply` method. +The `apply` method should accept the query `Builder` as the first argument and any optional arguments passed via the scope. +Once added to the `Query` class the scope will be available by the class name with the first letter lowecase. + +```php +// Create a custom scope class. +use Query\Scope; +use Query\Builder; + +class PostID implements Scope { + public function apply( Builder $builder, $id = null ) { + return $builder->where( 'p', $id ); + } +} + +// Add the scope to the Query. +Query::addScope( new PostID ); + +// Use the scope in the Query. +$results = Query::postID( 123 )->get(); +``` + ## Notes * The library is still in active development and not intended for production use. diff --git a/composer.json b/composer.json index 1c94d88..de50708 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,11 @@ "Query\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Query\\Tests\\": "tests/" + } + }, "require": { "php": ">=7.2" }, diff --git a/src/Concerns/BootsTraits.php b/src/Concerns/BootsTraits.php new file mode 100644 index 0000000..1989069 --- /dev/null +++ b/src/Concerns/BootsTraits.php @@ -0,0 +1,33 @@ +getTraitNames(); + + // Loop over traits and call their bootable method. + foreach ($traits as $trait) { + // Create the bootable method string. + $method = 'boot' . (new ReflectionClass($trait))->getShortName(); + + // Skip, if no bootable method available. + if (!method_exists($this, $method)) { + continue; + } + + // Call the bootable method. + $this->{$method}(); + } + } +} diff --git a/src/Concerns/HasScopes.php b/src/Concerns/HasScopes.php index 22d0da6..d6398b0 100644 --- a/src/Concerns/HasScopes.php +++ b/src/Concerns/HasScopes.php @@ -4,10 +4,18 @@ use Query\Scope; use Query\Builder; +use Closure; use ReflectionClass; trait HasScopes { + /** + * Scopes added to the Query. + * + * @var array + */ + private static $scopes = []; + /** * The available scope objects. * @@ -23,48 +31,69 @@ trait HasScopes private $aliases = []; /** - * Setup scopes. + * Add a scope to the Query. + * + * @param \Query\Scope|\Closure|string $scope + * @param \Query\Scope|\Closure|null $implementation * * @return void */ - protected function buildScopes(array $scopes) + public static function addScope($scope, $implementation = null) { - array_walk($scopes, [$this, 'setupScope']); + if (is_string($scope) && $implementation instanceof Closure) { + static::$scopes[$scope] = $implementation; + } elseif ($scope instanceof Scope) { + static::$scopes[static::getScopeKey($scope)] = $scope; + } } /** - * Setup each individual scope. + * Get the scope key, classname with first letter lowercase. * - * @param string $scope + * @param Scope $scope * - * @return void + * @return string */ - protected function setupScope(string $scope) + protected static function getScopeKey(Scope $scope): string { - // Create scope key from class name. - $key = $this->getScopeKey($scope); - - // Instantiate and add available scope. - $this->available[$key] = new $scope; - - // Add scopes aliases. - $aliases = $this->getScopeAliases($this->available[$key]); + return lcfirst((new ReflectionClass($scope))->getShortName()); + } - foreach ($aliases as $alias) { - $this->aliases[$alias] = $key; + /** + * Setup scopes. + * + * @return void + */ + protected function buildScopes() + { + foreach (static::$scopes as $scope => $implementation) { + $this->setupScope($scope, $implementation); } } /** - * Get the scope key, classname with first letter lowercase. + * Setup each individual scope. * - * @param string $scope + * @param string $scope + * @param Query\Scope|\Closure $implementation * - * @return string + * @return void */ - protected function getScopeKey(string $scope): string + protected function setupScope(string $scope, $implementation) { - return lcfirst((new ReflectionClass($scope))->getShortName()); + if ($implementation instanceof Scope) { + // Instantiate and add available scope. + $this->available[$scope] = new $implementation; + + // Add scopes aliases. + $aliases = $this->getScopeAliases($this->available[$scope]); + + foreach ($aliases as $alias) { + $this->aliases[$alias] = $scope; + } + } else { + $this->available[$scope] = $implementation; + } } /** @@ -109,7 +138,7 @@ protected function hasScope(string $scope): bool */ protected function callScope(string $scope, array $arguments = []): Builder { - return call_user_func_array([$this->resolveScope($scope), 'apply'], $arguments); + return call_user_func_array($this->resolveScope($scope), $arguments); } /** @@ -126,6 +155,12 @@ protected function resolveScope(string $scope) $scope = $this->aliases[$scope]; } - return $this->available[$scope]; + $callable = $this->available[$scope]; + + if ($callable instanceof Scope) { + return [$callable, 'apply']; + } + + return $callable; } } diff --git a/src/Concerns/QueriesPosts.php b/src/Concerns/QueriesPosts.php new file mode 100644 index 0000000..439d5cc --- /dev/null +++ b/src/Concerns/QueriesPosts.php @@ -0,0 +1,41 @@ +addScope($scope); + } + } +} diff --git a/src/Query.php b/src/Query.php index d98b890..9023e64 100644 --- a/src/Query.php +++ b/src/Query.php @@ -3,39 +3,13 @@ namespace Query; use Query\Builder; +use Query\Concerns\BootsTraits; use Query\Concerns\HasScopes; +use Query\Concerns\QueriesPosts; class Query { - use HasScopes; - - /** - * Scopes available to the query. - * - * @var array - */ - protected $scopes = [ - \Query\Scopes\Author::class, - \Query\Scopes\AuthorIn::class, - \Query\Scopes\AuthorNotIn::class, - \Query\Scopes\Comments::class, - \Query\Scopes\Meta::class, - \Query\Scopes\Order::class, - \Query\Scopes\OrderBy::class, - \Query\Scopes\Page::class, - \Query\Scopes\ParentIn::class, - \Query\Scopes\ParentNotIn::class, - \Query\Scopes\Password::class, - \Query\Scopes\Post::class, - \Query\Scopes\PostIn::class, - \Query\Scopes\PostNotIn::class, - \Query\Scopes\PostParent::class, - \Query\Scopes\PostStatus::class, - \Query\Scopes\PostType::class, - \Query\Scopes\PostsPerPage::class, - \Query\Scopes\Search::class, - \Query\Scopes\Taxonomy::class, - ]; + use BootsTraits, HasScopes, QueriesPosts; /** * The Query Builder object. @@ -61,8 +35,13 @@ class Query */ public function __construct(array $query = []) { - $this->buildScopes($this->scopes); + // Boot traits. + $this->bootTraits(); + + // Build scopes. + $this->buildScopes(); + // Setup the Builder instances. $this->builder = $this->setup(new Builder($query)); } diff --git a/tests/Unit/Query/Concerns/HasScopesTest.php b/tests/Unit/Query/Concerns/HasScopesTest.php new file mode 100644 index 0000000..e9990ac --- /dev/null +++ b/tests/Unit/Query/Concerns/HasScopesTest.php @@ -0,0 +1,36 @@ +post_type('event')->getParameters(); + + $this->assertEquals(['s' => 'test', 'post_type' => 'event'], $result); + } + + public function test_query_can_add_scopes_with_scope_class() + { + Query::addScope(new \Query\Tests\Unit\Query\Stubs\TestScope); + + $result = Query::test(true)->getParameters(); + + $this->assertEquals(['test' => true], $result); + } + + public function test_query_can_add_scopes_with_closure() + { + Query::addScope('test', function (Builder $builder, $var) { + return $builder->where('test', $var); + }); + + $result = Query::test(true)->getParameters(); + + $this->assertEquals(['test' => true], $result); + } +} diff --git a/tests/Unit/Query/QueryTest.php b/tests/Unit/Query/QueryTest.php index de92cdb..b3e094b 100644 --- a/tests/Unit/Query/QueryTest.php +++ b/tests/Unit/Query/QueryTest.php @@ -31,7 +31,7 @@ public function test_query_can_proxy_methods_to_builder() { $query = Query::where('limit', 10); - $this->assertEquals(['limit' => 10], $query->getBuilder()->getParameters()); + $this->assertEquals(['limit' => 10], $query->getParameters()); } public function test_query_can_chain_and_proxy_methods_to_builder() @@ -44,7 +44,7 @@ public function test_query_can_chain_and_proxy_methods_to_builder() 'fields' => 'ids', ]; - $this->assertEquals($expected, $query->getBuilder()->getParameters()); + $this->assertEquals($expected, $query->getParameters()); } public function test_query_can_be_passed_through_via_builder_methods() @@ -54,4 +54,12 @@ public function test_query_can_be_passed_through_via_builder_methods() $this->assertNotInstanceOf(Query::class, $parameters); $this->assertEquals(['limit' => 10], $parameters); } + + public function test_query_can_return_builder() + { + $builder = Query::where('limit', 10)->getBuilder(); + + $this->assertInstanceOf(Builder::class, $builder); + $this->assertEquals(['limit' => 10], $builder->getParameters()); + } } diff --git a/tests/Unit/Query/Stubs/TestScope.php b/tests/Unit/Query/Stubs/TestScope.php new file mode 100644 index 0000000..b19370d --- /dev/null +++ b/tests/Unit/Query/Stubs/TestScope.php @@ -0,0 +1,18 @@ +where('test', $value); + } +}