Just wondering if it is possible to only use some parts of the symfony form handling. For exampe, when creating CRUD actions via generate:doctrine:crud I get something in my Controller (for handling create User POST requests) that looks like this:
$entity = new User();
$form = $this->createForm(new UserType(), $entity,
array(
'action' => $this->generateUrl('user_create'),
'method' => 'POST',
));
$form->handleRequest($request);
//Here I have a filled Entity
But what I want is to have this in a more reusable solution. Currently I have my business logic in a service called UserModel. Here I also want to have the create method to create, validate and persist a new entity. Tough the UserModel should also be callable from some Command scripts via the console, so I probably won't always have Request nor a Form.
So now from the above code I know that Symfony is already somehow populating data to an Entity according to the UserType definition, but how could I do that manually without having a Form or a Request and instead just some array of data?
So that I don't have to take care of setting the properties myself.
Edit:
The validation is no issue for populating the entity, I'm using the validator later on the populated entity before persisting the data.
And also important for me would be that the passed related entity ids will be handled/loaded automatically.
you may still use the Form component, but instead of using handleRequest, you should use directly submit.
If you are curious, you should look up the code on github for both handleRequest and what it actually does ; you'll see that it just do some verification, takes the data from the Request, and then uses the submit method of the Form.
So, basically, you can use only the submit method with the data you wish to use. It even validates your entity. :)
UPDATE
And for the concern of creating / updating related entities, if your relation have a persist / update cascade, it should roll out from itself without you doing a single thing, except persist + flush on your main entity.
If you do not worry about handling validation like things, you can do something like I have done.
You can define a trait or include the fromArray function in your entity classes.
trait EntityHydrationMethod
{
public function fromArray($data = array())
{
foreach ($data as $property => $value) {
$method = "set{$property}";
$this->$method($value);
}
}
}
If you defined trait, you can use it in your entities like:
class User{
use EntityHydrationMethod;
}
Then from your user model you can define your create function something like:
public function create($data = array())
{
$entity = new User();
$entity->fromArray($data);
return $entity;
}
-Updated-
As you updated your question. you may achieve this by defining a trait or include the createFromArray function in your EntityRepository classes.
trait RepositoryCreateMethod {
public function createFromArray($data)
{
$class = $this->getClassName();
$object = new $class();
$meta = $this->getClassMetadata();
foreach ($data as $property => $value) {
$v = $value;
if(!empty($value) && $meta->hasAssociation($property)) {
$map = $meta->getAssociationMapping($property);
$v = $this->_em->getRepository($map['targetEntity'])->find($value);
if(empty($v)){
throw new \Exception('Associate data not found');
}
}
$method = "set{$property}";
$object->$method($v);
}
return $object;
}
}
If you defined trait, you can use it in your Repository like:
class UserRepository{
use RepositoryCreateMethod;
}
Then you can use this something like call from controller:
$user = $this->getDoctrine()
->getRepository('YourBundle:User')
->createFromArray($data);
Related
I have a car with two columns: user_id and token
I would like to only pass the user_id on creation and create a token automatically:
$car = Car::create([
'user_id' => $user->id,
]);
this is my car class:
class Car extends Model
{
protected $guarded = [];
public function __construct()
{
parent::__construct();
$this->token = mb_substr(bin2hex(openssl_random_pseudo_bytes(32)),0,8);
}
When I create a car, the token field is correctly inserted. However the user_id field is null.
When I remove the __construct() method, then the user_id is correctly inserted (but then there is ofc no token).
I don't understand why the assignment in the constructor removes the user_id.
Any suggestions?
Rather than creating the token in your constructor, you could take advantage of Laravel's model events. In short, this allows you to listen for an event (e.g. "created," "updated," etc.), and perform an action on that event. If you replace your constructor with the following, it should solve the issue:
public static function boot()
{
self::created(function ($model) {
$model->update([
'token' = mb_substr(bin2hex(openssl_random_pseudo_bytes(32)),0,8);
]);
});
// If you're using the SoftDeletes trait, uncomment this line.
// static::bootSoftDeletes();
}
You'll create an instance of your Car model in your controller, and then the model event will update that instance with your token.
As an aside: since the token is generated randomly, and seems not to rely on any other data/functions, I don't believe there's any shame in dropping this line:
'token' = mb_substr(bin2hex(openssl_random_pseudo_bytes(32)),0,8);
Into the create method in your controller. Based on what you've provided, it would be the simplest way to solve for what you need.
The problem is your constructor doesn't have the correct method signature.
The create method in the laravel model creates a new model: $model = new static($attributes); The $attributes array is what sets the data on your fresh model. You need to make sure your constructor takes the attributes argument and passes it to the parent:
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->token = mb_substr(bin2hex(openssl_random_pseudo_bytes(32)),0,8);
}
For historical reasons, my pattern of running databases using Symfony is mixed. That is, the query uses DBAL and the insert uses ORM. Now you need to write a lot of data to the database. The flush in ORM can help me achieve business at the lowest cost.
All flush operations have been removed from the project. Put it in the __destruct of the controller.
However, doing so will cause DBAL to not find the latest changed data. Of course, these data ORMs can be obtained normally.
This is a very difficult problem. I hope to get guidance.
class BaseController extends Controller
{
public function __destruct()
{
$this->getDoctrine()->getManager()->flush();
}
public function indexAction()
{
$model = new CompanyModel();
$model->install(['company_name' => '1234']);
$model->update(['company_name' => 'abcd'], $model->lastInsertId);
}
}
class CompanyModel extends BaseController
{
public function validate($data, $id = false)
{
$this->entityManager = $this->getDoctrine()->getManager();
if(empty($id)){
$this->company_class = new Company();
}else{
if(!$this->is_exist($id)){
return false;
}
$this->company_class = $this->entityManager->getRepository(Company::class)->find($id);
}
if(array_key_exists('company_name', $data)){
$this->company_class->setCompanyName($data['company_name']);
}
if(self::$error->validate($this->company_class)){
return false;
}
return true;
}
public function insert($data)
{
if(!$this->validate($data)){
return false;
}
$this->company_class->setCreateAt(new \DateTime());
$this->entityManager->persist($this->company_class);
//$this->entityManager->flush();
$this->lastInsertId = $this->company_class->getId();
return true;
}
public function update($data, $id)
{
if(empty($id)){
self::$error->setError('param id is not null');
return false;
}
if(!$this->validate($data, $id)){
return false;
}
$this->company_class->setUpdateAt(new \DateTime());
//$this->entityManager->flush();
return true;
}
public function is_exist($id)
{
return $this->get('database_connection')->fetchColumn('...');
}
}
The final result of executing indexAction company_name is 1234; $ model-> update() was not executed successfully. The reason is that the $this-> is_exist() method that took the DBAL query did not find the ORM insert but did not flush the message.
Unchanging conditions,run
$this->entityManager->getRepository(Company::class)->find($id);
Is successful。
The problem is not the entity manager or dbal, as far as I can tell, but the usage of an anti-pattern, which I would call ... entanglement. What you should strive for is separation of concerns. Essentially: Your "CompanyModel" is an insufficient and bad wrapper for the EntityManager and/or EntityRepository.
No object should know about the entity manager. It should only be concerned with holding the data.
The entity manager should be concerned with persistence and ensuring integrity.
The controller is meant to orchestrate one "action", that can be adding one company, editing one company, batch-importing/updatig many companies.
Services can be implemented, when actions become to business-logic-heavy or when functionality is repeated.
(Note: the following code samples could be made way more elegant with using all the features that symfony provide, like ParamConverters, the Form component, the Validation component, I usually wouldn't write code this way, but I assume everything else would go way over your head - no offence.)
handling actions in the controller
controller actions (or service actions, really) are when you look at your problem from the task perspective. Like "I want to update that object with this data"). That's when you fetch/create that object, then give it the data.
use Doctrine\ORM\EntityManagerInterface;
class BaseController extends Controller {
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
public function addAction() {
$company = new Company(['name' => '1234']); // initial setting in constructor
$this->em->persist($company);
// since you have the object, you can do any changes to it.
// just change the object
$company->update(['name' => 'abcd']); // <-- don't need id
// updates will be flushed as well!
$this->em->flush();
}
public function editAction($id, $newData) {
$company = $this->em->find(Company::class, $id);
if(!$company) {
throw $this->createNotFoundException();
}
$company->update($newData);
$this->em->flush();
}
// $companiesData should be an array of arrays, each containing
// a company with an id for update, or without an id for creation
public function batchAction(array $companiesData) {
foreach($companies as $companyData) {
if($companyData['id']) {
// has id -> update existing company
$company = $this->em->find(Company::class, $companyData['id']);
//// optional:
// if(!$company) { // id was given, but company does not exist
// continue; // skip
// // OR
// $company = new Company($companyData); // create
// // OR
// throw new \Exception('company not found: '.$companyData['id']);
// }
$company->update($companyData);
} else {
// no id -> create new company
$company = new Company($companyData);
$this->em->persist($company);
}
}
$this->em->flush(); // one flush.
}
}
the base controller should handle creating objects, and persisting it, so very basic business logic. some would argue, that some of those operations should be done in an adapted Repository for that class, or should be encapsulated in a Service. And they would be right, generally.
the entity handles it's internal state
Now, the Company class handles its own properties and tries to stay consistent. You just have to make some assumptions here. First of all: the object itself shouldn't care if it exists in the database or not. it's not its purpose! it should handle itself. Separation of concerns! The functions inside the Company entity should concern simple business logic, that concerns its INNER state. It doesn't need the database, and it should not have any reference to the database, it only cares about it's fields.
class Company {
/**
* all the database fields as public $fieldname;
*/
// ...
/**
* constructor for the inital state. You should never want
* an inconsistent state!
*/
public function __construct(array $data=[]) {
$this->validate($data); // set values
if(empty($this->createAt)) {
$this->createAt = new \DateTime();
}
}
/**
* update the data
*/
public function update(array $data) {
$this->validate($data); // set new values
$this->updateAt = new \DateTime();
}
public function validate(array $data) {
// this is simplified, but you can also validate
// here and throw exceptions and stuff
foreach($array as $key => $value) {
$this->$key = $value;
}
}
}
some notes
Now, there should be NO use case, where you get an object to persist and at the same time an update - with an id - that refers to the new object ... unless that object was given the id beforehand! HOWEVER. If you persist an object, that has an ID and you call $this->em->find(Company::class, $id) you would get that object back.
if you have many relations, there are always good ways to solve this problem without destroying separation of concerns! you should never inject an entity manager into an entity. the entity should not manage its own persistence! nor should it manage the persistence of linked objects. handling persistence is the purpose of the entity manager or entity repository. you should never need a wrapper around an object just to handle that object. be careful not to mix responsibilities of services, entities (objects) and controllers. In my example code, I have merged services and controllers, because in simple cases, it's good enough.
I have a many-to-many relationship between users (the owning side) and user groups, and am having issues using doctrine module's hydrator to create a new user group.
When I create a new user group and hydrate, persist, and flush it, the records change in the database, but the entity variable itself representing the user group doesn't end up with any users in it post-hydration.
Context: We have a REST controller route that we use to create a new user group via POST. It accepts parameters to initialize it with some users via hydration. This operation successfully updates the database, but its response is incorrect. It is supposed to extract the data from the now-persistent entity and echo it back to the client. However, it fails to extract any users, so the response incorrectly returns as an empty group. Not using the hydrator's extract method and instead using more basic doctrine commands fails too--it seems like the entity variable itself is just not kept up to date after being persisted.
So my question really is: why is the hydrator not extracting users? If we've messed up the owner/inverse assocation, why is it working at all (i.e. persisting the users to the database but not to the entity).
Here is the relevant code, probably only the first two blocks are needed.
public function create($data) {
...
$hydrator = $this->getHydrator();
$em = $this->getEntityManager();
$entity = $this->getEntity();
$entity = $hydrator->hydrate($data, $entity);
// Persist the newly created entity
$em->persist($entity);
// Flush the changes to the database
$em->flush();
return $this->createResponse(
201,
true,
'Created item',
$this->getHydrator()->extract($entity)
);
Here is are the setters and getters the hydrator is using:
... more fields...
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="groups")
*/
protected $users;
...
/**
* Getter for users
*
* #return mixed
*/
public function getUsers() {
return $this->users;
}
public function addUsers(Collection $users) {
foreach ($users as $user) {
$user->addGroups(new ArrayCollection(array($this)));
}
}
I think the above is the only relevant code, but I'll include some more in case I'm wrong. Here is the getHydrator method:
public function getHydrator() {
if(null === $this->hydrator) {
$hydrator = new DoctrineObject($this->getEntityManager(), $this->getEntityName());
// Use Entity metadata to add extraction stategies for associated fields
$metadata = $this->em->getClassMetadata($this->getEntityName());
foreach ($metadata->associationMappings as $field => $mapping) {
// Use our custom extraction strategies for single and collection valued associations
if($metadata->isSingleValuedAssociation($field)) {
$hydrator->addStrategy($field, new RestfulExtractionStrategy());
}
else if($metadata->isCollectionValuedAssociation($field)) {
$hydrator->addStrategy($field, new RestfulExtractionCollectionStrategy());
}
}
$this->hydrator = $hydrator;
}
return $this->hydrator;
}
Here is the RestfulExtractionCollectionStrategy (the other strategy isn't being used here, I have verified this).
namespace Puma\Controller;
use DoctrineModule\Stdlib\Hydrator\Strategy\AllowRemoveByValue;
use Doctrine\Common\Collections\Collection;
/**
* You can use this strategy with fields that are collections,
* e.g. one to many. You need to use the RestfulExtractionStrategy
* if you want to get extract entities from a singleton field, e.g. manyToOne.
**/
class RestfulExtractionCollectionStrategy extends AllowRemoveByValue
{
public function extract($value)
{
if ($value instanceof Collection) {
$return = array();
foreach ($value as $entity) {
if(method_exists($entity, 'getId')){
$return[] = $entity->getId();
}
else {
$return[] = $entity;
}
}
return $return;
}
return $value;
}
}
I am not quite familiar with hydration, etc., so your code looks kind of strange to me and I cannot guarantee this will work, but have you tried to refresh the entity after flushing (i.e. $em->refresh($entity)) and maybe return the entity instead of $this->getHydrator()->extract($entity)?
I think I've finally solved it--I added a line to the "setter" method, addUsers which manually updates the users property of the group after updating the related users. I would be a bit surprised if this was best practice, though. I had thought that updating the owning side (the users) would automatically update the inverse side (the user group). Perhaps I was wrong. If anyone else has a better idea I'll gladly give the answer credit to them.
public function addUsers(Collection $users) {
foreach ($users as $user) {
$user->addGroups(new ArrayCollection(array($this)));
// This is the new line (updating the usergroup property manually)
$this->users->add($user);
}
}
I'm currently doing this kind fo thing with symfony forms
$this->myForm = new MyForm();
$this->myForm->customConfigureMethod($this->getUser()->getGuardUser());
because I need to configure a DoctriineChoice widget on the basis of the user.
I would rather do this kind of thing
$this->myForm =new myCustomConfiguredForm($this->getUser()->getGuardUser());
With the customisation being part of the form instantiation.
Anyone know how I could achieve this? I think I might be a bit unclear about the difference between the configure() and setup() functions for the forms so can't think clearly about it.
You shpuld pass the user object as an option. Here is an exapmle:
class ProductForm extends BaseProductForm
{
public function configure()
{
// or use an instance variable if you need the user in an another method too
$user = $this->getOption('user');
if (!$user instanceof sfBasicSecurityUser)
{
throw new InvalidArgumentException('A user object is required as "user" option in ' . __METHOD__);
}
// do something with the user...
}
}
$form = new ProductForm(array(), array('user' => $this->getUser()));
Is it possible to call repository method on entity?
I mean something like this
$article = $em->getRepository('Entities\Articles')->findOneBy(array('id' => $articleId));
$category = $em->getRepository('Entities\Categories')->findOneBy(array('id' => 86));
$article->addArticleToCategory($category);
Where addArticleToCategory is method in repository (just an example code)
public function addArticleToCategory($category){
$categoryArticles = new CategoryArticles();
$categoryArticles->setArticle(!!/** This is where I want to have my variable $article from this method call **/!!);
$categoryArticles->setCategory($category);
$this->getEntityManager()->persist($categoryArticles);
$this->getEntityManager()->flush();
}
What is the best way to do it?
Also I want to know is it a good practice to put custom set/create methods in repository?
By definition you can't call a method of your repository class from an entity object... This is basic object-oriented programming.
I think you should create addArticle function in the Category entity, something like this:
function addArticle($article)
{
$this->articles[] = $article;
$article->setCategory($this);
}
And then you do
$article = $em->getRepository('Entities\Articles')->findOneBy(array('id' => $articleId));
$category = $em->getRepository('Entities\Categories')->findOneBy(array('id' => 86));
$category->addArticle($article);
$em->persist($category);
$em->flush();
If the cascades are correctly configured, this will work
You can write your own repository manager and create a method for your needs.
http://docs.doctrine-project.org/en/2.0.x/reference/working-with-objects.html#custom-repositories