How to toggle laravel trashed filter off temporarily? - laravel

Say I've got an A, which has a B that has a C that has a D. I want to go from A to D, but any one (or all) of the objects might have been deleted. So I have to do this:
$d = $a->b()->withTrashed()->first()->c()->withTrashed()->first()->d()->withTrashed()->first()
Which is horrible. I would really rather do this:
turnOffTrashedFilter();
$d = $a->b->c->d;
Does laravel have such an ability?
Note that this is just an example - the situation that prompted this question is actually a lot more complicated, with various calls nested in other calls such that it's not practically possible to use withTrashed as above. I need to turn off the filter for the duration of the request, without having to modify huge swathes of code to incorporate two parallel paths.

No built-in, but it can be done
There is no built in way to disable the automatic soft delete filtering. However, it is possible. The soft delete filter is a global scope, added to the boot method of the class. It can be removed like so:
\Event::listen('eloquent.booted:*', function($name) {
$name = substr($name, 17); // event name is "eloquent.booted: some/class"
$class = new \ReflectionClass($name);
$prop = $class->getProperty('globalScopes');
$prop->setAccessible(true);
$scopes = $prop->getValue();
foreach ($scopes as $c => &$s) {
unset($s['Illuminate\Database\Eloquent\SoftDeletingScope']);
}
$prop->setValue($scopes);
});
This hooks into the booted event, which is fired immediately after the global scope gets added to the class. It then opens the (private static) attribute globalScopes, which is a list of the attached global scopes, and removes the soft deleting one. This will prevent the softdelete scope from being attached to any models, provided their static boot method is called after the event listener is added.

Instead of this you can use withTrashed() in your relations:
public function aTrashed()
{
return $this->hasOne(A::class)->withTrashed();
}
public function bTrashed()
{
return $this->hasMany(B::class)->withTrashed();
}
public function cTrashed()
{
return $this->belongsToMany(C:class)->withTrashed();
}
// Then use it
$d = $z->aTrashed->bTrashed->cTrashed;

Related

Invalidate Shopware 6 page cache on entity update via API

We created a custom CMS element which displays entries which are managed via API.
Now when entries are updated and Shopware 6 runs ins production mode, the changes are not reflected on the page. I believe this is due to the page cache. (APP_ENV=prod)
What do we have to do to invalidate the cache automatically?
I checked the docs, but did not find the necessary information.
For the product box it works: When I place a product CMS element on the main page and change the product, the page is updated when I reload in the browser.
I was expecting to find some hint in \Shopware\Core\Content\Product\Cms\ProductBoxCmsElementResolver but there are no cache tags or something like this added there.
EDIT: Actually I was a bit inaccurate. The page I have lists the entities, and it is a category page.
So I believe I have too hook into CategoryRouteCacheTagsEvent.
For testing I hard-coded into:
\Shopware\Core\Content\Category\SalesChannel\CachedCategoryRoute::extractProductIds
$slots = $page->getElementsOfType('jobs');
/** #var CmsSlotEntity $slot */
foreach ($slots as $slot) {
$box = $slot->getData();
$ids = array_merge($ids, $box['jobs']->getIds());
}
But this does not yet work.
PS: Also I noticed some code duplication in the core in \Shopware\Core\Content\Category\SalesChannel\CachedCategoryRoute::extractProductIds and \Shopware\Core\Content\LandingPage\SalesChannel\CachedLandingPageRoute::extractIds
The Shopware\Core\Framework\Adapter\Cache\CacheInvalidationSubscriber listens to a lot of events, including indexer and entity-written events. This in turn uses the CacheInvalidator to invalidate cached data based on tags/cache keys.
You should be able to add invalidation based on your own entity in a similar fashion.
For this to work with a custom entity, you will probably have to tag any cache entries with something you can generate on invalidation. For CMS pages, I would probably start with the CachedLandingPageRoute as a reference.
I suggest you should have a look at the CacheInvalidationSubscriber and its service definition. You can see that there are already a bunch of events that are dispatched when write operations to certain entities occur. When you then look at the respective handler you can see how it invalidates the cache for whatever kind of routes it should affect.
When you speak of entries I assume you implemented your own custom entities for use in your CMS element? If that is the case just replicate the listener for your own entities. Otherwise you'll have to look for another event that is dispatched once you save your changes and then invalidate the cache likewise.
Based on the answers of dneustadt and Uwe, as for the job listings I solved it like with this two subscribes. I do not need any single ID here, because the full listing page has to be invalidated in case a job is deleted or added. This is why I went with the any-jobs tag:
use Shopware\Core\Content\Category\Event\CategoryRouteCacheTagsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CacheKeyEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
CategoryRouteCacheTagsEvent::class => 'generateTags'
];
}
public function generateTags(CategoryRouteCacheTagsEvent $event): void
{
$page = $event->getResponse()->getCategory()->getCmsPage();
$slots = $page->getElementsOfType('jobs');
if (!empty($slots)) {
$event->setTags(
array_merge($event->getTags(), ['any-jobs'])
);
}
}
}
and
class CacheInvalidationSubscriber implements EventSubscriberInterface
{
private CacheInvalidator $cacheInvalidator;
public static function getSubscribedEvents(): array
{
return [
EntityWrittenContainerEvent::class => 'invalidateJobs'
];
}
public function __construct(CacheInvalidator $cacheInvalidator)
{
$this->cacheInvalidator = $cacheInvalidator;
}
public function invalidateJobs(EntityWrittenContainerEvent $event): void
{
if (!empty($event->getPrimaryKeys(\ApplicationManagement\Core\Content\JobAd\JobAdDefinition::ENTITY_NAME))) {
$this->cacheInvalidator->invalidate(
['any-jobs']
);
}
}
}

