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');
Related
So, I am trying the get the types of the methods, to instantiate the classes, for example:
I have a class called mycontroller and a simple method called page which has a class Type hint, for example:
class MyController
{
public function page(AnotherClass $class)
{
$class->intro;
}
}
I also have another class, litterly called anotherclass (very original, I know)
class AnotherClass
{
public $intro = "Hello";
}
Okay, so that's the basics, now I am trying to get the type of MYControllers method arguments page: anotherclass
You can see the logic of my code below:
Class Route
{
/**
* Method paramaters
*
* #var array
*/
protected $params;
/**
* The class and method
*
* #var array
*/
protected $action;
/**
* Get the paramaters of a callable function
*
* #return void
*/
public function getParams()
{
$this->params = (new ReflectionMethod($this->action[0], $this->action[1]))->getParameters();
}
/**
* Seperate the class and method
*
* #param [type] $action
* #return void
*/
public function getClassAndMethod($action = null)
{
$this->action = explode("#", $action);
}
/**
* A get request
*
* #param string $route
* #return self
*/
public function get($route = null)
{
if(is_null($route)) {
throw new Exception("the [$route] must be defined");
}
return $this;
}
public function uses($action = null)
{
if(is_null($action)){
throw new Exception("the [$action] must be set");
}
if(is_callable($action)){
return call_user_func($action);
}
// Get the action
$this->getClassAndMethod($action);
// Get the params of the method
$this->getParams();
foreach ($this->params as $param) {
print_R($param->getType());
}
// var_dump($action[0]);
}
}
Which is simply being called like so:
echo (new Route)->get('hello')->uses('MyController#page');
So, what the above does, is it splits the uses method paramater via the # sign, the [0] will be the class and the [1] will be the class's method, then I am simply ReflectionMethod to get the parameters of said method, and then I am trying to get the parameter type, which, is what I am stuck on, because it just keeps returning an empty object:
ReflectionNamedType Object { )
So, my question is, why is it returning an empty object, and how can I get the type of the parameter?
You have to echo instead of print_r :
foreach ($this->params as $param) {
echo $param->getType() ; //AnotherClass
}
Because ReflectionType use __toString() to display it.
Or
foreach ($this->params as $param) {
print_r($param->getClass()) ; //AnotherClass
}
Is there any option to inform PhpStorm that method which it says that not exist, is beyond his scope and is defined somewhere else ?
In simpler words:
I have method execution:
Db::transactional($this)->transactionalUpdate($result);
I have got method definition also:
public function transactionalUpdate(ImportantObjectButNotMuch $baconWithButter)
{
echo 'Do a lot of tricks...';
}
Unfortunately PhpStorm doesn't know that execution : ->transactionalUpdate($result); should run public function transactionalUpdate.
Is there any option to write PhpDoc or some other tag to inform it that in case of name refactorization it should change the original function name too ?
P.S. My class structure looks like this:
class Db
{
public static function transactional($object)
{
return TransactionalProxy::newInstance($object); //3. It returns ApiObject object
}
}
class ApiObject
{
public function update_record()
{
//1. I am starting from there
$result = new ImportantObjectButNotMuch();
Db::transactional($this)->transactionalUpdate($result); //2. Next i am passing $this to Db class, to transactional method //4. It should run below transactionalUpdate method
}
public function transactionalUpdate(ImportantObjectButNotMuch $baconWithButter)
{
echo 'Do a lot of tricks...'; //5. It ends there, it is working but PhpStorm doesn't see it
}
}
EDIT AFTER ANSWER:
#Nukeface and #Dmitry caused me to come up with the answer on my Question:
Lets see again into my files structure:
class Db
{
public static function transactional($object)
{
return TransactionalProxy::newInstance($object); //3. It returns ApiObject object
}
}
class ApiObject
{
public function update_record()
{
//1. I am starting from there
$result = new ImportantObjectButNotMuch();
//EDIT//Db::transactional($this)->transactionalUpdate($result); //2. Next i am passing $this to Db class, to transactional method //4. It should run below transactionalUpdate method
/** #var self $thisObject */
//Line above informs PhpStorm that $thisObject is ApiObject indeed
$thisObject = Db::transactional($this)
$thisObject->transactionalUpdate($result);
}
public function transactionalUpdate(ImportantObjectButNotMuch $baconWithButter)
{
echo 'Do a lot of tricks...'; //5. It ends there, it is working but PhpStorm doesn't see it
}
}
You should make use of Typehints. Updated your code below:
/**
* Class Db
* #package Namespace\To\Db
*/
class Db
{
/**
* #param $object
* #return ApiObject (per your line comment)
*/
public static function transactional($object)
{
return TransactionalProxy::newInstance($object); //3. It returns ApiObject object
}
}
/**
* Class ApiObject
* #package Namespace\To\ApiObject
*/
class ApiObject
{
/**
* #return void (I see no "return" statement)
*/
public function update_record()
{
//1. I am starting from there
$result = new ImportantObjectButNotMuch();
Db::transactional($this)->transactionalUpdate($result); //2. Next i am passing $this to Db class, to transactional method //4. It should run below transactionalUpdate method
}
/**
* #param ImportantObjectButNotMuch $baconWithButter
* #return void
*/
public function transactionalUpdate(ImportantObjectButNotMuch $baconWithButter)
{
echo 'Do a lot of tricks...'; //5. It ends there, it is working but PhpStorm doesn't see it
}
}
You can quickly create basic docblocks and typehints by typing /** then pressing either "enter" or "space". Enter if you want a docblock and space if you want a typehint.
Examples of own code below:
/**
* Class AbstractEventHandler
* #package Hzw\Mvc\Event
*/
abstract class AbstractEventHandler implements EventManagerAwareInterface
{
/**
* #var EventManagerInterface
*/
protected $events;
/**
* #var EntityManager|ObjectManager
*/
protected $entityManager;
/**
* AbstractEvent constructor.
* #param ObjectManager $entityManager
*/
public function __construct(ObjectManager $entityManager)
{
$this->setEntityManager($entityManager);
}
/**
* #param EventManagerInterface $events
*/
public function setEventManager(EventManagerInterface $events)
{
$events->setIdentifiers([
__CLASS__,
get_class($this)
]);
$this->events = $events;
}
/**
* #return EventManagerInterface
*/
public function getEventManager()
{
if (!$this->events) {
$this->setEventManager(new EventManager());
}
return $this->events;
}
/**
* #return ObjectManager|EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
/**
* #param ObjectManager|EntityManager $entityManager
* #return AbstractEventHandler
*/
public function setEntityManager($entityManager)
{
$this->entityManager = $entityManager;
return $this;
}
}
In the above example, PhpStorm knows what every function requires and returns. It knows the types and as some "return $this" it knows about the possibility to chain functions.
As an addition, the above code example uses only "docblocks". Below some "inline typehints" from within a function. Especially useful when it's not going to be immediately clear what is going to be returned. That way, again, PhpStorm knows from where to get functions, options, etc. to show you.
/** #var AbstractForm $form */
$form = $this->getFormElementManager()->get($formName, (is_null($formOptions) ? [] : $formOptions));
/** #var Request $request */
$request = $this->getRequest();
As a final hint. If you create a bunch of properties for a class, such as in my example protected $events or protected $entityManager, you can also generate the getters & setters. If your properties contain the docblocks, it will also generate the docblocks for you on these functions.
E.g. the property below
/**
* #var EntityManager|ObjectManager
*/
protected $entityManager;
When using "Alt + Insert" you get a menu at cursor location. Choose "Getters/Setters". In the pop-up, select "entityManager" and check the box at the bottom for "fluent setters". Then the code below is generated for you:
/**
* #return ObjectManager|EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
/**
* #param ObjectManager|EntityManager $entityManager
* #return AbstractEventHandler
*/
public function setEntityManager($entityManager)
{
$this->entityManager = $entityManager;
return $this;
}
The closes thing you can do to what you want to do is to use #return with multiple types.
/**
* #param $object
* #return ApiObject|AnotherApiObject|OneMoreApiObject
*/
public static function transactional($object)
{
return TransactionalProxy::newInstance($object);
}
I am new to PHP and trying to a third party code namely Big blue button api
https://github.com/bigbluebutton/bigbluebutton/tree/master/labs/bbb-api-php
I try to call BigBlueButton->createMeeting() function which looks like this :
public function createMeeting($createMeetingParams, $xml = '')
{
$xml = $this->processXmlResponse($this
->getCreateMeetingURL($createMeetingParams), $xml);
//$xml is fine
return new CreateMeetingResponse($xml);
}
CreateMeetingResponse class
namespace BigBlueButton\Responses;
/**
* Class CreateMeetingResponse
* #package BigBlueButton\Responses
*/
class CreateMeetingResponse extends BaseResponse
{
/**
* #return string
*/
public function getMeetingId()
{
return $this->rawXml->meetingID->__toString();
}
/**
* #return string
*/
public function getAttendeePassword()
{
return $this->rawXml->attendeePW->__toString();
}
/**
* #return string
*/
public function getModeratorPassword()
{
return $this->rawXml->moderatorPW->__toString();
}
/**
* Creation timestamp.
*
* #return double
*/
public function getCreationTime()
{
return doubleval($this->rawXml->createTime);
}
/**
* #return int
*/
public function getVoiceBridge()
{
return intval($this->rawXml->voiceBridge);
}
/**
* #return string
*/
public function getDialNumber()
{
return $this->rawXml->dialNumber->__toString();
}
/**
* Creation date at the format "Sun Jan 17 18:20:07 EST 2016".
*
* #return string
*/
public function getCreationDate()
{
return $this->rawXml->createDate->__toString();
}
/**
* #return true
*/
public function hasUserJoined()
{
return $this->rawXml->hasUserJoined->__toString() == 'true';
}
/**
* #return int
*/
public function getDuration()
{
return intval($this->rawXml->duration);
}
/**
* #return bool
*/
public function hasBeenForciblyEnded()
{
return $this->rawXml->hasBeenForciblyEnded->__toString() == 'true';
}
/**
* #return string
*/
public function getMessageKey()
{
return $this->rawXml->messageKey->__toString();
}
/**
* #return string
*/
public function getMessage()
{
$this->rawXml->message->__toString();
}
}
BaseResponse class
namespace BigBlueButton\Parameters;
/**
* Class BaseParameters.
*/
abstract class BaseParameters
{
/**
* #param $array
*
* #return string
*/
protected function buildHTTPQuery($array)
{
return http_build_query(array_filter($array));
}
/**
* #return string
*/
abstract public function getHTTPQuery();
}
Now when I do call the BigBlueButton->createMeeting() function, I am expecting an object which I can encode to json ,But what I get is this (I have used print_r() here..):
BigBlueButton\Responses\CreateMeetingResponse Object
(
[rawXml:protected] => SimpleXMLElement Object
(
[returncode] => FAILED
[messageKey] => idNotUnique
[message] => A meeting already exists with that meeting ID. Please use a different meeting ID.
)
)
I am not sure what is happening but I think the prefixed namespace 'BigBlueButton\Responses\CreateMeetingResponse Object' is the problem. I want to parse the response I get to an json object in php but cannot
Here is where I try to parse it
function easymeet_create_meeting($id) {
// Create BBB object
$bbb = new BigBlueButton\BigBlueButton();
//creating meeting parameter
$meetingParas=new BigBlueButton\Parameters\CreateMeetingParameters('123456','sned');
//Creatign meeting
return json_encode($bbb->createMeeting($meetingParas));
///print_r($bbb->createMeeting($meetingParas)) give the xml response shown above
}
The return part looks right. The error you are getting is coming from BigBlueButton->createMeeting()
You already have created a meeting with the ID you used. Are you generating a new Meeting ID to pass in with the XML when you create a new meeting?
Edit:
To be able to json_encode the response you will need to use the getRawXml() function since $rawXml is a protected property of the base class and the rest of the class is just methods. So:
public function createMeeting($createMeetingParams, $xml = '')
{
$xml = $this->processXmlResponse($this
->getCreateMeetingURL($createMeetingParams), $xml);
//$xml is fine
$resp = new CreateMeetingResponse($xml);
return $resp->getRawXml();
}
Should return just the SimpleXMLElement which you can then json_encode.
I'm a little bit confused about how to unit test a constructor, particularly since it returns no value.
Let's assume I have this class:
class MyClass {
/** #var array */
public $registered_items;
/**
* Register all of the items upon instantiation
*
* #param array $myArrayOfItems an array of objects
*/
public function __construct($myArrayOfItems) {
foreach($myArrayOfItems as $myItem) {
$this->registerItem($myItem);
}
}
/**
* Register a single item
*
* #param object $item a single item with properties 'slug' and 'data'
*/
private function registerItem($item) {
$this->registered_items[$item->slug] = $item->data;
}
}
Obviously this is a bit contrived and incredibly simple, but it's for the sake of the question. =)
So yeah, how would I go about writing a unit test for the constructor here?
Bonus question: am I right in thinking that no unit test for registerItem() would be needed in a case such as this?
EDIT
How about if I re-factored to remove the logic from the constructor. How would I test registerItem() in this case?
class MyClass {
/** #var array */
public $registered_items;
public function __construct() {
// Nothing at the moment
}
/**
* Register all of the items
*
* #param array $myArrayOfItems an array of objects
*/
public function registerItem($myArrayOfItems) {
foreach($myArrayOfItems as $item) {
$this->registered_items[$item->slug] = $item->data;
}
}
}
Add a method to look up a registered item.
class MyClass {
...
/**
* Returns a registered item
*
* #param string $slug unique slug of the item to retrieve
* #return object the matching registered item or null
*/
public function getRegisteredItem($slug) {
return isset($this->registered_items[$slug]) ? $this->registered_items[$slug] : null;
}
}
Then check that each item passed to the constructor in the test has been registered.
class MyClassTest {
public function testConstructorRegistersItems() {
$item = new Item('slug');
$fixture = new MyClass(array($item));
assertThat($fixture->getRegisteredItem('slug'), identicalTo($item));
}
}
Note: I'm using the Hamcrest assertions, but PHPUnit should have an equivalent.
For First Code
public function testConstruct{
$arrayOfItems = your array;
$myClass = new MyClass($arrayOfItems);
foreach($arrayOfItems as $myItem) {
$expected_registered_items[$item->slug] = $item->data;
}
$this->assertEquals($expected_registered_items, $myClass->registered_items);
}
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.