API-Platform - updating nullable string error - symfony

I have an entity which is exposed as an api-platform resource, and contains the following property:
/**
* #ORM\Column(type="string", nullable=true)
*/
private $note;
When I try to update the entity (via PUT), sending the following json:
{
"note": null
}
I get the following error from the Symfony Serializer:
[2017-06-29 21:47:33] request.CRITICAL: Uncaught PHP Exception Symfony\Component\Serializer\Exception\UnexpectedValueException: "Expected argument of type "string", "NULL" given" at /var/www/html/testapp/server/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php line 196 {"exception":"[object] (Symfony\Component\Serializer\Exception\UnexpectedValueException(code: 0):Expected argument of type \"string\", \"NULL\" given at /var/www/html/testapp/server/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php:196, Symfony\Component\PropertyAccess\Exception\InvalidArgumentException(code: 0): Expected argument of type \"string\", \"NULL\" given at /var/www/html/testapp/server/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:275)"} []
It seems like I'm missing some config to allow nulls on this property? To make things weirder, when I GET a resource containing a null note, then the note is correctly returned as null:
{
"#context": "/contexts/RentPayment",
"#id": "/rent_payments/1",
"#type": "RentPayment",
"id": 1,
"note": null,
"date": "2016-03-01T00:00:00+00:00"
}
What am I missing - ps I'm a massive newb to api-platform

