class MainTest extends TestCase
{
public function testMain()
{
$stub = $this->createMock(Project\NotImplementedClass::class);
$stub->method('doSomething')
->will($this->returnCallback(function ($string) {
return strtoupper($string);
}));
$this->assertEquals('ABC', $stub->doSomething('abc'));
}
}
PhpStorm tells that method doSomething doesn't exists. I searched any plugin which can autocomplete methods. Is any plugin for this?
PHPStorm's autocomplete relies heavily on type hints. In your case - since $this->createMock() will return a PHPUnit_Framework_MockObject_MockObject which does not have the method it will complain.
What you can do is "overwrite" the type hint for the variable:
/** #var Project\NotImplementedClass|PHPUnit_Framework_MockObject_MockObject $stub */
$stub = $this->createMock(Project\NotImplementedClass::class);
or you could put the mock creation in a method with a similar #return docblock.
This will tell PHPStorm to look at both classes for autocomplete.
We use the Dynamic Return Type-plugin to improve the type hinting of PHPUnit. It's not perfect, but is easy to set up and use. The plugin let you define return types for methods based on the value of a parameter.
Add the file dynamicReturnTypeMeta.json to the root of your project with the following contents:
{
"methodCalls": [
{
"class": "\\PHPUnit_Framework_TestCase",
"method": "createMock",
"position": 0,
"mask": "%s|PHPUnit_Framework_MockObject_MockObject"
}
],
"functionCalls": []
}
Related
API-Platform will generate Swagger/OpenAPI route documentation and then below documentation for the Schemas (AKA Models) (the docs show them as "Models" but current versions such as 2.7 show them as "Schemas").
Where is the content generated to show these schemas/models? How can some be removed? The functionality to display them is part of Swagger-UI, but API-Platform must be responsible for providing the JSON configuration and thus which to change API-Platform and not Swagger-UI. Note that this post shows how to add a schema but not how to remove one. Is there any documentation on the subject other than this which doesn't go into detail?
As seen by the output below, I am exposing AbstractOrganization, however, this class is extended by a couple other classes and is not meant to be exposed, but only schemas for the concrete classes should be exposed. Note that my AbstractOrganization entity class is not tagged with #ApiResource and is not shown in the Swagger/OpenAPI routing documentation but only the schema/model documentation.
Thank you
I am pretty certain there are better ways to implement this, however, the following will work and might be helpful for others.
<?php
declare(strict_types=1);
namespace App\OpenApi;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model\Paths;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class OpenApiRouteHider implements OpenApiFactoryInterface {
public function __construct(private OpenApiFactoryInterface $decorated, private TokenStorageInterface $tokenStorage)
{
}
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
$removedPaths = $this->getRemovedPaths();
$paths = new Paths;
$pathArray = $openApi->getPaths()->getPaths();
foreach($openApi->getPaths()->getPaths() as $path=>$pathItem) {
if(!isset($removedPaths[$path])) {
// No restrictions
$paths->addPath($path, $pathItem);
}
elseif($removedPaths[$path]!=='*') {
// Remove one or more operation
foreach($removedPaths[$path] as $operation) {
$method = 'with'.ucFirst($operation);
$pathItem = $pathItem->$method(null);
}
$paths->addPath($path, $pathItem);
}
// else don't add this route to the documentation
}
$openApiTest = $openApi->withPaths($paths);
return $openApi->withPaths($paths);
}
private function getRemovedPaths():array
{
// Use $user to determine which ones to remove.
$user = $this->tokenStorage->getToken()->getUser();
return [
'/guids'=>'*', // Remove all operations
'/guids/{guid}'=>'*', // Remove all operations
'/tenants'=>['post', 'get'], // Remove only post and get operations
'/tenants/{uuid}'=>['delete'], // Remove only delete operation
'/chart_themes'=>'*',
'/chart_themes/{id}'=>['put', 'delete', 'patch'],
];
}
}
I am migrating legacy project routing (Yii1) to Symfony 5
Right now my config/routing.yaml looks something like this:
- {path: '/login', methods: ['GET'], controller: 'App\Controller\RestController::actionLogin'}
- {path: '/logout', methods: ['GET'], controller: 'App\Controller\RestController::actionLogout'}
# [...]
- {path: '/readme', methods: ['GET'], controller: 'App\Controller\RestController::actionReadme'}
As you can see there is plenty of repetitive url to action conversion.
Is it possible to dynamically resolve controller method depending on some parameter. E.g.
- {path: '/{action<login|logout|...|readme>}', methods: ['GET'], controller: 'App\Controller\RestController::action<action>'}
One option would be to write annotations, but that somehow does not work for me and throws Route.php not found
The controller is determined by a RequestListener, specifically the router RouterListener. This in turn uses UrlMatcher to check the uri against the RouteCollection. You could implement a Matcher that resolves the controller based on the route. All you have to do is return an array with a _controller key.
Take note that this solution won't allow you to generate a url from a route name, since that's a different Interface, but you could wire it together.
// src/Routing/NaiveRequestMatcher
namespace App\Routing;
use App\Controller\RestController;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\RequestContext;
class NaiveRequestMatcher implements UrlMatcherInterface
{
private $matcher;
/**
* #param $matcher The original 'router' service (implements UrlMatcher)
*/
public function __construct($matcher)
{
$this->matcher = $matcher;
}
public function setContext(RequestContext $context)
{
return $this->matcher->setContext($context);
}
public function getContext()
{
return $this->matcher->getContext();
}
public function match(string $pathinfo)
{
try {
// Check if the route is already defined
return $this->matcher->match($pathinfo);
} catch (ResourceNotFoundException $resourceNotFoundException) {
// Allow only GET requests
if ('GET' != $this->getContext()->getMethod()) {
throw $resourceNotFoundException;
}
// Get the first component of the uri
$routeName = current(explode('/', ltrim($pathinfo, '/')));
// Check that the method is available...
$baseControllerClass = RestController::class;
$controller = $baseControllerClass.'::action'.ucfirst($routeName);
if (is_callable($controller)) {
return [
'_controller' => $controller,
];
}
// Or bail
throw $resourceNotFoundException;
}
}
}
Now you need to override the Listener configuration:
// config/services.yaml
Symfony\Component\HttpKernel\EventListener\RouterListener:
arguments:
- '#App\Routing\NaiveRequestMatcher'
App\Routing\NaiveRequestMatcher:
arguments:
- '#router.default'
Not sure if it's the best approach, but seems the simpler one. The other option that comes to mind is to hook into the RouteCompiler itself.
We're building a REST API in Symfony and in many Controllers we're repeating the same code for parsing and settings properties of objects/entities such as this:
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
I'm aware that Symfony forms provide this functionality, however, we've decided in the company that we want to move away from Symfony forms and want to use something simplier and more customisable instead.
Could anybody please provide any ideas or examples of libraries that might achieve property parsing and settings to an object/entity? Thank you!
It seems like a good use case for ParamConverter. Basically it allows you, by using #ParamConverter annotation to convert params which are coming into your controller into anything you want, so you might just create ParamConverter with code which is repeated in many controllers and have it in one place. Then, when using ParamConverter your controller will receive your entity/object as a parameter.
class ExampleParamConverter implements ParamConverterInterface
{
public function apply(Request $request, ParamConverter $configuration)
{
//put any code you want here
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
//now you are setting object which will be injected into controller action
$request->attributes->set($configuration->getName(), $solution);
return true;
}
public function supports(ParamConverter $configuration)
{
return true;
}
}
And in controller:
/**
* #ParamConverter("exampleParamConverter", converter="your_converter")
*/
public function action(Entity $entity)
{
//you have your object available
}
I am currently migrating an existent application to Symfony2 that has about 100 controllers with approximately 8 actions in each controller. All the current Actions are named as follow:
public function index(){}
However the default naming convention for Symfony is indexAction().
Is it possible to keep all my current actions and tell Symfony to use as it is without the "Action" word after the method name?
thank you.
Yes, this is possible. You should be able to define routes as normal, but you need to change the way the kernel finds the controller. The best way to do this is to replace/decorate/extends the service 'controller_name_converter'. This is a private service and is injected into the 'controller_resolver' service.
The source code of the class you want to replace is at 'Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser'.
Basically, the code runs like this. The 'bundle:controller:action' you specified when creating the route is saved in the cache. When a route is matched, that string is given back to the kernel, which in turn calls 'controller_resolver' which calls 'controller_name_resolver'. This class convert the string into a "namespace::method" notation.
Take a look at decorating services to get an idea of how to do it.
Here is an untested class you can work with
class ActionlessNameParser
{
protected $parser;
public function __construct(ControllerNameParser $parser)
{
$this->parser = $parser;
}
public function parse($controller)
{
if (3 === count($parts = explode(':', $controller))) {
list($bundle, $controller, $action) = $parts;
$controller = str_replace('/', '\\', $controller);
try {
// this throws an exception if there is no such bundle
$allBundles = $this->kernel->getBundle($bundle, false);
} catch (\InvalidArgumentException $e) {
return $this->parser->parse($controller);
}
foreach ($allBundles as $b) {
$try = $b->getNamespace().'\\Controller\\'.$controller.'Controller';
if (class_exists($try)) {
// You can also try testing if the action method exists.
return $try.'::'.$action;
}
}
}
return $this->parser->parse($controller);
}
public function build($controller)
{
return $this->parser->build($controller);
}
}
And replace the original service like:
actionless_name_parser:
public: false
class: My\Namespace\ActionlessNameParser
decorates: controller_name_converter
arguments: ["#actionless_name_parser.inner"]
Apparently the Action suffix is here to distinguish between internal methods and methods that are mapped to routes. (According to this question).
The best way to know for sure is to try.
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class HelloController
{
/**
* #Route("/hello/{name}", name="hello")
*/
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
Try to remove the Action from the method name and see what happens.
I found that PHPUnit's annotation #expectedException does not want to read class namespace paths from use statements (I'm using psr-0 for autoloading).
Take this as an example:
<?php
namespace Outrace\Battleship\Tests;
use Outrace\Battleship\Collection\MastCollection;
use Outrace\Battleship\Exception\CollectionOverflowException;
class MastCollectionTest extends \PHPUnit_Framework_TestCase
{
/**
* #expectedException CollectionOverflowException
*/
public function testAcceptOnlyMasts()
{
$notMastObject = new \stdClass();
$mastCollection = new MastCollection();
$mastCollection->attach($notMastObject);
}
}
The test, when run, will result in this error:
ReflectionException: Class CollectionOverflowException does not exist
To remedy the situation, I tried adding autoload-dev to my compose.json and dumping autoload file again:
"autoload-dev": {
"classmap": [
"src/Outrace/Battleship/Exception/"
]
},
or with psr-4:
"autoload-dev": {
"psr-4": {
"Outrace\\Battleship\\Tests\\": "src/Outrace/Battleship/Tests/",
"Outrace\\Battleship\\Exception\\": "src/Outrace/Battleship/Exception/"
}
},
None of the above would solve the problem, the error would persist.
However, the test would work well if the annotation references a fullu qualified name of the exception class:
/**
* #expectedException Outrace\Battleship\Exception\CollectionOverflowException
*/
public function testAcceptOnlyMasts()
Is this a limitation of PHPUnit or am I doing something wrong here?
This is a limitation with how phpunit works.
Internally it uses php's ReflectionClass which expects the FQCN of the exception. It just takes the string you give it in the annotation.
TestCase.php has the following when checking exceptions $reflector = new ReflectionClass($this->expectedException); and the expectedException property is populated either from the annotation or a call to setExpectedException().
You can use simplified names if you use the setExpectedException() method as you could then do something such as
<?php
namespace Outrace\Battleship\Tests;
use Outrace\Battleship\Collection\MastCollection;
use Outrace\Battleship\Exception\CollectionOverflowException;
class MastCollectionTest extends \PHPUnit_Framework_TestCase
{
public function testAcceptOnlyMasts()
{
$this->setExpectedException(CollectionOverflowException::class);
$notMastObject = new \stdClass();
$mastCollection = new MastCollection();
$mastCollection->attach($notMastObject);
}
}