I am using a library, and it has the following process to attach many operations to one event:
$action = (new EventBuilder($target))->addOperation($Operation1)->addOperation($Operation2)->addOperation($Operation3)->compile();
I am not sure how to dynamically add operations depending on what I need done.
Something like this
$action = (new EventBuilder($target));
while (some event) {
$action = $action->addOperation($OperationX);
}
$action->compile();
I need to be able to dynamically add operations in while loop and when all have been added run it.
Your proposed solution will work. The EventBuilder provides what is known as a Fluent Interface, which means that there are methods that return an instance of the builder itself, allowing you to chain calls to addOperation as many times as you want, then call the compile method to yield a result. However you are free to ignore the return value of addOperation as long as you have a variable containing an instance of the builder that you can eventually call compile on.
Take a walk with me...
// Some boilerplate classes to work with
class Target
{
private ?string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
}
class Operation
{
private ?string $verb;
public function __construct(string $verb)
{
$this->verb = $verb;
}
public function getVerb(): string
{
return $this->verb;
}
}
class Action
{
private ?Target $target;
private array $operations = [];
public function __construct(Target $target, array $operations)
{
$this->target = $target;
$this->operations = $operations;
}
/**
* Do the things
* #return array
*/
public function run(): array
{
$output = [];
foreach ($this->operations as $currOperation)
{
$output[] = $currOperation->getVerb() . ' the ' . $this->target->getName();
}
return $output;
}
}
Here is a basic explanation of what your EventBuilder is doing under the covers:
class EventBuilder
{
private ?Target $target;
private array $operations = [];
public function __construct(Target $target)
{
$this->target = $target;
}
/**
* #param Operation $operation
* #return $this
*/
public function addOperation(Operation $operation): EventBuilder
{
$this->operations[] = $operation;
// Fluent interface - return a reference to the instance
return $this;
}
public function compile(): Action
{
return new Action($this->target, $this->operations);
}
}
Let's try both techniques and prove they will produce the same result:
// Mock some operations
$myOperations = [
new Operation('Repair'),
new Operation('Clean'),
new Operation('Drive')
];
// Create a target
$target = new Target('Car');
/*
* Since the EventBuilder implements a fluent interface (returns an instance of itself from addOperation),
* we can chain the method calls together and just put a call to compile() at the end, which will return
* an Action instance
*/
$fluentAction = (new EventBuilder($target))
->addOperation($myOperations[0])
->addOperation($myOperations[1])
->addOperation($myOperations[2])
->compile();
// Run the action
$fluentResult = $fluentAction->run();
// Traditional approach, create an instance and call the addOperation method as needed
$builder = new EventBuilder($target);
// Pass our mocked operations
while (($currAction = array_shift($myOperations)))
{
/*
* We can ignore the result from addOperation here, just keep calling the method
* on the builder variable
*/
$builder->addOperation($currAction);
}
/*
* After we've added all of our operations, we can call compile on the builder instance to
* generate our Action.
*/
$traditionalAction = $builder->compile();
// Run the action
$traditionalResult = $traditionalAction->run();
// Verify that the results from both techniques are identical
assert($fluentResult == $traditionalResult, 'Results from both techniques should be identical');
// Enjoy the fruits of our labor
echo json_encode($traditionalResult, JSON_PRETTY_PRINT).PHP_EOL;
Output:
[
"Repair the Car",
"Clean the Car",
"Drive the Car"
]
Rob Ruchte thank you for detailed explanation, one thing I did not include was that each operation itself had ->build() call and I needed to move that to each $builder for it to work.
Related
I then have a Base DTO class
class BaseDto
{
public function __construct(array $dtoValues)
{
$this->properties = array_map(static function (ReflectionProperty $q) {
return trim($q->name);
}, (new ReflectionClass(get_class($this)))->getProperties(ReflectionProperty::IS_PUBLIC));
foreach ($dtoValues as $propertyName => $value) {
$propertyName = Str::camel($propertyName);
if (in_array($propertyName, $this->properties, false)) {
$this->{$propertyName} = $value;
}
}
}
}
I also have an actual DTO class
class ModelDTO extends BaseDto
{
public int $id
public string $name;
}
I have the following Trait in PHP
trait ToDtoTrait
{
/**
* #param string $dtoClassName
* #return BaseDto
* #throws InvalidArgumentException
*/
public function toDto(string $dtoClassName): BaseDto;
{
$this->validateDtoClass($dtoClassName, BaseDto::class);
return new $dtoClassName($this->toArray());
}
/**
* #param string $dtoClassName
* #param string $baseClassName
* #return void
*/
private function validateDtoClass(string $dtoClassName, string $baseClassName)
{
if (!class_exists($dtoClassName)) {
throw new InvalidArgumentException("Trying to create a DTO for a class that doesn't exist: {$dtoClassName}");
}
if (!is_subclass_of($dtoClassName, $baseClassName)) {
throw new InvalidArgumentException("Can not convert current object to '{$dtoClassName}' as it is not a valid DTO class: " . self::class);
}
}
}
That trait is then used inside of my Model classes
class MyDbModel
{
use ToDtoTrait;
}
So this allows me to get an entry from the DB via the Model and then call toDto to receive an instance of the DTO class. Simple enough.
Now I have a service and this service will basically find the entry and return the DTO.
class MyService
{
public function find(int $id): ?ModelDTO
{
$model = MyModel::find($id);
if (empty($model)) {
return null;
}
return $model->toDto();
}
}
When I do it this way, I get a warning in the IDE:
Return value is expected to be '\App\Dtos\ModelDto|null', '\App\Dtos\BaseDto' returned
How do I declare this so that people can see that MyService::find() returns an instance of ModelDto so they will have autocomplete for the attributes of the DTO and any other base functions that come with it (not shown here).
The warning is raised because the return type of ToDtoTrait::toDto isBaseDto while the return type of MyService::find is ?ModelDTO, which are polymorphically incompatible (a BaseDto is not necessarily a ModelDTO).
An easy solution is to narrow down the DTO type using instanceof:
// MyService::find
$model = MyModel::find($id);
if (empty($model)) {
return null;
}
$dto = $model->toDto();
if (!$dto instanceof ModelDTO) {
return null;
}
return $dto;
Sidenote: Why is ToDtoTrait::toDto called without arguments in MyService (return $model->toDto();)? Looks like you want to pass ModelDTO::class to it: return $model->toDto(ModelDTO::class);.
Disclaimer: I would have preferred a more generic question like "How do I keep track of the state of a recursive method?" on the code review stack exchange site, as that better describes where the problem is currently at. But the constraint on that board is that code must first be working
Background: I have a container that
can recursively hydrate and inject constructor arguments
concretes can be provided to it for use to circumvent hydration
provision context exist in two states: for classes being auto-wired, and for classes using the container as a service locator
Three of these target behaviours function as expected, save when the last 2 are combined.
Main problem: when 3a is followed by 3b, container uses an incorrect context, and I don't know how to inspect that particular state since the hydrator/service locator is recursive. Unit testing the individual methods all work correctly. Integration test of either of the two works. But at the level of multi layer hydration, I can't mock out any of the involved parts, thus, I have no way of determining what context is used there
I feel the problem is more of a philosophical one, where more than one answer is applicable. But instead of down-voting, kindly migrate to the appropriate stack exchange site
The code is in PHP, but if you aren't conversant with it, pseudo-code or a verbal solution is welcome. An acceptable solution may even be a test that demonstrates how to simulate and observe container state after hydration and service location. I already have this, but it fails
public function test_hydrated_class_with_getClass_correctly_uses_needs () {
$ourB = new BCounter; // given
$this->container->provideSelf();
$this->container->whenTypeAny()->needsAny([
BCounter::class => $ourB
]);
$this->assertSame( // then
$this->container->getClass($this->aRequires)
->getInternalB(), // when
$this->ourB
);
}
Relevant parts of the container below
<?php
use ReflectionMethod, ReflectionClass, ReflectionFunction, ReflectionType, ReflectionFunctionAbstract, ReflectionException;
class Container {
const UNIVERSAL_SELECTOR = "*";
private $provisionedNamespaces = [], // NamespaceUnit[]
$hydratingForStack = [], // String[]. Doubles as a dependency chain. #see [lastHydratedFor] for main usage
$internalMethodHydrate = false, // Used when [getMethodParameters] is called directly without going through instance methods such as [instantiateConcrete]
$hydratingArguments = false,
$constructor = "__construct",
$externalHydrators = [], $externalContainerManager,
$interfaceHydrator,
$provisionContext, // the active Type before calling `needs`
$provisionSpace; // same as above, but for namespaces
protected $provisionedClasses = []; // ProvisionUnit[]
public function __construct () {
$this->initializeUniversalProvision();
}
public function initializeUniversalProvision ():void {
$this->provisionedClasses[self::UNIVERSAL_SELECTOR] = new ProvisionUnit;
}
public function getInterfaceHydrator ():InterfaceHydrator {
return $this->interfaceHydrator;
}
/**
* Looks for the given class in this order
* 1) pre-provisioned caller list
* 2) Provisions it afresh if an interface or recursively wires in its constructor dependencies
*
* #param {includeSub} Regular provision: A wants B, but we give C sub-class of B. Sub-classes of A can't obtain B unless this parameter is used
*
* #return A class instance, if found
*/
public function getClass (string $fullName, bool $includeSub = false) {
$concrete = $this->decorateProvidedConcrete($fullName);
if (!is_null($concrete)) return $concrete;
if ($includeSub && $parent = $this->hydrateChildsParent($fullName))
return $parent;
$externalManager = $this->externalContainerManager;
if (
!is_null($externalManager) &&
$concrete = $externalManager->findInManagers($fullName)
) {
$this->saveWhenImplements($fullName, $concrete);
return $concrete;
}
return $this->initializeHydratingForAction($fullName, function ($className) {
if ($this->getReflectedClass($className)->isInterface())
return $this->provideInterface($className);
return $this->instantiateConcrete($className);
});
}
public function decorateProvidedConcrete (string $fullName) {
$freshlyCreated = $this->initializeHydratingForAction($fullName, function ($className) {
return new HydratedConcrete(
$this->getProvidedConcrete($className),
$this->lastHydratedFor()
);
});
if (!is_null($freshlyCreated->getConcrete()))
return $this->getDecorator()->scopeInjecting(
$freshlyCreated->getConcrete(),
$freshlyCreated->getCreatedFor()
); // decorator runs on each fetch (rather than only once), since different callers result in different behavior
}
public function getProvidedConcrete (string $fullName) {
$context = $this->getRecursionContext();
if ($context->hasConcrete($fullName))
return $context->getConcrete($fullName);
$globalContext = $this->provisionedClasses[self::UNIVERSAL_SELECTOR];
if ($globalContext->hasConcrete($fullName)) // current provision doesn't include this class. check in global
return $globalContext->getConcrete($fullName);
}
/**
* Switches unit being provided to universal if it doesn't exist
* #return currently available provision unit
*/
public function getRecursionContext ():ProvisionUnit {
$hydrateFor = $this->lastHydratedFor();
if (!array_key_exists($hydrateFor, $this->provisionedClasses))
$hydrateFor = self::UNIVERSAL_SELECTOR;
return $this->provisionedClasses[$hydrateFor];
}
/**
* This tells us the class we are hydrating arguments for
*/
public function lastHydratedFor ():?string {
$stack = $this->hydratingForStack;
if(empty($stack) ) return null;
$index = $this->hydratingArguments ? 2: 1; // If we're hydrating class A -> B -> C, we want to get provisions for B (who, at this point, is indexed -2 while C is -1). otherwise, we'll be looking through C's provisions instead of B
$length = count($stack);
return $stack[$length - $index];
}
/**
* Not explicitly decorating objects from here since it calls [getClass]
*/
private function hydrateChildsParent (string $fullName) {
$providedParent = $this->getProvidedParent($fullName);
if (!is_null($providedParent))
return $this->getClass($providedParent);
}
/**
* #return the first provided parent of the given class
*/
private function getProvidedParent (string $class):?string {
$allSuperiors = array_keys($this->provisionedClasses);
$classSuperiors = array_merge(
class_parents($class, true),
class_implements($class, true)
);
return current(
array_intersect($classSuperiors, $allSuperiors)
);
}
private function saveWhenImplements (string $interface, $concrete):void {
if (!($concrete instanceof $interface))
throw new InvalidImplementor($interface, get_class($concrete));
$this->storeConcrete( $interface, $concrete);
}
private function storeConcrete (string $fullName, $concrete):ProvisionUnit {
return $this->getRecursionContext()->addConcrete($fullName, $concrete);
}
private function getReflectedClass (string $className):ReflectionClass {
try {
return new ReflectionClass($className);
}
catch (ReflectionException $re) {
$message = "Unable to hydrate ". $this->lastHydratedFor() . ": ". $re->getMessage();
$hint = "Hint: Cross-check its dependencies";
throw new HydrationException("$message. $hint");
}
}
/**
* Wrap any call that internally attempts to read from [lastHydratedFor] in this i.e. calls that do some hydration and need to know what context/provision they're being hydrated for
*/
public function initializeHydratingForAction (string $fullName, callable $action) {
$this->initializeHydratingFor($fullName);
$result = $action($fullName);
$this->popHydratingFor($fullName);
return $result;
}
/**
* Tells us who to hydrate arguments for
*/
protected function initializeHydratingFor (string $fullName):void {
$isFirstCall = is_null($this->lastHydratedFor());
$hydrateFor = $isFirstCall ? $this->lastCaller(): $fullName;
$this->pushHydratingFor($hydrateFor);
}
private function lastCaller ():string {
$stack = debug_backtrace (2 ); // 2=> ignore concrete objects and their args
$caller = "class";
foreach ($stack as $execution)
if (array_key_exists($caller, $execution) && $execution[$caller] != get_class()) {
return $execution[$caller];
}
}
/**
* Updates the last element in the context hydrating stack, to that whose provision dependencies should be hydrated for
*/
protected function pushHydratingFor (string $fullName):void {
$this->hydratingForStack[] = $fullName;
}
/**
* #param {completedHydration} To guarantee push-pop consistency. When the name of what is expected to be removed doesn't match the last item in stack, it indicates we're currently hydrating an interface (where its name differs from concretes involved). When this happens, we simply ignore popping our list since those concretes were not the ones that originally got pushed
*/
private function popHydratingFor (string $completedHydration):void {
if (end($this->hydratingForStack) == $completedHydration)
array_pop($this->hydratingForStack);
}
/**
* #throws InvalidImplementor
*
* #return Concrete of the given [Interface] if it was bound
*/
protected function provideInterface (string $interface) {
$caller = $this->lastHydratedFor();
if ($this->hasRenamedSpace($caller)) {
$newIdentity = $this->relocateSpace($interface, $caller);
$concrete = $this->instantiateConcrete($newIdentity);
}
else {
$concrete = $this->getInterfaceHydrator()->deriveConcrete($interface);
if (!is_null($concrete))
$this->saveWhenImplements($interface, $concrete);
}
if (is_null($concrete))
throw new InvalidImplementor($interface, "No matching concrete" );
return $concrete;
}
/**
* A shorter version of [getClass], but neither checks in cache or contextual provisions. This means they're useful to:
* 1) To hydrate classes we're sure doesn't exist in the cache
* 2) In methods that won't be called more than once in the request cycle
* 3) To create objects that are more or less static, or can't be overidden by an extension
*
* All objects internally derived from this trigger decorators if any are applied
*/
public function instantiateConcrete (string $fullName) {
$freshlyCreated = $this->initializeHydratingForAction ($fullName, function ($className) {
if (!method_exists($className, $this->constructor))
return new HydratedConcrete(new $className, $this->lastHydratedFor() );
return $this->hydrateConcreteForCaller($className);
});
$this->storeConcrete($fullName, $freshlyCreated->getConcrete());
return $this->getDecorator()->scopeInjecting(
$freshlyCreated->getConcrete(),
$freshlyCreated->getCreatedFor()
);
}
public function hydrateConcreteForCaller (string $className):HydratedConcrete {
$dependencies = $this->internalMethodGetParameters(function () use ($className) {
return array_values($this->getMethodParameters($this->constructor, $className));
});
return new HydratedConcrete(
new $className (...$dependencies),
$this->lastHydratedFor()
);
}
public function internalMethodGetParameters (callable $action) {
$this->internalMethodHydrate = true;
$result = $action();
$this->internalMethodHydrate = false;
return $result;
}
/**
* Fetch appropriate dependencies for a callable's arguments
*
* #param {callable}:string|Closure
* #param {anchorClass} the class the given method belongs to
*
* #return {Array} associative. Contains hydrated parameters to invoke given callable with
*/
public function getMethodParameters ( $callable, string $anchorClass = null):array {
$context = null;
if (is_null($anchorClass))
$reflectedCallable = new ReflectionFunction($callable);
else {
$reflectedCallable = new ReflectionMethod($anchorClass, $callable);
if (!$this->internalMethodHydrate)
$this->initializeHydratingFor($anchorClass);
$context = $this->getRecursionContext();
}
$dependencies = $this->populateDependencies($reflectedCallable, $context);
if (is_null($anchorClass)) return $dependencies;
elseif (!$this->internalMethodHydrate)
$this->popHydratingFor($anchorClass);
return $this->getDecorator()->scopeArguments( $anchorClass, $dependencies, $callable);
}
public function populateDependencies (ReflectionFunctionAbstract $reflectedCallable, ?ProvisionUnit $callerProvision):array {
$dependencies = [];
foreach ($reflectedCallable->getParameters() as $parameter) {
$parameterName = $parameter->getName();
$parameterType = $parameter->getType();
if (!is_null($callerProvision) )
$dependencies[$parameterName] = $this->hydrateProvidedParameter($callerProvision, $parameterType, $parameterName);
elseif (!is_null($parameterType))
$dependencies[$parameterName] = $this->hydrateUnprovidedParameter($parameterType);
elseif ($parameter->isOptional() )
$dependencies[$parameterName] = $parameter->getDefaultValue();
else $dependencies[$parameterName] = null;
}
return $dependencies;
}
/**
* Pulls out a provided instance of a dependency when present, or creates a fresh one
*
* #return object matching type at given parameter
*/
private function hydrateProvidedParameter (ProvisionUnit $callerProvision, ReflectionType $parameterType, string $parameterName) {
if ($callerProvision->hasArgument($parameterName))
return $callerProvision->getArgument($parameterName);
$typeName = $parameterType->getName();
if ($callerProvision->hasArgument($typeName))
return $callerProvision->getArgument($typeName);
return $this->hydrateUnprovidedParameter($parameterType);
}
private function hydrateUnprovidedParameter (ReflectionType $parameterType) {
$typeName = $parameterType->getName();
if ( $parameterType->isBuiltin()) {
$defaultValue = null;
settype($defaultValue, $typeName);
return $defaultValue;
}
if (!in_array($typeName, $this->hydratingForStack)) {
$this->hydratingArguments = true;
$concrete = $this->getClass($typeName);
$this->hydratingArguments = false;
return $concrete;
}
if ($this->getReflectedClass($typeName)->isInterface())
throw new HydrationException ("$typeName's concrete cannot depend on its dependency's concrete");
trigger_error("Circular dependency detected while hydrating $typeName", E_USER_WARNING);
}
public function whenType (string $toProvision):self {
if (!array_key_exists($toProvision, $this->provisionedClasses))
$this->provisionedClasses[$toProvision] = new ProvisionUnit;
$this->provisionContext = $toProvision;
return $this;
}
public function whenTypeAny ():self {
return $this->whenType(self::UNIVERSAL_SELECTOR);
}
public function needs (array $dependencyList):self {
if (is_null ($this->provisionContext))
throw new HydrationException("Undefined provisionContext");
$this->provisionedClasses[$this->provisionContext]->updateConcretes($dependencyList);
return $this;
}
public function needsAny (array $dependencyList):self {
$this->needs($dependencyList)
->needsArguments($dependencyList);
$this->provisionContext = null;
return $this;
}
public function needsArguments (array $argumentList):self {
if (is_null ($this->provisionContext))
throw new HydrationException("Undefined provisionContext");
$this->provisionedClasses[$this->provisionContext]->updateArguments($argumentList);
return $this;
}
public function provideSelf ():void {
$this->whenTypeAny()->needsAny([get_class() => $this]);
}
protected function getDecorator ():DecoratorHydrator {
return $this->decorator;
}
}
?>
The relevant parts are:
decorateProvidedConcrete (who I want to assert was called twice, but by mocking, getClass can no longer function)
public function test_hydrated_class_with_getClass_reads_provision () {
// given
$container = $this->positiveDouble(Container::class, [
"getDecorator" => $this->stubDecorator(),
"getProvidedConcrete" => $this->returnCallback(function ($subject) {
return $this->positiveDouble($subject, []); // return a stub
})
], [
"getProvidedConcrete" => [2, [
$this->callback(function ($subject) {
return BCounter::class == $subject; // this obviously won't work, since method attempts to hydrate other classes, as well
})
]] // then 1
]);
$this->bootContainer($container);
$this->entityBindings();
// when
$container->getClass($this->aRequires)->getInternalB();
}
getRecursionContext, who I have no way of observing until getClass returns. But I would like to know what context it's working with by the time we're doing the service location. And, that's difficult to figure out since getClass is recursive
Finally, ARequiresBCounter. I want to DI this class with provided BCounter. Then when getInternalB runs, it equally uses the provided BCounter
class ARequiresBCounter {
private $b1, $container, $primitive;
public function __construct (BCounter $b1, Container $container, string $primitive) {
$this->b1 = $b1;
$this->container = $container;
$this->primitive = $primitive;
}
public function getConstructorB ():BCounter {
return $this->b1;
}
public function getInternalB ():BCounter {
return $this->container->getClass(BCounter::class);
}
}
This is probably a stupid question but I can't figure out what to google. I'm learning OOP and get the general concept but trying to implement it and I'm getting some questions.
I'm making a web scraper using simple html dom. I want to create a class which connects to a page and contains several different functions I can call on that page depending on what I want to do. I then can call this classes methods from another file.
This is the class I have written:
<?php
include_once '/simple_html_dom.php';
class CountyScraping
{
protected $demoMode = 0; //set 1 for demo
protected $cinemas = NULL;
protected $url = NULL;
protected $html = NULL;
protected $counties = array();
function __construct($url)
{
$this->setUrl($url);
}
public function setUrl($url)
{
$this->url = $url;
}
public function getUrl()
{
return $this->url;
}
public function setHtml()
{
$this->html = file_get_html($this->url);
}
public function getHtml()
{
return $this->html;
}
public function setCounties()
{
foreach($this->html->find('#UserLocation option') as $element)
{
if (($element->value != NULL) && ($element->plaintext))
{
$county = array();
$county['id'] = $element->value;
$county['county_name'] = $element->plaintext;
$this->counties[] = $county;
}
}
}
public function getCounties()
{
return $this->counties;
}
?>
As you can see from the class above I'm looking to get a list of counties from my page and it's all working as should be. The main method I want to call is to get an array of counties. Using the code above to get a list of counties I have to do something like this currently:
$scrape = new CountyScraping("http://example.com");
$scrape->setHtml();
$scrape->setCounties();
$counties = $scrape->getCounties();
This works fine and I could continue with this however I feel like I'm making a lot of calls to getters and setters from outside the class. I think I should be making one call, getCounties and have everything be handled inside the class.
Am I correct in assuming this? Should my getCounties() method call my setHtml and setCounties methods? Or should I be keeping my getters and setters as minimal as possible and have another function instead?
Any advice about any part of my code would be welcome.
PHP magic methods will help you a bit more also.
Here is a link to the php manual for __set()
http://php.net/manual/en/language.oop5.overloading.php#object.set
here is also a nice articale that will assist you with more information on magic methods
http://culttt.com/2014/04/16/php-magic-methods/
Here is a basic example of magic methods
<?php
class PropertyTest
{
/** Location for overloaded data. */
private $data = array();
/** Overloading not used on declared properties. */
public $declared = 1;
/** Overloading only used on this when accessed outside the class. */
private $hidden = 2;
public function __set($name, $value)
{
echo "Setting '$name' to '$value'\n";
$this->data[$name] = $value;
}
public function __get($name)
{
echo "Getting '$name'\n";
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}
$trace = debug_backtrace();
trigger_error(
'Undefined property via __get(): ' . $name .
' in ' . $trace[0]['file'] .
' on line ' . $trace[0]['line'],
E_USER_NOTICE);
return null;
}
/** As of PHP 5.1.0 */
public function __isset($name)
{
echo "Is '$name' set?\n";
return isset($this->data[$name]);
}
/** As of PHP 5.1.0 */
public function __unset($name)
{
echo "Unsetting '$name'\n";
unset($this->data[$name]);
}
/** Not a magic method, just here for example. */
public function getHidden()
{
return $this->hidden;
}
}
echo "<pre>\n";
$obj = new PropertyTest;
$obj->a = 1;
echo $obj->a . "\n\n";
var_dump(isset($obj->a));
unset($obj->a);
var_dump(isset($obj->a));
echo "\n";
echo $obj->declared . "\n\n";
echo "Let's experiment with the private property named 'hidden':\n";
echo "Privates are visible inside the class, so __get() not used...\n";
echo $obj->getHidden() . "\n";
echo "Privates not visible outside of class, so __get() is used...\n";
echo $obj->hidden . "\n";
?>
Output is here:
Setting 'a' to '1'
Getting 'a'
1
Is 'a' set?
bool(true)
Unsetting 'a'
Is 'a' set?
bool(false)
1
Let's experiment with the private property named 'hidden':
Privates are visible inside the class, so __get() not used...
2
Privates not visible outside of class, so __get() is used...
Getting 'hidden'
Notice: Undefined property via __get(): hidden in <file> on line 70 in <file> on line 29
This example has been sourced from http://php.net/manual/en/language.oop5.overloading.php#object.set
I would suggest you to always use an SOA (service oriented architecture) when you want to develop with OOP. With SOA your service coming from the class CountyScraper should be unique. This is why we use a singleton. A service shouldn't keep a state from an action. This is why you shouldn't set the url or html as properties.
<?php
include_once '/simple_html_dom.php';
class CountyScraper
{
/**
* Returns the *Singleton* instance of this class.
*
* #staticvar Singleton $instance The *Singleton* instances of this class.
*
* #return Singleton The *Singleton* instance.
*/
public static function getInstance()
{
static $instance = null;
if (null === $instance) {
$instance = new static();
}
return $instance;
}
/**
* Protected constructor to prevent creating a new instance of the
* *Singleton* via the `new` operator from outside of this class.
*/
protected function __construct()
{
}
/**
* Private clone method to prevent cloning of the instance of the
* *Singleton* instance.
*
* #return void
*/
private function __clone()
{
}
/**
* Private unserialize method to prevent unserializing of the *Singleton*
* instance.
*
* #return void
*/
private function __wakeup()
{
}
public function findCounties($url)
{
$counties = array();
$html = file_get_html($url);
foreach ($html->find('#UserLocation option') as $element)
{
if (($element->value != NULL) && ($element->plaintext))
{
$county = array();
$county['id'] = $element->value;
$county['county_name'] = $element->plaintext;
$counties[] = $county;
}
}
}
}
Then, you can use it like that:
$countyScraper = CountyScraper::getInstance();
$countyScraper->findCounties('http://example.com');
I've recently learned about the advantages of using Dependency Injection (DI) in my PHP application.
However, I'm still unsure how to create my container for the dependencies. Before, I use a container from a framework and I want to understand how is he doing things in back and reproduce it.
For example:
The container from Zend 2. I understand that the container make class dynamic, he does not have to know about them from the beginning, he checks if he already has that class in his registry and if he has not he check if that class exist and what parameters has inside constructor and put it in his own registry so next time could take it from there, practical is doing everything dynamic and it is completing his own registry, so we do not have to take care of nothing once we implement the container as he can give as any class we want even if we just make that class.
Also if I want to getInstance for A which needs B and B needs C I understand that he doing this recursive and he goes and instantiate C then B and finally A.
So I understand the big picture and what is he suppose to do but I am not so sure about how to implement it.
You may be better off using one of the existing Dependency Containers out there, such as PHP-DI or Pimple. However, if you are looking for a simpler solution, then I've implemented a Dependency Container as part of an article that I wrote here: http://software-architecture-php.blogspot.com/
Here is the code for the container
class Container implements \DecoupledApp\Interfaces\Container\ContainerInterface
{
/**
* This function resolves the constructor arguments and creates an object
* #param string $dataType
* #return mixed An object
*/
private function createObject($dataType)
{
if(!class_exists($dataType)) {
throw new \Exception("$dataType class does not exist");
}
$reflectionClass = new \ReflectionClass($dataType);
$constructor = $reflectionClass->getConstructor();
$args = null;
$obj = null;
if($constructor !== null)
{
$block = new \phpDocumentor\Reflection\DocBlock($constructor);
$tags = $block->getTagsByName("param");
if(count($tags) > 0)
{
$args = array();
}
foreach($tags as $tag)
{
//resolve constructor parameters
$args[] = $this->resolve($tag->getType());
}
}
if($args !== null)
{
$obj = $reflectionClass->newInstanceArgs($args);
}
else
{
$obj = $reflectionClass->newInstanceArgs();
}
return $obj;
}
/**
* Resolves the properities that have a type that is registered with the Container.
* #param mixed $obj
*/
private function resolveProperties(&$obj)
{
$reflectionClass = new \ReflectionClass(get_class($obj));
$props = $reflectionClass->getProperties();
foreach($props as $prop)
{
$block = new \phpDocumentor\Reflection\DocBlock($prop);
//This assumes that there is only one "var" tag.
//If there are more than one, then only the first one will be considered.
$tags = $block->getTagsByName("var");
if(isset($tags[0]))
{
$value = $this->resolve($tags[0]->getType());
if($value !== null)
{
if($prop->isPublic()) {
$prop->setValue($obj, $value);
} else {
$setter = "set".ucfirst($prop->name);
if($reflectionClass->hasMethod($setter)) {
$rmeth = $reflectionClass->getMethod($setter);
if($rmeth->isPublic()){
$rmeth->invoke($obj, $value);
}
}
}
}
}
}
}
/**
*
* #param string $dataType
* #return object|NULL If the $dataType is registered, the this function creates the corresponding object and returns it;
* otherwise, this function returns null
*/
public function resolve($dataType)
{
$dataType = trim($dataType, "\\");
$obj = null;
if(isset($this->singletonRegistry[$dataType]))
{
//TODO: check if the class exists
$className = $this->singletonRegistry[$dataType];
$obj = $className::getInstance();
}
else if(isset($this->closureRegistry[$dataType]))
{
$obj = $this->closureRegistry[$dataType]();
}
else if(isset($this->typeRegistry[$dataType]))
{
$obj = $this->createObject($this->typeRegistry[$dataType]);
}
if($obj !== null)
{
//Now we need to resolve the object properties
$this->resolveProperties($obj);
}
return $obj;
}
/**
* #see \DecoupledApp\Interfaces\Container\ContainerInterface::make()
*/
public function make($dataType)
{
$obj = $this->createObject($dataType);
$this->resolveProperties($obj);
return $obj;
}
/**
*
* #param Array $singletonRegistry
* #param Array $typeRegistry
* #param Array $closureRegistry
*/
public function __construct($singletonRegistry, $typeRegistry, $closureRegistry)
{
$this->singletonRegistry = $singletonRegistry;
$this->typeRegistry = $typeRegistry;
$this->closureRegistry = $closureRegistry;
}
/**
* An array that stores the mappings of an interface to a concrete singleton class.
* The key/value pair corresond to the interface name/class name pair.
* The interface and class names are all fully qualified (i.e., include the namespaces).
* #var Array
*/
private $singletonRegistry;
/**
* An array that stores the mappings of an interface to a concrete class.
* The key/value pair corresond to the interface name/class name pair.
* The interface and class names are all fully qualified (i.e., include the namespaces).
* #var Array
*/
private $typeRegistry;
/**
* An array that stores the mappings of an interface to a closure that is used to create and return the concrete object.
* The key/value pair corresond to the interface name/class name pair.
* The interface and class names are all fully qualified (i.e., include the namespaces).
* #var Array
*/
private $closureRegistry;
}
The above code can be found here: https://github.com/abdulla16/decoupled-app (under the /Container folder)
You can register your dependencies as a singleton, as a type (every time a new object will be instantiated), or as a closure (the container will call the function that you register and that function is expected to return the instance).
For example,
$singletonRegistry = array();
$singletonRegistry["DecoupledApp\\Interfaces\\UnitOfWork\\UnitOfWorkInterface"] =
"\\DecoupledApp\\UnitOfWork\\UnitOfWork";
$typeRegistry = array();
$typeRegistry["DecoupledApp\\Interfaces\\DataModel\\Entities\\UserInterface"] =
"\\DecoupledApp\\DataModel\\Entities\\User";
$closureRegistry = array();
$closureRegistry["DecoupledApp\\Interfaces\\DataModel\\Repositories\\UserRepositoryInterface"] =
function() {
global $entityManager;
return $entityManager->getRepository("\\DecoupledApp\\DataModel\\Entities\\User");
};
$container = new \DecoupledApp\Container\Container($singletonRegistry, $typeRegistry, $closureRegistry);
This Container resolves properties of a class as well as the constructor parameters.
I have done a very simple IoC class which works as intended. I've investigated the IoC and DI pattern and especially after reading this answer. Let me know if something is not right or you have any questions .
<?php
class Dependency {
protected $object = null;
protected $blueprint = null;
/**
* #param $instance callable The callable passed to the IoC object.
*/
public function __construct($instance) {
if (!is_object($instance)) {
throw new InvalidArgumentException("Received argument should be object.");
}
$this->blueprint = $instance;
}
/**
* (Magic function)
*
* This function serves as man-in-the-middle for method calls,
* the if statement there serves for lazy loading the objects
* (They get created whenever you call the first method and
* all later calls use the same instance).
*
* This could allow laziest possible object definitions, like
* adding annotation parsing functionality which can extract everything during
* the call to the method. once the object is created it can get the annotations
* for the method, automatically resolve its dependencies and satisfy them,
* if possible or throw an error.
*
* all arguments passed to the method get passed to the method
* of the actual code dependency.
*
* #param $name string The method name to invoke
* #param $args array The array of arguments which will be passed
* to the call of the method
*
* #return mixed the result of the called method.
*/
public function __call($name, $args = array())
{
if (is_null($this->object)) {
$this->object = call_user_func($this->blueprint);
}
return call_user_func_array(array($this->object, $name), $args);
}
}
/*
* If the object implements \ArrayAccess you could
* have easier access to the dependencies.
*
*/
class IoC {
protected $immutable = array(); // Holds aliases for write-protected definitions
protected $container = array(); // Holds all the definitions
/**
* #param $alias string Alias to access the definition
* #param $callback callable The calback which constructs the dependency
* #param $immutable boolean Can the definition be overriden?
*/
public function register ($alias, $callback, $immutable = false) {
if (in_array($alias, $this->immutable)) {
return false;
}
if ($immutable) {
$this->immutable[] = $alias;
}
$this->container[$alias] = new Dependency($callback);
return $this;
}
public function get ($alias) {
if (!array_key_exists($alias, $this->container)) {
return null;
}
return $this->container[$alias];
}
}
class FooBar {
public function say()
{
return 'I say: ';
}
public function hello()
{
return 'Hello';
}
public function world()
{
return ', World!';
}
}
class Baz {
protected $argument;
public function __construct($argument)
{
$this->argument = $argument;
}
public function working()
{
return $this->argument->say() . 'Yep!';
}
}
/**
* Define dependencies
*/
$dic = new IoC;
$dic->register('greeter', function () {
return new FooBar();
});
$dic->register('status', function () use ($dic) {
return new Baz($dic->get('greeter'));
});
/**
* Real Usage
*/
$greeter = $dic->get('greeter');
print $greeter->say() . ' ' . $greeter->hello() . ' ' . $greeter->world() . PHP_EOL . '<br />';
$status = $dic->get('status');
print $status->working();
?>
I think the code is pretty self-explanatory, but let me know if something is not clear
Because I haven't find anything near what I wanted,I tried to implement on my own a container and I want to hear some opinion about how is looking,because I've start to learn php and oop a month ago a feedback is very important for me because I know I have many things to learn,so please feel free to bully my code :))
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<?php
class ioc
{
private $defs;
static $instance;
private $reflection;
private function __construct()
{
$defs = array();
$reflection = array();
}
private function __clone()
{
;
}
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new ioc();
}
return self::$instance;
}
public function getInstanceOf($class)
{
if (is_array($this->defs) && key_exists($class, $this->defs)) {
if (is_object($this->defs[$class])) {
return $this->defs[$class];
}
} else {
if (class_exists($class)) {
if (is_array($this->reflection) && key_exists($class, $this->reflection)) {
$reflection = $this->reflection[$class];
} else {
$reflection = new ReflectionClass($class);
$this->reflection[$class] = $reflection;
}
$constructor = $reflection->getConstructor();
if ($constructor) {
$params = $constructor->getParameters();
if ($params) {
foreach ($params as $param) {
$obj[] = $this->getInstanceOf($param->getName());
}
$class_instance = $reflection->newInstanceArgs($obj);
$this->register($class, $class_instance);
return $class_instance;
}
}
if (!$constructor || !$params) {
$class_instance = new $class;
$this->register($class, $class_instance);
return $class_instance;
}
}
}
}
public function register($key, $class)
{
$this->defs[$key] = $class;
}
}
?>
I have created a base repository and am now extending it to add caching, but the problem I seem to be experiencing more than most is pagination
in my all() method, I do the following without caching:
public function all($pagination = 20)
{
try
{
$query = $this->model->newQuery();
return $this->handlePagination($query, $pagination);
}
catch (Exception $e)
{
throw $e;
}
}
protected function handlePagination($query, $pagination)
{
if (is_null($pagination))
{
return $query;
}
$collection = $query->paginate($pagination);
return $collection;
}
This is working well, but when I try to implement caching, I want to cache each model individually and store the keys for each collection, so, if I was paginating all of the entries, I would store each paginated collection in a cache:
cache set.1 [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
cache set.2 [21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40]
etc...
the problem is that is seems impossible to use the actual pagination class to return the results since you are paginating only the id
I could return the data and the paginator separately but that seems very hacky.
Is there a way to repopulate the Paginator class with model data without over-writing the whole thing?
EDIT
I was thinking about something like this:
public function all($pagination = 20)
{
try
{
$cache_key = $this->cache_key . '.all';
$ids = Cache::get($cache_key);
if (! $ids)
{
$query = $this->model->newQuery();
$ids = $query->pluck('id');
Cache::put($cache_key, $ids, $this->cache_ttl);
}
$ids = $this->handlePagination($ids, $pagination);
$collection = new Collection();
foreach ($ids as $id)
{
if ($model = $this->find($id))
{
$collection->put($id, $model);
}
}
return $collection;
}
catch (Exception $e)
{
throw $e;
}
}
/**
* #var \Illuminate\Pagination\Paginator
*/
protected $paginator;
public function handlePagination($array, $pagination = 20)
{
if (!is_null($pagination))
{
$this->paginator = Paginator::make($array, count($array), $pagination);
return $this->paginator->getItems();
}
return $array;
}
public function getPaginator()
{
return $this->paginator;
}
I had to implement caching in a project where I work and I faced a similar issue but not with pagination. But the approach should be the same.
Laravel handles internally by default query caching if the model is told to do so.
What I did was creating a class that all the objects I want to cache should extend somehow. Then you can use pagination without even thinking about caching.
In the code below, pay special attention to the following method overrides:
newQuery
newBaseQueryBuilder
newFromBuilder
The CacheModel class looks like this:
<?php
class CacheModel extends Eloquent
{
/**
* Holds the query builder from which this model was fetched from.
*
* #var Illuminate\Database\Query\Builder
*/
protected $queryBuilder;
/**
* Overrides Illuminate\Database\Eloquent\Model's newQuery().
* We need to do this to store the query builder in our class for caching purpuses.
*
* #return \Illuminate\Database\Eloquent\Builder
*/
public function newQuery($excludeDeleted = true)
{
$eloquentBuilder = parent::newQuery($excludeDeleted);
return $eloquentBuilder->rememberForever();
}
/**
* Overrides Illuminate\Database\Eloquent\Model's newBaseQueryBuilder().
* We need to do this to store the query builder in our class for caching purpuses.
*
* #return \Illuminate\Database\Query\Builder
*/
protected function newBaseQueryBuilder()
{
$queryBuilder = parent::newBaseQueryBuilder();
$this->queryBuilder = $queryBuilder;
return $queryBuilder;
}
/**
* Overrides Illuminate\Database\Eloquent\Model's newFromBuilder().
* We need to do this to update the cache.
*
* #return an instance of the specified resource
*/
public function newFromBuilder($attributes = array())
{
$object = parent::newFromBuilder($attributes);
$that = $this;
$referencedCacheKeysFromObject = Cache::rememberForever($object->getCacheIdKey(), function() use ($that){
return array( $that->getQueryBuilder()->getCacheKey() => true );
});
if ( !isset($referencedCacheKeysFromObject[$this->getQueryBuilder()->getCacheKey()] ))
{
# Update the cache entries that hold the object
$referencedCacheKeysFromObject[$this->getQueryBuilder()->getCacheKey()] = true;
Cache::forget($object->getCacheIdKey());
Cache::forever($object->getCacheIdKey(), $referencedCacheKeysFromObject);
}
$referencedCacheKeysFromObjectTable = Cache::rememberForever($object->getCacheTableKey(), function() use ($that){
return array( $that->getQueryBuilder()->getCacheKey() => true );
});
if ( !isset( $referencedCacheKeysFromObjectTable[$this->getQueryBuilder()->getCacheKey()] ))
{
# Udate the cache entries that hold objects from the object's table.
$referencedCacheKeysFromObjectTable[$this->getQueryBuilder()->getCacheKey()] = true;
Cache::forget($object->getCacheTableKey());
Cache::forever($object->getCacheTableKey(), $referencedCacheKeysFromObjectTable);
}
return $object;
}
/**
* Overrides Illuminate\Database\Eloquent\Model's save().
* We need to do this to clean up the cache entries related to this object.
*
* #return \Illuminate\Database\Query\Builder
*/
public function save(array $attributes = array())
{
if (!$this->exists)
{
# If the object doesn't exists, it means that the object is gonna be created. So refresh all queries involving the object table.
# This is needed because the new created object might fell within one of the cache entries holding references to objects of this type.
$this->cleanUpCacheQueriesOfObjectTable();
}
$this->cleanUpCacheQueriesOfObject();
return parent::save();
}
/**
* Overrides Illuminate\Database\Eloquent\Model's delete().
* We need to do this to clean up the cache entries related to this object.
*
*/
public function delete()
{
$this->cleanUpCacheQueriesOfObject();
return parent::delete();
}
/**
* Overrides Illuminate\Database\Eloquent\Model's delete().
* We need to do this to clean up the cache entries related to this object.
*
*/
public static function destroy($id)
{
$this->find($id)->cleanUpCacheQueriesOfObject();
Cache::forget($this->getCacheIdKey($id));
return parent::destroy($id);
}
/**
* Returns the asociated query builder from which the model was created
*
* #return \Illuminate\Database\Query\Builder
*/
public function getQueryBuilder()
{
return $this->queryBuilder;
}
/**
* Cleans up all the cache queries that involve this object, if any.
*
*/
private function cleanUpCacheQueriesOfObject()
{
# Clean up the cache entries referencing the object as we need to re-fetch them.
if ( $referencedCacheKeys = Cache::get($this->getCacheIdKey()) )
{
foreach ($referencedCacheKeys as $cacheKey => $dummy)
{
Cache::forget($cacheKey);
}
}
}
/**
* Cleans up all the cache queries that involve this object table, if any.
* Needed when a a new object of this type is created.
* The cache needs to be refreshed just in case the object fells into
* one of the cache queries holding entries from the same type of the object.
*
*/
private function cleanUpCacheQueriesOfObjectTable()
{
# Clean up the cache entries referencing the object TABLE as we need to re-fetch them.
if ( $referencedCacheKeys = Cache::get($this->getCacheTableKey()) )
{
foreach ($referencedCacheKeys as $cacheKey => $dummy)
{
Cache::forget($cacheKey);
}
}
}
/**
* Returns a string containing a key for the table cache
*
*/
private function getCacheTableKey()
{
return '_' . $this->getTable();
}
/**
* Returns a string containing a key for the object cache
*
*/
private function getCacheIdKey($id = null)
{
if (!isset($id))
{
if (isset($this->id))
$id = $this->id;
else
$id = md5(serialize($this->getAttributes()));
}
return $this->getCacheTableKey() . '_' . $id;
}
}
Then you can do another class extending this CacheModel that could be called PaginateModel and you can do whatever pagination operations you'd like to do.
Of course the rest of the objects should extend this PaginateModel class.
EDIT: Added some if conditions in the method newFromBuilder as I just figuered out that there's a bug in APC since 3.1.3.
Pass the page number along as well for caching. Sonething like this
$currentPg = Input::get('page') ? Input::get('page') : '1';
$boards = Cache::remember('boards'.$currentPg, 60, function(){ return WhatEverModel::paginate(15); });
You need create the paginator manually as instructed in Laravel documentation. So basically you need to return paginator objects from your repository and those can be cached easily. Other option is to generate the paginator on the fly based on the cached/queried parameters.