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);
}
}
I used to have a PHPUnit code like that (probably not a good one as I am not able to rewrite it):
$authorizator->expects($this->at(0))
->method('isAllowed')
->willReturn($hasSuperPrivilege);
if (!$hasSuperPrivilege) {
$authorizator->expects($this->at(1))
->method('isAllowed')
->willReturn($hasStandardPrivilege);
}
How to achieve the same behavior after the deprecation of at() method?
I don't know how many times the method will be called, so probably the willReturnOnConsecutiveCalls way is not the right one?
If the first call returns false, then the isAllowed() method must be called twice, if it returned true, then only once.
Related issue: https://github.com/sebastianbergmann/phpunit/issues/4297
Tested method:
public function canUserdoSomething(Foo $resource, Identity $identity): bool
{
if (
$this->authorizator->isAllowed(
$identity,
$resource,
'superPrivilege'
)
) {
return true;
}
if (
$this->authorizator->isAllowed(
$identity,
$resource,
'standardPrivilege'
)
) {
return $resource->hasSomeProperty();
}
return false;
}
I'd go with a handwritten fake so you don't have to worry about the number of calls. I did it inline in my example for brevity but if you're using the authorizator in multiple test, it makes sense to extract it into a separate class.
/**
* #covers \MyClass
*/
class MyClassTest extends TestCase
{
private $subject;
private $authorizator;
/**
* #testWith ["superPrivilege", false, true]
* ["standardPrivilege", true, true]
* ["standardPrivilege", false, false]
* ["invalidPrivilege", true, false]
*/
public function testCanUserdoSomething($privelege, $resourceProperty, $expected)
{
$identity = new Identity();
$resource = new Foo($resourceProperty);
$this->authorizator->addPrivilege($identity, $resource, $privelege);
$result = $this->subject->canUserdoSomething($resource, $identity);
self::assertEquals($expected, $result);
}
protected function setUp(): void
{
parent::setUp();
$this->authorizator = new class implements Authorizator {
private $privileges = [];
public function addPrivilege(Identity $identity, Foo $resource, string $privilege): void
{
$this->privileges[] = spl_object_id($identity) . spl_object_id($resource) . $privilege;
}
public function isAllowed(Identity $identity, Foo $resource, string $privilege): bool
{
return in_array(spl_object_id($identity) . spl_object_id($resource) . $privilege, $this->privileges);
}
};
$this->subject = new MyClass($this->authorizator);
}
}
I have these 2 classes :
AbstractTaskDispatcher
<?php
declare(strict_types=1);
namespace MyExample;
abstract class AbstractTaskDispatcher
{
public final function getResult(Task $task) : Result
{
if($worker = $this->getWorker($task))
return $worker->getResult();
else
return Result::getUnprocessableTaskResult();
}
abstract protected function getWorker(Task $task) : Worker;
}
?>
Result
<?php
declare(strict_types=1);
namespace MyExample;
class Result
{
private $code;
public function __construct(int $code = 0)
{
$this->code = $code;
}
public static function getUnprocessableTaskResult() : Result
{
return new Result(1000);
}
public function getCode() : int
{
return $this->code;
}
}
?>
I want to write a unit test with PHPUnit to get sure that AbstractTaskDispatcher::getResult() returns Result::getUnprocessableTaskResult() if no suitable Worker is found to process the Task.
I dont want to do this :
Arrange : $expectedResult = Result::getUnprocessableTaskResult();
Act : $result = $dispatcherStub->getResult(New Task());
Assert : assertEquals($result, $expectedResult);
Because it relies on Result class implementation and would not be a unit test.
I tried to do something :
<?php
use PHPUnit\Framework\TestCase;
use MyExample as ex;
class AbstractDispatcherTest extends TestCase
{
public function test_getResultSouldReturnUnprocessableTaskResultIfNoWorkerFound()
{
$dispatcher = $this->getMockForAbstractClass(ex\AbstractDispatcher::class);
$arbitraryCode = 6666;
$expectedResult = new ex\Result($arbitraryCode);
$resultClass = $this->getMockClass('Result', ['getUnprocessableTaskResult']);
$resultClass::staticExpects($this->any())
->method('getUnprocessableTaskResult')
->will($this->returnValue($expectedResult));
$result = $dispatcher->getResult(new ex\Task([]));
$this->assertEquals($expectedResult, $result);
}
}
?>
But staticExpects() method was deprecated and no longer exists in current PHPUnit version.
How can I write this test ?
You can simply test as follow:
public function test_getResultSouldReturnUnprocessableTaskResultIfNoWorkerFound()
{
$dispatcher = $this->getMockForAbstractClass(ex\AbstractTaskDispatcher::class);
$dispatcher->expects($this->once())
->method('getWorker')
->willReturn(false);
$result = $dispatcher->getResult(new ex\Task([]));
// Unuseful: this is implicit by the method signature
$this->assertInstanceOf(ex\Result::class, $result);
$this->assertEquals(1000, $result->getCode());
}
NB: I change the method definition of the classe AbstractTaskDispatcher as follow in order to return a false value:
/**
* #param Task $task
* #return Result|false The Result of the task or false if no suitable Worker is found to process the Task
*/
abstract protected function getWorker(Task $task);
EDIT:
As You commented, you can't check as follow instead of hardcode the result code:
$this->assertEquals(ex\Result::getUnprocessableTaskResult(), $result);
// Or
$this->assertEquals(ex\Result::getUnprocessableTaskResult()->getCode(), $result->getCode());
Hope this help
I have a simple problem, I can't pass data from one function to another. This is my code:
/**
* #dataProvider myProvider
*/
public function testIfPathIsString($path){
$isValid = (is_string($path)) ? true : false;
$this->assertEquals(true, $isValid);
return $path;
}
/**
* #depends testIfPathIsString
*/
public function testIfFileExists($help){
$exists = $this->fileController->checkIfFileExists($help);
$this->assertEquals(true, $exists);
return $help;
}
public function myProvider()
{
return array(
array("C:/xampp/htdocs/mycustom.txt")
);
}
First function gets path just fine but passing with annotation depends to second function fails, it sends null or empty.
I explicitly have one correct data path in dataset so that doesn't make any sense?