Yii: Catching all exceptions for a specific controller - yii

I am working on a project which includes a REST API component. I have a controller dedicated to handling all of the REST API calls.
Is there any way to catch all exceptions for that specific controller so that I can take a different action for those exceptions than the rest of the application's controllers?
IE: I'd like to respond with either an XML/JSON formatted API response that contains the exception message, rather than the default system view/stack trace (which isn't really useful in an API context). Would prefer not having to wrap every method call in the controller in its own try/catch.
Thanks for any advice in advance.

You can completely bypass Yii's default error displaying mechanism by registering onError and onException event listeners.
Example:
class ApiController extends CController
{
public function init()
{
parent::init();
Yii::app()->attachEventHandler('onError',array($this,'handleError'));
Yii::app()->attachEventHandler('onException',array($this,'handleError'));
}
public function handleError(CEvent $event)
{
if ($event instanceof CExceptionEvent)
{
// handle exception
// ...
}
elseif($event instanceof CErrorEvent)
{
// handle error
// ...
}
$event->handled = TRUE;
}
// ...
}

I wasn't able to attach events in controller, and I did it by redefinition CWebApplication class:
class WebApplication extends CWebApplication
{
protected function init()
{
parent::init();
Yii::app()->attachEventHandler('onError',array($this, 'handleApiError'));
Yii::app()->attachEventHandler('onException',array($this, 'handleApiError'));
}
/**
* Error handler
* #param CEvent $event
*/
public function handleApiError(CEvent $event)
{
$statusCode = 500;
if($event instanceof CExceptionEvent)
{
$statusCode = $event->exception->statusCode;
$body = array(
'code' => $event->exception->getCode(),
'message' => $event->exception->getMessage(),
'file' => YII_DEBUG ? $event->exception->getFile() : '*',
'line' => YII_DEBUG ? $event->exception->getLine() : '*'
);
}
else
{
$body = array(
'code' => $event->code,
'message' => $event->message,
'file' => YII_DEBUG ? $event->file : '*',
'line' => YII_DEBUG ? $event->line : '*'
);
}
$event->handled = true;
ApiHelper::instance()->sendResponse($statusCode, $body);
}
}
In index.php:
require_once(dirname(__FILE__) . '/protected/components/WebApplication.php');
Yii::createApplication('WebApplication', $config)->run();

You can write your own actionError() function per controller. There are several ways of doing that described here

I'm using the following Base controller for an API, it's not stateless API, mind you, but it can serve just aswell.
class BaseJSONController extends CController{
public $data = array();
public $layout;
public function filters()
{
return array('mainLoop');
}
/**
* it all starts here
* #param unknown_type $filterChain
*/
public function filterMainLoop($filterChain){
$this->data['Success'] = true;
$this->data['ReturnMessage'] = "";
$this->data['ReturnCode'] = 0;
try{
$filterChain->run();
}catch (Exception $e){
$this->data['Success'] = false;
$this->data['ReturnMessage'] = $e->getMessage();
$this->data['ReturnCode'] = $e->getCode();
}
echo json_encode($this->data);
}
}
You could also catch dbException and email those, as they're somewhat critical and can show underlying problem in the code/db design.

Add this to your controller:
Yii::app()->setComponents(array(
'errorHandler'=>array(
'errorAction'=>'error/error'
)
));

Related

Is Sylius PayumBundle handling payment details incorrectly?

