Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SchemaExtension #258

Open
wants to merge 2 commits into
base: v3.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/DI/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ public function loadConfig(string $file, Config\Loader $loader = null)

/**
* Returns configuration.
* @deprecated
*/
public function getConfig(): array
{
Expand Down
142 changes: 142 additions & 0 deletions src/DI/Extensions/SchemaExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);

namespace Nette\DI\Extensions;

use Nette;
use Nette\DI\Definitions\Statement;
use Nette\Schema;
use Nette\Schema\Expect;

/**
* Based on ParametersSchemaExtension from PHPStan
* https://github.com/phpstan/phpstan-src/blob/6d523028e399c15dc77aec3affd2ea97ff735925/src/DependencyInjection/ParametersSchemaExtension.php
* @property-read Statement[] $config
*/
final class SchemaExtension extends Nette\DI\CompilerExtension
{
public function getConfigSchema(): Schema\Schema
{
return Expect::arrayOf(
Expect::type(Statement::class)
);
}


public function loadConfiguration(): void
{
$schema = Expect::structure($this->processArray($this->config))
->otherItems(Expect::mixed());

$this->validateSchema($schema, $this->compiler->getConfig());
}


/**
* @param mixed $value
* @return mixed
*/
private function process($value)
{
if ($value instanceof Statement) {
return $this->processStatement($value);
}

if (is_array($value)) {
return $this->processArray($value);
}

return $value;
}


private function processStatement(Statement $statement): Schema\Schema
{
if ($statement->entity === 'schema') {
$arguments = [];
foreach ($statement->arguments as $value) {
if (!$value instanceof Statement) {
$valueType = gettype($value);
throw new Nette\InvalidArgumentException("schema() should contain another statement(), $valueType given.");
}

$arguments[] = $value;
}

if (count($arguments) === 0) {
throw new Nette\InvalidArgumentException('schema() should have at least one argument.');
}

return $this->buildSchemaFromStatements($arguments);
}

return $this->buildSchemaFromStatements([$statement]);
}


/**
* @param mixed[] $array
* @return mixed[]
*/
private function processArray(array $array): array
{
return array_map(
function ($value) {
return $this->process($value);
},
$array
);
}


/**
* @param Statement[] $statements
*/
private function buildSchemaFromStatements(array $statements): Schema\Schema
{
$schema = null;
foreach ($statements as $statement) {
$processedArguments = array_map(
function ($argument) {
return $this->process($argument);
},
$statement->arguments
);

if ($schema === null) {
$methodName = $statement->getEntity();
assert(is_string($methodName));

$schema = Expect::{$methodName}(...$processedArguments);
assert(
$schema instanceof Schema\Elements\Type ||
$schema instanceof Schema\Elements\AnyOf ||
$schema instanceof Schema\Elements\Structure
);

$schema->required();
} else {
$schema->{$statement->getEntity()}(...$processedArguments);
}
}

return $schema;
}


/**
* @param mixed[] $config
*/
private function validateSchema(Schema\Elements\Structure $schema, array $config): void
{
$processor = new Schema\Processor;
try {
$processor->process($schema, $config);
} catch (Schema\ValidationException $e) {
throw new Nette\DI\InvalidConfigurationException($e->getMessage());
}
foreach ($processor->getWarnings() as $warning) {
trigger_error($warning, E_USER_DEPRECATED);
}
}
}
167 changes: 167 additions & 0 deletions tests/DI/SchemaExtension.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

/**
* Test: SchemaExtension.
*/

declare(strict_types=1);

use Nette\DI;
use Nette\DI\Extensions\SchemaExtension;
use Nette\Schema\Expect;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';

$voidExtension = new class extends DI\CompilerExtension {
public function getConfigSchema(): Nette\Schema\Schema
{
return Expect::mixed();
}


public function setConfig($config): void
{
// Nothing
}
};

$loader = new DI\Config\Loader;

