imagine I have some doctrine Entity and I can have some records of this entity in the database which I dont want to be deleted, but I want them to be visible.
In general I can have entities, for which I have default records, which must stay there - must not be deleted, but must be visible.
Or for example, I want to have special User account only for CRON operations. I want this account to be visible in list of users, but it must not be deleted - obviously.
I was searching and best what I get was SoftDeletable https://github.com/Atlantic18/DoctrineExtensions/blob/v2.4.x/doc/softdeleteable.md It prevents fyzical/real deletion from DB, but also makes it unvisible on the Front of the app. It is good approach - make a column in the Entity's respective table column - 1/0 flag - which will mark what can not be deleted. I would also like it this way because it can be used as a Trait in multiple Entities. I think this would be good candidate for another extension in the above Atlantic18/DoctrineExtensions extension. If you think this is good idea (Doctrine filter) what is the best steps to do it?
The question is, is this the only way? Do you have a better solution? What is common way to solve this?
EDIT:
1. So, we know, that we need additional column in a database - it is easy to make a trait for it to make it reusable
But
2. To not have any additional code in each repository, how to accomplish the logic of "if column is tru, prevent delete" with help of Annotation? Like it is in SoftDeletable example above.
Thank you in advance.
You could do this down at the database level. Just create a table called for example protected_users with foreign key to users and set the key to ON DELETE RESTRICT. Create a record in this table for every user you don't want to delete. That way any attempt to delete the record will fail both in Doctrine as well as on db level (on any manual intervention in db). No edit to users entity itself is needed and it's protected even without Doctrine. Of course, you can make an entity for that protected_users table.
You can also create a method on User entity like isProtected() which will just check if related ProtectedUser entity exists.
You should have a look at the doctrine events with Symfony:
Step1: I create a ProtectedInterface interface with one method:
public function isDeletable(): boolean
Step2: I create a ProtectionTrait trait which create a new property. This isDeletable property is annotated with #ORM/Column. The trait implements the isDeletable(). It only is a getter.
If my entity could have some undeletable data, I update the class. My class will now implement my DeleteProtectedInterface and use my ProtectionTrait.
Step3: I create an exception which will be thrown each time someone try to delete an undeletable entity.
Step4: Here is the tips: I create a listener like the softdeletable. In this listener, I add a condition test when my entity implements the ProtectedInterface, I call the getter isDeleteable():
final class ProtectedDeletableSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $onFlushEventArgs): void
{
$entityManager = $onFlushEventArgs->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof ProtectedInterface && !$entity->isDeletable()) {
throw new EntityNotDeletableException();
}
}
}
}
I think that this code could be optimized, because it is called each time I delete an entity. On my application, users don't delete a lot of data. If you use the SoftDeletable component, you should replace it by a mix between this one and the original one to avoid a lot of test. As example, you could do this:
final class ProtectedSoftDeletableSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $onFlushEventArgs): void
{
$entityManager = $onFlushEventArgs->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof ProtectedInterface && !$entity->isDeletable()) {
throw new EntityNotDeletableException();
}
if (!$entity instance SoftDeletableInterface) {
return
}
//paste the code of the softdeletable subscriber
}
}
}
Well the best way to achieve this is to have one more column in the database for example boolean canBeDeleted and set it to true if the record must not be deleted. Then in the delete method in your repository you can check if the record that is passed to be deleted can be deleted and throw exception or handle the situation by other way. You can add this field to a trait and add it to any entity with just one line.
Soft delete is when you want to mark a record as deleted but you want it to stay in the database.
Related
When I want to save 1 record to the database, everything works correctly.
When I want to save 2 records right after each other - an error occurs:
[Doctrine\ORM\ORMInvalidArgumentException]
A new entity was found through the relationship 'App\Entity\User#directory' that was not configured to cascade persist
operations for entity:. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity
or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you
cannot find out which entity causes the problem implement 'Main\Entity\Directory#__toString()' to get a clue.
I just started learning symfony and can't figure out what the problem is.
In my case: I create a new user who takes certain information from a directory that is already in my database.
For example, my new user is a cat lover, then the entry "loves cats" should pull up from the directory.
The text of the error says that I have to set up a connection and save a new record about the love of cats. But my reference book is already full, it does not need to be supplemented, just take information from there.
When I create one user, everything is fine. When I create two users who are supposed to take information from the directory and save the data, an error occurs.
Help me please.
I tried in different places to use method: $this->em->clear(). But it did not help.
class User:
ManyToOne(targetEntity="App\Entity\Directory")
JoinColumn(nullable=true)
private $directory;
public function createData($data) {
$this->setData($data);
$this->em->flush();
}
public function setData($data) {
$user = new User();
$directory = $this->em->getRepository(Directory::class)->findOneBy(['id' => $data['id']]);
$user->setDirectory($directory);
return $this;
}
You need to configure cascade operations for Directory entity which relates to user. Or you can do $em->persist($directory) before flushing
I need to create a custom FE user with some custom fields.
Also, it needs to be assignable through the frontend to different user groups.
You can find my first approach here. Didn't work out that well.
Second approach was to create another extension and follow the guide which is shown here.
First thing I did was to add \TYPO3\CMS\Extbase\Domain\Model\FrontendUser into the Extend existing model class-field for my CustomFEU-model.
Then I created another model which I named FEgroup and I mapped it to the table fe_groups. After that, I connected an n:m relation to the CustomFEU.
When I try to create a new CustomFEU with the new action, it returns a white empty page after submitting the form and no user is being added.
The only strange thing I found was that the /Classes/Domain/Repository/ folder is empty.
TYPO3 7.6.8
Although I didn't edit the files yet, here they are:
Model / Controller / Setup
Did anyone encounter similar problems?
First you need to create the repositories that handle the new user and usergroup models.
Second you try to save the user with $this->customFEURepository->add($newCustomFEU); and the variable customFEURepository does not exist. It would be the best to inject it, it has to be the repository that you should create first. You can inject it like that:
/**
* CustomFEUController
*/
class CustomFEUController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
/**
* #var \Vendor\Feregistration\Repository\CustomFEURepository
* #inject
*/
protected $customFEURepository;
// other code ...
}
Don't forget to clear the system cache after adding inject annotations, otherwise it wont work.
Last but not least i can't see the mapping to the database table for your model. You need to add it to your TypoScript (setup.txt)
config.tx_extbase.persistence.classes {
Vendor\Feregistration\Domain\Model\CustomFEU {
mapping {
recordType = 0
tableName = fe_users
}
}
Vendor\Feregistration\Domain\Model\FEGroups {
mapping {
recordType = 0
tableName = fe_groups
}
}
}
Let's assume there is a OneToMany doctrine2 association between blogposts and comments. A blogposts might have many comments. Every comment remains inactive and therefore hidden in the frontend until a moderator will activate the comment manually.
I'm now trying to have some kind of security facade to ensure that only "active" comments will be provided to the view by accessing them in a loop over the {{blogpost.comments}} variable in the twig template.
Trying to use the getComments() method in the blogpost entity I was trying to filter the ArrayCollection of comments like so
/**
* #return ArrayCollection
*/
public function getComments()
{
return $this->comments->filter(function ($condition) {
return $condition->getActive() === true;
});
}
unfortunately Doctrine will entirely load every single comment even if the relations fetch mode is set to "EXTRA_LAZY". So that would influence the performance of an application in a way i'd like to avoid.
Is there any way to hide the inactive comments globally or do I have to take care of filtering them every time I'm accessing the blogpost.comments relations in the view?
You should use the matching method of your collection. If your collection is not loaded, it will add filters to the SQL query to only load what you need. If your collection is already loaded, it will filter the PHP array.
use Doctrine\Common\Collections\Criteria;
public function getComments()
{
return $this->comments->matching(
Criteria::create()->where(
Criteria::expr()->eq('active', true)
)
);
}
More informations here: http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html#filtering-collections
Regards
We are trying to detect the changes in Laravel related models at attribute level, as we have to keep audit trail of all the changes which are made via the application.
We can track the changes via isDirty method on the Eloquent model for single model that is not related to any other model, but there is no way that we can track the changes on the related eloquent models. isDirty doesn't work on related models attributes. Can some one please help us on this?
Update to original question:
Actually we are trying to track changes on the pivot table that has extra attributes as well defined on it. IsDirty method doesn't work on those extra attributes which are defined in the pivot table.
Thanks
As much I understand your question, It's can achieve through Model Event and some sort of extra code with current and relation model.
Laravel Model Events
If you dont want to use any additional stuff, you can just use the Laravel Model Events (that in fact Ardent is wrapping in the hooks). Look into the docs http://laravel.com/docs/5.1/eloquent#events
Eloquent models fire several events, allowing you to hook into various
points in the model's lifecycle using the following methods: creating,
created, updating, updated, saving, saved, deleting, deleted,
restoring, restored.
Whenever a new item is saved for the first time, the creating and
created events will fire. If an item is not new and the save method is
called, the updating / updated events will fire. In both cases, the
saving / saved events will fire.
If false is returned from the creating, updating, saving, or deleting
events, the action will be cancelled:
Finally, reffering to your question you can utilize the above approaches in numerous ways but most obviously you can combine it (or not) with the Eloquent Models' getDirty() api docs here method and getRelation() api docs here method
It will work for example with the saving event.
Model::saving(function($model){
foreach($model->getDirty() as $attribute => $value){
$original= $model->getOriginal($attribute);
echo "Changed";
}
$relations = $model->getRelations();
foreach($relations as $relation){
$relation_model = getRelation($relation);
foreach($relation_model->getDirty() as $attribute => $value){
$original= $relation_model->getOriginal($attribute);
echo "Relation Changed";
}
}
return true; //if false the model wont save!
});
Another Thought might help you. when you saving
save() will check if something in the model has changed. If it hasn't it won't run a db query.
Here's the relevant part of code in Illuminate\Database\Eloquent\Model#performUpdate:
protected function performUpdate(Builder $query, array $options = [])
{
$dirty = $this->getDirty();
if (count($dirty) > 0)
{
// runs update query
}
return true;
}
The getDirty() method simply compares the current attributes with a copy saved in original when the model is created. This is done in the syncOriginal() method:
public function __construct(array $attributes = array())
{
$this->bootIfNotBooted();
$this->syncOriginal();
$this->fill($attributes);
}
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
check model is dirty isDirty():
if($user->isDirty()){
// changes have been made
}
Or check certain attribute:
if($user->isDirty('price')){
// price has changed
}
I did not check this code but hopeful to use as your answer by thoughts, if you have any confusion to deal such requirement or something need to optimize or change please let me know.
Using Symfony 1.4 and doctrine I'd like to save a retrieved model to a different database connection:
retrieve model from master-database
change database connection to slave-database
save the model to the slave-database
I have the 2 connections defined in databases.yml.
here in pseudo-code:
$model = [retrieved from master-database];
$slaveConnection = Doctrine_Manager::getInstance()
->getConnection('slave-connection');
$model->save($slaveConnection);
If I create a new model, $model=new model(); the "code" above successfully saves the model to the slave-connection.
What is going wrong?
According to the Symfony log, Symfony recognizes the model as existing and issues an update (instead of an insert).
UPDATE model SET updated_at = '2011-10-21 17:37:32' WHERE id = '1';
Although Symfony is using the correct database connection ('slave-connection'), the update fails because the model isn't present in the slave-database, yet.
And the insert into the slave-database should use all values of the model, not only the changed ones, too.
Anyone can point me to the right direction to save an existing model to a different database?
edit with my solution.
Thanks samura!
Just some additions:
After performing deep copy Symfony saved a new id. But I wanted to really clone the model object to the slave db and so, I had to modify the id.
That caused unique constraint exceptions, so I had to delete first. So this is it:
$id = $model->getId();
$slaveConnection->execute("delete from modeltable where id=".$id);
$model_copy = $model->copy(true); # deep copy
$model_copy->setId($id);
$model_copy->save($slaveConnection);
hope this helps if someone else stumbles.
You could use the public function copy($deep = false) method of the Doctrine_Record class.
$model = [retrieved from master-database];
$slaveConnection = Doctrine_Manager::getInstance()
->getConnection('slave-connection');
$model_copy = $model->copy(true); # deep copy
$model_copy->save($slaveConnection);