I am testing Bitbag/PayUPlugin and I was stopped by gateway API with error "Required data missing".
After some debugging, I realised that Sylius Payment entity, specifically "details" property, is not fulfilled with data.
After change condition on line 53:
https://github.com/Sylius/Sylius/blob/4e06a4dfb8dc56731470016bb97165f3025947b7/src/Sylius/Bundle/PayumBundle/Action/CapturePaymentAction.php#L53
to
if ($status->isNew() || $status->isUnknown()) {
payment gateway seems to work correctly.
Is it a bug or am I doing something wrong ?
Sylius/Sylius v1.4.6
Bitbag/PayUPlugin v1.8.0
Unlikely there is an error in PayumBundle/CapturePaymentAction (because more people used PayumBundle than PayUPlugin, so probability of bug is less), conceptually payment object status at the beginning should be "new" instead of "unknown", so the condition should work.
So you should find out https://github.com/BitBagCommerce/SyliusPayUPlugin/blob/master/src/Action/StatusAction.php#L58 class, why it doesn't reach markNew() line.
I guess the BitBagCommerce/SyliusPayUPlugin is dead as this issue hasn't been addressed yet, since July.
In order to fix this I had to decorate the StatusAction class:
<?php
declare(strict_types=1);
namespace App\Payment\PayU;
use BitBag\SyliusPayUPlugin\Action\StatusAction;
use BitBag\SyliusPayUPlugin\Bridge\OpenPayUBridgeInterface;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Request\GetStatusInterface;
final class StatusActionDecorator implements ActionInterface
{
private $action;
public function __construct(StatusAction $action)
{
$this->action = $action;
}
public function setApi($api): void
{
$this->action->setApi($api);
}
public function execute($request): void
{
/** #var $request GetStatusInterface */
RequestNotSupportedException::assertSupports($this, $request);
$model = ArrayObject::ensureArrayObject($request->getModel());
$status = $model['statusPayU'] ?? null;
if (empty($status) || OpenPayUBridgeInterface::NEW_API_STATUS === $status) {
$request->markNew();
return;
}
if (OpenPayUBridgeInterface::PENDING_API_STATUS === $status) {
$request->markPending();
return;
}
if (OpenPayUBridgeInterface::CANCELED_API_STATUS === $status) {
$request->markCanceled();
return;
}
if (OpenPayUBridgeInterface::WAITING_FOR_CONFIRMATION_PAYMENT_STATUS === $status) {
$request->markSuspended();
return;
}
if (OpenPayUBridgeInterface::COMPLETED_API_STATUS === $status) {
$request->markCaptured();
return;
}
$request->markUnknown();
}
public function supports($request): bool
{
return $this->action->supports($request);
}
}
then in the services.yaml:
App\Payment\PayU\StatusActionDecorator:
decorates: bitbag.payu_plugin.action.status
arguments: ['#App\Payment\PayU\StatusActionDecorator.inner']

how to use SimpleSAMLphp in yii framework?

I have two project in yii framework and I want to use both project using SimpleSAMLphp with SSO. The condition, I need is if I login from the first project, i want access to the second project.
Thank you in advance.
First you load the SAML library by temporarily disabling the Yii autoloader. This is just to let you use the SAML classes and methods:
<?php
class YiiSAML extends CComponent {
private $_yiiSAML = null;
static private function pre() {
require_once (Yii::app()->params['simpleSAML'] . '/lib/_autoload.php');
// temporary disable Yii autoloader
spl_autoload_unregister(array(
'YiiBase',
'autoload'
));
}
static private function post() {
// enable Yii autoloader
spl_autoload_register(array(
'YiiBase',
'autoload'
));
}
public function __construct() {
self::pre();
//We select our authentication source:
$this->_yiiSAML = new SimpleSAML_Auth_Simple(Yii::app()->params['authSource']);
self::post();
}
static public function loggedOut($param, $stage) {
self::pre();
$state = SimpleSAML_Auth_State::loadState($param, $stage);
self::post();
if (isset($state['saml:sp:LogoutStatus'])) {
$ls = $state['saml:sp:LogoutStatus']; /* Only for SAML SP */
} else return true;
return $ls['Code'] === 'urn:oasis:names:tc:SAML:2.0:status:Success' && !isset($ls['SubCode']);
}
public function __call($method, $args) {
$params = (is_array($args) and !empty($args)) ? $args[0] : $args;
if (method_exists($this->_yiiSAML, $method)) return $this->_yiiSAML->$method($params);
else throw new YiiSAMLException(Yii::t('app', 'The method {method} does not exist in the SAML class', array(
'{method}' => $method
)));
}
}
class YiiSAMLException extends CException {
}
Then you define a filter extending the CFilter Yii class:
<?php
Yii::import('lib.YiiSAML');
class SAMLControl extends CFilter {
protected function preFilter($filterChain) {
$msg = Yii::t('yii', 'You are not authorized to perform this action.');
$saml = new YiiSAML();
if (Yii::app()->user->isGuest) {
Yii::app()->user->loginRequired();
return false;
} else {
$saml_attributes = $saml->getAttributes();
if (!$saml->isAuthenticated() or Yii::app()->user->id != $saml_attributes['User.id'][0]) {
Yii::app()->user->logout();
Yii::app()->user->loginRequired();
return false;
}
return true;
}
}
}
And finally, in the controllers you are interested to restrict, you override the filters() method:
public function filters() {
return array(
array(
'lib.SAMLControl'
) , // perform access control for CRUD operations
...
);
}
Hope it helps.
It can be done simply using "vendors" directory.
Download PHP Library from https://simplesamlphp.org/
Implement it in Yii Framework as a vendor library. (http://www.yiiframework.com/doc/guide/1.1/en/extension.integration)
Good Luck :)
I came across an Yii Extension for SimpleSAMLphp in github
https://github.com/asasmoyo/yii-simplesamlphp
You can load the simplesamlphp as a vendor library and then specify the autoload file in the extension.
Apart from the extension you can copy all the necessary configs and metadatas into the application and configure SimpleSAML Configuration to load the configurations from your directory, so you can keep the vendor package untouched for future updates.

Zend Framework 2 - Service method require as parameter InputFilter

I have a bit OOD question.
I have service:
namespace Front\Service\Course;
use Front\ORM\EntityManagerAwareInterface;
use Zend\Http\Request;
use Zend\InputFilter\InputFilter;
use Front\InputFilter\Course\CreateFilter;
class Create implements EntityManagerAwareInterface
{
/**
* #var \Doctrine\Orm\EntityManager
*/
protected $entityManager = null;
public function create(CreateFilter $createFilter)
{
if (!$createFilter->isValid()) return false;
/* #var $courseRepository \Front\Repositories\CourseRepository */
$courseRepository = $this->getEntityManager()->getRepository('Front\Entities\Course');
$course = $courseRepository->findByName($createFilter->getCourse());
}
/* (non-PHPdoc)
* #see \Front\ORM\EntityManagerAwareInterface::getEntityManager()
*/
public function getEntityManager()
{
return $this->entityManager;
}
/* (non-PHPdoc)
* #see \Front\ORM\EntityManagerAwareInterface::setEntityManager()
*/
public function setEntityManager(\Doctrine\ORM\EntityManager $entityManager)
{
$this->entityManager = $entityManager;
return $this;
}
}
And controller :
class CreateController extends \Zend\Mvc\Controller\AbstractController
{
public function onDispatch(MvcEvent $e)
{
$jsonModel = new JsonModel();
/* #var $courseCreateService \Front\Service\Course\Create */
$courseCreateService = $this->getServiceLocator()->get('Front\Service\Course\Create');
$courseCreateFilter = new CreateFilter();
$courseCreateFilter->setData($this->params()->fromPost());
if (!$courseCreateFilter->isValid()) {
$jsonModel->setVariable('status', 0);
$jsonModel->setVariable('message', $courseCreateFilter->getMessages());
return;
}
$courseCreateService->create($courseCreateFilter);
}
}
By service method declaration :
public function create(CreateFilter $createFilter)
i force user of the Service to use CreateFilter container which derived from Zend/InputFilter every time when he want to create new Course.
My question is: Might it be better when i will send to the service layer not the Typed object but simple value?
On example in my case it is might looks like:
public function create($courseName)
My CreateFilter looks like:
class CreateFilter extends InputFilter
{
public function __construct()
{
$input = new Input('name');
$validatorChain = new ValidatorChain();
$validatorChain->addValidator(new StringLength(array('max'=>60)))
->addValidator(new NotEmpty());
$input->setRequired(true)->setValidatorChain($validatorChain);
$this->add($input);
}
/**
* #return string | null
*/
public function getCourse()
{
return $this->getValue('name');
}
}
If you provide a concrete class name as you're doing now, you're forever tied to a concrete implementation of the class or one derived from it. If you decide later that you want to use a different class entirely, you have to refactor your service class code, whereas with an interface, you only need to implement it in your new class and your service will continue to work without any changes.
Without any interface at all, your service class would have to do extra checks to first see if it's an object and then if it implements the method you're expecting before it can even begin doing its job. By requiring an interface you remove the uncertainty, and negate the need for checks.
By providing an interface you create a contract between your methods and the classes they're expecting as arguments without restricting which classes may enter into the contract. All in all, contract by interface is preferable to contract by class name, but both are preferable to no contract at all.
I usually bind my entities to my form, so they are populated with the data from the form. This way, you inject the entity to your service and imho that's much cleaner. The service should not be aware of how you got your data.
My "admin" controller for an entity Bar usually is injected with three objects: the repository (to query objects), the service (to persist/update/delete objects) and the form (to modify objects for the user). A standard controller is then very CRUD based and only pushes entities to the service layer:
<?php
namespace Foo\Controller;
use Foo\Repository\Bar as Repository;
use Foo\Form\Bar as Form;
use Foo\Service\Bar as Service;
use Foo\Entity\Bar as Entity;
use Foo\Options\ModuleOptions;
use Zend\Mvc\Controller\AbstractActionController;
class BarController extends AbstractActionController
{
/**
* #var Repository
*/
protected $repository;
/**
* #var Service
*/
protected $service;
/**
* #var Form
*/
protected $form;
/**
* #var ModuleOptions
*/
protected $options;
public function __construct(Repository $repository, Service $service, Form $form, ModuleOptions $options = null)
{
$this->repository = $repository;
$this->service = $service;
$this->form = $form;
if (null !== $options) {
$this->options = $options;
}
}
public function getService()
{
return $this->service;
}
public function getRepository()
{
return $this->repository;
}
public function getForm()
{
return $this->form;
}
public function getOptions()
{
if (null === $this->options) {
$this->options = new ModuleOptions;
}
return $this->options;
}
public function indexAction()
{
$bars = $this->getRepository()->findAll();
return array(
'bars' => $bars,
);
}
public function viewAction()
{
$bar = $this->getBar();
return array(
'bar' => $bar,
);
}
public function createAction()
{
$bar = $this->getBar(true);
$form = $this->getForm();
$form->bind($bar);
if ($this->getRequest()->isPost()) {
$data = $this->getRequest()->getPost();
$form->setData($data);
if ($form->isValid()) {
// Bar is populated with form data
$this->getService()->create($bar);
return $this->redirect()->toRoute('bar/view', array(
'bar' => $bar->getId(),
));
}
}
return array(
'form' => $form,
);
}
public function updateAction()
{
$bar = $this->getBar();
$form = $this->getForm();
$form->bind($bar);
if ($this->getRequest()->isPost()) {
$data = $this->getRequest()->getPost();
$form->setData($data);
if ($form->isValid()) {
$this->getService()->update($bar);
return $this->redirect()->toRoute('bar/view', array(
'bar' => $bar->getId(),
));
}
}
return array(
'bar' => $bar,
'form' => $form,
);
}
public function deleteAction()
{
if (!$this->getRequest()->isPost()) {
$this->getRequest()->setStatusCode(404);
return;
}
$bar = $this->getBar();
$this->getService()->delete($bar);
return $this->redirect()->toRoute('bar');
}
protected function getBar($create = false)
{
if (true === $create) {
$bar = new Entity;
return $bar;
}
$id = $this->params('bar');
$bar = $this->getRepository()->find($id);
if (null === $bar) {
throw new Exception\BarNotFoundException(sprintf(
'Bar with id "%s" not found', $id
));
}
return $bar;
}
}
I made a gist file on Github with this full code (it's better readable) and the service. The service relies on the interface, so you can even swap out the entity object by another one having the same interface.
Check the full thing out here: https://gist.github.com/juriansluiman/5472787
Thanks all for answering, owing to answers and analyzing, i have reached conclusion which most applicable for my situation. I agree that Service in my case should not wait concrete object, it is should wait an abstraction with getCourse method.
And i completely agree with "Crisp" answer:
All in all, contract by interface is preferable to contract by class name, but both are preferable to no contract at all.
So i need to extract Interface with one method
getCourse
or
getName
, and remove
if (!$createFilter->isValid()) return false;
so Interface:
interface CourseInterface
{
/**
* #return String
**/
public function getName();
}
and Service:
class Create implements EntityManagerAwareInterface
{
/**
* #var \Doctrine\Orm\EntityManager
*/
protected $entityManager = null;
/**
* #param CourseInterface $course
* #param UserInterface $creator
*/
public function create(CourseInterface $course)
{
$courseEntity = new Course();
$courseEntity->setName($course->getName());
$this->entityManager->persist($courseEntity);
$this->entityManager->flush();
.....
Thanks all.

Load a listener before the security or custom provider

I'm working on a custom provider that works exactly like a classical user form, however I have to give a second parameter to identify the user: a websiteId (I'm creating a dynamic website plateform).
So a username is no more unique, but the combinaison of username and websiteId it is.
I successfully created my custom authentication, the last problem I have is to get the websiteId from the domain thanks to a listener, it works, but infortunately the method that get the website id from the domain is loaded after my authentication provider, so I can't get the websiteId in time :(
I tried to change the listener priority (test 9999, 1024, 255 and 0, and negative numbers -9999, -1024, -255 etc...), in vain, it's loaded always after.
Here my code:
services.yml:
services:
# Listeners _________________
website_listener:
class: Sybio\Bundle\WebsiteBundle\Services\Listener\WebsiteListener
arguments:
- #doctrine
- #sybio.website_manager
- #translator
- %sybio.states%
tags:
- { name: kernel.event_listener, event: kernel.request, method: onDomainParse, priority: 255 }
# Security _________________
sybio_website.user_provider:
class: Sybio\Bundle\WebsiteBundle\Security\Authentication\Provider\WebsiteUserProvider
arguments: [#website_listener, #doctrine.orm.entity_manager]
My listener is "website_listener", and you can see i use it for my sybio_website.user_provider as argument.
WebsiteListener:
// ...
class WebsiteListener extends Controller
{
protected $doctrine;
protected $websiteManager;
protected $translator;
protected $websiteId;
/**
* #var array
*/
protected $entityStates;
public function __construct($doctrine, $websiteManager, $translator, $entityStates)
{
$this->doctrine = $doctrine;
$this->websiteManager = $websiteManager;
$this->translator = $translator;
$this->entityStates = $entityStates;
}
/**
* #param Event $event
*/
public function onDomainParse(Event $event)
{
$request = $event->getRequest();
$website = $this->websiteManager->findOne(array(
'domain' => $request->getHost(),
'state' => $this->entityStates['website']['activated'],
));
if (!$website) {
throw $this->createNotFoundException($this->translator->trans('page.not.found'));
}
$this->websiteId = $website->getId();
}
/**
* #param integer $websiteId
*/
public function getWebsiteId()
{
return $this->websiteId;
}
}
$websiteId is hydrated, not in time as you will see in my provider...
WebsiteUserProvider:
<?php
namespace Sybio\Bundle\WebsiteBundle\Security\Authentication\Provider;
// ...
class WebsiteUserProvider implements UserProviderInterface
{
private $em;
private $websiteId;
private $userEntity;
public function __construct($websiteListener, EntityManager $em)
{
$this->em = $em;
$this->websiteId = $websiteListener->getWebsiteId(); // Try to get the website id from my listener, but it's method onDomainParse is not called in time
$this->userEntity = 'Sybio\Bundle\CoreBundle\Entity\User';
}
public function loadUserByUsername($username)
{
// I need the websiteId here to identify the user by its username and the website:
if ($user = $this->findUserBy(array('username' => $username, 'website' => $this->websiteId))) {
return $user;
}
throw new UsernameNotFoundException(sprintf('No record found for user %s', $username));
}
// ...
}
So any idea will be appreciate ;)
I spent a lot of time to set up my authentication configuration, but now I can't get the websiteId in time, too bad :(
Thanks for your anwsers !
EDIT:
I had also other files of my authentication system to understand, I don't think I can control the provider position when loading, because they're witten in the security.yml config:
WebsiteAuthenticationProvider:
// ...
class WebsiteAuthenticationProvider extends UserAuthenticationProvider
{
private $encoderFactory;
private $userProvider;
/**
* #param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider
* #param UserCheckerInterface $userChecker
* #param $providerKey
* #param EncoderFactoryInterface $encoderFactory
* #param bool $hideUserNotFoundExceptions
*/
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, EncoderFactoryInterface $encoderFactory, $hideUserNotFoundExceptions = true)
{
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
$this->encoderFactory = $encoderFactory;
$this->userProvider = $userProvider;
}
/**
* {#inheritdoc}
*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
$user = $token->getUser();
if ($user instanceof UserInterface) {
return $user;
}
try {
$user = $this->userProvider->loadUserByUsername($username);
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
}
return $user;
} catch (UsernameNotFoundException $notFound) {
throw $notFound;
} catch (\Exception $repositoryProblem) {
throw new AuthenticationServiceException($repositoryProblem->getMessage(), $token, 0, $repositoryProblem);
}
}
// ...
}
The factory:
// ...
class WebsiteFactory extends FormLoginFactory
{
public function getKey()
{
return 'website_form_login';
}
protected function getListenerId()
{
return 'security.authentication.listener.form';
}
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'security.authentication_provider.sybio_website.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('security.authentication_provider.sybio_website'))
->replaceArgument(0, new Reference($userProviderId))
->replaceArgument(2, $id)
;
return $provider;
}
}
SybioWebsiteBundle (dependency):
// ...
class SybioWebsiteBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new WebsiteFactory());
}
}
Security:
security:
firewalls:
main:
provider: website_provider
pattern: ^/
anonymous: ~
website_form_login:
login_path: /login.html
check_path: /login
logout:
path: /logout.html
target: /
providers:
website_provider:
id: sybio_website.user_provider
Firewall::onKernelRequest is registered with a priority of 8 (sf2.2). A priority of 9 should ensure that your listener is called first (works for me).
I had a similar problem, which was to create subdomain-specific "Campaign" sites within a single sf2.2 app: {campaign}.{domain} . Every User has many Campaigns and I, like you, wanted to prevent a User without the given Campaign from logging in.
My solution was to create a Doctrine filter to add my campaign criteria to every relevant query made under {campaign}.{domain}. A kernel.request listener (with priority 9!) is responsible for activating the filter before my generic user provider tries to loadUserByUsername. I use mongodb, but the idea is similar for ORM.
The best part is that I'm still using stock authentication classes. This is basically all there is to it:
config.yml:
doctrine_mongodb:
document_managers:
default:
filters:
campaign:
class: My\Filter\CampaignFilter
enabled: false
CampaignFilter.php:
class CampaignFilter extends BsonFilter
{
public function addFilterCriteria(ClassMetadata $targetMetadata)
{
$class = $targetMetadata->name;
$campaign = $this->parameters['campaign'];
$campaign = $campaign instanceof Campaign ? $campaign->getId() : $campaign;
if ($targetMetadata->hasField('campaign')) {
return array('campaign' => $this->parameters['campaign']);
}
if ($targetMetadata->hasField('campaigns')) {
return array('campaigns' => $this->parameters['campaign']);
}
return array();
}
}
My listener is declared as:
<service id="my.campaign_listener" class="My\EventListener\CampaignListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="9" />
<argument type="service" id="doctrine.odm.mongodb.document_manager" />
</service>
The listener class:
class CampaignListener
{
private $dm;
public function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
return;
}
$request = $event->getRequest();
if ($campaign = $request->attributes->get('campaign', false)) {
$filters = $this->dm->getFilterCollection();
$filter = $filters->enable('campaign');
$filter->setParameter('campaign', $campaign);
}
}
}
'campaign' is available in the request here thanks to my routing configuration:
campaign:
resource: "#My/Controller/CampaignController.php"
type: annotation
host: "{campaign}.{domain}"
defaults:
campaign: test
domain: %domain%
requirements:
domain: %domain%
.. and %domain% is a parameter from config.yml or config_dev.yml
Like the response provide by benki07 it's a question of prority, you have to put your listener before the Firewall::onKernelRequest
Then, your listener will be called -> Firewall is call and your authentification listener are called with the webSiteId registered.
As you can see in the SecurityExtension.php The factories used do not have any sort of priority system. It just adds your factory to the end of the array, that's it.
Therefore it is impossible to put your custom authentication before that of symfony's security component.
An option may be to override the DaoAuthenticationProvider class paramater with your class. I hope that symfony2 will change from factories to a registry where you can add your custom authentication with a tag and a priority because this is not open/closed enough for me.

How to set default action dynamically in Yii

i want to change default action of a controller depends on which user is logged in.
Ex. There are two users in my site : publisher and author and i want to set publisher action as default action when a publisher is logged in, and same for author.
what should i do? when can I check my roles and set their relevant actions?
Another way to do this would be setting the defaultAction property in your controller's init() method. Somewhat like this:
<?php
class MyAwesomeController extends Controller{ // or extends CController depending on your code
public function init(){
parent::init(); // no need for this call if you don't have anything in your parent init()
if(array_key_exists('RolePublisher', Yii::app()->authManager->getRoles(Yii::app()->user->id)))
$this->defaultAction='publisher'; // name of your action
else if (array_key_exists('RoleAuthor', Yii::app()->authManager->getRoles(Yii::app()->user->id)))
$this->defaultAction='author'; // name of your action
}
// ... rest of your code
}
?>
Check out CAuthManager's getRoles(), to see that the returned array will have format of 'role'=>CAuthItem object, which is why i'm checking with array_key_exists().
Incase you don't know, the action name will be only the name without the action part, for example if you have public function actionPublisher(){...} then action name should be: publisher.
Another, simpler, thing you can do is keep the default action the same, but that default action simply calls an additional action function depending on what kind of user is logged in. So for example you have the indexAction function conditionally calling this->userAction or this->publisherAction depending on the check for who is logged in.
I think you can save "first user page" in user table. And when a user is authenticated, you can load this page from database. Where you can do this? I think best place is UserIdentity class. After that, you could get this value in SiteController::actionLogin();
You can get or set "first page" value:
if (null === $user->first_page) {
$firstPage = 'site/index';
} else {
$firstPage = $user->first_page;
}
This is a complete class:
class UserIdentity extends CUserIdentity
{
private $_id;
public function authenticate()
{
$user = User::model()->findByAttributes(array('username' => $this->username));
if ($user === null) {
$this->errorCode = self::ERROR_USERNAME_INVALID;
} else if ($user->password !== $user->encrypt($this->password)) {
$this->errorCode = self::ERROR_PASSWORD_INVALID;
} else {
$this->_id = $user->id;
if (null === $user->first_page) {
$firstPage = 'site/index';
} else {
$firstPage = $user->first_page;
}
$this->errorCode = self::ERROR_NONE;
}
return !$this->errorCode;
}
public function getId()
{
return $this->_id;
}
}
/**
* Displays the login page
*/
public function actionLogin()
{
$model = new LoginForm;
// if it is ajax validation request
if (isset($_POST['ajax']) && $_POST['ajax'] === 'login-form') {
echo CActiveForm::validate($model);
Yii::app()->end();
}
// collect user input data
if (isset($_POST['LoginForm'])) {
$model->attributes = $_POST['LoginForm'];
// validate user input and redirect to the previous page if valid
if ($model->validate() && $model->login())
$this->redirect(Yii::app()->user->first_page);
}
// display the login form
$this->render('login', array('model' => $model));
}
Also, you can just write right code only in this file. In SiteController file.