Skip to content

Commit

Permalink
Register query builders from a namespace. (#8)
Browse files Browse the repository at this point in the history
* Add a Schema class to register query builders

Test structure has been slightly refactored to move the builders out of the
feature directory.

* Fix syntax error for PHP <= 7.2

* Remove Schema class in favour of config

Namespace lookup of query builders remains, but now a sensible default is given
so people won't have to create a schema class.

* Update documentation to reflect new changes

* Fix don't load the query builders when loader is resolved
  • Loading branch information
drtheuns authored Jan 16, 2020
1 parent f294fd8 commit fa6cf6f
Show file tree
Hide file tree
Showing 40 changed files with 418 additions and 116 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
},
"require-dev": {
"phpunit/phpunit": "^8.5",
"orchestra/testbench": "^4.0"
"orchestra/testbench": "^4.0",
"mockery/mockery": "^1.3"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions config/apitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,21 @@
'route_prefix' => 'apidoc',

/*
* The registered query builders.
*
* This should be set by the applications that use this library.
* Register the query builders of this project.
*/
'query_builders' => [],
'query_builders' => [
/*
* Individual classes can be registered here. Expects the fully qualified namespace.
*/
'classes' => [
//
],

/*
* Register all the query builders from the given namespaces.
*/
'namespaces' => [
'App\QueryBuilders',
],
],
];
28 changes: 15 additions & 13 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,23 @@ curl localhost:8000/api/users?fields=id

## Documentation

Either create a new config file: `/my_project/config/apitizer.php` or use
`./artisan vendor:publish --provider 'Apitizer\\ServiceProvider'` to publish the
configuration.
If you have followed along with the setup, you can now generate documentation by
starting the webserver with `./artisan serve` and navigating to
`localhost:8000/apidoc`. However, if you used different namespaces, you will
need to register them in the configuration:

Within this config file, add (or modify) the following:

