How to add an Action to Account Controller in Shopware - events

How to add a custom action to an existing Controller in Shopware?
Examples (url structure):
/account/bonus
/account/custom
/account/...
Usually it's easier and cleaner to create a new controller for that purpose, but in some cases it's necessary.

You should not replace the "account" controller.
You can define you own action for existing controller with following:
public static function getSubscribedEvents()
{
return [
'Enlight_Controller_Action_Frontend_Account_MyBonus' => 'onAccountMyBonus',
];
}
and then
public function onAccountMyBonus(\Enlight_Event_EventArgs $args)
{
$args->setProcessed(true);
.....
your code here
}

Spoiler: Replace the controller
There is no cleaner way than to replace the whole controller and extend it's functionality, so it's nearly as clean as Shopware's hooks.
Guide
Add a new Subscriber to your Plugin
class AccountSubscriber implements SubscriberInterface
{
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
'Enlight_Controller_Dispatcher_ControllerPath_Frontend_Account' => 'getAccountController'
);
}
/**
* #return string
*/
public function getAccountController()
{
return $this->getPath() . '/Controllers/Frontend/AccountExtended.php';
}
/**
* #return string
*/
public function getPath()
{
$plugin = Shopware()->Container()->get('kernel')->getPlugins()['AcmeYourPlugin'];
return $plugin->getPath();
}
}
Downsides
Unfortunately some controller have private methods which impact the logic. Like the Account Controller. So it's not always possible to simply extend the controller.
In the end, try to add a new controller with a new route.
It's easier, and cleaner.

There is a cleaner way than replacing the whole Controller.
It is also not recommended to replace a whole controller due to the lack of update compatibility.
In the worst case something like that could kill the whole website.
A while ago I created a thread in the shopware forum (german) discussing the exact same issue. I wanted to extend an existing finishAction() in the checkout Controller.
public function onPostDispatchCheckout(\Enlight_Controller_ActionEventArgs $args)
{
/** #var \Enlight_Controller_Action $controller */
$controller = $args->getSubject();
/** #var \Enlight_Controller_Request_Request $request */
$request = $controller->Request();
if ($request->getActionName() !== 'finish') {
return;
}
// do your stuff here
}
So even though it is not the exact same issue you have, the procedure is quite the same.
First off you subscribe to the controller (in my case the PostDispatchCheckout Controller) afterwards you edit the controller in your Bootstrap.php
To make sure, that it just alters a specific action you have to use the if-construction so your code gets just triggered on the wished action [in my case the finishAction()].
I hope this helps. What wonders me though is why you have to add a new action to an already existing controller. I can think of no situation where something like that is more practicable than creating a complete new custom controller.
Kind regards,
Max

Related

How to overwrite bulk clone function for laravel backpack

I am trying to overwrite a bulk clone function.
I just want to clone some values, and the rest assign a static value. For example, I just want to clone the name and description values, and the date I want to assign it the current date.
And well I don't know how to do it.
use \Backpack\CRUD\app\Http\Controllers\Operations\BulkCloneOperation { bulkClone as traitBulkClone; }
public function bulkClone($id) {
// your custom code here
$this->traitBulkClone($id);
}
TLDR: The most efficient way would probably be to overwrite the replicate() method on your model. Note that it is not a Backpack method, but an Eloquent method that BulkCloneOperation uses to duplicate a particular entry.
WHY?
Inside the BulkCloneOperation that you're using, you'll notice the route calls the bulkClone() method, that itself is just making some calls to the replicate() method on the model. That means you have two options to override this behaviour:
(Option A). Override the bulkClone() method in your CrudController. This will override the behaviour only on that particular admin operation.
(Option B). Override the replicate() method in you Model. That way, any time replicate() is called (by your admin panel or any other part of your software), the duplication is done in the way you specified.
In most cases, I think Option B is more appropriate, since it would avoid future code duplication. Here's Laravel's replicate() method at this time, just copy-pasting it into your model and modifying it to fit your needs is the best solution, if you ask me:
/**
* Clone the model into a new, non-existing instance.
*
* #param array|null $except
* #return static
*/
public function replicate(array $except = null)
{
$defaults = [
$this->getKeyName(),
$this->getCreatedAtColumn(),
$this->getUpdatedAtColumn(),
];
$attributes = Arr::except(
$this->getAttributes(), $except ? array_unique(array_merge($except, $defaults)) : $defaults
);
return tap(new static, function ($instance) use ($attributes) {
$instance->setRawAttributes($attributes);
$instance->setRelations($this->relations);
$instance->fireModelEvent('replicating', false);
});
}

