I have the following Entity :
class Product
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\StockEntry", mappedBy="product")
*
* #Groups({"product:read", "product:read:all"})
*/
private Collection $stockEntries;
public function getStockEntries(): Collection
{
return $this->stockEntries;
}
public function getLastBuyPrice(): float
{
/** #var StockEntry $lastEntry */
$lastEntry = $this->getStockEntries()->last();
if ($lastEntry) {
return $lastEntry->getBuyPrice() ?: 0.;
}
return 0.;
}
}
My problem is that when I call my getLastBuyPrice() method, All StockEntries are retrieved, which can be very long (a product can have hundreds of stock entries). I'm looking for a way to rewrite getLastBuyPrice() so that only the most recent StockEntry is retrieved to compute the lastBuyPrice.
I recommend that you create a method in "StockEntryRepository".
This method will retrieve the last item you need without iterating through all of the items.
Related
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.
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.
I have a Cart entity that contains an ArrayCollection of CartItems. The relationship in code -
Cart.php:
class Cart
{
/**
* #ORM\OneToMany(targetEntity="CartItem", mappedBy="cart")
*/
private $cartItems;
public function __construct(User $user)
{
$this->cartItems = new ArrayCollection();
}
}
CartItems are essentially a DB pointer (for lack of a better term) to a Product entity, with an additional quantity field.
CartItem.php:
class CartItem
{
/**
* #ORM\ManyToOne(targetEntity="Cart", inversedBy="cartItems")
*/
private $cart;
/**
* #ORM\OneToOne(targetEntity="Product")
*/
private $product;
/**
* #ORM\Column(type="float")
*
* #Assert\NotBlank(message="You must specify how much of the item you wish to buy")
* #Assert\Regex(pattern="/^\d+(\.\d{1,2})?$/", message="The quantity must be a number")
*/
private $quantity;
}
In my app, when a user wants to add a Product to their Cart, I want to check to see if that particular Product is already one of the CartItems. If so, I want to increase the quantity, otherwise add it to the collection. I'm not exactly sure how to do this.
ArrayCollection methods exists and contains simply return true/false. Given my setup, I'm not sure what I'd use as a key for the collection.
Any suggestions?
You can filter CartItems for new Product. If filtered CartItems is not empty increase the quantity, otherwise add new CartItem.
$newProduct // Product Object
$cartItems = $cart->getCartItems()->filter(function(CartItem $cartItem) (use $newProduct) {
return $cartItem->getProduct() === $newProduct;
});
if ($cartItems->count()) {
foreach ($cartItems as $cartItem) {
$cartItem->setQuantity($cartItem->getQuantity() + 1);
}
} else {
$cart->addCartItem(
(new CartItem())
->setProduct($newProduct)
->setQuantity(1)
);
}
I am currently trying to filter a Doctrine-generated association property by using Criteria as described in the Doctrine manual: Filtering Collections
Here is some sample code: A person who owns several cars. For the sake of simplicity we save the date when the person bought and sold the car in the car entity instead of creating an association entity.
class Car
{
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="cars")
* #ORM\JoinColumn(name="owner_id", referencedColumnName="id", nullable=false)
*/
private $owner;
/**
* #ORM\Column(name="bought", type="date")
*/
private $bought;
/**
* #ORM\Column(name="sold", type="date")
*/
private $sold;
}
class Person
{
/**
* #ORM\OneToMany(targetEntity="Car", mappedBy="owner")
*/
private $cars;
public function __construct
{
$this->cars = new ArrayCollection();
}
/**
* Get cars
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCars()
{
return $this->cars;
}
public function getCarsOwnedAtDate(\DateTime $date)
{
$allCars = $this->getCars();
$criteria = Criteria::create()->where(
Criteria::expr()->andX(
Criteria::expr()->lte("bought", $date),
Criteria::expr()->gte("sold", $date)
)
);
return $allCars->matching($criteria);
}
}
The documentation for getCars says that it returns a Collection. Note that this documentation is auto-generated by Doctrine.
So when I call matching on Collection in my getCarsOwnedAtDate method my IDE issues a warning that Collection does not have a matching method. Turns out, it is right.
The object returned by getCars is an ArrayCollection and has a matching method so the code runs fine.
My question is: Can I rely on the fact that methods auto-generated by Doctrine will always return an ArrayCollection? Or will the above code fail in a certain case? If it does always return ArrayCollection, then why does the PhpDoc comment not mention this class explicitly?
Long story short: I'm building a "privacy" page where uses can chose what shows up and what does not show up on their profiles.
I am considering having a 1:m table user:privacy and just have entries for the keys they want private. If they don't exist they are public. Hope this makes sense.
Table would be user_privacy and will have 3 columns: id, user_id, privacy_key (string, i.e. email/phone/cell/etc)
Is there a way to simple query by the keys i will define that i can run to determine if the user has a key or not or do i have to go extra lengths to add a function to the user model to do this (trying to avoid, love the magic-ness of eloquent)
Basically i want to have a condition that sounds like "if ($user->privacy->email or $user->privacy->phone)"
Thanks and hope i was clear enough, lol
You could add a function to your user model:
public function isPrivate($attribute){
$privacyAttribute = $this->privacy->first(function($model) use ($attribute){
return $model->key == $attribute; // key being the column in the privacy model
});
return !is_null($privacyAttribute);
}
And then do your if statement this way:
if ($user->isPrivate('email') or $user->isPrivate('phone'))
Or a different implementation (usage is the same)
private $privacyAttributes = null;
public function isPrivate($attribute){
if($this->privacyAttributes == null){
$this->privacyAttributes = $this->privacy()->lists('key');
}
return in_array($attribute, $this->privacyAttributes);
}
User Model header:
/**
* Class User
* #package Interallmas
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
/**
* #var null|array
*/
protected $privacy_keys = NULL;
Privacy Relationship:
/**
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function privacy() {
return $this->hasMany('Interallmas\Privacy');
}
Privacy functions:
/**
* #return bool
*/
public function privacy_initialized() {
return ($this->privacy_keys !== NULL);
}
/**
* #return void
*/
public function initialize_privacy() {
if (!$this->privacy_initialized()) {
$this->privacy_keys = [];
foreach ($this->privacy as $privacy) {
$this->privacy_keys[] = $privacy->privacy_key;
}
}
}
/**
* #param $key
* #return bool
*/
public function isPrivate($key) {
$this->initialize_privacy();
return (in_array($key,$this->privacy_keys));
}
So: Whenever i access the isPrivate($key) method, i cache the result for the next use so i don't hit the server too hard - the function may be accessed once or more - i just query once, the first time. I believe for my needs, this is the best way to do it.
I think a simple count > 0 check should suffice. This requires you to have defined the relationship with the hasMany method for the User Model.
if (count($user->privacy) > 0) {
...
}
Consider an entity which has a foreign key. We receive an array which contains the foreign key as an integer and we want to populate the current entity from the array (this could be either an update or a create, makes no difference). So far I have been unable in the documentation to find an example of how to perform this update in the "symfony2/doctrine" way.
If we change the type of the private variable to be an integer instead of an object of the type of the Foreign entity, we lose the ORM mapping/auto-instantiation/etc. If we leave as is, we cannot set it with a simple integer.
Doctrine documentation dictates we shall not access the entity manager from within an Entity (for the purpose of "find"ing the foreign entity from the key before setting current entities value) and anyway I haven't found documentation of how to do it if I wanted to. Best practices dictate that population of an object from an array should occur as an object method on that object. Common sense dictates that a simple array should be supported and should not require end user/controller to know to create the foreign entity.
Can someone point me in the direction of sanity?
Example Code:
<?php
namespace Prefix\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class Item
{
private $itemid;
/*
* --- omitted for brevity ---
*/
private $categoryid;
public function getItemid()
{
return $this->itemid;
}
/**
* --- omitted for brevity again ---
*/
public function setCategoryid(\Prefix\MyBundle\Entity\Category $categoryid = null)
{
$this->categoryid = $categoryid;
return $this;
}
public function getCategoryid()
{
return $this->categoryid;
}
public function fromArray($data = array())
{
$updated = false;
if ( isset($data['Category']) )
{
/* We know that $data['Category'] will be an int */
$this->setCategoryid($data['Category']); //Fails invalid type!
$updated = true;
}
return $updated;
}
}
So, you have to create your Category Object (owning side, many side), then fetch every Item object from db (you have an array of integer that are IDs, so you can do something like ->findById() or a custom DQL query where you can fetch them into a shot). Subsequently you have to call, for every fetched object, a ->setItems() or ->addItem() and use ->persist(); ->flush(); of entity manager.
When you use doctrine you shouldn't be working with foreign keys (
but with category objects. So in fact you should have converted the id to category outside your function.
Another option is to add a new column called category which is of type integer and you set that directly so that you know have 2 class variables pointing to the same object but one has the relation the other just the id
/**
* #ManyToOne...
*/
protected $category;
/**
* #Column(type="integer")
*/
protected $categoryid;
public function setCategory(\Prefix\MyBundle\Entity\Category $category = null)
{
$this->category = $category;
return $this;
}
public function getCategory()
{
return $this->category;
}
public function setCategoryId($categoryid = null)
{
$this->categoryid = $categoryid;
return $this;
}
public function getCategoryId()
{
return $this->categoryid;
}