I'm calling a class by a string variable passed on a function argument.
ApiTester.php
use MyApp\Sites\Site;
abstract class ApiTester extends TestCase() {
/**
* Make a new record in the DB
*
* #param $type
* #param array $fields
* #throws BadMethodCallException
*/
protected function make($type, array $fields = [])
{
while($this->times--)
{
$stub = array_merge($this->getStub(), $fields);
$type::create($stub);
}
}
SitesTester.php
class SitesTester extends ApiTester() {
/** #test */
public function it_fetches_a_single_site()
{
// arrange
$this->make('Site');
// act
$site = $this->getJson('api/v1/sites/1')->data;
// assertion
$this->assertResponseOk();
$this->assertObjectHasAttributes($site, 'name', 'address');
}
Site.php // Eloquent Model
namespace MyApp\Sites;
class Site extends \Eloquent {
}
But if I call the class that the string variable $type contains, for example; string variable $type contains 'Site', it says class 'Site' not found.
I tried to manually type Site::create($stub) and finally accepts it.
I also tried
call_user_func($type::create(), $stub);
and
$model = new $type;
$model->create($stub);
but unfortunately it says class 'Site' not found.
Any ideas?
You're almost there:
class X {
static function foo($arg) {
return 'hi ' . $arg;
}
};
$cls = 'X';
print call_user_func("$cls::foo", 'there');
If your php is very old (<5.3 I believe), you have to use an array instead:
print call_user_func(array($cls, "foo"), 'there');
You may want to replace that static class call with the following :
while( $this->times-- )
{
$stub = array_merge( $this->getStub(), $fields );
call_user_func( "$type::create", $stub );
}
Runnable code here : http://runnable.com/VIqy4CDePeY-AeMV/output
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);.
I'm using PHPCS to check my class name are valid with the rule PEAR.NamingConventions.ValidClassName
It works fine on class declaration:
class My_Class {} // Valid
class My_class {} // Invalid - expected behaviour
Is there any rule to check class name at instantiation?
new My_class() // Should complain about invalid class name
My_class::someMethod() // Should complain too
I ended up writing my own sniff.
It checks class name when it finds a new or a static call with ::
<?php
namespace MySniffs\Sniffs\Classes;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
class MySniffs_Sniffs_Classes_ConsistentClassNameCaseSniff implements Sniff
{
/**
* Returns the token types that this sniff is interested in.
*
* #return array(int)
*/
public function register() {
return array(T_NEW, T_DOUBLE_COLON);
}//end register()
/**
* Processes this sniff, when one of its tokens is encountered.
*
* #param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked.
* #param int $stackPtr The position of the current token in the
* stack passed in $tokens.
*
* #return void
*/
public function process(File $phpcsFile, $stackPtr) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
if ($token['type'] === 'T_NEW') {
$className = $phpcsFile->findNext(T_STRING, $stackPtr);
} elseif ($token['type'] === 'T_DOUBLE_COLON') {
$className = $phpcsFile->findPrevious(T_STRING, $stackPtr);
}
$name = trim($tokens[$className]['content']);
$words = explode('_', $name);
foreach ($words as $word) {
if (!preg_match("/^[A-Z]/", $word)) {
$phpcsFile->addError(
'Invalid class name case : ' . $name,
$stackPtr,
'ClassNameCase',
array('message' => 'Boom')
);
}
}
}//end process()
}
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'm trying to wrap my head around how to inject Laravel's model observers with repositories.
Currently, I have this setup.
UserPetServiceProvider.php
<?php namespace Bunny\Providers;
use Illuminate\Support\ServiceProvider;
use Bunny\Observers\Pet\UserPetObserver;
class UserPetServiceProvider extends ServiceProvider {
public function register()
{
// User pets
$this->app->bind('Bunny\Repos\Pet\IUserPetRepo', 'Bunny\Repos\Pet\UserPetRepo');
// User pet layers
$this->app->bind('Bunny\Repos\Pet\IUserPetLayerRepo', 'Bunny\Repos\Pet\UserPetLayerRepo');
// User pet markings
$this->app->bind('Bunny\Repos\Pet\IUserPetMarkingRepo', 'Bunny\Repos\Pet\UserPetMarkingRepo');
$this->app->events->subscribe(new UserPetObserver());
}
}
It binds all the interfaces and repositories fine and would with the observer, but I need repository injection which I do in the constructor. Nothing is being passed in the constructor so it would explain why it fails.
UserPetRepo.php
<?php namespace Bunny\Repos\Pet;
use Bunny\Repos\BaseRepo;
use Bunny\Models\Pet\UserPet;
use Bunny\Repos\User\IUserRepo;
use Bunny\Repos\Breed\IStageRepo;
use Bunny\Repos\Breed\IBreedLayerRepo;
use Illuminate\Config\Repository as Config;
use Illuminate\Support\Str as String;
use Illuminate\Session\SessionManager as Session;
use Illuminate\Events\Dispatcher;
class UserPetRepo extends BaseRepo implements IUserPetRepo {
public function __construct(UserPet $pet, IUserRepo $user, IStageRepo $stage, IBreedLayerRepo $layer, Config $config, String $string, Session $session, Dispatcher $events)
{
$this->model = $pet;
$this->user = $user;
$this->stage = $stage;
$this->layer = $layer;
$this->config = $config;
$this->string = $string;
$this->session = $session;
$this->events = $events;
$this->directory = $this->config->get('pathurl.userpets');
$this->url = $this->config->get('pathurl.userpets_url');
}
/**
* Create new user pet
* #param attributes Attributes
*/
public function createWithImage( array $attributes, array $colors, $domMarkings = null, $domMarkingColors = null, $recMarkings = null, $recMarkingColors = null )
{
$this->model->name = $attributes['name'];
$this->model->breed_id = $attributes['breed_id'];
$this->model->user_id = $this->user->getId();
$this->model->stage_id = $this->stage->getBaby()->id;
// Get image
$image = $this->layer->compile(
$attributes['breed_id'],
'baby',
$colors,
$domMarkings,
$domMarkingColors
);
// Write image and set
$file = $this->writeImage( $image );
if( ! $file )
{
return false;
}
$this->model->base_image = $file;
$this->model->image = $file;
if( ! $this->model->save() )
{
return false;
}
$this->events->fire('userpet.create', array($this->model));
return $this->model;
}
/**
* Write image
* #param image Imagick Object
*/
protected function writeImage( $image )
{
$fileName = $this->string->random(50) . '.png';
if( $image->writeImage( $this->directory . $fileName ) )
{
return $fileName;
}
$this->model->errors()->add('writeImage', 'There was an error writing your image. Please contact an administrator');
return false;
}
}
UserPetObserver.php
use Bunny\Repos\Pet\IUserPetLayerRepo;
use Bunny\Repos\Pet\IUserPetMarkingRepo;
use Bunny\Observers\BaseObserver;
class UserPetObserver extends BaseObserver {
public function __construct(IUserPetLayerRepo $layers, IUserPetMarkingRepo $markings)
{
$this->layers = $layers;
$this->markings = $markings;
}
/**
* After create
*/
public function onCreate( $model )
{
$this->layers->user_pet_id = $model->id;
dd(Input::all());
$this->layers->breed_layer_id = $model->id;
}
public function subscribe( $event )
{
$event->listen('userpet.create', 'UserPetObserver#onCreate');
}
}
It throws this as the error:
Argument 1 passed to
Bunny\Observers\Pet\UserPetObserver::__construct() must be an instance
of Bunny\Repos\Pet\IUserPetLayerRepo, none given, called in H:\WD
SmartWare.swstor\HALEY-HP\Source2\bunny-meadows\app\Bunny\Providers\UserPetServiceProvider.php
on line 22 and defined
Which makes sense since I'm not passing anything in the constructor. So I try to pass my repository manually.
$this->app->events->subscribe(new UserPetObserver(new UserPetLayerRepo, new UserPetMarkingRepo));
But then it throws errors of UserPetLayerRepo needing injections... and it just chains on and on. Is there anyway of doing this that I'm just overthinking?
Thanks.
EDIT:::
This is the only thing I could think of doing. This seems like a really ugly/bad way of doing it though:
$this->app->events->subscribe(new UserPetObserver($this->app->make('Bunny\Repos\Pet\UserPetLayerRepo'), $this->app->make('Bunny\Repos\Pet\UserPetMarkingRepo')));
Any other ideas?
Try just:
$this->app->events->subscribe($this->app->make('UserPetObserver'));
When Laravel makes the UserPetObserver object, it will read the type-hinted dependencies in the constructor and automatically make them, as well.
Is there a way to get the name of the action in a Symfony2 controller?
public function createAction(Request $request, $title) {
// Expected result: create
$name = $this->getActionName();
}
useļ¼
$request->attributes->get('_controller');
// will get yourBundle\Controller\yourController::CreateAction
$params = explode('::',$request->attributes->get('_controller'));
// $params[1] = 'createAction';
$actionName = substr($params[1],0,-6);
// $actionName = 'create';
I found this snippet (here):
$matches = array();
$controller = $this->getRequest()->attributes->get('_controller');
preg_match('/(.*)\\\(.*)Bundle\\\Controller\\\(.*)Controller::(.*)Action/', $controller, $matches);
which seems to be a promising approach. This regexp actually doesn't work. But it won't be hard to fetch the action name by using strstr(). Works!
And returns (see example)
Array
(
[0] => Acme\MyBundle\Controller\MyController::myAction
[1] => Acme
[2] => My
[3] => My
[4] => my
)
If input was Acme\MyBundle\Controller\MyController::myAction.
Now, I am using this with Symfony 2.8, (and Symfony3):
<?php
namespace Company\Bundle\AppBundle\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Request as BaseRequest;
/**
* Request shortcuts.
*/
class Request extends BaseRequest
{
/**
* Extract the action name.
*
* #return string
*/
public function getActionName()
{
$action = $this->get('_controller');
$action = explode('::', $action);
// use this line if you want to remove the trailing "Action" string
//return isset($action[1]) ? preg_replace('/Action$/', '', $action[1]) : false;
return $action[1];
}
/**
* Extract the controller name (only for the master request).
*
* #return string
*/
public function getControllerName()
{
$controller = $this->get('_controller');
$controller = explode('::', $controller);
$controller = explode('\\', $controller[0]);
// use this line if you want to remove the trailing "Controller" string
//return isset($controller[4]) ? preg_replace('/Controller$/', '', $controller[4]) : false;
return isset($controller[4]) ? $controller[4] : false;
}
}
To use this custom request class, you must "use" it in your web/app*.php controllers:
use Company\Bundle\AppBundle\Component\HttpFoundation\Request;
// ...
$request = Request::createFromGlobals();
// ...
Then in your controller:
class AppController extends Controller
{
/**
* #Route("/", name="home_page")
* #Template("")
*
* #return array
*/
public function homePageAction(Request $request)
{
$controllerName = $request->getControllerName();
$actionName = $request->getActionName();
dump($controllerName, $actionName); die();
// ...
}
Will output:
AppController.php on line 27:
"AppController"
AppController.php on line 27:
"homePageAction"
You can also access these functions through the RequestStack service:
class MyService
{
/**
* #param RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function myService()
{
$this->controllerName = $this->requestStack->getMasterRequest()->getControllerName();
$this->actionName = $this->requestStack->getMasterRequest()->getActionName();
// ...
}
If you use Controller as a Service than the schema is different:
$request->attributes->get('_controller'); will return "service_id:createAction"
A possible solution for both schemas:
$controller = $request->attributes->get('_controller');
$controller = str_replace('::', ':', $controller);
list($controller, $action) = explode(':', $controller);
In all version of symfony and without $request or container, service or nothing else... , directly in your method
public function myMethod(){
$methodName = __METHOD__;
return $methodName;
}
// return App\Controller\DefaultController::myMethod
public function mySecondMethod(){
$methodName = explode('::', __METHOD__);
return $methodName[1];
}
// return mySecondMethod