Drupal 8 - Add custom cache context

I have the following situation: I want to hide or show some local Tasks (Tabs) based on a field on the current user. Therefore I have implemented a hook_menu_local_tasks_alter() in my_module/my_module.module:
function my_module_menu_local_tasks_alter(&$data, $route_name, \Drupal\Core\Cache\RefinableCacheableDependencyInterface &$cacheability) {
... some logic ...
if ($user->get('field_my_field')->getValue() === 'some value')
unset($data['tabs'][0]['unwanted_tab_0']);
unset($data['tabs'][0]['unwanted_tab_1']);
... some logic ...
}
This works fine but I need to clear the caches if the value of field_my_field changes.
So I found that I need to implement a Cache Context like this in my my_module_menu_local_tasks_alter:
$cacheability
->addCacheTags([
'user.available_regions',
]);
I have defined my Cache Context like this:
my_module/my_module.services.yml:
services:
cache_context.user.available_regions:
class: Drupal\my_module\CacheContext\AvailableRegions
arguments: ['#current_user']
tags:
- { name: cache.context }
my_module/src/CacheCotext/AvailableRegions.php:
<?php
namespace Drupal\content_sharing\CacheContext;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\CacheContextInterface;
use Drupal\Core\Session\AccountProxyInterface;
/**
* Class AvailableRegions.
*/
class AvailableRegions implements CacheContextInterface {
protected $currentUser;
/**
* Constructs a new DefaultCacheContext object.
*/
public function __construct(AccountProxyInterface $current_user) {
$this->currentUser = $current_user;
}
/**
* {#inheritdoc}
*/
public static function getLabel() {
return t('Available sub pages.');
}
/**
* {#inheritdoc}
*/
public function getContext() {
// Actual logic of context variation will lie here.
$field_published_sites = $this->get('field_published_sites')->getValue();
$sites = [];
foreach ($field_published_sites as $site) {
$sites[] = $site['target_id'];
}
return implode('|', $sites);
}
/**
* {#inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}
But every time I change the value of my field field_my_field I still need to clear the caches, so the Context is not working. Could anybody point me in the right direction how to get this solved or how to debug such kind of thigs?
Instead of providing a custom cache context, you should be able to use the default cacheability provided by core. I believe the issue is not so much the creation of the cacheable metadata, its seems that your hook_menu_local_tasks_alter is altering content that doesn't know it now relies on the user. So I believe you need 2 things:
General cache contexts that says 'this menu content now relies on the user', eg. user cache context.
Additional use of cache tag for a specific user to say: 'once this is cached, when this user entity changes, go regenerate the local tasks for this user'.
Note that HOOK_menu_local_tasks_alter provides a helper for cacheability, the third param of $cacheability. Drupal core also provides a mechanism here that allows us to say 'this piece of cache data relies on this other piece of cache data'.
Thus you should be able to do something like:
function my_module_menu_local_tasks_alter(&$data, $route_name, RefinableCacheableDependencyInterface &$cacheability) {
... some logic ...
// We are going to alter content by user.
$cacheability->addCacheableDependency($user);
// Note if you still really need your custom context, you could add it.
// Also note that any user.* contexts should already be covered above.
$cacheability->addCacheContexts(['some_custom_contexts']);
if ($user->get('field_my_field')->getValue() === 'some value')
unset($data['tabs'][0]['unwanted_tab_0']);
unset($data['tabs'][0]['unwanted_tab_1']);
... some logic ...
}

Is there any decent way to Decorate models returned from a Magento `[model]_load_after`event?

I'm trying to overwrite some methods in models, and I'm on a mission to avoid overwrites and rewrites of models for maximum compatibility with other modules.
I figured the best way would be to simply decorate models after they are loaded from Magento, however as far as I can tell because of the way the observer pattern in Magento is written it's impossible to accomplish this. ( As Magento always returns the reference to $this ), and the lack of interfaces might also cause trouble later down the road? See this partial of Mage/Core/Model/Abstract.php
/**
* Processing object after load data
*
* #return Mage_Core_Model_Abstract
*/
protected function _afterLoad()
{
Mage::dispatchEvent('model_load_after', array('object'=>$this));
Mage::dispatchEvent($this->_eventPrefix.'_load_after', $this->_getEventData());
return $this;
}
My question boils down to the title, is there a decent way of accomplishing this?, or am I simply stuck with rewrites :(?
The path I would like to take is;
On event [model]_load_after
return new Decorator($event->getObject())
Where the decorator class in my case would be something like;
public function __construct(Mage_Sales_Model_Order_Invoice $model)
{
parent::__construct($model); // sets $this->model on parent class, see below
}
// overwrite the getIncrementId method
public function getIncrementId()
{
return '12345';
}
// partial of parent class
public function __call($method, array $args)
{
return call_user_func_array(array($this->model, $method), $args);
}
And just some pseudo-code for extra clarification;
$model = Mage::getModel('sales/order_invoice')->load(1);
echo get_class($model);
Namespace_Decorator **INSTEAD OF** Mage_Sales_Model_...
echo $model->getIncrementId();
'12345' **INSTEAD OF** '1000001' ( or whatever the format might be )
Thanks for your time reading / commenting, I really hope there actually is a way to accomplish this in a clean fashion without making use of code overrides or rewrites of models.
Edit: extra clarification
Basically what I would like is to return an instance of the Decorator in a few cases, the sales_invoice being one of them and customer the other. So when any load() call is made on these models, it will always return the instance of the Decorator instead of the Model. Only method calls that the decorator overrides would be returned, and any other method calls would "proxied" through __call to the decorated object.
I'm not sure if I got your question right but here goes.
I think you can use the event [model]_load_after and simply do this:
$object = $event->getObject();
$object->setIncrementId('12345');
Or if you want to use a decorator class make it look like this:
public function __construct(Mage_Sales_Model_Order_Invoice $model)
{
parent::__construct($model);
$model->setIncrementId($this->getIncrementId());
}
public function getIncrementId()
{
return '12345';
}
I know that this is not exactly a decorator pattern but it should work.
I know that when adding a new method to the 'decorator' class you need to add it to attach data to the main model.
This is just my idea. I haven't got an other.
[EDIT]
You can try to rewrite the load method on the object to make it return what you need. But I wouldn't go that way. You can end up screwing a lot of other things.
I don't think there is an other way to do it because load always returns the current object no mater what you do in the events dispatched in the method. see Mage_Core_Model_Abstract::load()
public function load($id, $field=null)
{
$this->_beforeLoad($id, $field);
$this->_getResource()->load($this, $id, $field);
$this->_afterLoad();
$this->setOrigData();
$this->_hasDataChanges = false;
return $this;
}
By making it return new Decorator($this), you might achieve what you need, but just make sure that when calling $model->doSomething() and doSomething() is not a method in your decorator you still end up calling the original method on the model.

ZF2 and EntityManager (Doctrine)

I have a problem. I try to get the Entity-Manager without a Controller, but I found no way.
At this time, I get the Entity-Manager like this:
(Controller)
public function getEntityManager()
{
if (null === $this->_em) {
$this->_em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
}
return $this->_em;
}
(Plugin)
public function getEntityManager()
{
if($this->_em == null){
$this->_em = $this->getController()->getServiceLocator()->get('doctrine.entitymanager.orm_default');
}
return $this->_em;
}
You see, I need allways a controller. But, if I need the EntityManager in a model, i have a problem. I can give the model the controller, but I think this is really a bad way.
Have you any idea to get the EntityManager without a controller?
The way I handle Doctrine is through Services, i do it like the following:
//some Controller
public function someAction()
{
$service = $this->getServiceLocator()->get('my_entity_service');
return new ViewModel(array(
'entities' => $service->findAll()
));
}
The Service->findAll() would look something like this:
public function findAll()
{
return $this->getEntityRepository()->findAll();
}
Now we need to define the my_entity_service. I do this inside my Module.php
public function getServiceConfig()
{
return array(
'factories' => array(
'my_entity_service' => 'Namespace\Factory\MyServiceFactory'
)
);
}
Next I create the Factory (note: this could also be done via anonymous function inside the Module.php)
<?php
namespace Namespace\Factory;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
use Namespace\Model\MyModel;
class MyServiceFactory implements FactoryInterface
{
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
* #return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$myModel= new MyModel();
$myModel->setEntityManager($serviceLocator->get('Doctrine\ORM\EntityManager'));
return $myModel;
}
}
Now this is a lot to chew :D I totally get that. What is happening here is actually very simple though. Instead of creating your model and somehow get to the EntityManager, you call ZF2's ServiceManager to create the Model for you and inject the EntityManager into it.
If this is still confusing to you, I'll gladly try to explain myself better. For further clarification however I'd like to know about your use case. I.e.: what for do you need the EntityManager or where exactly do u need it.
This code example is outside of the question scope
Just to give you a live example of something I do via ServiceFactories with forms:
public function createService(ServiceLocatorInterface $serviceLocator)
{
$form = new ReferenzwertForm();
$form->setHydrator(new DoctrineEntity($serviceLocator->get('Doctrine\ORM\EntityManager')))
->setObject(new Referenzwert())
->setInputFilter(new ReferenzwertFilter())
->setAttribute('method', 'post');
return $form;
}
Your real question is "How to get an Instance of ServiceManager in my own classes"
For this, take a look at the docu: (bottom of page http://zf2.readthedocs.org/en/latest/modules/zend.service-manager.quick-start.html)
By default, the Zend Framework MVC registers an initializer that will
inject the ServiceManager instance, which is an implementation of
Zend\ServiceManager\ServiceLocatorInterface, into any class
implementing Zend\ServiceManager\ServiceLocatorAwareInterface. A
simple implementation looks like the following.
so implent the ServiceLocatorInterface in your classes and then inside your class you can call:
$this->getServiceLocator()->get('doctrine.entitymanager.orm_default');
or any other service you have registered.

ZF2: How to propagate Controller return to layout template?

I'm returning data from Controller like this:
/**
* Password request sent
*
* #return array
*/
public function passwordRequestSentAction ()
{
return array(
'foo' => $this->bar,
);
}
But $this->foo is null within layout.phtml even though its correct within controller/passwordRequestSent.phtml
I had to create postDispatch method in my abstract controller and link to it in attachDefaultListeners() and do this in postDispatch:
$e->getViewModel()->setVariables($e->getResult()->getVariables());
Is that really the way to go? I simply want to share all my variables across, no matter if its layout or page template.
You can access the layout-template by calling $this->layout():
class MyController extends AbstractActionController
{
public function myAction()
{
$layout = $this->layout();
// Returns the ViewModel of the Layout
}
}
For more information & samples check the manual's examples.
However in most cases I'd suggest writing a viewhelper for these tasks - especially for navigation/... This encapsulates the controller's logic from viewing tasks like I want the navigation displayed here or Show me the user's login box. Same goes for almost every type of status messages.

Resources