beforeFind() in Yii ActiveRecord and cache

In some model classes I want to implement cache. And I wanted to do that like:
UsersModel::model()->findByAttributes([...])
In that class I wanted to override method beforeFind() to send request first to cache server, but it seems that method does not take any additional parameters, nor does have object with attributes.
Putting additional conditions/checks in top level code something like :
$response = Yii::app()->cache->get('userUserLogin');
if(empty($response) == true) {
//fetch data from db and set to cache
$userModel = UsersModel::model->findByAttributes([...])
Yii::app()->cache->set('user' . $userModel->username, $userModel->getAttributes());
}
is not nice and trivial, leading to many branches.
You should not use beforeFind() for that. Besides technical problems in implementation, you may get many side effects and hard to debug bugs because of that. That is because cache may be out of date and many internal Yii logic may rely on assumption, that findByAttributes() (and other methods) always fetches fresh data from database. You will also not be able to ignore cache and get model directly from database.
In general you have 2 options:
1. Use CActiveRecord::cache()
$model = UsersModel::model()->cache(60)->findByAttributes([...])
This will query cache results for 60 seconds.
2. Custom helpers
You may add custom methods, which will simplify using cached active records:
public static function findByAttributesFromCache($attributes = []) {
$result = Yii::app()->cache->get(json_encode($attributes));
if ($result === false) {
//fetch data from db and set to cache
$result = static::model()->findByAttributes($attributes);
Yii::app()->cache->set(json_encode($attributes), $result, 60);
}
return $result;
}
You can add such method to trait and reuse it in multiple models. Then all you need is:
$userModel = UsersModel::findByAttributesFromCache([...]);

Different state for Eloquent model fields depending on current user in laravel

I have the model:
class Task extends Model {
}
with some fields
protected $fillable = ['message', 'due_time', 'status', 'etc...'];
I've added custom function:
public function getEditableStateFor{AttributeName}
In my helper function I check that if
method_exists($class, 'getEditableStateForField1')
than I allow to edit this field depending on boolean value returned from this function.
Example:
if( ! $class->getEditableStateForField1() ) {
return "You can not edit field field1";
}
Here is how looks like some functions in Task:
private function isCreator() {
$user = Auth::user();
if($user) {
return $user->id === $this->creator_id;
}
return false;
}
public function getEditableStateForMessage() {
return $this->isCreator();
}
public function getEditableStateForDueTime() {
return $this->isCreator();
}
Is this a good way to do it or it is very bad design because of hidden dependency on Auth::user()?
What is a better way?
I do not want to put this logic inside controllers because this logic propagates to another models and is universal across application.
I'm like you and like to have Models that contain as much of the business logic as possible while remaining totally free of depencies on the "web" part of the application, which I believe should stay in Controllers, Request objects, etc. Ideally, Models should be easily usable from command line interfaces to the application, from within the Tinker REPL, and elsewhere while still guaranteeing data integrity and that business rules are observed.
That said, it seems the Laravel creators had slightly different ideas, hence the Auth facade being easily available in the model.
What I would likely do is add a parameter of type User to the getEditableStateFor series functions, and then in turn pass that parameter to isCreator ($user) and elsewhere. That also frees you up to be able to allow associated users to edit each other's Tasks if that ever became a desired feature in the future.
Edit: another, perhaps better or perhaps worse, is to have an instance method like setCurrentUser ($user) then use setFieldNameAttribute methods so that the controller doesn't have to check the editability of fields, keeping that the model's responsibility. Then you could call the getEditableStateFor methods, which now check for the current user set by the above method (maybe falling back to Auth::user() or throwing a helpful error), inside the setter.

How to override save() method in a component

