Skip to content

Commit b6fb017

Browse files
authored
Merge pull request #6 from jjgrainger/feature/add-scopes-methods
add addScopes method
2 parents 153e1a0 + 1e96efc commit b6fb017

10 files changed

Lines changed: 264 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Changelog
2+
3+
### v0.2.0
4+
5+
* Add `Query::addScope` method for custom scopes
6+
* Create `BootableTraits` trait
7+
* Create `QueriesPosts` trait

README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# WordPress Query Builder v0.1.0
1+
# WordPress Query Builder v0.2.0
22

33
> A fluent interface for creating WordPress Queries
44
@@ -57,13 +57,15 @@ class FeaturedPostsQuery extends Query
5757
public function setup( Builder $builder ): Builder
5858
{
5959
// Setup a tax_query for posts with the 'featured' term.
60-
$featured = [
61-
'taxonomy' => 'featured',
62-
'fields' => 'slugs',
63-
'terms' => [ 'featured' ],
60+
$tax_query = [
61+
[
62+
'taxonomy' => 'featured',
63+
'fields' => 'slugs',
64+
'terms' => [ 'featured' ],
65+
],
6466
];
6567

66-
return $builder->taxonomy( $featured );
68+
return $builder->where( 'tax_query', $tax_query );
6769
}
6870
}
6971
```
@@ -90,6 +92,44 @@ $query = new Featured( $args );
9092
$results = $query->limit( 3 )->get();
9193
```
9294

95+
### Custom Scopes
96+
97+
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.
98+
99+
```php
100+
// Create a new scope with a closure.
101+
Query::addScope( 'events', function( Builder $builder ) {
102+
return $builder->where( 'post_type', 'event' );
103+
} );
104+
105+
// Call the scope when needed.
106+
$results = Query::events()->limit( 3 );
107+
```
108+
109+
#### Custom Scope Classes
110+
111+
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.
112+
The `apply` method should accept the query `Builder` as the first argument and any optional arguments passed via the scope.
113+
Once added to the `Query` class the scope will be available by the class name with the first letter lowecase.
114+
115+
```php
116+
// Create a custom scope class.
117+
use Query\Scope;
118+
use Query\Builder;
119+
120+
class PostID implements Scope {
121+
public function apply( Builder $builder, $id = null ) {
122+
return $builder->where( 'p', $id );
123+
}
124+
}
125+
126+
// Add the scope to the Query.
127+
Query::addScope( new PostID );
128+
129+
// Use the scope in the Query.
130+
$results = Query::postID( 123 )->get();
131+
```
132+
93133
## Notes
94134

95135
* The library is still in active development and not intended for production use.

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"Query\\": "src/"
1616
}
1717
},
18+
"autoload-dev": {
19+
"psr-4": {
20+
"Query\\Tests\\": "tests/"
21+
}
22+
},
1823
"require": {
1924
"php": ">=7.2"
2025
},

src/Concerns/BootsTraits.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Query\Concerns;
4+
5+
use ReflectionClass;
6+
7+
trait BootsTraits
8+
{
9+
/**
10+
* Bootstrap bootable traits.
11+
*
12+
* @return void
13+
*/
14+
public function bootTraits()
15+
{
16+
// Get traits associated to the class.
17+
$traits = (new ReflectionClass(static::class))->getTraitNames();
18+
19+
// Loop over traits and call their bootable method.
20+
foreach ($traits as $trait) {
21+
// Create the bootable method string.
22+
$method = 'boot' . (new ReflectionClass($trait))->getShortName();
23+
24+
// Skip, if no bootable method available.
25+
if (!method_exists($this, $method)) {
26+
continue;
27+
}
28+
29+
// Call the bootable method.
30+
$this->{$method}();
31+
}
32+
}
33+
}

src/Concerns/HasScopes.php

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44

55
use Query\Scope;
66
use Query\Builder;
7+
use Closure;
78
use ReflectionClass;
89