```php
// File: /my_project/config/apitizer.php
```
// File: /project_root/config/apitizer.php
return [
'query_builders' => [
\App\QueryBuilders\UserBuilder::class
],
'classes' => [
// Register individual classes here, for example:
\App\Api\UserBuilder::class,
],
'namespaces' => [
// Register entire namespaces here (non recursive)
'App\Api'
]
]
];
```

Now when you start the webserver (`./artisan serve`) and navigate to
`localhost:8000/apidoc`, you will see the generated documentation, including the
documentation for the user builder.
18 changes: 5 additions & 13 deletions src/Apitizer/Apitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,18 @@

namespace Apitizer;

use Apitizer\QueryBuilder;
use Apitizer\Types\ApidocCollection;

class Apitizer
{
public static function getQueryBuilderDocumentation(): ApidocCollection
public static function getQueryBuilders()
{
return ApidocCollection::forQueryBuilders(
config('apitizer.query_builders', [])
);
return app(QueryBuilderLoader::class)->getQueryBuilders();
}

/**
* @return QueryBuilder[]
*/
public static function getQueryBuilders(): array
public static function getQueryBuilderDocumentation(): ApidocCollection
{
return array_map(function (string $builderClass) {
return new $builderClass;
}, config('apitizer.query_builders', []));
return ApidocCollection::forQueryBuilders(self::getQueryBuilders());
}

public static function getFieldKey()
Expand All @@ -46,7 +38,7 @@ public static function getLimitKey()

public static function getQueryParams()
{
return array_values(config('apitizer.query_parameters', []));
return config('apitizer.query_parameters', []);
}

public static function getRouteUrl(): ?string
Expand Down
16 changes: 16 additions & 0 deletions src/Apitizer/Exceptions/ClassFinderException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Apitizer\Exceptions;

class ClassFinderException extends ApitizerException
{
public static function composerFileNotFound(string $path)
{
return new static("Could not find composer file on path [$path]");
}

public static function psr4NotFound()
{
return new static("Could not find PSR-4 definition in the composer.json");
}
}
1 change: 0 additions & 1 deletion src/Apitizer/Parser/InputParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Apitizer\Parser;

use Apitizer\Exceptions\InvalidInputException;
use Apitizer\Parser\Context;
use Apitizer\Parser\Relation;
use Apitizer\Parser\Sort;
Expand Down
4 changes: 2 additions & 2 deletions src/Apitizer/Policies/Policy.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ interface Policy
* Check if the value passes the validation.
*
* @param $value the current value that is being evaluated.
*
* @param Model|array|mixed $row the current row that is being rendered.
* This value will usually be a Model instance; however, the query builders
* can be used to render just about any data. This should be taken into
* account when writing a policy.
*
* @param Field|Association $fieldOrAssoc the field or association instance
* that is currently being rendered. This instance also holds a reference to
* the current query builder if that is needed in the policy. Furthermore,
Expand Down
5 changes: 4 additions & 1 deletion src/Apitizer/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,10 @@ public function paginate(int $perPage = null, ...$rest): LengthAwarePaginator

// Ensure the all the supported query parameters that were passed in are
// also present in the pagination links.
$queryParameters = Arr::only($this->getRequest()->query(), Apitizer::getQueryParams());
$queryParameters = Arr::only(
$this->getRequest()->query(),
array_values(Apitizer::getQueryParams())
);
$paginator->appends($queryParameters);
});
}
Expand Down
53 changes: 53 additions & 0 deletions src/Apitizer/QueryBuilderLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Apitizer;

use Apitizer\Exceptions\ClassFinderException;
use Apitizer\QueryBuilder;
use Apitizer\Support\ComposerNamespaceClassFinder;

class QueryBuilderLoader
{
/**
* @var string[]
*/
protected $queryBuilders;

public function loadFromConfig()
{
$this->queryBuilders = array_unique(array_merge(
$this->loadIndividualClasses(),
$this->loadNamespaces()
));
}

public function loadIndividualClasses()
{
return config('apitizer.query_builders.classes');
}

public function loadNamespaces()
{
$classes = [];

foreach (config('apitizer.query_builders.namespaces', []) as $namespace) {
$classes = array_merge($classes, $this->loadFromNamespace($namespace));
}

return $classes;
}

public function loadFromNamespace(string $namespace)
{
return ComposerNamespaceClassFinder::make($namespace, QueryBuilder::class)->all();
}

public function getQueryBuilders()
{
if (is_null($this->queryBuilders)) {
$this->loadFromConfig();
}

return $this->queryBuilders;
}
}
13 changes: 7 additions & 6 deletions src/Apitizer/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@

class ServiceProvider extends \Illuminate\Support\ServiceProvider
{
public $bindings = [
Strategy::class => Raise::class,
Parser::class => InputParser::class,
Renderer::class => BasicRenderer::class,
];

/**
* Register the service provider
*
Expand All @@ -26,6 +20,13 @@ public function register()
{
$configPath = __DIR__ . '/../../config/apitizer.php';
$this->mergeConfigFrom($configPath, 'apitizer');

$this->app->bind(Strategy::class, Raise::class);
$this->app->bind(Parser::class, InputParser::class);
$this->app->bind(Renderer::class, BasicRenderer::class);
$this->app->singleton(QueryBuilderLoader::class, function () {
return new QueryBuilderLoader();
});
}

/**
Expand Down
63 changes: 63 additions & 0 deletions src/Apitizer/Support/ClassFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Apitizer\Support;

use FilterIterator;
use ReflectionClass;
use Iterator;

/**
* An iterator that filters php files that follow PSR-4 to only those that
* extend some class and returns the fully-qualified namespace for those
* classes.
*/
class ClassFilter extends FilterIterator
{
/**
* @var string the base namespace to use for all classes.
*/
protected $namespace;

/**
* @var string the class that must be extended from.
*/
protected $class;

/**
* @var string the namespace to the current file we're handling. This will
* be the return value of it passes the accept function.
*/
protected $current;

public function __construct(string $namespace, string $class, Iterator $iterator)
{
parent::__construct($iterator);
$this->namespace = $namespace;
$this->class = $class;
}

public function current()
{
return $this->current;
}

public function accept()
{
$fileInfo = $this->getInnerIterator()->current();

// TODO: What if we're using a recursive iterator?
$this->current = $this->namespace . '\\' . $fileInfo->getBasename('.php');

try {
$reflection = new ReflectionClass($this->current);

if (! $reflection->isInstantiable()) {
return false;
}

return $reflection->isSubclassOf($this->class);
} catch (\Exception $e) {
return false;
}
}
}
Loading

0 comments on commit fa6cf6f

Please sign in to comment.