Let's say I've got an entity called User with some properties like firstName, lastName, age and collection of Posts.
Is there a way to load this User entity with the EntityManager and Find-method without loading all properties/collections?
I'm using a REST API to retrieve the User and I want to seperate the calls:
api/user/1 - should retrieve the User object with only firstName, lastName and age.
api/user/1/posts - should retrieve the User object with the properties and a collection of Posts.
I know I can use QueryBuilder and create seperate methods to retrieve the desired data, but I want to have it this way:
The scenario:
api/user/1
$userId = 1;
$user = getEntityManager()->find('entity\User', $userId);
return $user; // should only load the firstName, lastName, age properties.
api/user/1/posts
$userId = 1;
$user = getEntityManager()->find('entity\User', $userId);
return $user->getPosts(); // should load the firstName, lastName, age properties and the collection of posts.
I already tried 'Extra Lazy Load' functionality that is built-in Doctrine, without any result.
My code:
<?php
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name = "USER")
*
* #JMS\ExclusionPolicy("all")
* #JMS\AccessorOrder("custom", custom = { "id", "firstName", "lastName" })
*/
class User {
/**
* #ORM\Id
* #ORM\Column(name = "id", type = "integer", options={"unsigned"=true})
* #ORM\GeneratedValue(strategy = "IDENTITY")
* #JMS\SerializedName("id")
* #JMS\Expose
* #JMS\Type("integer")
* #var int
*/
private $id;
/**
* #ORM\Column(name = "firstName", type = "string", length = 255)
* #JMS\SerializedName("firstName")
* #JMS\Expose
* #JMS\Type("string")
* #var string
*/
private $firstName;
/**
* #ORM\Column(name = "lastName", type = "string", length = 255)
* #JMS\SerializedName("lastName")
* #JMS\Expose
* #JMS\Type("string")
* #var string
*/
private $lastName;
/**
* #ORM\OneToMany(targetEntity = "Post", mappedBy = "user", cascade = { "persist" }, fetch="LAZY")
* #JMS\Expose
* #var ArrayCollection<Post>
*/
private $posts;
public function getId() {
return $this->id;
}
public function getFirstName() {
return $this->firstName;
}
public function getLastName() {
return $this->lastName;
public function getPosts() {
return $this->posts->toArray();
}
}
The posts are always loaded after fetching the user object, even though I use fetch type lazy or extra_lazy.
Why not using Serialization Groups for it?
You can annotate your group (e.g. {'default'} and {'collection'} or even more specific {'posts'}.
Then you simply set your Serialization Context in your Controller and only the Group-related properties will be serialized.
See as well documentation
You should write proper dql/querybuilder query and select only required for You fields.
It's not safe to return all posts on API just with $user->getPosts() - you will need pagination here.
You can also use JMSSerializerBundle. It gives you option to define needed property for Entity.
Related
so, I have a performance issue with my extbase plugin.
The scenario is such:
I have 4 database tables with data for artists, artworks, exhibitions and publications.
Exhibitions can have artists, artworks and publications as mm relations.
publications and artwork can have artists as mm relations.
In my Model Classes I have those relations as ObjectStorage and use simple findAll() Method for my List View.
So if I get to the Exhibition List View I get every Exhibition and their related Artists and all related Artworks/Publications/Exhibitions of that Artist.
The performance is really bad and a not cached page needs almost a full minute to load.
And this is all because of the heavy data load from the db.
I only need the MM relations on the first level and not further. Is there anyway to config this?
Classes\Domain\Model\Exhibition.php
class Exhibition extends AbstractEntity
{
/**
* #var string
*/
protected $name = '';
/**
* #var string
*/
protected $location = '';
/**
* artists
*
* #var ObjectStorage<\Vendor\Project\Domain\Model\Artist>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $artists = null;
/**
* artworks
*
* #var ObjectStorage<\Vendor\Project\Domain\Model\Artwork>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $artworks;
/**
* publications
*
* #var ObjectStorage<\Vendor\Project\Domain\Model\Publication>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $publications;
/**
* Fal media items
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $falMedia;
public function __construct(string $name = '', string $description = '', string $location = '')
{
$this->setName($name);
$this->setLocation($location);
}
/**
* #param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
/**
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #return string
*/
public function getLocation(): string
{
return $this->location;
}
/**
* #param string $location
*/
public function setLocation(string $location): void
{
$this->location = $location;
}
/**
* Returns the artist
*
* #return ObjectStorage<\Vendor\Project\Domain\Model\Artist> $artists
*/
public function getArtists()
{
return $this->artists;
}
/**
* Sets the artist
*
* #param ObjectStorage<\Vendor\GKG\Domain\Model\Artist> $artists
* #return void
*/
public function setArtists(ObjectStorage $artists)
{
$this->artists = $artists;
}
/**
* #param $artworks
*/
public function setArtworks($artworks)
{
$this->artworks = $artworks;
}
/**
* #return ObjectStorage
*/
public function getArtworks()
{
return $this->artworks;
}
/**
* Sets the publications
*
* #param ObjectStorage<\Vendor\Project\Domain\Model\Publication> $oublications
* #return void
*/
public function setPublications(ObjectStorage $publications)
{
$this->publications = $publications;
}
/**
* Returns the publications
*
* #return ObjectStorage<\Vendor\Project\Domain\Model\Publication> $publications
*/
public function getPublications()
{
return $this->publications;
}
/**
* Get the Fal media items
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage
*/
public function getFalMedia()
{
return $this->falMedia;
}
}
This gets all the Artist data in which all related data is fetched as well (artwork, exhibition and publication). And this is a heavy overload :/
I tried to write my own query and only select the values I need in my frontend:
Classes\Domain\Repository\ExhibitionRepository.php
public function findExhibitions()
{
$languageAspect = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance
(\TYPO3\CMS\Core\Context\Context::class)->getAspect('language');
$sys_language_uid = $languageAspect->getId();
$query = $this->createQuery();
$query->getQuerySettings()->setRespectSysLanguage(true);
$query->statement( "
SELECT DISTINCT e.name, e.uid, e.location, e.fal_media, a.name, a.uid
FROM tx_project_domain_model_exhibition e
LEFT JOIN tx_project_exhibitions_artists_mm
ON tx_project_exhibitions_artists_mm.uid_local = e.uid
LEFT JOIN tx_project_domain_model_artist a
ON tx_project_exhibitions_artists_mm.uid_foreign = a.uid
WHERE e.sys_language_uid = $sys_language_uid
GROUP BY tx_project_exhibitions_artists_mm.uid_local
" );
return $query->execute();
}
But with this approach I’m not able to get the relation data to my assigned view variable inside my Controller. Only the exhibition related stuff.
Classes\Controller\ExhibitionController.php
public function indexAction()
{
$queryResult = $this->exhibitionRepository->findExhibitions();
$this->view->assign('exhibitions', $queryResult);
}
Any insight and advice on how to tackle this problem and only get the needed Data and not everything from the ObjectStorage?
Thanks in advance and best regards
It looks like your query cost is very high with all the JOINS and without a condition on a indexed column.
You could check it with an EXPLAIN PLAN(MySQL) / EXECUTION PLAN(MS).
Set condition with an index to boost Performance
Rather using the Comparing operations leads to Performance.
https://docs.typo3.org/m/typo3/book-extbasefluid/main/en-us/6-Persistence/3-implement-individual-database-queries.html
Another performance boost is to use Ajax, just search for "TYPO3 Ajax":
get an identifier and display it in the Frontend:
SELECT DISTINCT e. uid, e.name FROM tx_project_domain_model_exhibition
On identifier click (name or whatever) make a call of the AjaxController.php:
...
$this->exhibitionRepository->findByUid($uid);
...
or make your own repository comparing
...
$query->matching(
$query->logicalAnd(
$query->equals('uid', $uid),
$query->equals('name', $name)
)
);
...
show the data on a Modal Window,Accordion or a new View.
As I understand using repositories restricts controller from accessing database layer, and all queries goes through repository. But can controller use model (laravel can inject model instead of ID in a controller) to pass it to repository or service - for example to make a transaction between users? Or better to send IDs to repository, to find users and apply business logic (do user have money, or is he banned).
And more generic question, can you use models outside of the repository, because if you change some tables from postgres or mysql to something else your models will change also. And this means your repository should have get method to send back some DTO object?
Note: This is a general perspective on the matter, appliable to any application based on MVC, not only to Laravel.
An application based on the MVC pattern should be composed of three parts:
delivery mechanism: UI logic (user request handling and server response creation),
service layer: application logic,
domain model: business logic.
Here are some graphical representations (of my own making):
As shown above (and described in detail in the resources below), the controllers and the views are part of the delivery mechanism. They should interact with the domain model only through the service layer objects (services). Consequently, they should have no knowledge of the domain model components (entities - also known as domain objects, data mappers, repositories, etc). More of it, the controllers should have only one responsibility: to pass the values of the user request to the service layer, in order for it to update the model.
So, to answer your first question: No, controllers should not be able to create any instances of elements of the domain model (so instances of what you're calling "models" - in respect of Laravel's Active Record), or even to pass such objects to other components (like repositories, services, etc). Instead, the controllers should just pass the values of the request (the user id, for example) to the corresponding services. These services will then create the proper domain model objects and use the proper repositories, data mappers, etc, in order to save/fetch to/from database.
As for the second question (if I understood it correctly): The repositories are to be seen as collections of entities - which are domain model components. As such, elements (e.g. entity instances) can be fetched, stored, altered, or removed to/from them. So, by definition, the entities must be defined/used separately from the repositories. In regard of Laravel, the same should apply: The "models" should be defined/used separately from the repositories.
A "general" MVC implementation (for more clarity):
Controller:
<?php
namespace MyApp\UI\Web\Controller\Users;
use MyApp\Domain\Service\Users;
use Psr\Http\Message\ServerRequestInterface;
/**
* Add a user.
*/
class AddUser {
/**
* User service.
*
* #var Users
*/
private $userService;
/**
*
* #param Users $userService User service.
*/
public function __construct(Users $userService) {
$this->userService = $userService;
}
/**
* Invoke.
*
* #param ServerRequestInterface $request Request.
* #return void
*/
public function __invoke(ServerRequestInterface $request) {
// Read request values.
$username = $request->getParsedBody()['username'];
// Call the corresponding service.
$this->userService->addUser($username);
}
}
Service:
<?php
namespace MyApp\Domain\Service;
use MyApp\Domain\Model\User\User;
use MyApp\Domain\Model\User\UserCollection;
use MyApp\Domain\Service\Exception\UserExists;
/**
* Service for handling the users.
*/
class Users {
/**
* User collection (a repository).
*
* #var UserCollection
*/
private $userCollection;
/**
*
* #param UserCollection $userCollection User collection.
*/
public function __construct(UserCollection $userCollection) {
$this->userCollection = $userCollection;
}
/**
* Find a user by id.
*
* #param int $id User id.
* #return User|null User.
*/
public function findUserById(int $id) {
return $this->userCollection->findUserById($id);
}
/**
* Find all users.
*
* #return User[] User list.
*/
public function findAllUsers() {
return $this->userCollection->findAllUsers();
}
/**
* Add a user.
*
* #param string $username Username.
* #return User User.
*/
public function addUser(string $username) {
$user = $this->createUser($username);
return $this->storeUser($user);
}
/**
* Create a user.
*
* #param string $username Username.
* #return User User.
*/
private function createUser(string $username) {
$user = new User();
$user->setUsername($username);
return $user;
}
/**
* Store a user.
*
* #param User $user User.
* #return User User.
*/
private function storeUser(User $user) {
if ($this->userCollection->userExists($user)) {
throw new UserExists('Username "' . $user->getUsername() . '" already used');
}
return $this->userCollection->storeUser($user);
}
}
Repository:
<?php
namespace MyApp\Domain\Infrastructure\Repository\User;
use MyApp\Domain\Model\User\User;
use MyApp\Domain\Infrastructure\Mapper\User\UserMapper;
use MyApp\Domain\Model\User\UserCollection as UserCollectionInterface;
/**
* User collection.
*/
class UserCollection implements UserCollectionInterface {
/**
* User mapper (a data mapper).
*
* #var UserMapper
*/
private $userMapper;
/**
*
* #param UserMapper $userMapper User mapper.
*/
public function __construct(UserMapper $userMapper) {
$this->userMapper = $userMapper;
}
/**
* Find a user by id.
*
* #param int $id User id.
* #return User|null User.
*/
public function findUserById(int $id) {
return $this->userMapper->fetchUserById($id);
}
/**
* Find all users.
*
* #return User[] User list.
*/
public function findAllUsers() {
return $this->userMapper->fetchAllUsers();
}
/**
* Store a user.
*
* #param User $user User.
* #return User User.
*/
public function storeUser(User $user) {
return $this->userMapper->saveUser($user);
}
/**
* Check if the given user exists.
*
* #param User $user User.
* #return bool True if user exists, false otherwise.
*/
public function userExists(User $user) {
return $this->userMapper->userExists($user);
}
}
Entity:
<?php
namespace MyApp\Domain\Model\User;
/**
* User.
*/
class User {
/**
* Id.
*
* #var int
*/
private $id;
/**
* Username.
*
* #var string
*/
private $username;
/**
* Get id.
*
* #return int
*/
public function getId() {
return $this->id;
}
/**
* Set id.
*
* #param int $id Id.
* #return $this
*/
public function setId(int $id) {
$this->id = $id;
return $this;
}
/**
* Get username.
*
* #return string
*/
public function getUsername() {
return $this->username;
}
/**
* Set username.
*
* #param string $username Username.
* #return $this
*/
public function setUsername(string $username) {
$this->username = $username;
return $this;
}
}
Data mapper:
<?php
namespace MyApp\Domain\Infrastructure\Mapper\User;
use PDO;
use MyApp\Domain\Model\User\User;
use MyApp\Domain\Infrastructure\Mapper\User\UserMapper;
/**
* PDO user mapper.
*/
class PdoUserMapper implements UserMapper {
/**
* Database connection.
*
* #var PDO
*/
private $connection;
/**
*
* #param PDO $connection Database connection.
*/
public function __construct(PDO $connection) {
$this->connection = $connection;
}
/**
* Fetch a user by id.
*
* Note: PDOStatement::fetch returns FALSE if no record is found.
*
* #param int $id User id.
* #return User|null User.
*/
public function fetchUserById(int $id) {
$sql = 'SELECT * FROM users WHERE id = :id LIMIT 1';
$statement = $this->connection->prepare($sql);
$statement->execute([
'id' => $id,
]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return ($data === false) ? null : $this->convertDataToUser($data);
}
/**
* Fetch all users.
*
* #return User[] User list.
*/
public function fetchAllUsers() {
$sql = 'SELECT * FROM users';
$statement = $this->connection->prepare($sql);
$statement->execute();
$data = $statement->fetchAll(PDO::FETCH_ASSOC);
return $this->convertDataToUserList($data);
}
/**
* Check if a user exists.
*
* Note: PDOStatement::fetch returns FALSE if no record is found.
*
* #param User $user User.
* #return bool True if the user exists, false otherwise.
*/
public function userExists(User $user) {
$sql = 'SELECT COUNT(*) as cnt FROM users WHERE username = :username';
$statement = $this->connection->prepare($sql);
$statement->execute([
':username' => $user->getUsername(),
]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return ($data['cnt'] > 0) ? true : false;
}
/**
* Save a user.
*
* #param User $user User.
* #return User User.
*/
public function saveUser(User $user) {
return $this->insertUser($user);
}
/**
* Insert a user.
*
* #param User $user User.
* #return User User.
*/
private function insertUser(User $user) {
$sql = 'INSERT INTO users (username) VALUES (:username)';
$statement = $this->connection->prepare($sql);
$statement->execute([
':username' => $user->getUsername(),
]);
$user->setId($this->connection->lastInsertId());
return $user;
}
/**
* Update a user.
*
* #param User $user User.
* #return User User.
*/
private function updateUser(User $user) {
$sql = 'UPDATE users SET username = :username WHERE id = :id';
$statement = $this->connection->prepare($sql);
$statement->execute([
':username' => $user->getUsername(),
':id' => $user->getId(),
]);
return $user;
}
/**
* Convert the given data to a user.
*
* #param array $data Data.
* #return User User.
*/
private function convertDataToUser(array $data) {
$user = new User();
$user
->setId($data['id'])
->setUsername($data['username'])
;
return $user;
}
/**
* Convert the given data to a list of users.
*
* #param array $data Data.
* #return User[] User list.
*/
private function convertDataToUserList(array $data) {
$userList = [];
foreach ($data as $item) {
$userList[] = $this->convertDataToUser($item);
}
return $userList;
}
}
View:
<?php
namespace MyApp\UI\Web\View\Users;
use MyApp\UI\Web\View\View;
use MyApp\Domain\Service\Users;
use MyLib\Template\TemplateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ResponseFactoryInterface;
/**
* Add a user.
*/
class AddUser extends View {
/**
* User service.
*
* #var Users
*/
private $userService;
/**
*
* #param ResponseFactoryInterface $responseFactory Response factory.
* #param TemplateInterface $template Template.
* #param Users $userService User service.
*/
public function __construct(ResponseFactoryInterface $responseFactory, TemplateInterface $template, Users $userService) {
parent::__construct($responseFactory, $template);
$this->userService = $userService;
}
/**
* Display a form for adding a user.
*
* #return ResponseInterface Response.
*/
public function index() {
$body = $this->template->render('#Template/Users/add-user.html.twig', [
'activeMainMenuItem' => 'addUser',
'action' => '',
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($body);
return $response;
}
/**
* Add a user.
*
* #return ResponseInterface Response.
*/
public function addUser() {
$body = $this->template->render('#Template/Users/add-user.html.twig', [
'activeMainMenuItem' => 'addUser',
'message' => 'User successfully added.',
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($body);
return $response;
}
}
Resources:
How should a model be structured in MVC?
Keynote: Architecture the Lost Years
GeeCON 2014: Sandro Mancuso - Crafted Design
This is an opiniated answer but here's my take. What I suggest is to not add a repository layer for the sake of having a repository in Laravel. whatever methods you need, add them to the model classes, When they are bloated/expect it to be bloated then only think about repositories (Most probably you would need a service class or some other abstraction here).
Since all these eloquent model classes can be resolved from container its easy to use them. it's accessible anywhere and even in the controller like you have mentioned can be injected which provides a great level of ease.
And repositories help to change for example the underlying database, But eloquent provides us with that flexibility already. And when you plan to change your database, I don't think its going to be a simple change so why wrap the logic up in another layer of abstraction (unneccessarily).
At least from my experience the repository pattern doesn't suite well with Active Record Pattern. Which Laravel follows. Where repository suites very well for data mapper pattern (for example Symfony uses it). Thats why in laravel documentation you don't see them embracing the repository pattern. Rather in symfony documentation you can see it.
So I suggest to embrace the framework than to fight it
I have the following structure:
Category property that contains link to property and its value:
<?php
class CategoryProperty
{
// ...
/**
* #var Property
*
* #ORM\ManyToOne(targetEntity="App\Entity\Property")
* #ORM\JoinColumn(onDelete="cascade", nullable=false)
*/
private $property;
/**
* Набор значений свойства доступных в product builder, null если любое значение.
*
* #var PropertyValueEntry
* #Assert\Valid
*
* #ORM\OneToOne(targetEntity="App\Entity\Properties\PropertyValues\PropertyValueEntry",
* cascade={"persist", "remove"})
*/
private $propertyValue;
// ...
}
Abstract property value type with a discriminator map:
<?php
/**
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="integer")
* #ORM\DiscriminatorMap({
* "1": "StringValue",
* "2": "IntegerValue",
* "3": "BooleanValue",
* "4": "TextValue",
* "6": "EnumValue",
* "7": "SetValue",
* "9": "LengthValue",
* "10": "AreaValue",
* "11": "VolumeValue",
* "12": "MassValue",
* })
* #ORM\Table(name="properties_values__value_entry")
*/
abstract class PropertyValueEntry
{
/**
* #var Property
*
* #ORM\ManyToOne(targetEntity="App\Entity\Property")
*/
private $property;
public function __construct(Property $property)
{
$this->property = $property;
}
public function getProperty(): Property
{
return $this->property;
}
/**
* #return mixed
*/
abstract public function getValue();
/**
* #param mixed $value
*/
abstract public function setValue($value): void;
}
And a sample concrete value type:
<?php
/**
* #ORM\Entity
* #ORM\Table(name="properties_values__integer_value")
*/
class IntegerValue extends PropertyValueEntry
{
/**
* #var int
* #Assert\NotNull
*
* #ORM\Column(type="integer")
*/
private $value = 0;
public function getValue(): int
{
return $this->value;
}
/**
* #param int|null $value
*/
public function setValue($value): void
{
if (!\is_int($value)) {
throw new InvalidArgumentException('BooleanValue accepts integer values only');
}
$this->value = $value;
}
}
For some reason, when form is submitted, instead of updating a value for IntegerValue, a new entity gets created, and new row in properties_values__value_entry / properties_values__integer_value. I tried tracking through the $this->em->persist($entity), where $entity is CategoryProperty, and it seems that IntegerValue gets marked as dirty and created anew. How can I track the cause of this happening? My form processing is pretty standard:
<?php
public function editAction(): Response
{
$id = $this->request->query->get('id');
$easyadmin = $this->request->attributes->get('easyadmin');
$entity = $easyadmin['item'];
$isReload = 'reload' === $this->request->request->get('action');
$editForm = $this->createForm(CategoryPropertyType::class, $entity, [
'category' => $this->getCatalog(),
'is_reload' => $isReload,
]);
$deleteForm = $this->createDeleteForm($this->entity['name'], $id);
$editForm->handleRequest($this->request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
if (!$isReload) {
$this->em->persist($entity);
$this->em->flush();
return $this->redirectToReferrer();
}
}
return $this->render($this->entity['templates']['edit'], [
'form' => $editForm->createView(),
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
UPDATE #1
What I already tried:
Retrieve category property by ID from entity manager through
$entity = $this->em->find(CategoryProperty::class, $id);
Altogether it seems this may be related to the fact that I have a dynamic form being created based on the selection. When I add a category property, I display a dropdown with a list of property types (integer, string, area, volume etc), and after selection a new form for that property is displayed. Though this works fine and adds new property without a problem, it seems that the code for EDITING same property is missing something, and instead of update it creates it anew.
Possibility #1: Load entity from entity manager directly
You don't appear to be retrieving an existing entity to modify at all.
$entity = $easyadmin['item'];
Shouldn't this be using Doctrine to retrieve an existing entity? For example:
if (!($entity = $this->getRepository(CategoryProperty::class)->findOneById($id))) {
throw $this->createNotFoundException("Category property not found.");
}
Semi-related: You may also want to check that a integer ID was specified at all, as $id = $this->request->query->get('id'); is very assumptive:
if (intval($id = $this->request->query->get('id')) < 1) {
throw $this->createNotFoundException("Category property not specified.");
}
Possibility 2: Missing identifier reference with one-to-one relationship
I think you may be getting duplication because CategoryProperty doesn't persist any reference to a PropertyValueEntry.
/**
* Набор значений свойства доступных в product builder, null если любое значение.
*
* #var PropertyValueEntry
* #Assert\Valid
*
* #ORM\OneToOne(targetEntity="App\Entity\Properties\PropertyValues\PropertyValueEntry", cascade={"persist", "remove"})
*/
private $propertyValue;
However PropertyValueEntry doesn't have an inverse relationship back to CategoryProperty.
A unidirectional one-to-one is fine, but it must have a #ORM\JoinColumn directive to ensure the identifier of the foreign PropertyValueEntry is persisted. Otherwise an edit form won't have any information to know which existing PropertyValueEntry (or derivative) it needs to edit. This is why your "properties_values__value_entry" form field is being reset with a new instance of PropertyValueEntry (or derivative) created when submitting the form.
You've not shown the source for entity class Property so I can't inspect for any further issues in your entity relationships.
Thanks to everyone participating, I have been reading through Symfony documentation and came across the 'by_reference' form attribute. Having considered that my form structure overall looks like this:
Category => CategoryPropertyType => PropertyValueType => [Set, Enum, Integer, Boolean, String, Volume]
for the form, I decided to set it to true in PropertyValueType configureOptions method. As it is explained in the documentation, with it being set to false, the entity is getting cloned (which in my case is true), thus creating a new object at the end.
Note that I'm still learning Symfony and will be refining the answer when I get a better understanding of what's going on behind the scenes.
In my Symfony project I have this entity named Order.php:
<?php
namespace myApp\EntityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Datetime;
/**
*
* #ORM\Table(name="`order`")
* #ORM\Entity(repositoryClass="myApp\EntityBundle\Repository\OrderRepository")
*
*/
class Order
{
/**
* #var integer
*
* #ORM\Column(name="order_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* Command name ref for right accounting
*
* #var string
*
* #Assert\Type(
* type="string",
* message="The value {{ value }} is not a valid {{ type }}."
* )
* #Assert\NotNull()
* #Assert\NotBlank()
* #ORM\Column(name="order_name", type="string", length=255, nullable=true, unique=true)
*/
protected $orderName;
// etc other associations, object and getters setters...
/**
* Constructor
*/
public function __construct()
{
$this->generateOrderName();
}
/**
* generate order_name with base ref
*
* #return Order
*/
public function generateOrderName()
{
// trying to make the good practice for generated orderName value
//$now = new Datetime();
//$year = $now->format("Y");
//$month = $now->format("m");
//$orderNameCommon = $year."_".$month."_";
$this->setOrderName();
return $this;
}
/**
* Set orderName
*
* #param string $orderName
*
* #return Order
*/
public function setOrderName($orderName)
{
$this->orderName = $orderName;
return $this;
}
/**
* Get orderName
*
* #return string
*/
public function getOrderName()
{
return $this->orderName;
}
}
The purpose is to generate an orderName like this: '2016_09_00001' which means that I registered order by date Year_Month_orderNumber.
I have to take care that the orderName have to be unique, and for example if I have already an order named '2016_09_00001', I have to register the orderName like this '2016_09_00002'. Like an auto-incrementation for example.
As you can see, I have made this code directly in entity class Order:
/**
* Constructor
*/
public function __construct()
{
// call the order name generation function
$this->generateOrderName();
}
/**
* generate order_name with base ref
*
* #return Order
*/
public function generateOrderName()
{
$now = new Datetime();
$year = $now->format("Y");
$month = $now->format("m");
$prefix = $year."_".$month."_";
// That I had planned:
// to create a prefix with Datetime argument, year and month
// Then check all order in database with this prefix
// Finally count the results and add +1 on the final value
$this->setOrderName();
return $this;
}
As you can see, I have called the generateOrderName() function in the constructor, and then I would like to make this process:
That I had planned:
to create a prefix with Datetime argument, year and month
Then check all order in database with this prefix
Finally count the results and add +1 on the final value
What is the best way to make this properly in the entity directly? I think it's not a good thing to have a repository dql query call in the entity class itself.
I have create this one for example:
public function searchOrderName($prefix)
{
return
$this->getEntityManager()
->createQuery(
"SELECT o.orderName FROM EntityBundle:Order o
WHERE o.orderName
LIKE :prefix
ORDER BY o.id ASC"
)
->setParameter("prefix", $prefix.'%')
->getResult();
}
How can I proceed to create the generateOrderName() function in the entity to make the generated process I would like directly in entity class ?
You will need a service for it. You cannot access DB from your Entity. So make OrderFactory, inject there OrderRepository and you will easily reach your goal
I have 2 tables called Advert and User. The User Id gets written into the Advert Table. I have now created a third table called Bookmark. The table got 2 columns, advert_id and user_id. When I see an Advert I like, I can add a Bookmark to be able to find it easier in my Private Section. When I am in my Private Section to see my Bookmarks, I want to see the Advert Details straight away, so in my Repository I want to create a Join to read the information from the Advert Table. I thought this will be a OneToOne Relationship. I also have to make sure that if the Advert gets deleted, then all the Bookmarks need to be deleted, so I thought its a Bi-directional relationship. So I have below:
Entity/Bookmark.php
/**
* Bookmark
*
* #ORM\Table(name="bookmark")
* #ORM\Entity(repositoryClass="Advert\Repository\BookmarkRepository")
*/
class Bookmark
{
/**
* #var integer
* #ORM\Id
* #ORM\Column(name="advert_id", type="integer", nullable=false)
* #ORM\OneToOne(targetEntity="Advert", mappedBy="bookmark")
* #ORM\JoinColumn(name="advert_id", referencedColumnName="id")
*/
private $advertId;
/**
* #var integer
* #ORM\Id
* #ORM\Column(name="user_id", type="integer", nullable=false)
* #ORM\OneToOne(targetEntity="User")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $userId;
public function setAdvertId($advertId)
{
$this->advertId = $advertId;
return $this;
}
public function getAdvertId()
{
return $this->advertId;
}
public function setUserId($userId)
{
$this->userId = $userId;
return $this;
}
public function getUserId()
{
return $this->userId;
}
Entity\Advert.php
/** Advert
*
* #ORM\Table(name="advert")
* #ORM\Entity(repositoryClass="Advert\Repository\AdvertRepository")
*/
class Advert
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity="Bookmark", inversedBy="advert")
* #ORM\JoinColumn(name="id", referencedColumnName="advert_id")
**/
private $bookmark;
public function setBookmark($bookmark)
{
$this->bookmark = $bookmark;
return $this;
}
public function getBookmark()
{
return $this->bookmark;
}
public function addBookmark($bookmark)
{
$this->bookmark->add($bookmark);
}
public function removeBookmark($bookmark)
{
$this->bookmark->removeElement($bookmark);
}
Advert\Repository\Advert\Repository.php
class BookmarkRepository extends EntityRepository
{
public function getBookmarksByUserIds($userId)
{
$query =$this->_em->getRepository($this->getEntityName())->createQueryBuilder('b')
->join('b.advertId', 'a')
->andWhere('a.userId=:userid')
->setParameter('userid',$userId)
;
return $query->getQuery()->getResult();
}
What do I do wrong or where is my missunderstanding? I get the Error Message:
Advert\Entity\Bookmark has no association named advertId
As I said, the table Bookmark gets only filled, when I click on "Add Advert to Bookmarks". I need a Join to be able to display the Advert Details when I click on "Show my Bookmarks" and if an Advert or a User gets deleted, all Bookmarks need to be removed from the Bookmark table. Is this a OneToOne Bi-directional relationship and what is wrong?
UPDATE 1 BELOW NOT WORKING
I have updated the 2 Files below, but I do not get any Bookmarks shown. Instead I should see a list of Bookmarked adverts plus the advert details. I have not even tried yet to get my Service to "Bookmark Advert" or the method to check if an Advert is bookmarked working again. I got it working before, but I guess I am just really confused now.
AdvertController.php
public function watchlistAction()
{
$user_id = $this->zfcUserAuthentication()->getIdentity()->getId();
$adverts = $this->getEntityManager()->getRepository('Advert\Entity\User')->findBookmarksByUserId($user_id);
return new ViewModel(array(
'adverts' => $adverts,
));
}
Repository\UserRepository.php
class UserRepository extends EntityRepository
{
public function findBookmarksByUserId($userId)
{
$query =$this->_em->getRepository($this->getEntityName())->createQueryBuilder('b')
->join('b.bookmarks', 'a')
->join('b.adverts', 'c')
->andWhere('a.user=:userid')
->setParameter('userid',$userId)
;
return $query->getQuery()->getResult();
}
UPDATE 2 BELOW WORKING
You were right, I don't need the UserRepository Query see the List of Bookmarked Adverts. I just had to change the
AdvertController.php
public function watchlistAction()
{
$user_id = $this->zfcUserAuthentication()->getIdentity()->getId();
// get User by reference (no queries executed)
$user = $this->getEntityManager()->getReference('Advert\Entity\User' , $user_id);
$adverts = $user->getBookmarks();
return new ViewModel(array(
'adverts' => $adverts,
));
}
Good news also, in the moment I delete an Advert, the Bookmark gets automatically removed in the Bookmark Database table. Now I only have to find out how to add the Bookmark, so I will have to change my Service. As soon I get this working I will update this post for others to see.
UPDATE 3 BELOW NOT WORKING
Unfortunately I do not get the below 3 Methods in my Service working. I obviously have to now pick 1 record, to either check the Status (Bookmarked already or not), remove the Bookmark (defined by advertId) or add a Bookmark (defined by advertId)
public function checkAdvertBookmarkStatus($advertId)
{
$userId = $this->getUserEntity()->getId();
// get User by reference (no queries executed)
$user = $this->getEntityManager()->getReference('Advert\Entity\User' , $userId);
$bookmarkStatus = $this->getEntityManager()->getRepository('Advert\Entity\User')
->findOneBy(array('advert' => $advertId, 'userId' => $userId));
return $bookmarkStatus;
}
public function saveAdvertBookmark($advertId)
{
$bookmark = new UserEntity();
$userId = $this->getUserEntity()->getId();
// $bookmark->addBookmark($advertId);
$bookmark->setAdvertId($advertId);
$bookmark->setUserId($userId);
# write new bookmmark to database tbl bookmark
$this->getEntityManager()->persist($bookmark);
$this->getEntityManager()->flush();
}
public function removeAdvertBookmark($advertId)
{
$bookmark = new UserEntity();
$userId = $this->getUserEntity()->getId();
$bookmark = $this->getEntityManager()->getRepository('Advert\Entity\Bookmark')
->findOneBy(array('advertId' => $advertId, 'userId' => $userId));
# remove bookmmark from tbl bookmark
$this->getEntityManager()->remove($bookmark);
$this->getEntityManager()->flush();
}
I suppose the answer is in the Tutorial, which I keep reading, but I do not understand it fully. I was able to add Bookmarks before, when I was using the BookmarkEntity, but I have no idea how to do it via the UserEntity
A OneToOne relationship would be the wrong choice here, this would mean that a user can bookmark only one advert and that an advert can only be bookmarked by one user. Because a user should be able to bookmark many adverts and an advert should be bookmarked by many users, you need a ManyToMany relationship.
You idea to create a mapping table bookmarks is not wrong if you work with a database. However, you don't need to create it as an entity in Doctrine. You can simply add Adverts in an association called bookmarks in User to display the bookmarked Adverts and vice versa:
User Entity:
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Application\Entity\Advert", inversedBy="bookmarks", cascade={"persist"})
* #ORM\JoinTable(name="bookmarks",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="advert_id", referencedColumnName="id")}
* )
*/
private $bookmarks;
Advert Entity
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Application\Entity\User", mappedBy="bookmarks", cascade={"persist"})
* #ORM\JoinTable(name="bookmarks",
* joinColumns={#ORM\JoinColumn(name="advert_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")}
* )
*/
private $bookmarks;
You might want to read this article, too:
http://doctrine-orm.readthedocs.org/en/latest/reference/working-with-associations.html
Edit: How to add and remove bookmarks
Associations in Doctrine are something entirely different from fields, although both are properties in your Entity. To handle bookmarks you add or remove the Advert Entity in your User Entity directly. For example:
$bookmarks = $user->getBookmarks();
$bookmarks[] = $advert;
This would add a bookmark to the user and will be stored as soon as you persist and flush. To make this even easier, you can define remover and adder:
Use statements:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
and the code:
/**
* #param Collection $bookmarks
*/
public function addBookmarks(Collection $bookmarks)
{
foreach ($bookmarks as $bookmark) {
$this->bookmarks->add($bookmark);
}
}
/**
* #param Collection $bookmarks
*/
public function removeBookmarks(Collection $bookmarks)
{
foreach ($bookmarks as $bookmark) {
$this->bookmarks->removeElement($bookmark);
}
}
You can now remove and add adverts given in collection like this:
$user->addBookmarks(new ArrayCollection(array($advert)));
It is always recommended to define adder and remover in toMany relationships, because many Doctrine components will need them, for example the very useful DoctrineObject, a hydrator used by DoctrineModule for Zend 2