How could I extend objects provided with Document Object Model? Seems that there is no way according to this issue.
class Application_Model_XmlSchema extends DOMElement
{
const ELEMENT_NAME = 'schema';
/**
* #var DOMElement
*/
private $_schema;
/**
* #param DOMDocument $document
* #return void
*/
public function __construct(DOMDocument $document) {
$this->setSchema($document->getElementsByTagName(self::ELEMENT_NAME)->item(0));
}
/**
* #param DOMElement $schema
* #return void
*/
public function setSchema(DOMElement $schema){
$this->_schema = $schema;
}
/**
* #return DOMElement
*/
public function getSchema(){
return $this->_schema;
}
/**
* #param string $name
* #param array $arguments
* #return mixed
*/
public function __call($name, $arguments) {
if (method_exists($this->_schema, $name)) {
return call_user_func_array(
array($this->_schema, $name),
$arguments
);
}
}
}
$version = $this->getRequest()->getParam('version', null);
$encoding = $this->getRequest()->getParam('encoding', null);
$source = 'http://www.w3.org/2001/XMLSchema.xsd';
$document = new DOMDocument($version, $encoding);
$document->load($source);
$xmlSchema = new Application_Model_XmlSchema($document);
$xmlSchema->getAttribute('version');
I got an error:
Warning: DOMElement::getAttribute():
Couldn't fetch
Application_Model_XmlSchema in
C:\Nevermind.php on line newvermind
Try this: http://www.php.net/manual/en/domdocument.registernodeclass.php
I use it in my DOMDocument extended class and it works great, allowing me to add methods to DOMNode and DOMElement.
Since getAttribute is already defined in DOMElement, your __call will not be used. As a consequence, any calls made to Application_Model_XmlSchema::getAttribute will go through the inherited DOMElement::getAttribute resulting in your problem.
A quick workaround would be to get rid of extends DOMElement from the class definition and route calls through to DOMElement methods/properties with magic methods if you need that functionality: have your class act as a wrapper rather than child.
Workaround is:
$xmlSchema->getSchema()->getAttribute('version');
But I would like use "normal" access to methods.
Related
I'm writing my own implementation of the Laravel Service Container to practice some design patterns and later make a private microframework.
The class looks like this right now:
class Container implements ContainerInterface
{
/**
* Concrete bindings of contracts.
*
* #var array
*/
protected $bindings = [];
/**
* Lists of arguments used for a class instantiation.
*
* #var array
*/
protected $arguments = [];
/**
* Container's storage used to store already built or customly setted objects.
*
* #var array
*/
protected $storage = [];
/**
* Returns an instance of a service
*
* #param $name
* #return object
* #throws \ReflectionException
*/
public function get($name) {
$className = (isset($this->bindings[$name])) ? $this->bindings[$name] : $name;
if (isset($this->storage[$className])) {
return $this->storage[$className];
}
return $this->make($className);
}
/**
* Creates an instance of a class
*
* #param $className
* #return object
* #throws \ReflectionException
*/
public function make($className) {
$refObject = new \ReflectionClass($className);
if (!$refObject->isInstantiable()) {
throw new \ReflectionException("$className is not instantiable");
}
$refConstructor = $refObject->getConstructor();
$refParameters = ($refConstructor) ? $refConstructor->getParameters() : [];
$args = [];
// Iterates over constructor arguments, checks for custom defined parameters
// and builds $args array
foreach ($refParameters as $refParameter) {
$refClass = $refParameter->getClass();
$parameterName = $refParameter->name;
$parameterValue =
isset($this->arguments[$className][$parameterName]) ? $this->arguments[$className][$parameterName]
: (null !== $refClass ? $refClass->name
: ($refParameter->isOptional() ? $refParameter->getDefaultValue()
: null));
// Recursively gets needed objects for a class instantiation
$args[] = ($refClass) ? $this->get($parameterValue)
: $parameterValue;
}
$instance = $refObject->newInstanceArgs($args);
$this->storage[$className] = $instance;
return $instance;
}
/**
* Sets a concrete implementation of a contract
*
* #param $abstract
* #param $concrete
*/
public function bind($abstract, $concrete) {
$this->bindings[$abstract] = $concrete;
}
/**
* Sets arguments used for a class instantiation
*
* #param $className
* #param array $arguments
*/
public function setArguments($className, array $arguments) {
$this->arguments[$className] = $arguments;
}
}
It works fine but I clearly see a violation of SRP in the make() method. So I decided to delegate an object creational logic to a separate class.
A problem that I encountered is that this class will be tightly coupled with a Container class. Because it needs an access to $bindings and $arguments arrays, and the get() method. And even if we pass these parameters to the class, the storage still stays in a container. So basically all architecture is wrong and we need, like, 2 more classes: StorageManager and ClassFactory. Or maybe ClassBuilder? And should ClassFactory be able to build constructor arguments or it needs another class — ArgumentFactory?
What do you think guys?
When I deserialize my doctrine entity, the initial object is constructed/initiated correctly, however all child relations are trying to be called as arrays.
The root level object's addChild(ChildEntity $entity) method is being called, but Symfony is throwing an error that addChild is receiving an array and not an instance of ChildEntity.
Does Symfony's own serializer have a way to deserialize nested arrays (child entities) to the entity type?
JMS Serializer handles this by specifying a #Type("ArrayCollection<ChildEntity>") annotation on the property.
I believe the Symfony serializer attempts to be minimal compared to the JMS Serializer, so you might have to implement your own denormalizer for the class. You can see how the section on adding normalizers.
There may be an easier way, but so far with Symfony I am using Discriminator interface annotation and type property for array of Objects. It can also handle multiple types in one array (MongoDB):
namespace App\Model;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* #DiscriminatorMap(typeProperty="type", mapping={
* "text"="App\Model\BlogContentTextModel",
* "code"="App\Model\BlogContentCodeModel"
* })
*/
interface BlogContentInterface
{
/**
* #return string
*/
public function getType(): string;
}
and parent object will need to define property as interface and get, add, remove methods:
/**
* #var BlogContentInterface[]
*/
protected $contents = [];
/**
* #return BlogContentInterface[]
*/
public function getContents(): array
{
return $this->contents;
}
/**
* #param BlogContentInterface[] $contents
*/
public function setContents($contents): void
{
$this->contents = $contents;
}
/**
* #param BlogContentInterface $content
*/
public function addContent(BlogContentInterface $content): void
{
$this->contents[] = $content;
}
/**
* #param BlogContentInterface $content
*/
public function removeContent(BlogContentInterface $content): void
{
$index = array_search($content, $this->contents);
if ($index !== false) {
unset($this->contents[$index]);
}
}
I'm having a bit of a problem trying to get a correct autocompletion for the following code example. I'm using PHPStorm 7 on a Win7 machine.
First just a simple class.
/**
* Class myObject
*/
class myObject
{
/**
* some method
*/
public function myMethod()
{
// do something
}
}
This one is the collection class which can contain multiple instances of the prior class and implements the IteratorAggregate interface.
/**
* Class myCollection
*/
class myCollection implements IteratorAggregate
{
/**
* #var myObject[]
*/
protected $_objects = array();
/**
* #param myObject $object
* #return myCollection
*/
public function add(myObject $object)
{
$this->_objects[] = $object;
return $this;
}
/**
* #return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator($this->_objects);
}
}
And here is the code example.
$collection = new myCollection;
$collection->add(new myObject);
$collection->add(new myObject);
foreach ($collection as $object) {
$object->myMethod(); // gets no autocompletion
}
As you may have guessed (and read in the example) the myMethod() call gets not autocompleted and is beeing listed in the code analysis. The only way i found is adding a comment block for $object, which i find, to be honest, extremely annoying.
/** #var $object myObject */
foreach ($collection as $object) {
$object->myMethod(); // gets autocompletion now, but sucks
}
So, any ideas or fundamented knowledge on how to solve this?
/**
* #return ArrayIterator|myObject[]
*/
public function getIterator()
{
return new ArrayIterator($this->_objects);
}
For extended classes (the base class is above):
/**
* #method myObject[] getIterator()
*/
class ExtendedClass extends BaseCollection
{
}
or
/**
* #method iterable<myObject> getIterator()
*/
class ExtendedClass extends BaseCollection
{
}
I think this will be best way to handle such case. at least it works with PHPStorm
Your
/** #var $object myObject */
block is indeed the correct way to accomplish this. The syntax you are expecting to do the work,
/**
* #var myObject[]
*/
is not standard phpdoc notation, although it is in informal use and has some effort ongoing to standardize. Until such standardization does happen, IDEs recognizing it will probably be hit-or-miss. IDE coverage of your $object local var block is also hit-or-miss, actually.
In your myCollection class, override current() as follows:
/** #return myObject */
public function current() {
return parent::current();
}
Possible workaround (also ugly) is to create static "constructor", that will return myObject. At least it works in eclipse. If you want to see collection methods too, then just add myCollection to return as "#return myObject[]|myCollection"
class myCollection implements \IteratorAggregate
{
/**
* #return myObject[]
*/
public function create()
{
return new static();
}
}
Writing group of parsers that rely on one abstract class which implements shared methods and asks to implement addition method which contains per parser logic.
Abstract parser code:
<?
abstract class AbstractParser {
/*
* The only abstract method to implement. It contains unique logic of each feed passed to the function
*/
public abstract function parse($xmlObject);
/**
* #param $feed string
* #return SimpleXMLElement
* #throws Exception
*/
public function getFeedXml($feed) {
$xml = simplexml_load_file($feed);
return $xml;
}
/**
* #return array
*/
public function getParsedData() {
return $this->data;
}
/**
* #param SimpleXMLElement
* #return Array
*/
public function getAttributes($object) {
// implementation here
}
}
Concrete Parser class:
<?php
class FormulaDrivers extends AbstractParser {
private $data;
/**
* #param SimpleXMLElement object
* #return void
*/
public function parse($xmlObject) {
if (!$xmlObject) {
throw new \Exception('Unable to load remote XML feed');
}
foreach($xmlObject->drivers as $driver) {
$driverDetails = $this->getAttributes($driver);
var_dump($driver);
}
}
}
Instantiation:
$parser = new FormulaDrivers();
$parser->parse( $parser->getFeedXml('http://api.xmlfeeds.com/formula_drivers.xml') );
As you can see, I pass the result of getFeedXml method to parse method, basically delegating the validation of result of getFeedXml to parse method.
How can I avoid it, make sure it returns correct XML object before I pass it to parse method?
Increasing instantiation process and amount of called methods leads to the need of some factory method...
Anyway, how would you fix this small issue?
Thanks!
Make parse protected, so that only parse_xml_file calls it:
abstract class AbstractParser {
/*
* The only abstract method to implement. It contains unique logic of each feed passed to the function
*/
protected abstract function parse($xmlObject);
/**
* #param $feed string
* #return [whatever .parse returns]
* #throws Exception
*/
public function parseFile($feed) {
$xml = simplexml_load_file($feed);
if (!$xml) {
throw new \Exception('Unable to load remote XML feed');
}
return $this->parse($xml);
}
/**
* #return array
*/
public function getParsedData() {
return $this->data;
}
/**
* #param SimpleXMLElement
* #return Array
*/
public function getAttributes($object) {
// implementation here
}
}
$parser->parseFile('http://api.xmlfeeds.com/formula_drivers.xml');
registerNodeClass is great for extending the various DOMNode-based DOM classes in PHP, but I need to go one level deeper.
I've created an extDOMElement that extends DOMElement. This works great with registerNodeClass, but I would like to have something that works more like this:
registerNodeClass("DOMElement->nodeName='XYZ'", 'extDOMXYZElement')
Consider the following XML document, animals.xml:
<animals>
<dog name="fido" />
<dog name="lucky" />
<cat name="scratchy" />
<horse name="flicka" />
</animals>
Consider the following code:
extDomDocument extends DOMDocument {
public function processAnimals() {
$animals = $this->documentElement->childNodes;
foreach($animals as $animal) {
$animal->process();
}
}
}
extDOMElement extends DOMElement {
public function process() {
if ($this->nodeName=='dog'){
$this->bark();
} elseif ($this->nodeName=='cat'){
$this->meow();
} elseif ($this->nodeName=='horse'){
$this->whinny();
}
this->setAttribute('processed','true');
}
private function bark () {
echo "$this->getAttribute('name') the $this->nodeName barks!";
}
private function meow() {
echo "$this->getAttribute('name') the $this->nodeName meows!";
}
private function whinny() {
echo "$this->getAttribute('name') the $this->nodeName whinnies!";
}
}
$doc = new extDOMDocument();
$doc->registerNodeClass('DOMElement', 'extDOMElement');
$doc->loadXMLFile('animals.xml');
$doc->processAnimals();
$doc->saveXMLFile('animals_processed_' . Now() . '.xml');
Output:
fido the dog barks!
lucky the dog barks!
scratchy the cat meows!
flicka the horse whinnies!
I don't want to have to put bark(), meow() and whinny() into extDOMElement - I want to put them into extDOMDogElement, extDOMCatElement and extDOMHorseElement, respectively.
I've looked at the Decorator and Strategy patterns here, but I'm not exactly sure how to proceed. The current setup works OK, but I'd prefer to have shared properties and methods in extDOMElement with separate classes for each ElementName, so that I can separate methods and properties specific to each Element out of the main classes.
I had the same problem. My solution was to write an own parser based on the XMLReader extension. The resulting AdvancedParser class works very well for me. A separate element class can be registered for each element name. By extending the AdvancedParser class and overwriting the getClassForElement() method it's also possible to dynamically calculate the name of the desired class based on the element name.
/**
* Specialized Xml parser with element based class registry.
*
* This class uses the XMLReader extension for document parsing and creates
* a DOM tree with individual DOMElement subclasses for each element type.
*
* #author Andreas Traber < a.traber (at) rivo-systems (dot) com >
*
* #since April 21, 2012
* #package XML
*/
class AdvancedParser
{
/**
* Map with registered classes.
* #var array
*/
protected $_elementClasses = array();
/**
* Default class for unknown elements.
* #var string
*/
protected $_defaultElementClass = 'DOMElement';
/**
* The reader for Xml parsing.
* #var XMLReader
*/
protected $_reader;
/**
* The document object.
* #var DOMDocument
*/
protected $_document;
/**
* The current parsing element.
* #var DOMElement
*/
protected $_currentElement;
/**
* Gets the fallback class for unknown elements.
*
* #return string
*/
public function getDefaultElementClass()
{
return $this->_defaultElementClass;
}
/**
* Sets the fallback class for unknown elements.
*
* #param string $class
* #return void
* #throws Exception $class is not a subclass of DOMElement.
*/
public function setDefaultElementClass($class)
{
switch (true) {
case $class === null:
$this->_defaultElementClass = 'DOMElement';
break;
case !$class instanceof DOMElement:
throw new Exception($class.' must be a subclass of DOMElement');
default:
$this->_defaultElementClass = $class;
}
}
/**
* Registers the class for a specified element name.
*
* #param string $elementName.
* #param string $class.
* #return void
* #throws Exception $class is not a subclass of DOMElement.
*/
public function registerElementClass($elementName, $class)
{
switch (true) {
case $class === null:
unset($this->_elementClasses[$elementName]);
break;
case !$class instanceof DOMElement:
throw new Exception($class.' must be a subclass of DOMElement');
default:
$this->_elementClasses[$elementName] = $class;
}
}
/**
* Gets the class for a given element name.
*
* #param string $elementName
* #return string
*/
public function getClassForElement($elementName)
{
return $this->_elementClasses[$elementName]
? $this->_elementClasses[$elementName]
: $this->_defaultElementClass;
}
/**
* Parse Xml Data from string.
*
* #see XMLReader::XML()
*
* #param string $source String containing the XML to be parsed.
* #param string $encoding The document encoding or NULL.
* #param string $options A bitmask of the LIBXML_* constants.
* #return DOMDocument The created DOM tree.
*/
public function parseString($source, $encoding = null, $options = 0)
{
$this->_reader = new XMLReader();
$this->_reader->XML($source, $encoding, $options);
return $this->_parse();
}
/**
* Parse Xml Data from file.
*
* #see XMLReader::open()
*
* #param string $uri URI pointing to the document.
* #param string $encoding The document encoding or NULL.
* #param string $options A bitmask of the LIBXML_* constants.
* #return DOMDocument The created DOM tree.
*/
public function parseFile($uri, $encoding = null, $options = 0)
{
$this->_reader = new XMLReader();
$this->_reader->open($uri, $encoding, $options);
return $this->_parse();
}
/**
* The parser.
*
* #return DOMDocument The created DOM tree.
*/
protected function _parse()
{
$this->_document = new DOMDocument('1.0', 'utf-8');
$this->_document->_elements = array(); // keep references to elements
$this->_currentElement = $this->_document;
while ($this->_reader->read()) {
switch ($this->_reader->nodeType) {
case XMLReader::ELEMENT:
$this->_reader->isEmptyElement
? $this->_addElement()
: $this->_currentElement = $this->_addElement();
break;
case XMLReader::END_ELEMENT:
$this->_currentElement = $this->_currentElement->parentNode;
break;
case XMLReader::CDATA:
$this->_currentElement->appendChild(
$this->_document->createCDATASection($this->_reader->value)
);
break;
case XMLReader::TEXT:
case XMLReader::SIGNIFICANT_WHITESPACE:
$this->_currentElement->appendChild(
$this->_document->createTextNode($this->_reader->value)
);
break;
case XMLReader::COMMENT:
$this->_currentElement->appendChild(
$this->_document->createComment($this->_reader->value)
);
break;
}
}
$this->_reader->close();
return $this->_document;
}
/**
* Adds the current element into the DOM tree.
*
* #return DOMElement The added element.
*/
protected function _addElement()
{
$element = $this->_createElement();
// It's important to keep a reference to each element.
// Elements without any reference were destroyed by the
// garbage collection and loses their type.
$this->_document->_elements[] = $element;
$this->_currentElement->appendChild($element);
$this->_addAttributes($element);
return $element;
}
/**
* Creates a new element.
*
* #return DOMElement The created element.
*/
protected function _createElement()
{
$class = $this->getClassForElement($this->_reader->localName);
return new $class(
$this->_reader->name,
$this->_reader->value,
$this->_reader->namespaceURI
);
}
/**
* Adds the current attributes to an $element.
*
* #param DOMElement $element
* #return void
*/
protected function _addAttributes(DOMElement $element)
{
while ($this->_reader->moveToNextAttribute()) {
$this->_reader->prefix && ($uri = $this->_reader->lookupNamespace($this->_reader->prefix))
? $element->setAttributeNS($uri, $this->_reader->name, $this->_reader->value)
: $element->setAttribute($this->_reader->name, $this->_reader->value);
}
}
}
I can't really pin point it but what you're trying has a certain smell, like Gordon already pointed out.
Anyway... you could use __call() to expose different methods on your extDOMElement object depending on the actual node (type/contents/...). For that purpose your extDOMElement object could store an helper object which is instantiated according to the "type" of the element and then delegate method calls to this helper object. Personally I don't like that too much as it doesn't exactly make documentation, testing and debugging any easier. If that sounds feasible to you I can write down a self-contained example.
This certainly needs comments/documentation ...work in progress since I don't have the time right now...
<?php
$doc = new MyDOMDocument('1.0', 'iso-8859-1');
$doc->loadxml('<animals>
<Foo name="fido" />
<Bar name="lucky" />
<Foo name="scratchy" />
<Ham name="flicka" />
<Egg name="donald" />
</animals>');
$xpath = new DOMXPath($doc);
foreach( $xpath->query('//Foo') as $e ) {
echo $e->name(), ': ', $e->foo(), "\n";
}
echo "----\n";
foreach( $xpath->query('//Bar') as $e ) {
echo $e->name(), ': ', $e->bar(), "\n";
}
echo "====\n";
echo $doc->savexml();
class MyDOMElement extends DOMElement {
protected $helper;
public function getHelper() {
// lazy loading and caching the helper object
// since lookup/instantiation can be costly
if ( is_null($this->helper) ) {
$this->helper = $this->resolveHelper();
}
return $this->helper;
}
public function isType($t) {
return $this->getHelper() instanceof $t;
}
public function __call($name, $args) {
$helper = $this->getHelper();
if ( !method_exists($helper, $name) ) {
var_dump($name, $args, $helper);
throw new Exception('yaddayadda');
}
return call_user_func_array( array($this->helper, $name), $args);
}
public function releaseHelper() {
// you might want to consider something like this
// to help php with the circular references
// ...or maybe not, haven't tested the impact circual references have on php's gc
$this->helper = null;
}
protected function resolveHelper() {
// this is hardcored only for brevity's sake
// add any kind of lookup/factory/... you like
$rv = null;
switch( $this->tagName ) {
case 'Foo':
case 'Bar':
$cn = "DOMHelper".$this->tagName;
return new $cn($this);
default:
return new DOMHelper($this);
break;
}
}
}
class MyDOMDocument extends DOMDocument {
public function __construct($version=null,$encoding=null) {
parent::__construct($version,$encoding);
$this->registerNodeClass('DOMElement', 'MyDOMElement');
}
}
class DOMHelper {
protected $node;
public function __construct(DOMNode $node) {
$this->node = $node;
}
public function name() { return $this->node->getAttribute("name"); }
}
class DOMHelperFoo extends DOMHelper {
public function foo() {
echo 'foo';
$this->node->appendChild( $this->node->ownerDocument->createElement('action', 'something'));
}
}
class DOMHelperBar extends DOMHelper {
public function bar() {
echo 'bar';
$this->node->setAttribute('done', '1');
}
}
prints
fido: foo
scratchy: foo
----
lucky: bar
====
<?xml version="1.0"?>
<animals>
<Foo name="fido"><action>something</action></Foo>
<Bar name="lucky" done="1"/>
<Foo name="scratchy"><action>something</action></Foo>
<Ham name="flicka"/>
<Egg name="donald"/>
</animals>
EDIT for the code you show, wouldn't it be easier not to extend DOMElement at all? Just pass in the regular DOMElements to your processing Strategies, e.g.
class AnimalProcessor
{
public function processAnimals(DOMDocument $dom) {
foreach($dom->documentElement->childNodes as $animal) {
$strategy = $animal->tagName . 'Strategy';
$strategy = new $strategy($animal);
$strategy->process();
}
}
}
$dom = new DOMDocument;
$dom->load('animals.xml');
$processor = new AnimalProcessor;
$processor->processAnimals($dom);
Original answer before question update
Not sure if this is what you are looking for, but if you want specialized DOMElements, you can simply create them and use them directly, e.g. bypassing createElement, so you dont have to registerNodeClass at all.
class DogElement extends DOMElement
{
public function __construct($value)
{
parent::__construct('dog', $value);
}
}
class CatElement extends DOMElement
{
public function __construct($value)
{
parent::__construct('cat', $value);
}
}
$dom = new DOMDocument;
$dom->loadXML('<animals/>');
$dom->documentElement->appendChild(new DogElement('Sparky'));
$dom->documentElement->appendChild(new CatElement('Tinky'));
echo $dom->saveXml();
I don't think you can easily use registerNodeClass to instantiate elements based on the tagname or influence parsing that much. But you can override DOMDocument's createElement class, e.g.
class MyDOMDocument extends DOMDocument
{
public function createElement($nodeType, $value = NULL)
{
$nodeType = $nodeType . 'Element';
return new $nodeType($value);
}
}
$dom = new MyDOMDocument;
$dom->loadXML('<animals/>');
var_dump( $dom->createElement('Dog', 'Sparky') ); // DogElement
var_dump( $dom->createElement('Cat', 'Tinky') ); // CatElement
echo $dom->saveXml();