Eloquent - How to wait until after create() without Observer - php

The Error
Call to a member function media() on null
The Scenario
I have several related models. I have an "Event" model. Attached to this (via 1 to 1) is a Gallery model. Attached to that (via one to many) is a "Media" model. In the "created" observer for the Event, I'm trying to attach its gallery. I can create the gallery no problem, but when I then try and attach media to it I get the above error.
My code
if ($model->gallery === null) {
$this->createGallery($model);
}
// product images or banner images
$file = Storage::put("public/event_images/", $image);
$file = str_replace("public/event_images/", "", $file);
$file = "/" . $file;
$model->gallery->media()->create([
"path" => $file,
"type" => "image"
]);
// The createGallery() function
private function createGallery($model)
{
$model->gallery()->create();
}
So I know to fix this, I have to "wait" until the gallery has been created before I try to access its relationships. But I can't figure out how to do this. This code works the second time it's run, suggesting that the gallery is indeed created - just not quickly enough before the code hits media().

PHP is a synchronous programming language, so it is impossible that you have to wait for something to complete.
The problem is that you loaded the relation already and this loaded relation won't re-validate until you load it again. which can be done using the load() function.
Change your code to create the gallery like this:
if ($model->gallery === null) {
// Create the related model
$this->createGallery($model);
// Load the relation again
$model->load('gallery');
}

I think you might need to refresh the model before trying to access its relationship. Try $model->refresh() before you attempt to attach the image to the gallery. Something like
if ($model->gallery === null) {
$this->createGallery($model);
$model->refresh();
}
The model won't be aware of the newly created gallery otherwise.

Related

Properly cache a type-hinted model in Laravel

I'm using Redis to cache different parts of my app. My goal is to not make a database query when the user is not logged in, as the app's content don't get updated regularly.
I cache the archive queries in my controller, however when I type hint a model in the controller, the model is retrieved from the database and then passed to the controller:
// My route
Route::get('page/{page:id}', [ PageController::class, 'show' ] );
// My controller
public function show ( Page $page ) {
// Here, the $page will be the actual page model.
// It's already been queried from the database.
}
What I'm trying to do is to try and resolve the page from the cache first, and then if the cache does not contain this item, query the database. If I drop the Page type-hint, I get the desired result ( only the id is passed to controller ) but then I will lose the benefit of IoC, automatic ModelNotFoundException, and more.
I've come across ideas such as binding the page model to a callback and then parsing the request(), but seems like a bad idea.
Is there any way to properly achieve this? I noticed that Laravel eloquent does not have a fetching event, which would be perfect for this purpose.
You can override the default model binding logic:
Models\Page.php
public function resolveRouteBinding($value, $field = null)
{
return \Cache::get(...) ?? $this->findOrFail($value);
}
Read more here https://laravel.com/docs/8.x/routing#customizing-the-resolution-logic
In order to check for existence of the data in Redis, you shouldn't type-hint the model into the controller's action. Do it like this:
public function show($pageId) {
if(/* check if cached */) {
// Read page from cache
} else {
Page::where('id', $pageId)->first();
}
}

Need More Clarification on Events YII2

