using symfony 2.8, i'm working with subdomains and i want to show different (lets say)home pages depending on the subdomain, i'm storing the subdomains in Domain table with a column named subdomain. ideally when the user visits sub.example.com i want to search the database for 'sub' and get the id of that row and set that as a global parameter for that specific domain, so that i can load the websitesettings and load other dynamic data from the database (using domain_id as the key)
this is what i presume to be correct, if there are better methods to deal with this same problem, please let me know, i might get a friend to give out a bounty if its new to me.
I suggest you listen to the kernel.controller event. Make sure your listener is container aware so that you can set the parameter by doing $this->container->setParameter('subdomain', $subdomain);
At this point you just need to check the parameter you set where it suits you, for example in your controller action so that you can return, for example, different views according to the current subdomain.
Reference:
Container aware dispatcher
Symfony2 framework events
Have a look at my implementation, using a YAML configuration instead of a database: https://github.com/fourlabsldn/HostsBundle. You might be able to get some inspiration.
<?php
namespace FourLabs\HostsBundle\Service;
use Symfony\Component\HttpFoundation\RequestStack;
use FourLabs\HostsBundle\Model\DomainRepository;
use FourLabs\HostsBundle\Exception\NotConfiguredException;
abstract class AbstractProvider
{
/**
* #var RequestStack
*/
protected $requestStack;
/**
* #var DomainRepository
*/
protected $domainRepository;
/**
* #var boolean
*/
protected $requestActive;
public function __construct(RequestStack $requestStack, DomainRepository $domainRepository, $requestActive)
{
$this->requestStack = $requestStack;
$this->domainRepository = $domainRepository;
$this->requestActive = $requestActive;
}
protected function getDomainConfig()
{
$request = $this->requestStack->getCurrentRequest();
if(is_null($request) || !$this->requestActive) {
return;
}
$host = parse_url($request->getUri())['host'];
if(!($domain = $this->domainRepository->findByHost($host))) {
throw new NotConfiguredException('Domain configuration for '.$host.' missing');
}
return $domain;
}
}
and the listener
<?php
namespace FourLabs\HostsBundle\EventListener;
use FourLabs\HostsBundle\Service\LocaleProvider;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class LocaleListener
{
/**
* #var LocaleProvider
*/
private $localeProvider;
public function __construct(LocaleProvider $localeProvider) {
$this->localeProvider = $localeProvider;
}
public function onKernelRequest(GetResponseEvent $event) {
if(HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$event->getRequest()->setLocale($this->localeProvider->getLocale());
}
}
Related
How can I fetch and render the uid of the FE User via a Viewhelper? The below is working via a Controller ... but not in a Viewhelper. Where is the difference? I'm using 7.6.11 and at the end I would like to have the uid of the FE User and the usergroup uid of him and further use it in the html of the extension and in general partials ...
/typo3conf/ext/extension/Classes/ViewHelpers/UserViewHelper.php
<?php
namespace Vendor\Extension\ViewHelpers;
class UserViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper {
/**
* User Repository
*
* #var \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository
* #inject
*/
protected $userRepository;
/**
* #var \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserGroupRepository
* #inject
*/
protected $frontendUserGroupRepository;
public function render() {
$userIDTest = $this->userRepository->findByUid($GLOBALS['TSFE']->fe_user->user['uid']);
$this->view->assign('userIDTest', $userIDTest);
}
}
List.html
<f:layout name="Default" />
<f:section name="main">
{userIDTest.uid}
</f:section>
As suggested by Dimitry I replaced
$this->view->assign('userIDTest', $userIDTest);
with
return $userIDTest;
And in List.html I have this:
{namespace custom=Vendor\Extension\ViewHelpers}
<f:layout name="Default" />
<f:section name="main">
<f:alias map="{user: '{custom:user()}'}">
{user.uid} {user.username}
</f:alias>
</f:section>
... and after clearing all Caches (FE/BE/Install) and deleting typo3temp ... now its working!
In 7.x and upwards ViewHelpers are compiled, resulting in the render method being called only once for compiling. Afterwards, only the static method renderStatic() is called. You could overwrite renderStatic, it will be called every time:
<?php
namespace Vendor\Extension\ViewHelpers;
use TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper;
class UserIdViewHelper extends AbstractViewHelper
{
public function render()
{
return static::renderStatic(
[],
$this->renderChildrenClosure,
$this->renderingContext
);
}
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
) {
$userData = $GLOBALS['TSFE']->fe_user->user;
return null !== $userData ? (int)$userData['uid'] : null;
}
}
If you need to use some service in your ViewHelper, things get more complicated, since dependency injection won't work with compiled ViewHelpers. You need to get an object manager, and fetch an instance of the service with the object manager.
This could look like this, assuming you would want to use the FrontendUserRepository as service, because you want to return the entire user object, not only the users uid:
<?php
namespace Vendor\Extension\ViewHelpers;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper;
class UserViewHelper extends AbstractViewHelper
{
/**
* #var FrontendUserRepository
*/
private static $frontendUserRepository = null;
public function render()
{
return static::renderStatic(
[],
$this->renderChildrenClosure,
$this->renderingContext
);
}
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
) {
$userData = $GLOBALS['TSFE']->fe_user->user;
if (null === $userData) {
return null;
}
return static::getFrontendUserRepository()->findByUid((int)$userData['uid']);
}
private static function getFrontendUserRepository()
{
if (null === static::$frontendUserRepository) {
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
static::$frontendUserRepository = $objectManager->get(FrontendUserRepository::class);
}
return static::$frontendUserRepository;
}
}
Disclaimer: All the code is written without actually running it, thus there are bugs in it.
if you want to return the user or the uid of the user in the viewhelper, just return it.
instead of
$this->view->assign('userIDTest', $userIDTest);
do this
return $userIDTest;
In your fluid you can use the user variables in different ways. The easiest one is to use the "alias" viewhelper: https://fluidtypo3.org/viewhelpers/fluid/master/AliasViewHelper.html
<f:alias map="{user: '{namespace:user()}'}">
{user.uid} {user.username}
</f:alias>
Using latest Symfony and FOSUserbundle, after successfully registering a new user, the user is automatically logged in. I want to prevent this. My reason is that only a special user should be able to register new users.
I guess I have to override the registerAction in the RegisterController of the bundle, but I don't know how.
I tried: http://symfony.com/doc/current/bundles/FOSUserBundle/overriding_controllers.html, but it seems to be outdated, no user is created with this method.
Any hints are appreciated.
Edit:
I found out that I did not create the child bundle correctly. I also had to create my own EventListener. It works now when I overwrite the FOSUserEvents::REGISTRATION_SUCCESS event.
Strange thing is that when I use the FOSUserEvents::REGISTRATION_COMPLETEDevent, both events are dispatched, my bundle's and the FOSUserbundle's, so that the user is redirected to the correct site, but logged in as the new user.
Edit 2:
So this is in my listener:
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
FOSUserEvents::REGISTRATION_COMPLETED => 'onRegistrationCompleted',
);
}
public function onRegistrationSuccess(FormEvent $event)
{
$url = $this->router->generate('admin');
$event->setResponse(new RedirectResponse($url));
}
public function onRegistrationCompleted(FilterUserResponseEvent $event)
{
}
I set the redirection in the REGISTRATION_SUCCESSevent and the REGISTRATION_COMPLETEDis empty. With the debugger I can verify that my own listener's event is called, but the original event is also called.
Actually, there is no need to do any of these. The fos_user.listener.authentication service is removed from the container if use_authentication_listener is set to false.
See line 74-76 in FOS\UserBundle\DependencyInjection\FOSUserExtension.
This information is also included in document FOS UserBundle Configuration.
You can solve this problem with a Listener, In fos user bundle, it authenticates user with after registration.
file :friendsofsymfony/user-bundle/EventListener/AuthenticationListener.php
class : FOS\UserBundle\EventListener\AuthenticationListener
If you check this class you would see it tracks REGISTRATION_COMPLETED Event.
In Authenticatiton Listener It dispatches Event after triggering logInUser function. Therefore you have to logout user in your listener which subscribes `REGISTRATION COMPLETED.
you can check https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/controller_events.rst for writing your listener to logout user.
Note : It may not be a good way log-in log-out user in every registration process, but if you use fosuserbundle easiest way and minimum footprint would be this, if there is already a yml configuration doesn't exists, actually in code there is no direction of yml conf. So this approach would be min. footprint.
try {
$this->loginManager->logInUser($this->firewallName, $event->getUser(), $event->getResponse());
$eventDispatcher->dispatch(FOSUserEvents::SECURITY_IMPLICIT_LOGIN, new UserEvent($event->getUser(), $event->getRequest()));
} catch (AccountStatusException $ex) {
// We simply do not authenticate users which do not pass the user
// checker (not enabled, expired, etc.).
}
EDIT: This technique works on Symfony 3.3, I'm unaware if this works on lower versions.
The correct way of doing this is by creating a Compiler Pass.
You can also: Override the service by adding a new service using the same name: fos_user.listener.authentication on your app/config.yml file or on your bundle config file and adding your new class to it as I've done below and add this
Here is how to override the automatic logging when registering a new user using the compiler pass technique.
The Compiler Pass
namespace arpa3\UserBundle\DependencyInjection;
use arpa3\UserBundle\EventListener\AuthenticationListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OverrideServiceCompilerPass implements CompilerPassInterface {
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('fos_user.listener.authentication');
$definition->setClass(AuthenticationListener::class);
}
}
The Service Override
namespace arpa3\UserBundle\EventListener;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Event\UserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Security\LoginManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
class AuthenticationListener implements EventSubscriberInterface
{
/**
* #var LoginManagerInterface
*/
private $loginManager;
/**
* #var string
*/
private $firewallName;
/**
* AuthenticationListener constructor.
*
* #param LoginManagerInterface $loginManager
* #param string $firewallName
*/
public function __construct(LoginManagerInterface $loginManager, $firewallName)
{
$this->loginManager = $loginManager;
$this->firewallName = $firewallName;
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
// You can disable any of them or all of them as you want
//FOSUserEvents::REGISTRATION_COMPLETED => 'authenticate',
//FOSUserEvents::REGISTRATION_CONFIRMED => 'authenticate',
//FOSUserEvents::RESETTING_RESET_COMPLETED => 'authenticate',
);
}
/**
* #param FilterUserResponseEvent $event
* #param string $eventName
* #param EventDispatcherInterface $eventDispatcher
*/
public function authenticate(FilterUserResponseEvent $event, $eventName, EventDispatcherInterface $eventDispatcher)
{
try {
$this->loginManager->logInUser($this->firewallName, $event->getUser(), $event->getResponse());
$eventDispatcher->dispatch(FOSUserEvents::SECURITY_IMPLICIT_LOGIN, new UserEvent($event->getUser(), $event->getRequest()));
} catch (AccountStatusException $ex) {
// We simply do not authenticate users which do not pass the user
// checker (not enabled, expired, etc.).
}
}
}
Register your Compiler Pass on your main bundle file
namespace arpa3\UserBundle;
use arpa3\UserBundle\DependencyInjection\OverrideServiceCompilerPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class arpa3UserBundle extends Bundle {
public function getParent () {
return 'FOSUserBundle';
}
/**
*
* This injects a Compiler Pass that is used to override the automatic login after registration of a new user
* We have done this in order to disable the "by default" behaviour given that only admins can register users
* and logging in into the newly created account automatically is just not a desired behaviour
*
* #param ContainerBuilder $container
*/
public function build ( ContainerBuilder $container ) {
parent ::build( $container );
$container -> addCompilerPass( new OverrideServiceCompilerPass() );
}
}
There are other ways such as overriding the authentication service on your config.yml but the solution above is the cleanest and most maintainable solution I have found.
You are almost there, as you said your listeners are called but the order is not correct, so you need to make your listener be executed before the default one
In order to do that change
FOSUserEvents::REGISTRATION_SUCCESS =>
'onRegistrationSuccess'
to
FOSUserEvents::REGISTRATION_SUCCESS =>
['onRegistrationSuccess',-10],
Notice the -10 there, this changes the priority of the listener.
class RegistrationSuccessEventListener implements EventSubscriberInterface{
private $router;
public function __construct(UrlGeneratorInterface $router){
$this->router = $router;
}
public static function getSubscribedEvents()
{
//this will be called before
return array(
FOSUserEvents::REGISTRATION_SUCCESS => ['onUserRegistrationSuccess', -30],
);
}
/**
* #param FormEvent $event
* When the user registration is completed redirect
* to the employee list page and avoid the automatic
* mail sending and user authentication that happends
*
*/
public function onUserRegistrationSuccess(FormEvent $event){
$url = $this->router->generate('employees_list');
$event->setResponse(new RedirectResponse($url));
}
}
I am using symfony 2.8 with the FOSBundle version
friendsofsymfony/user-bundle dev-master 1f97ccf Symfony FOSUserBundle
according to the output of composer info
I want to personalize my Symfony project by letting the user choose a city in a selectbox in the top navi. For that I got a query string e.g. ?city=berlin that I fetch in my controllers and filter the results with.
Is there an easy way to keep that query string on every url alive or would you prefer an other solution without a query string? Maybe with cookies?
Thanks for your help!
Better than talking about cookies is the question about stateful or stateless session. Cookies is just the implementation of mapping the client to the session.
Let's say you have a visitor on one city-parametrized page. What do you except your page to look like, when someone copies the url and shares it with others? city is not any personal state, although you mentioned personalized above (e.g. having responsive pages where I can set the font to 120% size or setting higher contrast, would be a personalized configuration I actually don't want to share in the url).
city is part of the state of the page and not the session, thus we want city to be part of the url. Define a prefix route like /{city} and import another yml with that prefix (http://symfony.com/doc/current/book/routing.html#prefixing-imported-routes).
Every time you generate an url with a city you have to set it. You could do this manually or create some CityDecoratedRouter implements RouterInterface getting #router and #request_stack injected and appends the city parameter to all parameter-arrays in generate() calls.
#EvgeniyKuzmin's answer is imho too much magic no one expects. When dealing with those routes having a city parameter it's better to read it in the code, that the routes are treated differently. Of course you also have to define some new city_path function for twig, which uses our CityDecoratedRouter.
If you need to stick user to some condition base on route path (I will advice you use SEO urls instead of GET query) and then use it as stick filter for some behavior on other pages, then you can do such listener:
BaseKernelEvents:
namespace App\CoreBundle\Listener;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
abstract class BaseKernelEvents
{
/**
* #var \Symfony\Bundle\FrameworkBundle\Routing\Router
*/
private $router;
/**
* Initializes a new instance of the KernelEvents class.
*/
public function __construct(ContainerInterface $container, Router $router)
{
$this->container = $container;
$this->router = $router;
}
/*
* Store and get value by route param passed to uri to request and session
*/
protected function storeParam(GetResponseEvent $event, $name, $default = 'none')
{
/* #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
/* #var $session \Symfony\Component\HttpFoundation\Session\Session */
$session = $request->getSession();
// instead of attributes you can get query from request here
$value = $request->attributes->get($name);
if (!$value) {
$value = $session->get($name, $default);
}
return $this->setValues($event, $name, $value);
}
/*
* Set name/value to request context and session
*/
protected function setValues(GetResponseEvent $event, $name, $value)
{
/* #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
$context = $this->router->getContext();
$session = $request->getSession();
$session->set($name, $value);
$context->setParameter($name, $value);
return $value;
}
}
KernelEvents:
namespace LaMelle\ContentSectionBundle\Listener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use App\CoreBundle\Listener\BaseKernelEvents;
class KernelEvents extends BaseKernelEvents
{
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType())
{
$contentsectionSlug = $this->storeParam($event, 'city');
// DO SOMETHINK BASE ON FILTER
// LIKE CREATE GLOBAL TWIG VAR WITH FILTER VALUE
/* #var \Twig_Environment $globals */
$globals = $this->container->get('twig');
$globals->addGlobal('city', $contentsectionSlug);
}
}
}
So in shown example you will have 'city' be filled from session until you will visit route that change 'city' to other value
Namespaces omitted for brevity...
I have written the following service provider and registered in config/app.php:
class OfferServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerLossControlManager();
}
protected function registerLossControlManager()
{
$this->app->bind('LossControlInterface', 'LossControl');
}
}
Here is my LossControlInterface
interface LossControlInterface
{
/**
* #param int $demandId
* #param float $offerTotal
* #param float $productTotal
* #param null|int $partnerId
* #return mixed
*/
public function make($demandId, $offerTotal, $productTotal, $partnerId = null);
/**
* #return float
*/
public function getAcceptableLoss();
/**
* #return bool
*/
public function isAcceptable();
/**
* #return bool
*/
public function isUnacceptable();
/**
* #return null
*/
public function reject();
}
Now within the controller, I can inject the LossController as follows:
use LossControlInterface as LossControl;
class HomeController extends BaseController {
public function __construct(LossControl $lossControl)
{
$this->lossControl = $lossControl;
}
public function getLossThresholds()
{
$lossControl = $this->lossControl->make(985, 1000, null);
var_dump('Acceptable Loss: ' . $lossControl->getAcceptableLoss());
var_dump('Actual Loss: ' . $lossControl->calculateLoss());
var_dump('Acceptable? ' . $lossControl->isAcceptable());
}
}
However if I try to dependency inject the LossControlInterface from within a custom class called by a command:
[2014-09-02 13:09:52] development.ERROR: exception 'ErrorException' with message 'Argument 11 passed to Offer::__construct() must be an instance of LossControlInterface, none given, called in /home/vagrant/Code/.../ProcessOffer.php on line 44 and defined' in /home/vagrant/Code/.../Offer.php:79
It appears as though I am unable to dependency inject the interface into a custom class, but I can when dependency injecting into a controller.
Any thoughts on what Im doing wrong or have omitted to get the automatic resolution working?
The IoC is automatic within controllers, and you don't see the injection because Laravel handles the construction of controllers for you. When creating any other custom class by using the new keyword, you will still need to send in all of the parameters needed to it's constructor:
$myClass = new ClassWithDependency( app()->make('Dependency') );
You can hide this, to a degree, by funneling creation of your custom class through a service provider:
// Your service provider
public function register()
{
$this->app->bind('ClassWithDependency', function($app) {
return new ClassWithDependency( $app->make('Dependency') );
});
}
Then just have the IoC make it whenever you need it:
$myClass = app()->make('ClassWithDepenency');
In your case, you can change your code to look like this:
private function setOffer(Offer $offer = null) {
$this->processOffer = $offer ?:
new Offer( app()->make('LossControlInterface') );
}
A perhaps cleaner approach could be to create a service provider and an OfferFactory which gets injected into your controller. The controller can then request the factory to create the offer whenever it needs one:
// Controller
public function __construct(OfferFactory $offerFactory)
{
$this->offerFactory = $offerFactory;
}
public function setOffer(Offer $offer = null)
{
$this->processOffer = $offer ?: $this->offerFactory->createOffer();
}
// OfferFactory
class OfferFactory
{
public function createOffer()
{
return app()->make('Offer');
}
}
This has the benefit of completely decoupling your controller from the logic behind the creation of the offer, yet allowing you to have a spot to add any amount of complexity necessary to the process of creating offers.
In Laravel 5.2 the simplest solution for your particular problem would be to replace
new Offer();
with
App::make('Offer');
or even shorter
app('Offer');
which will use Laravel Container to take care of dependencies.
If however you want to pass additional parameters to the Offer constructor it is necessary to bind it in your service provider
App::bind('Offer', function($app, $args) {
return new Offer($app->make('LossControl'), $args);
});
And voila, now you can write
app('Offer', [123, 456]);
In laravel 5.4 (https://github.com/laravel/framework/pull/18271) you need to use the new makeWith method of the IoC container.
App::makeWith( 'App\MyNameSpace\MyClass', [ $id ] );
if you still use 5.3 or below, the above answers will work.
I appear to be having issues with my spec tests when it comes to stubs that are calling other methods.
I've been following Laracasts 'hexagonal' approach for my controller to ensure it is only responsible for the HTTP layer.
Controller
<?php
use Apes\Utilities\Connect;
use \OAuth;
class FacebookConnectController extends \BaseController {
/**
* #var $connect
*/
protected $connect;
/**
* Instantiates $connect
*
* #param $connect
*/
function __construct()
{
$this->connect = new Connect($this, OAuth::consumer('Facebook'));
}
/**
* Login user with facebook
*
* #return void
*/
public function initialise() {
// TODO: Actually probably not needed as we'll control
// whether this controller is called via a filter or similar
if(Auth::user()) return Redirect::to('/');
return $this->connect->loginOrCreate(Input::all());
}
/**
* User authenticated, return to main game view
* #return Response
*/
public function facebookConnectSucceeds()
{
return Redirect::to('/');
}
}
So when the route is initialised I construct a new Connect instance and I pass an instance of $this class to my Connect class (to act as a listener) and call the loginOrCreate method.
Apes\Utilities\Connect
<?php
namespace Apes\Utilities;
use Apes\Creators\Account;
use Illuminate\Database\Eloquent\Model;
use \User;
use \Auth;
use \Carbon\Carbon as Carbon;
class Connect
{
/**
* #var $facebookConnect
*/
protected $facebookConnect;
/**
* #var $account
*/
protected $account;
/**
* #var $facebookAuthorizationUri
*/
// protected $facebookAuthorizationUri;
/**
* #var $listener
*/
protected $listener;
public function __construct($listener, $facebookConnect)
{
$this->listener = $listener;
$this->facebookConnect = $facebookConnect;
$this->account = new Account();
}
public function loginOrCreate($input)
{
// Not the focus of this test
if(!isset($input['code'])){
return $this->handleOtherRequests($input);
}
// Trying to stub this method is my main issue
$facebookUserData = $this->getFacebookUserData($input['code']);
$user = User::where('email', '=', $facebookUserData->email)->first();
if(!$user){
// Not the focus of this test
$user = $this->createAccount($facebookUserData);
}
Auth::login($user, true);
// I want to test that this method is called
return $this->listener->facebookConnectSucceeds();
}
public function getFacebookUserData($code)
{
// I can't seem to stub this method because it's making another method call
$token = $this->facebookConnect->requestAccessToken($code);
return (object) json_decode($this->facebookConnect->request( '/me' ), true);
}
// Various other methods not relevant to this question
I've tried to trim this down to focus on the methods under test and my understanding thus far as to what is going wrong.
Connect Spec
<?php
namespace spec\Apes\Utilities;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use \Illuminate\Routing\Controllers\Controller;
use \OAuth;
use \Apes\Creators\Account;
class ConnectSpec extends ObjectBehavior
{
function let(\FacebookConnectController $listener, \OAuth $facebookConnect, \Apes\Creators\Account $account)
{
$this->beConstructedWith($listener, $facebookConnect, $account);
}
function it_should_login_the_user($listener)
{
$input = ['code' => 'afacebooktoken'];
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
So here's the spec that I'm having issues with. First I pretend that I've got a facebook token already. Then, where things are failing, is that I need to fudge that the getFacebookUserData method will return a sample user that exists in my users table.
However when I run the test I get:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` not found.
I had hoped that 'willReturn' would just ignore whatever was happening in the getFacebookUserData method as I'm testing that separately, but it seems not.
Any recommendations on what I should be doing?
Do I need to pull all of the OAuth class methods into their own class or something? It seems strange to me that I might need to do that considering OAuth is already its own class. Is there some way to stub the method in getFacebookUserData?
Update 1
So I tried stubbing the method that's being called inside getFacebookUserData and my updated spec looks like this:
function it_should_login_the_user($listener, $facebookConnect)
{
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$input = ['code' => 'afacebooktoken'];
// Try stubbing any methods that are called in getFacebookUserData
$facebookConnect->requestAccessToken($input)->willReturn('alongstring');
$facebookConnect->request($input)->willReturn($returnCurrentUser);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
The spec still fails but the error has changed:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` is not defined.
Interestingly if I place these new stubs after the $this->getFacebookUserData stub then the error is 'not found' instead of 'not defined'. Clearly I don't fully understand the inner workings at hand :D
Not everything, called methods in your dependencies have to be mocked, because they will in fact be called while testing your classes:
...
$facebookConnect->requestAccessToken($input)->willReturn(<whatever it should return>);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
...
If you don't mock them, phpspec will raise a not found.
I'm not familiar with the classes involved but that error implies there is not method Oauth:: requestAccessToken().
Prophecy will not let you stub non-existent methods.