src/PropertyProcessor/Property/AbstractPropertyProcessor.php
<?php
declare(strict_types = 1);
namespace PHPModelGenerator\PropertyProcessor\Property;
use PHPModelGenerator\Exception\SchemaException;
use PHPModelGenerator\Model\Property\BaseProperty;
use PHPModelGenerator\Model\Property\PropertyInterface;
use PHPModelGenerator\Model\Property\PropertyType;
use PHPModelGenerator\Model\Schema;
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
use PHPModelGenerator\Model\Validator\EnumValidator;
use PHPModelGenerator\Model\Validator\PropertyDependencyValidator;
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
use PHPModelGenerator\Model\Validator\SchemaDependencyValidator;
use PHPModelGenerator\PropertyProcessor\ComposedValueProcessorFactory;
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator;
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintTransferDecorator;
use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection;
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
use PHPModelGenerator\PropertyProcessor\PropertyProcessorInterface;
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
use PHPModelGenerator\Utils\TypeConverter;
/**
* Class AbstractPropertyProcessor
*
* @package PHPModelGenerator\PropertyProcessor\Property
*/
abstract class AbstractPropertyProcessor implements PropertyProcessorInterface
{
public function __construct(
protected PropertyMetaDataCollection $propertyMetaDataCollection,
protected SchemaProcessor $schemaProcessor,
protected Schema $schema
) {}
/**
* Generates the validators for the property
*
* @throws SchemaException
*/
protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void
{
if ($dependencies = $this->propertyMetaDataCollection->getAttributeDependencies($property->getName())) {
$this->addDependencyValidator($property, $dependencies);
}
if ($property->isRequired()) {
$property->addValidator(new RequiredPropertyValidator($property), 1);
}
if (isset($propertySchema->getJson()['enum'])) {
$this->addEnumValidator($property, $propertySchema->getJson()['enum']);
}
$this->addComposedValueValidator($property, $propertySchema);
}
/**
* Add a validator to a property which validates the value against a list of allowed values
*
* @throws SchemaException
*/
protected function addEnumValidator(PropertyInterface $property, array $allowedValues): void
{
if (empty($allowedValues)) {
throw new SchemaException(
sprintf(
"Empty enum property %s in file %s",
$property->getName(),
$property->getJsonSchema()->getFile(),
)
);
}
$allowedValues = array_unique($allowedValues);
// no type information provided - inherit the types from the enum values
if (!$property->getType()) {
$typesOfEnum = array_unique(array_map(
static fn($value): string => TypeConverter::gettypeToInternal(gettype($value)),
$allowedValues,
));
if (count($typesOfEnum) === 1) {
$property->setType(new PropertyType($typesOfEnum[0]));
}
$property->addTypeHintDecorator(new TypeHintDecorator($typesOfEnum));
}
if ($this->isImplicitNullAllowed($property) && !in_array(null, $allowedValues, true)) {
$allowedValues[] = null;
}
$property->addValidator(new EnumValidator($property, $allowedValues), 3);
}
/**
* @throws SchemaException
*/
protected function addDependencyValidator(PropertyInterface $property, array $dependencies): void
{
// check if we have a simple list of properties which must be present if the current property is present
$propertyDependency = true;
array_walk(
$dependencies,
static function ($dependency, $index) use (&$propertyDependency): void {
$propertyDependency = $propertyDependency && is_int($index) && is_string($dependency);
},
);
if ($propertyDependency) {
$property->addValidator(new PropertyDependencyValidator($property, $dependencies));
return;
}
if (!isset($dependencies['type'])) {
$dependencies['type'] = 'object';
}
$dependencySchema = $this->schemaProcessor->processSchema(
new JsonSchema($this->schema->getJsonSchema()->getFile(), $dependencies),
$this->schema->getClassPath(),
"{$this->schema->getClassName()}_{$property->getName()}_Dependency",
$this->schema->getSchemaDictionary(),
);
$property->addValidator(new SchemaDependencyValidator($this->schemaProcessor, $property, $dependencySchema));
$this->schema->addNamespaceTransferDecorator(new SchemaNamespaceTransferDecorator($dependencySchema));
$this->transferDependentPropertiesToBaseSchema($dependencySchema);
}
/**
* Transfer all properties from $dependencySchema to the base schema of the current property
*/
private function transferDependentPropertiesToBaseSchema(Schema $dependencySchema): void
{
foreach ($dependencySchema->getProperties() as $property) {
$this->schema->addProperty(
// validators and types must not be transferred as any value is acceptable for the property if the
// property defining the dependency isn't present
(clone $property)
->setRequired(false)
->setType(null)
->filterValidators(static fn(): bool => false),
);
}
}
/**
* @throws SchemaException
*/
protected function addComposedValueValidator(PropertyInterface $property, JsonSchema $propertySchema): void
{
$composedValueKeywords = ['allOf', 'anyOf', 'oneOf', 'not', 'if'];
$propertyFactory = new PropertyFactory(new ComposedValueProcessorFactory($property instanceof BaseProperty));
foreach ($composedValueKeywords as $composedValueKeyword) {
if (!isset($propertySchema->getJson()[$composedValueKeyword])) {
continue;
}
$propertySchema = $this->inheritPropertyType($propertySchema, $composedValueKeyword);
$composedProperty = $propertyFactory
->create(
$this->propertyMetaDataCollection,
$this->schemaProcessor,
$this->schema,
$property->getName(),
$propertySchema->withJson([
'type' => $composedValueKeyword,
'propertySchema' => $propertySchema,
'onlyForDefinedValues' => !($this instanceof BaseProcessor) &&
(!$property->isRequired()
&& $this->schemaProcessor->getGeneratorConfiguration()->isImplicitNullAllowed()),
]),
);
foreach ($composedProperty->getValidators() as $validator) {
$property->addValidator($validator->getValidator(), $validator->getPriority());
}
$property->addTypeHintDecorator(new TypeHintTransferDecorator($composedProperty));
if (!$property->getType() && $composedProperty->getType()) {
$property->setType($composedProperty->getType(), $composedProperty->getType(true));
}
}
}
/**
* If the type of a property containing a composition is defined outside of the composition make sure each
* composition which doesn't define a type inherits the type
*/
protected function inheritPropertyType(JsonSchema $propertySchema, string $composedValueKeyword): JsonSchema
{
$json = $propertySchema->getJson();
if (!isset($json['type'])) {
return $propertySchema;
}
if ($json['type'] === 'base') {
$json['type'] = 'object';
}
switch ($composedValueKeyword) {
case 'not':
if (!isset($json[$composedValueKeyword]['type'])) {
$json[$composedValueKeyword]['type'] = $json['type'];
}
break;
case 'if':
return $this->inheritIfPropertyType($propertySchema->withJson($json));
default:
foreach ($json[$composedValueKeyword] as &$composedElement) {
if (!isset($composedElement['type'])) {
$composedElement['type'] = $json['type'];
}
}
}
return $propertySchema->withJson($json);
}
/**
* Inherit the type of a property into all composed components of a conditional composition
*/
protected function inheritIfPropertyType(JsonSchema $propertySchema): JsonSchema
{
$json = $propertySchema->getJson();
foreach (['if', 'then', 'else'] as $composedValueKeyword) {
if (!isset($json[$composedValueKeyword])) {
continue;
}
if (!isset($json[$composedValueKeyword]['type'])) {
$json[$composedValueKeyword]['type'] = $json['type'];
}
}
return $propertySchema->withJson($json);
}
/**
* Check if implicit null values are allowed for the given property (a not required property which has no
* explicit null type and is passed with a null value will be accepted)
*/
protected function isImplicitNullAllowed(PropertyInterface $property): bool
{
return $this->schemaProcessor->getGeneratorConfiguration()->isImplicitNullAllowed() && !$property->isRequired();
}
}