Trying to learn events in Yii 2. I found a few resources. The link I got more attention is here.
How to use events in yii2?
In the first comment itself he explains with an example. Say for an instance we have 10 things to do after registration - events comes handy in that situation.
Calling that function is a big deal? The same thing is happening inside the model init method:
$this->on(self::EVENT_NEW_USER, [$this, 'sendMail']);
$this->on(self::EVENT_NEW_USER, [$this, 'notification']);
My question is what is the point of using events? How should I get full benefit of using them. Please note this question is purely a part of learning Yii 2. Please explain with an example. Thanks in advance.
I use triggering events for written (by default) events like before validation or before deletion. Here's an example why such things are good.
Imagine that you have some users. And some users (administrators, for example) can edit other users. But you want to make sure that specific rules are being followed (let's take this: Only main administrator can create new users and main administrator cannot be deleted). Then what you can do is use these written default events.
In User model (assuming User models holds all users) you can write init() and all additional methods you have defined in init():
public function init()
{
$this->on(self::EVENT_BEFORE_DELETE, [$this, 'deletionProcess']);
$this->on(self::EVENT_BEFORE_INSERT, [$this, 'insertionProcess']);
parent::init();
}
public function deletionProcess()
{
// Operations that are handled before deleting user, for example:
if ($this->id == 1) {
throw new HttpException('You cannot delete main administrator!');
}
}
public function insertionProcess()
{
// Operations that are handled before inserting new row, for example:
if (Yii::$app->user->identity->id != 1) {
throw new HttpException('Only the main administrator can create new users!');
}
}
Constants like self::EVENT_BEFORE_DELETE are already defined and, as the name suggests, this one is triggered before deleting a row.
Now in any controller we can write an example that triggers both events:
public function actionIndex()
{
$model = new User();
$model->scenario = User::SCENARIO_INSERT;
$model->name = "Paul";
$model->save(); // `EVENT_BEFORE_INSERT` will be triggered
$model2 = User::findOne(2);
$model2->delete(); // `EVENT_BEFORE_DELETE` will be trigerred
// Something else
}

Doctrine 2 ManyToMany and preUpdate

I'm using Symfony2.5 and Doctrine 2.4.2.
I have two entities, PageImages which extends Page
Page:
/**
* #ORM\ManyToMany(targetEntity="PageImage",cascade={"persist"})
*/
private $images;
Using form collection to create add images to the Page entity
When i create a new entry for PageImage it works just fine, PrePersist,PreUpdate,PostPersist,PostUpdate all run,
But when I try to change the image for an existing PageImage the event doesn't fire and doesn't upload new images.
Update:
It doesn’t updating because the $file field is just a virtual and doctrine doesn’t see new data, and events aren't fired, so the easiest solution to do:
public function setFile(UploadedFile $file = null)
{
$this->temp = $this->getPath();
$this->file = $file;
$this->preUpload();
}
I found a solution, maybe its not very beautiful and someone can suggest any other way to solve the problem.
I've figured out, what’s the problem, because the $file is virtual field doctrine thinks there’s no new data in the entity and no need to persist or update it and the events doesn’t fired so I need to run it myself ot setFile():
public function setFile(UploadedFile $file = null)
{
$this->temp = $this->getPath();
$this->file = $file;
$this->preUpload();
}
In your Edit action you will have to manually exploit the 'upload' method to current image like this
$currentImage->upload();
$entityManager->persist($currentImage);
$entityManager->flush();

Symfony 1.4 save all items' froms with one action

I want to create thumbnails for 200+ objects with one action in Symfony 1.4. The problem is that thmbnail generation takes place on saving the form.
class AuthorForm extends BaseAuthorForm
{
public function configure()
{
/* some configs */
}
public function save($con = null)
{
/* create thmbnail from original picture */
}
}
How can I write an (batch) action to be able to save them all at once, rather than going to each item in the backend and saving?
Please note, that just $author->save(); won't work, of course.
Thanks!
You have to fetch the objects, loop through them, create the form and save. Like the following.
$authors = Doctrine_Core::getTable('Author')->findAll();
foreach($authors as $author){
$form = new AuthorForm($author);
$form->save();
}
You'll probably have memory issues if you're running it on a hosting plan (not your dev machine). A better way to get thumbnails is using a plugin like sfImageTransformExtraPlugin (http://www.symfony-project.org/plugins/sfImageTransformExtraPlugin) that generates a cached thumbnail as you need them. You don't even need to go through the trouble of generating the thumbnails. And still can have multiple thumbnail versions of the same photo pretty easily.
If you still need to use this way, do some unset stuff during the loop, like the following.
$authors = Doctrine_Core::getTable('Author')->findAll();
foreach($authors as $author){
$form = new AuthorForm($author);
$form->save();
unset($form, $author);
}

Saving a embed sfForm

I'm making a form which consist in some text and uploading files. The problem is that the the file is not been saved in the db (blob field) because the client didn't want to so I made a UploadFileForm to make it clean and do the logic of uploading and saving into the File Table (which has the path of that file). So i have:
//HistoryForm
class HistoryForm extends BaseHistoryForm
{
public function configure()
{
unset($this['text']);
$this->setWidget('text', new sfWidgetFormTextareaTinyMCE(array(
'width' => 550,
'height' => 350,
'config' => 'theme_advanced_disable: "anchor,image,cleanup,help"',)));
$this->setValidator('text', new sfValidatorString());
$this->embedForm('uploadfile1', new UploadFileForm());
}
}
//UploadFileForm
class UploadFileForm extends sfForm{
public function configure() {
$this->setWidget('file', new sfWidgetFormInputFile());
$this->setValidator('file', new sfValidatorFile(array(
'required' => false,
'path' => sfConfig::get('sf_upload_dir')
)));
$this->setWidget('name', new sfWidgetFormInputText());
$this->setValidator('name', new sfValidatorString(array('required' => true)));
$this->widgetSchema->setNameFormat('uploadfile[%s]');
}
public function save({data_to_be_saved}){
//logic of saving
}
}
The main problem is that embeding a doctrine form works perfectly, but if I want to save a non doctrine form the save method is never called (seems fine because not all sfForms have to be saved) but if I override the saveEmbeddedForms the embed sfForm isn't binded! Reading the symfony's code found out that when embeding a form what really does is appending the fields to the main widgetSchema so using the embed form is practically usless... So, what I do is making a save method in the sfForm which does the saving by getting all needed variables from parameters. This is call is made in the overrided method save of the main doctrine form:
//HistoryForm
class HistoryForm extends BaseHistoryForm
{
...
public function save($con = null) {
$hc = parent::save($con);
foreach ($this->getEmbeddedForms() as $form) {
$values = $this->getValue($form->getName());
$form->save($hc, $values);
}
}
I spent all afternoon thinking and reading symfony's core code and didn't found a proper approach. So... does anyone knows (or thinks) a better approach?
The generally accepted approach to doing what I think you are trying to do is to infact not use an embedded form. Mixing doctrine form and sfForms is troublesome to say the least.
I'm assuming you're trying to associated a database record with a file?
If so, the usually way to do this is to create a filename field on the HistoryForm. make this field a sfWidgetInputFile widget. This widget will save to the file system, then overwrite the forms 'save' method to save the file's name filename field instead of the file contents.
Then add an accessor on the model or an asset helper class to get the record's associated file on the file system
Did you try this plugin? I had problems with embedding relations but after trying this out all of them were solved.

Categories