1

I'm trying to import a batch of records (bills) but I have a Many-to-One relationship (with Customer). How do I fill in the DB column for that relationship?

I'm using a fromArray-like function inside the entity where I pass in the field list and the values for one record (the source of data is a CSV). Then inside that function, I simply assign each of the column values to the corresponding property. However, the property Customer is an object so I would need to pass in an object which I don't have.

I've considered injecting the Entity Manager to my entity but that is regarded as a terrible practice so I'm kind of stuck.

I've also tried adding an extra property customerId hoping it would force-write the value but it seems to stick to the relationship value over the property value.

Here's my code:

class Bill

    /**
     * @var string
     *
     * @ORM\Column(name="docId", type="string", length=25, nullable=false)
     * @ORM\Id
     */
    private $id;

    /**
     * @var float|null
     *
     * @ORM\Column(name="amount", type="float", precision=10, scale=0, nullable=true)
     */
    private $amount;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="bills")
     * @ORM\JoinColumn(name="customerId", referencedColumnName="customerId", nullable=false)
     */
    private $customer;

    public function getCustomer(): ?Customer
    {
        return $this->customer;
    }

    public function setCustomer( $customer): self
    {
        $this->customer = $customer;

        return $this;
    }

    //this is where we import
    public static function fromCSVRecord($header, $line)
    {
        $object = new self;
        foreach($header as $key => $field){
            try{
                switch($field){
                    case 'customerId':
                        //get the customer here from EM but we don't have EM
                        $customer = $em->getRepository(Customer::class)->find($line[$key]);
                        $object->setCustomer($customer);
                        break;
                    default:
                        $object->$field = $line[$key];
                        break;
                }
            }catch(\Exception $e){
                dd($line[$key]);
            }

        }

        return $object;
    }
}

I was hoping there was a simple way to import record values using the ORM without having to inject the Entity Manager into the entity class.

yivi
  • 42,438
  • 18
  • 116
  • 138
enekomh
  • 95
  • 7
  • https://stackoverflow.com/a/24766285/2776343 – Manzolo Aug 28 '19 at 08:12
  • Have you seen the answer below? Any luck with it? – yivi Aug 28 '19 at 11:02
  • 1
    As @yivi has suggested, moving the import functionality to it's own class or the repository is probably the best bet. On the other hand, you might consider just adding the entity manager as an argument Bill::fromCSVRecord($header, $line, $entityManager). In any event, use EntityManager::getReference($customerId) instead of loading the entire customer object. It will be a bit faster. – Cerad Aug 28 '19 at 12:31

1 Answers1

2

Your problem is that you are trying to give this responsibility to the wrong class. The huge code-smell of you needing the entity manager inside of the entity, that you are correctly perceiving, is cluing you in that that's the wrong place to perform that operation.

Move that to the repository. It's a more logical place to handle this anyway, and you already have the entity manager available.

class BillRepository extends ServiceEntityRepository
{
  //..

  public function addFromCSVRecord($header, $line) {

    $em   = $this->getEntityManager();
    $bill = new Bill();

    foreach ($header as $key => $field) {
         try {
              switch ($field) {
                 case 'customerId':
                     $customer = $em->getRepository(Customer::class)->find($line[$key]);
                      // alternatively, if you are certain of the incoming data and you do not want to hit the DB...
                      // $customer = $this->getEntityManager()->getReference(Customer:class, $line[$key]);
                      $bill->setCustomer($customer);
                      break;
                 default:
                      $bill->$field = $line[$key];
                      break;
                 }
             } catch (\Exception $e) { dd($line[$key]); }
        }
        // return $object;
        $em->persist($bill);
        $em->flush();
    }
} 

I am leaving most of the logic as I found it in your method, since I do not know the specifics. Although changed the return for actually persisting the data, so a single call to addFromCsvRecord() will create and persist your new object in the DB.

Note that in my answer I show you how to generate an object reference for your Customer using EntityManager::getReference(). If you can trust the input file, this will be slightly faster since you won't need to hit the DB for that object.

yivi
  • 42,438
  • 18
  • 116
  • 138
  • Thanks!! You actually have a point with the repository bit. Coming from Laravel background I keep forgetting about the repository classes being there for something else than just fetching records :P – enekomh Aug 29 '19 at 08:07
  • Glad it helped you. Laravel/Eloquent use the ActiveRecord pattern, in Doctrine you use the DataMapper and Repository patterns; which I believe are much better all around. – yivi Aug 29 '19 at 08:08