Where and how I am overriding the save method in Joomla 3.0 custom component ?
Current situation:
Custom administrator component.
I have a list view that displays all people stored in table.
Clicking on one entry I get to the detailed view where a form is loaded and it's fields can be edited.
On save, the values are stored in the database. This all works fine.However, ....
When hitting save I wish to modify a field before storing it into the database. How do I override the save function and where? I have been searching this forum and googled quiet a bit to find ways to implement this. Anyone who give me a simple example or point me into the right direction ?
Thanks.
Just adding this for anyone who wants to know the answer to the question itself - this works if you explicitly wish to override the save function. However, look at the actual solution of how to manipulate values!
You override it in the controller, like this:
/**
* save a record (and redirect to main page)
* #return void
*/
function save()
{
$model = $this->getModel('hello');
if ($model->store()) {
$msg = JText::_( 'Greeting Saved!' );
} else {
$msg = JText::_( 'Error Saving Greeting' );
}
// Check the table in so it can be edited.... we are done with it anyway
$link = 'index.php?option=com_hello';
$this->setRedirect($link, $msg);
}
More details here: Joomla Docs - Adding Backend Actions
The prepareTable in the model (as mentioned above) is intended for that (prepare and sanitise the table prior to saving). In case you want to us the ID, though, you should consider using the postSaveHook in the controller:
protected function postSaveHook($model, $validData) {
$item = $model->getItem();
$itemid = $item->get('id');
}
The postSaveHook is called after save is done, thus allowing for newly inserted ID's to be used.
You can use the prepareTable function in the model file (administrator/components/yourComponent/models/yourComponent.php)
protected function prepareTable($table)
{
$table->fieldname = newvalue;
}

Codeigniter models loaded in controller overwritten by models loaded in models

I'm having Codeigniter object scope confusion.
Say I load a model in a controller:
$this->load->model('A');
$this->A->loadUser(123); // loads user with ID 123
// output of $this->A now shows user 123
$this->load->model('B');
$this->B->examineUser ();
// output of $this->A now shows user 345
class B extends Model
{
public function examineUser ()
{
$this->load->model('A');
$this->A->loadUser(345); // loads user with ID 345
}
}
I would have thought that $this->A would be different from $this->B->A but they are not. What is the best solution to this issue? It appears the ->load->model('A') in the examineUser () method does nothing because it was loaded in the controller. Then the call to loadUser () inside that method overwrites the stored properties of $this->A. This seems like a bugfest waiting to happen. If I needed global models, I would have use static classes. What I wanted was something scoped pretty much locally to the model object I was in.
Is there a way I can accomplish this but not go way outside of CI's normal way of operating?
Followup/related:
Where do most people put there "->load->model" calls? All at the beginning of a controller action? I figured it would be easier -- though perhaps not excellent programming from a dependency injection perspective -- to load them in the model itself (construct or each method).
Whenever you use the Loader Class ($this->load->), it will load the object into the main CI object. The CI object is the one you keep referring to as $this->. What you've done is load model A twice into the CI object.
Essentially, all object loaded using the Loader class goes into a single global scope. If you need two of the same type, give them different names, as per $this->load->model('A','C'). I don't know of any way around it unless you revert to using bog-standard PHP.
In my team's code, we generally load the models in the controller's constructor, then load the data to send to the view in the function, often _remap().
This is not how the loader works sadly. CodeIgniter implements a singleton pattern, which will check to see if the class is included, instantiated and set to $this->A then will be ignored if loaded again. Even if you are inside a model, $this->A will be referenced to the super-instance via the __get() in class Model. Alis it, or just do:
class B extends Model
{
public function examineUser ()
{
$user = new A;
$user->loadUser(345); // loads user with ID 345
}
}
Here's what I've decided to do, please comment if you have advice:
I've extended the CI Loader class:
<?php
class SSR_Loader extends CI_Loader
{
function __construct()
{
parent::__construct ();
}
/**
* Model Retriever
*
* Written by handerson#executiveboard.com to create and return a model instead of putting it into global $this
*
* Based on original 2.0.2 CI_Loader::model ()
*
*/
function get_model($model)
{
if (empty ($model))
{
return;
}
$name = basename ($model);
if (!in_array($name, $this->_ci_models, TRUE))
{
$this->model ($model);
}
$name = ucfirst($name);
return new $name ();
}
}
Do any CI guru's see a problem with that before I invest time in changing my code a bit to accept the return obj, ala:
// in a controller:
public function test ($user_id=null)
{
$this->_logged_in_user = $this->load->get_model ('/db/users');
$this->_viewed_user = $this->load->get_model ('/db/users');
$this->_logged_in_user->load($this->session->userdata ('user.id'));
$this->_viewed_user->load($user_id);
}
I could also do private $_logged_in_user to make it available in the controller but positively force it to be limited to just the current controller and not spill anywhere else, or I could just do $_logged_in_user = $this->load->get_model ('/db/users'); and limit it to just the current method, which is probably what I'll do more often.
This seems like a pretty straightforward way to "fix" this issue (I say "fix" b/c it's not really a bug, just a way of doing things that I think is a bad idea). Anyone see any flaws?

Resources