Skip to content

Commit

Permalink
NEW: Bulk loader (#432)
Browse files Browse the repository at this point in the history
* First pass at bulk loading

* Refactor to remove class loader, add filepathLoader

* Integration tests

* More tests

* Ready for review

* New default loader

* Allow module path resolution

* Add module resolution to exclude

* Update src/Schema/BulkLoader/ExtensionLoader.php

Co-authored-by: Steve Boyd <emteknetnz@gmail.com>

* Update src/Schema/Interfaces/Identifiable.php

Co-authored-by: Steve Boyd <emteknetnz@gmail.com>

* Revisions per review, fix elemental bug

Co-authored-by: Steve Boyd <emteknetnz@gmail.com>
  • Loading branch information
Aaron Carlino and emteknetnz authored Jan 20, 2022
1 parent e189879 commit 191ac40
Show file tree
Hide file tree
Showing 30 changed files with 1,401 additions and 4 deletions.
1 change: 0 additions & 1 deletion _config/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
Name: graphqlconfig
---
# Define the type parsers
SilverStripe\Core\Injector\Injector:
SilverStripe\GraphQL\QueryHandler\QueryHandlerInterface:
class: SilverStripe\GraphQL\QueryHandler\QueryHandler
Expand Down
4 changes: 4 additions & 0 deletions _config/schema-global.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ SilverStripe\GraphQL\Schema\Schema:
defaultResolver: 'SilverStripe\GraphQL\Schema\Resolver\DefaultResolver::defaultFieldResolver'
modelCreators:
- 'SilverStripe\GraphQL\Schema\DataObject\ModelCreator'
defaultBulkLoad:
inheritanceLoader:
include:
- SilverStripe\ORM\DataObject
modelConfig:
DataObject:
type_formatter: 'SilverStripe\Core\ClassInfo::shortName'
Expand Down
97 changes: 97 additions & 0 deletions src/Schema/BulkLoader/AbstractBulkLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace SilverStripe\GraphQL\Schema\BulkLoader;

use SilverStripe\Core\Injector\Injectable;
use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException;
use SilverStripe\GraphQL\Schema\Interfaces\ConfigurationApplier;
use SilverStripe\GraphQL\Schema\Interfaces\Identifiable;
use SilverStripe\GraphQL\Schema\Schema;

/**
* Provides base functionality to all bulk loaders. Should override the collect()
* method with computations that parse the include/exclude directives and return
* a collection of classes.
*/
abstract class AbstractBulkLoader implements Identifiable, ConfigurationApplier
{
use Injectable;

/**
* @var string[]
*/
protected $includeList;

/**
* @var string[]
*/
protected $excludeList;

/**
* AbstractBulkLoader constructor.
* @param array $include
* @param array $exclude
*/
public function __construct(array $include = [], array $exclude = [])
{
$this->includeList = $include;
$this->excludeList = $exclude;
}

/**
* @param array $include
* @return $this
*/
public function include(array $include): self
{
$this->includeList = $include;

return $this;
}

/**
* @param array $exclude
* @return $this
*/
public function exclude(array $exclude): self
{
$this->excludeList = $exclude;

return $this;
}

/**
* @param array $config
* @return AbstractBulkLoader
* @throws SchemaBuilderException
*/
public function applyConfig(array $config): self
{
Schema::assertValidConfig($config, ['include', 'exclude'], ['include']);
$include = $config['include'];
$exclude = $config['exclude'] ?? [];
if (!is_array($include)) {
$include = [$include];
}
if (!is_array($exclude)) {
$exclude = [$exclude];
}
return $this
->include($include)
->exclude($exclude);
}

/**
* @param Collection $collection
* @return Collection
*/
public function collect(Collection $collection): Collection
{
return Collection::create($collection->getManifest());
}

/**
* @return string
*/
abstract public static function getIdentifier(): string;
}
128 changes: 128 additions & 0 deletions src/Schema/BulkLoader/BulkLoaderSet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace SilverStripe\GraphQL\Schema\BulkLoader;

use InvalidArgumentException;
use SilverStripe\Core\ClassInfo;
use ReflectionException;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException;
use SilverStripe\GraphQL\Schema\Interfaces\ConfigurationApplier;
use SilverStripe\GraphQL\Schema\Logger;
use SilverStripe\GraphQL\Schema\Schema;

/**
* Composed with a list of bulk loaders to be executed in serial and return the aggregate result
* of all their collect() calls
*/
class BulkLoaderSet implements ConfigurationApplier
{
use Injectable;

/**
* @var AbstractBulkLoader[]
*/
private $loaders;

/**
* @var Collection
*/
private $initialCollection;

/**
* BulkLoaderSet constructor.
* @param AbstractBulkLoader[] $loaders
* @param Collection|null $initialCollection
* @throws ReflectionException
*/
public function __construct(array $loaders = [], ?Collection $initialCollection = null)
{
$this->setLoaders($loaders);
if ($initialCollection) {
$this->initialCollection = $initialCollection;
} else {
$this->initialCollection = Collection::createFromClassList(ClassInfo::allClasses());
}
}

/**
* @param array $config
* @return $this
* @throws SchemaBuilderException
*/
public function applyConfig(array $config): self
{
$registry = Registry::inst();
foreach ($config as $loaderID => $loaderConfig) {
/* @var AbstractBulkLoader $loader */
$loader = $registry->getByID($loaderID);
Schema::invariant($loader, 'Loader "%s" does not exist', $loaderID);
$loader->applyConfig($loaderConfig);
$this->addLoader($loader);
}

return $this;
}

/**
* @param AbstractBulkLoader $loader
* @return $this
*/
public function addLoader(AbstractBulkLoader $loader): self
{
$this->loaders[] = $loader;

return $this;
}

/**
* @return Collection
*/
public function process(): Collection
{
$logger = Logger::singleton();
$collection = $this->initialCollection;
$logger->debug(sprintf(
'Bulk loader initial collection size: %s',
count($collection->getClasses())
));
foreach ($this->loaders as $loader) {
$collection = $loader->collect($collection);
$logger->debug(sprintf(
'Loader %s reduced bulk load to %s',
$loader->getIdentifier(),
count($collection->getClasses())
));
}

return $collection;
}

/**
* @param array $loaders
* @return $this
*/
public function setLoaders(array $loaders): self
{
foreach ($loaders as $loader) {
if (!$loader instanceof AbstractBulkLoader) {
throw new InvalidArgumentException(sprintf(
'%s only accepts instances of %s',
static::class,
AbstractBulkLoader::class
));
}
}
$this->loaders = $loaders;

return $this;
}

/**
* @return AbstractBulkLoader[]
*/
public function getLoaders(): array
{
return $this->loaders;
}
}
115 changes: 115 additions & 0 deletions src/Schema/BulkLoader/Collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace SilverStripe\GraphQL\Schema\BulkLoader;

use SilverStripe\Core\Injector\Injectable;
use Exception;
use ReflectionClass;
use ReflectionException;

/**
* Defines a collection of class names paired with file paths
*/
class Collection
{
use Injectable;

/**
* @var array
*/
private $manifest;

/**
* Collection constructor.
* @param array $manifest An array of classname keys to filepath values ['My\Class' => '/path/to/Class.php']
* @throws Exception
*/
public function __construct(array $manifest = [])
{
$this->setManifest($manifest);
}

/**
* @param array $manifest
* @return $this
* @throws Exception
*/
public function setManifest(array $manifest): self
{
$this->manifest = $manifest;

return $this;
}

/**
* @param string $class
* @return $this
*/
public function removeClass(string $class): self
{
unset($this->manifest[$class]);

return $this;
}

/**
* @param string $path
* @return $this
*/
public function removeFile(string $path): self
{
$class = array_search($path, $this->manifest);
unset($this->manifest[$class]);

return $this;
}

/**
* @return array
*/
public function getClasses(): array
{
return array_keys($this->manifest);
}

/**
* @return array
*/
public function getFiles(): array
{
return array_values($this->manifest);
}

/**
* @return array
*/
public function getManifest(): array
{
return $this->manifest;
}

/**
* @param array $classList
* @return Collection
* @throws ReflectionException
* @throws Exception
*/
public static function createFromClassList(array $classList): Collection
{
$manifest = [];
foreach ($classList as $class) {
if (!class_exists($class)) {
continue;
}
$reflection = new ReflectionClass($class);
$filePath = $reflection->getFileName();
if (!$filePath) {
continue;
}

$manifest[$class] = $filePath;
}

return new static($manifest);
}
}
Loading

0 comments on commit 191ac40

Please sign in to comment.