-
-
Notifications
You must be signed in to change notification settings - Fork 848
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
Refactor XML loaders and various quality fixes #738
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,8 +20,12 @@ | |
* @author Teoh Han Hui <teohhanhui@gmail.com> | ||
* @author Vincent Chalamon <vincentchalamon@gmail.com> | ||
*/ | ||
abstract class QueryChecker | ||
final class QueryChecker | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. \o/ |
||
{ | ||
private function __construct() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we adding empty contruct functions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The constructor is private. It's to block the possibility of initializing this class (it should only conain static methods). |
||
{ | ||
} | ||
|
||
/** | ||
* Determines whether the query builder uses a HAVING clause. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,27 +14,27 @@ | |
use ApiPlatform\Core\Exception\InvalidArgumentException; | ||
use ApiPlatform\Core\Exception\ResourceClassNotFoundException; | ||
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; | ||
use Symfony\Component\Config\Util\XmlUtils; | ||
|
||
/** | ||
* Creates a resource metadata from xml {@see Resource} configuration. | ||
* Creates a resource metadata from XML {@see Resource} configuration. | ||
* | ||
* @author Antoine Bluchet <soyuka@gmail.com> | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
final class XmlResourceMetadataFactory implements ResourceMetadataFactoryInterface | ||
{ | ||
private $xmlParser; | ||
const RESOURCE_SCHEMA = __DIR__.'/../../schema/metadata.xsd'; | ||
|
||
private $paths; | ||
private $decorated; | ||
|
||
const RESOURCE_SCHEMA = __DIR__.'/../../../schema/metadata.xsd'; | ||
|
||
/** | ||
* @param string[] $paths | ||
* @param ResourceMetadataFactoryInterface|null $decorated | ||
*/ | ||
public function __construct(array $paths, ResourceMetadataFactoryInterface $decorated = null) | ||
{ | ||
$this->xmlParser = new \DOMDocument(); | ||
$this->paths = $paths; | ||
$this->decorated = $decorated; | ||
} | ||
|
@@ -53,170 +53,63 @@ public function create(string $resourceClass) : ResourceMetadata | |
} | ||
} | ||
|
||
try { | ||
new \ReflectionClass($resourceClass); | ||
} catch (\ReflectionException $reflectionException) { | ||
return $this->handleNotFound($parentResourceMetadata, $resourceClass); | ||
} | ||
|
||
$metadata = null; | ||
|
||
foreach ($this->paths as $path) { | ||
$resources = $this->getResourcesDom($path); | ||
|
||
$internalErrors = libxml_use_internal_errors(true); | ||
|
||
if (false === @$resources->schemaValidate(self::RESOURCE_SCHEMA)) { | ||
throw new InvalidArgumentException(sprintf('XML Schema loaded from path %s is not valid! Errors: %s', realpath($path), implode("\n", $this->getXmlErrors($internalErrors)))); | ||
} | ||
|
||
libxml_clear_errors(); | ||
libxml_use_internal_errors($internalErrors); | ||
|
||
foreach ($resources->getElementsByTagName('resource') as $resource) { | ||
$class = $resource->getAttribute('class'); | ||
|
||
if ($resourceClass !== $class) { | ||
continue; | ||
} | ||
|
||
$metadata = $resource; | ||
|
||
break 2; | ||
} | ||
} | ||
|
||
if (null === $metadata) { | ||
if (!class_exists($resourceClass) || empty($metadata = $this->getMetadata($resourceClass))) { | ||
return $this->handleNotFound($parentResourceMetadata, $resourceClass); | ||
} | ||
|
||
$xpath = new \DOMXpath($resources); | ||
|
||
$metadata = [ | ||
'shortName' => $metadata->getAttribute('shortName') ?: null, | ||
'description' => $metadata->getAttribute('description') ?: null, | ||
'iri' => $metadata->getAttribute('iri') ?: null, | ||
'itemOperations' => $this->getOperations($xpath->query('./itemOperations/operation', $metadata)) ?: null, | ||
'collectionOperations' => $this->getOperations($xpath->query('./collectionOperations/operation', $metadata)) ?: null, | ||
'attributes' => $this->getAttributes($xpath->query('./attributes/attribute', $metadata)), | ||
]; | ||
|
||
if (!$parentResourceMetadata) { | ||
return new ResourceMetadata( | ||
$metadata['shortName'], | ||
$metadata['description'], | ||
$metadata['iri'], | ||
$metadata['itemOperations'], | ||
$metadata['collectionOperations'], | ||
$metadata['attributes'] | ||
); | ||
} | ||
|
||
$resourceMetadata = $parentResourceMetadata; | ||
|
||
foreach (['shortName', 'description', 'itemOperations', 'collectionOperations', 'iri', 'attributes'] as $property) { | ||
if (!isset($metadata[$property])) { | ||
continue; | ||
} | ||
|
||
$resourceMetadata = $this->createWith($resourceMetadata, $property, $metadata[$property]); | ||
} | ||
|
||
return $resourceMetadata; | ||
return null === $parentResourceMetadata ? new ResourceMetadata(...$metadata) : $this->update($parentResourceMetadata, $metadata); | ||
} | ||
|
||
/** | ||
* Creates a DOMDocument based on `resource` tags of a file-loaded xml document. | ||
* Extracts metadata from the XML tree. | ||
* | ||
* @param string $path the xml file path | ||
* @param string $resourceClass | ||
* | ||
* @return \DOMDocument | ||
* @return array | ||
*/ | ||
private function getResourcesDom(string $path) : \DOMDocument | ||
private function getMetadata(string $resourceClass) : array | ||
{ | ||
$doc = new \DOMDocument('1.0', 'utf-8'); | ||
$root = $doc->createElement('resources'); | ||
$doc->appendChild($root); | ||
|
||
$this->xmlParser->loadXML(file_get_contents($path)); | ||
|
||
$xpath = new \DOMXpath($this->xmlParser); | ||
$resources = $xpath->query('//resource'); | ||
|
||
foreach ($resources as $resource) { | ||
$root->appendChild($doc->importNode($resource, true)); | ||
} | ||
|
||
return $doc; | ||
} | ||
|
||
/** | ||
* Get operations from xml. | ||
* | ||
* @param \DOMNodeList $query | ||
* | ||
* @return array|null | ||
*/ | ||
private function getOperations(\DOMNodeList $query) | ||
{ | ||
$operations = []; | ||
foreach ($query as $operation) { | ||
$key = $operation->getAttribute('key'); | ||
$operations[$key] = [ | ||
'method' => $operation->getAttribute('method'), | ||
]; | ||
foreach ($this->paths as $path) { | ||
try { | ||
$domDocument = XmlUtils::loadFile($path, self::RESOURCE_SCHEMA); | ||
} catch (\InvalidArgumentException $e) { | ||
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); | ||
} | ||
|
||
$path = $operation->getAttribute('path'); | ||
$xml = simplexml_import_dom($domDocument); | ||
foreach ($xml->resource as $resource) { | ||
if ($resourceClass !== (string) $resource['class']) { | ||
continue; | ||
} | ||
|
||
if ($path) { | ||
$operations[$key]['path'] = $path; | ||
return [ | ||
(string) $resource['shortName'] ?? null, | ||
(string) $resource['description'] ?? null, | ||
(string) $resource['iri'] ?? null, | ||
$this->getAttributes($resource, 'itemOperation') ?: null, | ||
$this->getAttributes($resource, 'collectionOperation') ?: null, | ||
$this->getAttributes($resource, 'attribute') ?: null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I get how this works but for the sake of consistency would it be possible to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO there is no interest:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It just adds an element to the stack. Indeed, makes code slightly more complex but YAML, Annotations and PropertyMetadata are using the |
||
]; | ||
} | ||
} | ||
|
||
return $operations ?: null; | ||
return []; | ||
} | ||
|
||
/** | ||
* Get Attributes. | ||
* Recursively transforms an attribute structure into an associative array. | ||
* | ||
* @param \DOMNodeList $query | ||
* @param \SimpleXMLElement $resource | ||
* @param string $elementName | ||
* | ||
* @return array|null | ||
* @return array | ||
*/ | ||
private function getAttributes(\DOMNodeList $query) | ||
private function getAttributes(\SimpleXMLElement $resource, string $elementName) : array | ||
{ | ||
$attributes = []; | ||
foreach ($query as $attribute) { | ||
$key = $attribute->getAttribute('key'); | ||
$attributes[$key] = $this->recursiveAttributes($attribute, $attributes[$key]); | ||
} | ||
|
||
return $attributes ?: null; | ||
} | ||
|
||
/** | ||
* Transforms random attributes in an array | ||
* <element (key="key"|int)>\DOMNodeList|\DOMText</element>. | ||
* | ||
* @param \DOMElement $element | ||
* @param array | ||
* | ||
* @return array|string | ||
*/ | ||
private function recursiveAttributes(\DOMElement $element, &$attributes) | ||
{ | ||
foreach ($element->childNodes as $child) { | ||
if ($child instanceof \DOMText) { | ||
if ($child->isWhitespaceInElementContent()) { | ||
continue; | ||
} | ||
|
||
$attributes = $child->nodeValue; | ||
break; | ||
} | ||
|
||
$key = $child->getAttribute('key') ?: count($attributes); | ||
$attributes[$key] = $child->childNodes->length ? $this->recursiveAttributes($child, $attributes[$key]) : $child->value; | ||
foreach ($resource->$elementName as $attribute) { | ||
$value = isset($attribute->attribute[0]) ? $this->getAttributes($attribute, 'attribute') : (string) $attribute; | ||
isset($attribute['name']) ? $attributes[(string) $attribute['name']] = $value : $attributes[] = $value; | ||
} | ||
|
||
return $attributes; | ||
|
@@ -245,47 +138,20 @@ private function handleNotFound(ResourceMetadata $parentPropertyMetadata = null, | |
* Creates a new instance of metadata if the property is not already set. | ||
* | ||
* @param ResourceMetadata $resourceMetadata | ||
* @param string $property | ||
* @param mixed $value | ||
* @param array $metadata | ||
* | ||
* @return ResourceMetadata | ||
*/ | ||
private function createWith(ResourceMetadata $resourceMetadata, string $property, $value) : ResourceMetadata | ||
private function update(ResourceMetadata $resourceMetadata, array $metadata) : ResourceMetadata | ||
{ | ||
$getter = 'get'.ucfirst($property); | ||
|
||
if (null !== $resourceMetadata->$getter()) { | ||
return $resourceMetadata; | ||
} | ||
|
||
$wither = 'with'.ucfirst($property); | ||
|
||
return $resourceMetadata->$wither($value); | ||
} | ||
foreach (['shortName', 'description', 'iri', 'itemOperations', 'collectionOperations', 'attributes'] as $key => $property) { | ||
if (null === $metadata[$key] || null !== $resourceMetadata->{'get'.ucfirst($property)}()) { | ||
continue; | ||
} | ||
|
||
/** | ||
* Returns the XML errors of the internal XML parser. | ||
* | ||
* @param bool $internalErrors | ||
* | ||
* @return array An array of errors | ||
*/ | ||
private function getXmlErrors($internalErrors) | ||
{ | ||
$errors = []; | ||
foreach (libxml_get_errors() as $error) { | ||
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', | ||
LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', | ||
$error->code, | ||
trim($error->message), | ||
$error->file ?: 'n/a', | ||
$error->line, | ||
$error->column | ||
); | ||
$resourceMetadata = $resourceMetadata->{'with'.ucfirst($property)}($metadata[$key]); | ||
} | ||
libxml_clear_errors(); | ||
libxml_use_internal_errors($internalErrors); | ||
|
||
return $errors; | ||
return $resourceMetadata; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😉