Skip to content

Commit

Permalink
Merge pull request #6 from jjgrainger/feature/add-scopes-methods
Browse files Browse the repository at this point in the history
add addScopes method
  • Loading branch information
jjgrainger committed Jun 26, 2022
2 parents 153e1a0 + 1e96efc commit b6fb017
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 62 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

### v0.2.0

* Add `Query::addScope` method for custom scopes
* Create `BootableTraits` trait
* Create `QueriesPosts` trait
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# WordPress Query Builder v0.1.0
# WordPress Query Builder v0.2.0

> A fluent interface for creating WordPress Queries
Expand Down Expand Up @@ -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 );
}
}
```
Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"Query\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Query\\Tests\\": "tests/"
}
},
"require": {
"php": ">=7.2"
},
Expand Down
33 changes: 33 additions & 0 deletions src/Concerns/BootsTraits.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Query\Concerns;

use ReflectionClass;

trait BootsTraits
{
/**
* Bootstrap bootable traits.
*
* @return void
*/
public function bootTraits()
{
// Get traits associated to the class.
$traits = (new ReflectionClass(static::class))->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}();
}
}
}
83 changes: 59 additions & 24 deletions src/Concerns/HasScopes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
}
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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;
}
}
41 changes: 41 additions & 0 deletions src/Concerns/QueriesPosts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Query\Concerns;

trait QueriesPosts
{
/**
* Add Post Scopes to the query on boot.
*
* @var array
*/
protected function bootQueriesPosts()
{
$scopes = [
new \Query\Scopes\Author,
new \Query\Scopes\AuthorIn,
new \Query\Scopes\AuthorNotIn,
new \Query\Scopes\Comments,
new \Query\Scopes\Meta,
new \Query\Scopes\Order,
new \Query\Scopes\OrderBy,
new \Query\Scopes\Page,
new \Query\Scopes\ParentIn,
new \Query\Scopes\ParentNotIn,
new \Query\Scopes\Password,
new \Query\Scopes\Post,
new \Query\Scopes\PostIn,
new \Query\Scopes\PostNotIn,
new \Query\Scopes\PostParent,
new \Query\Scopes\PostStatus,
new \Query\Scopes\PostType,
new \Query\Scopes\PostsPerPage,
new \Query\Scopes\Search,
new \Query\Scopes\Taxonomy,
];

foreach ($scopes as $scope) {
$this->addScope($scope);
}
}
}
39 changes: 9 additions & 30 deletions src/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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));
}

Expand Down
36 changes: 36 additions & 0 deletions tests/Unit/Query/Concerns/HasScopesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use Query\Query;
use Query\Builder;
use Query\Scope;
use PHPUnit\Framework\TestCase;

class HasScopesTest extends TestCase
{
public function test_query_has_bootable_scopes()
{
$result = Query::search('test')->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);
}
}
Loading

0 comments on commit b6fb017

Please sign in to comment.