Alright then as determined in the comments you were using a type hinted setter à la:
public function setNote(string $note) {
$this->note = $note;
return $this;
}
As of PHP 7.1 we have nullable types so the following would be preferred as it actually checks null or string instead of any type.
public function setNote(?string $note = null) {
In previous versions just remove the type hint and if preferred add some type checking inside.
public function setNote($note) {
if ((null !== $note) && !is_string($note)) {
// throw some type exception!
}
$this->note = $note;
return $this;
}
Another thing you might want to consider is using something like:
$this->note = $note ?: null;
This is the sorthand if (ternary operator). To set the value to null if the string is empty (but bugs on '0' so you might need to do the longer version).

Related

Claim based Authorization (Skipping the subject argument)

In the docs it shows that you can skip the subject and specify just the action but each time I do that, I get an error asking for subject. How can I implement claim based authorization without supplying the subject?
import { Ability } from '#casl/ability';
export default new Ability([
{
action: 'read',
}
]);
Property 'subject' is missing in type '{ action: string; }' but
required in type 'SubjectRawRule<string, ExtractSubjectType,
MongoQuery>'.ts(2741)
In the docs, it shows the rawRule interface showing subject as an optional parameter:
interface RawRule {
action: string | string[]
subject?: string | string[]
/** an array of fields to which user has (or not) access */
fields?: string[]
/** an object of conditions which restricts the rule scope */
conditions?: any
/** indicates whether rule allows or forbids something */
inverted?: boolean
/** message which explains why rule is forbidden */
reason?: string
}

API Platform + alice UserDataPersister not working with fixtures

I have the following UserDataPersister (taken straight from the tutorial) configured:
Information for Service "App\DataPersister\UserDataPersister"
=============================================================
Service ID App\DataPersister\UserDataPersister
Class App\DataPersister\UserDataPersister
Tags api_platform.data_persister (priority: -1000)
Public no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
and the following User fixture:
App\Entity\User:
user_{1..10}:
email: "usermail_<current()>\\#email.org"
plainPassword: "plainPassword_<current()>"
__calls:
- initUuid: []
But I get errors when loading this fixture:
An exception occurred while executing 'INSERT INTO "user" (id, uuid, roles, password, email) VALUES (?, ?, ?, ?, ?)' with params [281, "16ac40d3-53af-45dc-853f-e26f188d
1818", "[]", null, "usermail1#email.org"]:
SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "password" of relation "user" violates not-null constraint
DETAIL: Failing row contains (281, 16ac40d3-53af-45dc-853f-e26f188d1818, [], null, usermail1#email.org).
My implementation of UserDataPersister is identical with this.
Quote from Article at the end
If we stopped now... yay! We haven't... really... done anything: we
added this new plainPassword property... but nothing is using it! So,
the request would ultimately explode in the database because our
$password field will be null.
Next, we need to hook into the request-handling process: we need to
run some code after deserialization but before persisting. We'll do
that with a data persister.
Since unit test would POST the request, the data persistor is called by api-platform and it will pick up encoding logic by event. In case of fixtures, direct doctrine batch insert is done, this will bypass all persistence logic and would result in null password.
There is a way to solve this as mentioned by #rishta Use Processor to implement hash to your data fixtures as referenced in Documentation
<?php
namespace App\DataFixtures\Processor;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Fidry\AliceDataFixtures\ProcessorInterface;
use App\Entity\User;
final class UserProcessor implements ProcessorInterface
{
private $userPasswordEncoder;
public function __construct(EntityManagerInterface $entityManager, UserPasswordEncoderInterface $userPasswordEncoder) {
$this->userPasswordEncoder = $userPasswordEncoder;
}
/**
* #inheritdoc
*/
public function preProcess(string $fixtureId, $object): void {
if (false === $object instanceof User) {
return;
}
$object = $this->userPasswordEncoder(
$object,
$object->getPlainPassword()
);
}
/**
* #inheritdoc
*/
public function postProcess(string $fixtureId, $object): void
{
// do nothing
}
}
Register service :
# app/config/services.yml
services:
_defaults:
autoconfigure: true
App\DataFixtures\Processor\UserProcessor: ~
#add tag in case autoconfigure is disabled, no need for auto config
#tags: [ { name: fidry_alice_data_fixtures.processor } ]
One of the better ways to do input masking in API Platform is to use DTO Pattern as oppose to suggested by article, in which you are allowed to :
Create separate input & output data objects
Transform Underlying date to and from the objects
Choose Different IO objects for each operation whenever needed
More on DTO in documentation

Why does plainToClass not throw for invalid input types

Example:
class Test {
#IsString()
readonly table!: string;
}
// note, that we call it with a completely wrong type: string instead of JSON-object
const result = plainToClass(Test, 'not a class');
Then result is the input string 'not a class'!
I'd expect plainToClass to:
return an instance of type Test where all properties remain undefined, or
throw an exception
Is this a bug or am I missing something?
I also found not option to make this work as expected.
The workround for now is to explicitly typecheck and throw in my own code:
if (!(result instanceof Test) throw ...;
using class-transformer 0.2.3

Query on Doctrine class table inheritance with required fields

My domain has a parent IncidenceMessage class and several child classes (i.e. IncidenceMessageText).
I have the following class table inheritance configuration:
Domain\Model\IncidenceMessage\IncidenceMessage:
type: entity
repositoryClass: Infrastructure\Domain\Model\IncidenceMessage\DoctrineIncidenceMessageRepository
table: incidence_messages
inheritanceType: JOINED
discriminatorColumn:
name: type
type: string
length: 30
discriminatorMap:
text: IncidenceMessageText
image: IncidenceMessageImage
audio: IncidenceMessageAudio
video: IncidenceMessageVideo
fields:
...
I can create any IncidenceMessage entity correctly.
Having only a IncidenceMessageText in database, when I try to fetch incidence messages I get the following error:
TypeError: Argument 1 passed to Domain\Model\File\FileId::__construct() must be of the type string, null given
(FileId is a value object that represents the id of a File entity)
IncidenceMessageImage has a File field that is a foreign key and it is required.
It makes no sense to me that Doctrine fetches File when IncidenceMessageText doesn't have that field.
While debugging, I discovered that doctrine does a SELECT with LEFT JOINs with every single IncidenceMessage table and this calls my FileTypeId::convertToPHPValue method:
class FileIdType extends Type
{
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return new FileId($value);
}
}
AFAIK, the problem here is that child classes have required fields but that shouldn't be a stopper, right?
I found a possible workaround. On my custom DBAL Type FileIdType, I checked if the value was null before instantiating FileId:
use Doctrine\DBAL\Types\Type;
class FileIdType extends Type
{
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $value ? new FileId($value) : null;
}
}

Out of range Ids in Symfony route

I have a common structure for Symfony controller (using FOSRestBundle)
/**
* #Route\Get("users/{id}", requirements={"userId" = "(\d+)"})
*/
public function getUserAction(User $user)
{
}
Now if I request http://localhost/users/1 everything is fine. But if I request http://localhost/users/11111111111111111 I get 500 error and Exception
ERROR: value \"11111111111111111\" is out of range for type integer"
Is there a way to check id before it is transferred to database?
As a solution I can specify length of id
/**
* #Route\Get("users/{id}", requirements={"userId" = "(\d{,10})"})
*/
but then Symfony will say that there is no such route, instead of showing that the id is incorrect.
By telling Symfony that the getUserAction() argument is a User instance, it will take for granted that the {id} url parameter must be matched to the as primary key, handing it over to the Doctrine ParamConverter to fetch the corresponding User.
There are at least two workarounds.
1. Use the ParamConverter repository_method config
In the controller function's comment, we can add the #ParamConverter annotation and tell it to use the repository_method option.
This way Symfony will hand the url parameter to a function in our entity repository, from which we'll be able to check the integrity of the url parameter.
In UserRepository, let's create a function getting an entity by primary key, checking first the integrity of the argument. That is, $id must not be larger than the largest integer that PHP can handle (the PHP_INT_MAX constant).
Please note: $id is a string, so it's safe to compare it to PHP_INT_MAX, because PHP will automatically typecast PHP_INT_MAX to a string and compare it to $id. If it were an integer, the test would always fail (by design, all integers are less than or equal to PHP_INT_MAX).
// ...
use Symfony\Component\Form\Exception\OutOfBoundsException;
class UserRepository extends ...
{
// ...
public function findSafeById($id) {
if ($id > PHP_INT_MAX) {
throw new OutOfBoundsException($id . " is too large to fit in an integer");
}
return $this->find($id);
}
}
This is only an example: we can do anything we like before throwing the exception (for example logging the failed attempt).
Then, in our controller, let's include the ParamConverter annotation:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
and modify the function comment adding the annotation:
#ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"})
Our controller function should look like:
/**
* #Get("users/{id}")
* #ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"})
*/
public function getUserAction(User $user) {
// Return a "OK" response with the content you like
}
This technique allows customizing the exception, but does not give you control over the response - you'll still get a 500 error in production.
Documentation: see here.
2. Parse the route "the old way"
This way was the only viable one up to Symfony 3, and gives you a more fine-grained control over the generated response.
Let's change the action prototype like this:
/**
* #Route\Get("users/{id}", requirements={"id" = "(\d+)"})
*/
public function getUserAction($id)
{
}
Now, in the action we'll receive the requested $id and we'll be able to check whether it's ok. If not, we throw an exception and/or return some error response (we can choose the HTTP status code, the format and anything else).
Below you find a sample implementation of this procedure.
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\Form\Exception\OutOfBoundsException;
use Symfony\Component\HttpFoundation\JsonResponse;
class MyRestController extends FOSRestController {
/**
* #Get("users/{id}", requirements={"id" = "(\d+)"})
*/
public function getUserAction($id) {
try {
if ($id > PHP_INT_MAX) {
throw new OutOfBoundsException($id . " is too large to fit in an integer");
}
// Replace App\Entity\User with your actual Entity alias
$user = $this->getDoctrine()->getRepository('App\Entity\User')->find($id);
if (!$user) {
throw new \Doctrine\ORM\NoResultException("User not found");
}
// Return a "OK" response with the content you like
return new JsonResponse(['key' => 123]);
} catch (Exception $e) {
return new JsonResponse(['message' => $e->getMessage()], 400);
}
}

Resources