$createCompiler = static function () use ($voidExtension, $loader) {
$compiler = new DI\Compiler;
$compiler->addExtension('schema', new SchemaExtension);

$rootKeys = ['string', 'string2', 'structure', 'notValidatedValue'];
foreach ($rootKeys as $key) {
$compiler->addExtension($key, $voidExtension);
}

$schemaConfig = $loader->load(Tester\FileMock::create(/** @lang neon */ '
schema:
string: string()
structure: structure([
string: string(),
stringWithDefault: schema(string("default value"), required(false))
int: int(),
float: float(),
bool: bool(),
array: arrayOf(string())
list: listOf(string())
type: type("string|int")
schema1: schema(string())
schema2: schema(string(), nullable())
schema3: schema(string(), nullable(), required(false))
schema4: schema(int(), min(10), max(20))
])
', 'neon'));
$compiler->addConfig($schemaConfig);

$validConfig = $loader->load(Tester\FileMock::create(/** @lang neon */ '
string: string
structure:
string: text
int: 123
float: 123.456
bool: true
array: [key: string, anotherString]
list: [string, anotherString]
type: string
schema1: string
schema2: null
#schema3 is not required
schema4: 15
notValidatedValue: literally anything
', 'neon'));
$compiler->addConfig($validConfig);

return $compiler;
};

test('no error', static function () use ($createCompiler) {
$compiler = $createCompiler();

Assert::noError(static function () use ($compiler) {
eval($compiler->compile());
});
});

test('all values are required by default', static function () use ($createCompiler, $loader) {
$compiler = $createCompiler();

$config = $loader->load(Tester\FileMock::create(/** @lang neon */ '
schema:
string2: string()

string2: false
', 'neon'));

Assert::exception(static function () use ($compiler, $config) {
eval($compiler->addConfig($config)->compile());
}, DI\InvalidConfigurationException::class, "The item 'string2' expects to be string, false given.");
});

test('invalid type', static function () use ($createCompiler, $loader) {
$compiler = $createCompiler();

$config = $loader->load(Tester\FileMock::create(/** @lang neon */ '
structure:
string: false
', 'neon'));

Assert::exception(static function () use ($compiler, $config) {
eval($compiler->addConfig($config)->compile());
}, DI\InvalidConfigurationException::class, "The item 'structure › string' expects to be string, false given.");
});

test('invalid type argument', static function () use ($createCompiler, $loader) {
$compiler = $createCompiler();

$config = $loader->load(Tester\FileMock::create(/** @lang neon */ '
structure:
list: [arr: ayy!]
', 'neon'));

Assert::exception(static function () use ($compiler, $config) {
eval($compiler->addConfig($config)->compile());
}, DI\InvalidConfigurationException::class, "The item 'structure › list' expects to be list, array given.");
});

test('invalid type argument 2', static function () use ($createCompiler, $loader) {
$compiler = $createCompiler();

$config = $loader->load(Tester\FileMock::create(/** @lang neon */ '
structure:
schema4: 21
', 'neon'));

Assert::exception(static function () use ($compiler, $config) {
eval($compiler->addConfig($config)->compile());
}, DI\InvalidConfigurationException::class, "The item 'structure › schema4' expects to be in range 10..20, 21 given.");
});

test('empty schema()', static function () use ($createCompiler, $loader) {
$compiler = $createCompiler();

$config = $loader->load(Tester\FileMock::create(/** @lang neon */ '
schema:
emptySchema: schema()
', 'neon'));

Assert::exception(static function () use ($compiler, $config) {
eval($compiler->addConfig($config)->compile());
}, Nette\InvalidArgumentException::class, 'schema() should have at least one argument.');
});

test('invalid schema() argument', static function () use ($createCompiler, $loader) {
$compiler = $createCompiler();

$config = $loader->load(Tester\FileMock::create(/** @lang neon */ '
schema:
emptySchema: schema(123)
', 'neon'));

Assert::exception(static function () use ($compiler, $config) {
eval($compiler->addConfig($config)->compile());
}, Nette\InvalidArgumentException::class, 'schema() should contain another statement(), integer given.');
});