910
trait HasScopes
1011
{
12+
/**
13+
* Scopes added to the Query.
14+
*
15+
* @var array
16+
*/
17+
private static $scopes = [];
18+
1119
/**
1220
* The available scope objects.
1321
*
@@ -23,48 +31,69 @@ trait HasScopes
2331
private $aliases = [];
2432

2533
/**
26-
* Setup scopes.
34+
* Add a scope to the Query.
35+
*
36+
* @param \Query\Scope|\Closure|string $scope
37+
* @param \Query\Scope|\Closure|null $implementation
2738
*
2839
* @return void
2940
*/
30-
protected function buildScopes(array $scopes)
41+
public static function addScope($scope, $implementation = null)
3142
{
32-
array_walk($scopes, [$this, 'setupScope']);
43+
if (is_string($scope) && $implementation instanceof Closure) {
44+
static::$scopes[$scope] = $implementation;
45+
} elseif ($scope instanceof Scope) {
46+
static::$scopes[static::getScopeKey($scope)] = $scope;
47+
}
3348
}
3449

3550
/**
36-
* Setup each individual scope.
51+
* Get the scope key, classname with first letter lowercase.
3752
*
38-
* @param string $scope
53+
* @param Scope $scope
3954
*
40-
* @return void
55+
* @return string
4156
*/
42-
protected function setupScope(string $scope)
57+
protected static function getScopeKey(Scope $scope): string
4358
{
44-
// Create scope key from class name.
45-
$key = $this->getScopeKey($scope);
46-
47-
// Instantiate and add available scope.
48-
$this->available[$key] = new $scope;
49-
50-
// Add scopes aliases.
51-
$aliases = $this->getScopeAliases($this->available[$key]);
59+
return lcfirst((new ReflectionClass($scope))->getShortName());
60+
}
5261

53-
foreach ($aliases as $alias) {
54-
$this->aliases[$alias] = $key;
62+
/**
63+
* Setup scopes.
64+
*
65+
* @return void
66+
*/
67+
protected function buildScopes()
68+
{
69+
foreach (static::$scopes as $scope => $implementation) {
70+
$this->setupScope($scope, $implementation);
5571
}
5672
}
5773

5874
/**
59-
* Get the scope key, classname with first letter lowercase.
75+
* Setup each individual scope.
6076
*
61-
* @param string $scope
77+
* @param string $scope
78+
* @param Query\Scope|\Closure $implementation
6279
*
63-
* @return string
80+
* @return void
6481
*/
65-
protected function getScopeKey(string $scope): string
82+
protected function setupScope(string $scope, $implementation)
6683
{
67-
return lcfirst((new ReflectionClass($scope))->getShortName());
84+
if ($implementation instanceof Scope) {
85+
// Instantiate and add available scope.
86+
$this->available[$scope] = new $implementation;
87+
88+
// Add scopes aliases.
89+
$aliases = $this->getScopeAliases($this->available[$scope]);
90+
91+
foreach ($aliases as $alias) {
92+
$this->aliases[$alias] = $scope;
93+
}
94+
} else {
95+
$this->available[$scope] = $implementation;
96+
}
6897
}
6998

7099
/**
@@ -109,7 +138,7 @@ protected function hasScope(string $scope): bool
109138
*/
110139
protected function callScope(string $scope, array $arguments = []): Builder
111140
{
112-
return call_user_func_array([$this->resolveScope($scope), 'apply'], $arguments);
141+
return call_user_func_array($this->resolveScope($scope), $arguments);
113142
}
114143

115144
/**
@@ -126,6 +155,12 @@ protected function resolveScope(string $scope)
126155
$scope = $this->aliases[$scope];
127156
}
128157

129-
return $this->available[$scope];
158+
$callable = $this->available[$scope];
159+
160+
if ($callable instanceof Scope) {
161+
return [$callable, 'apply'];
162+
}
163+
164+
return $callable;
130165
}
131166
}

src/Concerns/QueriesPosts.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Query\Concerns;
4+
5+
trait QueriesPosts
6+
{
7+
/**
8+
* Add Post Scopes to the query on boot.
9+
*
10+
* @var array
11+
*/
12+
protected function bootQueriesPosts()
13+
{
14+
$scopes = [
15+
new \Query\Scopes\Author,
16+
new \Query\Scopes\AuthorIn,
17+
new \Query\Scopes\AuthorNotIn,
18+
new \Query\Scopes\Comments,
19+
new \Query\Scopes\Meta,
20+
new \Query\Scopes\Order,
21+
new \Query\Scopes\OrderBy,
22+
new \Query\Scopes\Page,
23+
new \Query\Scopes\ParentIn,
24+
new \Query\Scopes\ParentNotIn,
25+
new \Query\Scopes\Password,
26+
new \Query\Scopes\Post,
27+
new \Query\Scopes\PostIn,
28+
new \Query\Scopes\PostNotIn,
29+
new \Query\Scopes\PostParent,
30+
new \Query\Scopes\PostStatus,
31+
new \Query\Scopes\PostType,
32+
new \Query\Scopes\PostsPerPage,
33+
new \Query\Scopes\Search,
34+
new \Query\Scopes\Taxonomy,
35+
];
36+
37+
foreach ($scopes as $scope) {
38+
$this->addScope($scope);
39+
}
40+
}
41+
}

src/Query.php

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,13 @@
33
namespace Query;
44

55
use Query\Builder;
6+
use Query\Concerns\BootsTraits;
67
use Query\Concerns\HasScopes;
8+
use Query\Concerns\QueriesPosts;
79

810
class Query
911
{
10-
use HasScopes;
11-
12-
/**
13-
* Scopes available to the query.
14-
*
15-
* @var array
16-
*/
17-
protected $scopes = [
18-
\Query\Scopes\Author::class,
19-
\Query\Scopes\AuthorIn::class,
20-
\Query\Scopes\AuthorNotIn::class,
21-
\Query\Scopes\Comments::class,
22-
\Query\Scopes\Meta::class,
23-
\Query\Scopes\Order::class,
24-
\Query\Scopes\OrderBy::class,
25-
\Query\Scopes\Page::class,
26-
\Query\Scopes\ParentIn::class,
27-
\Query\Scopes\ParentNotIn::class,
28-
\Query\Scopes\Password::class,
29-
\Query\Scopes\Post::class,
30-
\Query\Scopes\PostIn::class,
31-
\Query\Scopes\PostNotIn::class,
32-
\Query\Scopes\PostParent::class,
33-
\Query\Scopes\PostStatus::class,
34-
\Query\Scopes\PostType::class,
35-
\Query\Scopes\PostsPerPage::class,
36-
\Query\Scopes\Search::class,
37-
\Query\Scopes\Taxonomy::class,
38-
];
12+
use BootsTraits, HasScopes, QueriesPosts;
3913

4014
/**
4115
* The Query Builder object.
@@ -61,8 +35,13 @@ class Query
6135
*/
6236
public function __construct(array $query = [])
6337
{
64-
$this->buildScopes($this->scopes);
38+
// Boot traits.
39+
$this->bootTraits();
40+
41+
// Build scopes.
42+
$this->buildScopes();
6543

44+
// Setup the Builder instances.
6645
$this->builder = $this->setup(new Builder($query));
6746
}
6847

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
use Query\Query;
4+
use Query\Builder;
5+
use Query\Scope;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class HasScopesTest extends TestCase
9+
{
10+
public function test_query_has_bootable_scopes()
11+
{
12+
$result = Query::search('test')->post_type('event')->getParameters();
13+
14+
$this->assertEquals(['s' => 'test', 'post_type' => 'event'], $result);
15+
}
16+
17+
public function test_query_can_add_scopes_with_scope_class()
18+
{
19+
Query::addScope(new \Query\Tests\Unit\Query\Stubs\TestScope);
20+
21+
$result = Query::test(true)->getParameters();
22+
23+
$this->assertEquals(['test' => true], $result);
24+
}
25+
26+
public function test_query_can_add_scopes_with_closure()
27+
{
28+
Query::addScope('test', function (Builder $builder, $var) {
29+
return $builder->where('test', $var);
30+
});
31+
32+
$result = Query::test(true)->getParameters();
33+
34+
$this->assertEquals(['test' => true], $result);
35+
}
36+
}

0 commit comments

Comments
 (0)