0

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.

rishta
  • 921
  • 1
  • 10
  • 24
  • Can you please edit your question to include the source code for `App\DataPersister\UserDataPersister`? – Arleigh Hix Apr 19 '21 at 20:20
  • @ArleighHix, I added a link to the code which I copied. – rishta Apr 19 '21 at 20:57
  • idk why they do it with field in model, best way to do this is to separate Models with your Data objects. That way you can always leverage good old data structures and then transform Data objects from endpoints coming in and going out separately. DTO / Normalizers are best way to implement this. I don't get why author chose to do it this way, it seems like abusing framework and doctrine literally. – Maulik Parmar Apr 20 '21 at 06:40
  • 1
    This error is generated because your fixture code will not trigger encodePassword, your persistor is only called when it's real request via endpoint. As per code, password field is sourced from DataPersistor whenever request comes and api paltform catches the event. Fixtures do not generate the kernel event that will trigger this persistor since it is not part of framework. What you can do it call encode password method in your fixture to fix this via custom Faker or other method. – Maulik Parmar Apr 20 '21 at 06:53
  • @MaulikParmar This tutorial has grown out of old Symfony versions, so I suppose they were just too "lazy" to modify it to fit newer patterns and practice. – rishta Apr 20 '21 at 08:24
  • @MaulikParmar, would you like to make this comment an answer? I wouldn't want to rob you by answering my own q. I suspected that there's some SNAFU with container type there, I even tried the context-aware persister, but it didn't help. To avoid code duplication, maybe it could be solved by some event listener or extending the console container? I suppose you would know better/sooner than I. – rishta Apr 20 '21 at 08:34
  • I'll add formatted answer so anyone looking for this in future would get it right. The tutorials are okay, api-platform is too complex to be understood in single go. Even core team themselves have alot on their plate at the moment, kudos for them to bring such amazing tech to everyone, I've been following them since a year :) – Maulik Parmar Apr 20 '21 at 08:51
  • @MaulikParmar, see Processors at https://github.com/theofidry/AliceDataFixtures/blob/master/doc/advanced-usage.md#processors – rishta Apr 20 '21 at 09:04
  • Another way is to use doctrine event listener, prePersist event, that way, all api-platform, fixtures and anyone using DBAL would just save plain password and it will be converted in encoded password just before saving. This is how most authentication bundles do it, it's clean, non interfering and done in single implementation. But that's different story, someone not using doctrine would need to hook into object lifecycle callbacks to transform it. – Maulik Parmar Apr 20 '21 at 10:03

1 Answers1

1

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

Maulik Parmar
  • 548
  • 2
  • 10