How to cache a JSON response in the Silverstripe Controller? - silverstripe

We have a Silverstripe 4 project which acts as a headless CMS returning a group of complex data models formatted as JSON.
Here's an example of the code:
class APIController extends ContentController
{
public function index(HTTPRequest $request)
{
$dataArray['model1'] = AccessPointController::getModel1();
$dataArray['model2'] = AccessPointController::getModel2();
$dataArray['model3'] = AccessPointController::getModel3();
$dataArray['model4'] = AccessPointController::getModel4();
$dataArray['model5'] = AccessPointController::getModel5();
$dataArray['model6'] = AccessPointController::getModel6();
$this->response->addHeader('Content-Type', 'application/json');
$this->response->addHeader('Access-Control-Allow-Origin', '*');
return json_encode($dataArray);
}
The problem we're having is the data models have got so complex the generation time for the JSON is running into seconds.
The JSON should only change when site content has been updates so ideally we'd like to cache the JSON & rather than dynamically generating it for each call.
What is the best way to cache the JSON in the above example?

Have you looked at the silverstripe docs about caching?
They do provide a programmatic way to store things in cache. And configuration options what back-ends are to be used to store the cache.
A simple example might be:
I've extended the cache live time here, but still you should note that this cache is not intended for storing generated static content, but rather to reduce load. Your application will still have to compute the api response every 86400 seconds (24 hours).
# app/_config/apiCache.yml
---
Name: apicacheconfig
---
# [... rest of your config config ...]
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.apiResponseCache:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "apiResponseCache"
defaultLifetime: 86400
<?php // app/src/FooController.php
class FooController extends \SilverStripe\Control\Controller {
public function getCache() {
return Injector::inst()->get('Psr\SimpleCache\CacheInterface.apiResponseCache');
// or your own cache (see below):
// return new MyCache();
}
protected function hasDataBeenChanged() {
// alternative to this method, you could also simply include Page::get()->max('LastEdited') or whatever in your cache key inside index(), but if you are using your own cache system, you need to handle deleting of old unused cache files. If you are using SilverStripe's cache, it will do that for you
$c = $this->getCache();
$lastCacheTime = $c->has('cacheTime') ? (int)$c->get('cacheTime') : 0;
$lastDataChangeTime = strtotime(Page::get()->max('LastEdited'));
return $lastDataChangeTime > $lastCacheTime;
}
public function index() {
$c = $this->getCache();
$cacheKey = 'indexActionResponse';
if ($c->has($cacheKey) && !$this->hasDataBeenChanged()) {
$data = $c->get($cacheKey);
} else {
$dataArray['model1'] = AccessPointController::getModel1();
$dataArray['model2'] = AccessPointController::getModel2();
$dataArray['model3'] = AccessPointController::getModel3();
$dataArray['model4'] = AccessPointController::getModel4();
$dataArray['model5'] = AccessPointController::getModel5();
$dataArray['model6'] = AccessPointController::getModel6();
$data = json_encode($dataArray);
$c->set($cacheKey, $data);
$c->set('cacheTime', time());
}
$this->response->addHeader('Content-Type', 'application/json');
$this->response->addHeader('Access-Control-Allow-Origin', '*');
return json_encode($dataArray);
}
}
If you are looking for a permanent/persistent cache, that will only ever update when data changed, I suggest you look for a different back-end or just implement a simple cache yourself and use that instead of the silverstripe cache.
class MyCache {
protected function fileName($key) {
if (strpos($key, '/') !== false || strpos($key, '\\') !== false) {
throw new \Exception("Invalid cache key '$key'");
}
return BASE_PATH . "/api-cache/$key.json";
}
public function get($key) {
if ($this->has($key)) {
return file_get_contents($this->fileName($key));
}
return null;
}
public function set($key, $val) {
file_put_contents($this->fileName($key), $val);
}
public function has($key) {
$f = $this->fileName($key);
return #file_exists($f);
}

Related

Conditionally displaying specific routes on OpenAPI (aka Swagger) documentation generated by API-Platform

I wish to limit the routes displayed by the API-Platform generated OpenAPI documentation based on each route's security attribute and the logged on user's roles (i.e. only ROLE_ADMIN can see the OpenApi documentation).
A similar question was earlier asked and this answer partially answered it but not completely:
This isn't supported out of the box (but it would be a nice
contribution). What you can do is to decorate the
DocumentationNormalizer to unset() the paths you don't want to appear
in the OpenAPI documentation.
More information:
https://api-platform.com/docs/core/swagger/#overriding-the-openapi-specification
It appears that DocumentationNormalizer is depreciated and that one should decorate OpenApiFactoryInterface instead.
Attempting to implement, I configured config/services.yaml to decorate OpenApiFactory as shown below.
The first issue is I am unable to "unset() the paths you don't want to appear". The paths exist within the \ApiPlatform\Core\OpenApi\Model\Paths property of \ApiPlatform\Core\OpenApi\OpenApi, but there is only the ability to add additional paths to Paths and not remove them. I've come up with a solution which is shown in the below code which creates new objects and only adds back properties if they do not require admin access, but I suspect that doing so is not the "right way" to do this. Also, I just realized while it removed the documentation from SwaggerUI's routes, it did not remove it from the Schema displayed below the routes.
The second issue is how to determine which paths to display, and I temporarily hardcoded them in my getRemovedPaths() method. First, I will need to add a logon form to the SwaggerUi page so that we know the user's role which is fairly straightforward. Next, however, I will need to obtain the security attributes associated with each route so that I could determine whether a given route should be displayed, however, I have no idea how to do so. I expected the necessary data to be in each ApiPlatform\Core\OpenApi\Model\PathItem, however, there does not appear to be any methods to retrieve it and the properties are private. I also attempted to access the information by using \App\Kernel::getContainer()->get('router'), but was not successful locating the route security attributes.
In summary, how should one prevent routes from being displayed by the API-Platform generated OpenAPI documentation if the user does not have authority to access the route?
config/services.yaml
services:
App\OpenApi\OpenApiFactory:
decorates: 'api_platform.openapi.factory'
arguments: [ '#App\OpenApi\OpenApiFactory.inner' ]
autoconfigure: false
App/OpenApi/OpenApiFactory
<?php
namespace App\OpenApi;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model\Paths;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Kernel;
class OpenApiFactory implements OpenApiFactoryInterface {
private $decorated, $tokenStorage, $kernel;
public function __construct(OpenApiFactoryInterface $decorated, TokenStorageInterface $tokenStorage, Kernel $kernel)
{
$this->decorated = $decorated;
$this->tokenStorage = $tokenStorage;
$this->kernel = $kernel;
}
public function __invoke(array $context = []): OpenApi
{
//$this->debug($context);
$openApi = $this->decorated->__invoke($context);
$removedPaths = $this->getRemovedPaths();
$paths = new Paths;
$pathArray = $openApi->getPaths()->getPaths();
foreach($openApi->getPaths()->getPaths() as $path=>$pathItem) {
if(!isset($removedPaths[$path])) {
// No restrictions
$paths->addPath($path, $pathItem);
}
elseif($removedPaths[$path]!=='*') {
// Remove one or more operation
foreach($removedPaths[$path] as $operation) {
$method = 'with'.ucFirst($operation);
$pathItem = $pathItem->$method(null);
}
$paths->addPath($path, $pathItem);
}
// else don't add this route to the documentation
}
$openApiTest = $openApi->withPaths($paths);
return $openApi->withPaths($paths);
}
private function getRemovedPaths():array
{
/*
Instead of hardcoding removed paths, remove all paths which $user does not have access to based on the route's security attributes and the user's credentials.
This hack returns an array with the path as the key, and either "*" to remove all operations or an array to remove specific operations.
*/
$user = $this->tokenStorage->getToken()->getUser();
return [
'/guids'=>'*', // Remove all operations
'/guids/{guid}'=>'*', // Remove all operations
'/accounts'=>['post'], // Remove only post operation
'/accounts/{uuid}'=>['delete'], // Remove only delete operation
];
}
private function debug(array $context = [])
{
$this->display($context, '$context');
$openApi = $this->decorated->__invoke($context);
$this->displayGetters($openApi);
$pathObject = $openApi->getPaths();
$this->displayGetters($pathObject, null, ['getPath', 'getPaths']);
$pathsArray = $pathObject->getPaths();
$this->display($pathsArray, '$openApi->getPaths()->getPaths()', true);
$pathItem = $pathsArray['/accounts'];
$this->displayGetters($pathItem);
$getGet = $pathItem->getGet();
$this->displayGetters($getGet, '$pathItem->getGet()', ['getResponses']);
$this->display($getGet->getTags(), '$getGet->getTags()');
$this->display($getGet->getParameters(), '$getGet->getParameters()');
$this->display($getGet->getSecurity(), '$getGet->getSecurity()');
$this->display($getGet->getExtensionProperties(), '$getGet->getExtensionProperties()');
$this->displayGetters($this->kernel, null, ['getBundles', 'getBundle']);
$container = $this->kernel->getContainer();
$this->displayGetters($container, null, ['getRemovedIds', 'getParameter', 'get', 'getServiceIds']);
$router = $container->get('router');
$this->displayGetters($router, null, ['getOption']);
$routeCollection = $router->getRouteCollection();
$this->displayGetters($routeCollection, null, ['get']);
$this->displayGetters($this, '$this');
$this->displayGetters($this->decorated, '$this->decorated');
$components = $openApi->getComponents ();
$this->displayGetters($components, null, []);
}
private function displayGetters($obj, ?string $notes=null, array $exclude=[])
{
echo('-----------------------------------------------------------'.PHP_EOL);
if($notes) {
echo($notes.PHP_EOL);
}
echo(get_class($obj).PHP_EOL);
echo('get_object_vars'.PHP_EOL);
print_r(array_keys(get_object_vars($obj)));
echo('get_class_methods'.PHP_EOL);
print_r(get_class_methods($obj));
foreach(get_class_methods($obj) as $method) {
if(substr($method, 0, 3)==='get') {
if(!in_array($method, $exclude)) {
$rs = $obj->$method();
$type = gettype($rs);
switch($type) {
case 'object':
printf('type: %s path: %s method: %s'.PHP_EOL, $type, $method, get_class($rs));
print_r(get_class_methods($rs));
break;
case 'array':
printf('type: %s method: %s'.PHP_EOL, $type, $method);
print_r($rs);
break;
default:
printf('type: %s method: %s, value: %s'.PHP_EOL, $type, $method, $rs);
}
}
else {
echo('Exclude method: '.$method.PHP_EOL);
}
}
}
}
private function display($rs, string $notes, bool $keysOnly = false)
{
echo('-----------------------------------------------------------'.PHP_EOL);
echo($notes.PHP_EOL);
print_r($keysOnly?array_keys($rs):$rs);
}
}

Symfony 3, detect browser language

I use Symfony 3.
My website is in 2 languages, French and English and people can switch via a select form.
Default language is French.
Main URL are:
example.com/fr for French version and example.com/en for English version
Well, now, I will like when the user arrives to the website to detect his browser language and redirect to the correct language automatically.
Exemple, if the browser is in French, he is redirected to the French version : example.com/fr
Else he is redirected to the English version: example.com/en
Is there a way to do that properly?
Thank you for your help
If you don't want to rely on other bundles like JMSI18nRoutingBundle
you have to make yourself familiar with Symfony's Event system, e.g. by reading up on the HttpKernel.
For your case you want to hook into the kernel.request event.
Typical Purposes: To add more information to the Request, initialize parts of the system, or return a Response if possible (e.g. a security layer that denies access).
In your custom EventListener you can listen to that event add information to the Request-object used in your router. It could look something like this:
class LanguageListener implements EventSubscriberInterface
{
private $supportedLanguages;
public function __construct(array $supportedLanguages)
{
if (empty($supportedLanguages)) {
throw new \InvalidArgumentException('At least one supported language must be given.');
}
$this->supportedLanguages = $supportedLanguages;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['redirectToLocalizedHomepage', 100],
];
}
public function redirectToLocalizedHomepage(GetResponseEvent $event)
{
// Do not modify sub-requests
if (KernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
// Assume all routes except the frontpage use the _locale parameter
if ($event->getRequest()->getPathInfo() !== '/') {
return;
}
$language = $this->supportedLanguages[0];
if (null !== $acceptLanguage = $event->getRequest()->headers->get('Accept-Language')) {
$negotiator = new LanguageNegotiator();
$best = $negotiator->getBest(
$event->getRequest()->headers->get('Accept-Language'),
$this->supportedLanguages
);
if (null !== $best) {
$language = $best->getType();
}
}
$response = new RedirectResponse('/' . $language);
$event->setResponse($response);
}
}
This listener will check the Accept-Language header of the request and use the Negotiation\LanguageNegotiator to determine the best locale. Be careful as I didn't add the use statements, but they should be fairly obvious.
For a more advanced version you can just read the source for the LocaleChoosingListener from JMSI18nRoutingBundle.
Doing this is usually only required for the frontpage, which is why both the example I posted and the one from the JMSBundle exclude all other paths. For those you can just use the special parameter _locale as described in the documentation:
https://symfony.com/doc/current/translation/locale.html#the-locale-and-the-url
The Symfony documentation also contains an example how to read the locale and make it sticky in a session using a Listener: https://symfony.com/doc/current/session/locale_sticky_session.html
This example also shows how to register the Listener in your services.yml.
Slight changes to #dbrumann's answer to work with my use case and setup:
List of available locales are defined in services.yml file:
parameters:
available_locales:
- nl
- en
- cs
I wanted to determine the locale on any landing page of the website. In case the parsing fails, it fallbacks to _locale parameter or the default one.
class LocaleDetermineSubscriber implements EventSubscriberInterface
{
private $defaultLocale;
private $parameterBag;
private $logger;
public function __construct(ParameterBagInterface $parameterBag,
LoggerInterface $logger,
$defaultLocale = 'en')
{
$this->defaultLocale = $defaultLocale;
$this->parameterBag = $parameterBag;
$this->logger = $logger;
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
//do this on first request only
if ($request->hasPreviousSession()) {
return;
}
$allowedLocales = $this->parameterBag->get('available_locales'); //defined in services.yml
$determinedLocale = null;
// use locale from the user preference header
$acceptLanguage = $event->getRequest()->headers->get('Accept-Language');
if ($acceptLanguage != null) {
$negotiator = new LanguageNegotiator();
try {
$best = $negotiator->getBest($acceptLanguage, $allowedLocales);
if ($best != null) {
$language = $best->getType();
$request->setLocale($language);
$determinedLocale = $language;
}
} catch (Exception $e) {
$this->logger->warning("Failed to determine language from Accept-Language header " . $e);
}
}
//check if locale is set with _locale parameter if user preference header parsing not happened
if($determinedLocale == null) {
if ($locale = $request->attributes->get('_locale')) {
if(in_array($locale, $allowedLocales)) {
$request->getSession()->set('_locale', $locale);
} else {
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
} else {
//fallback to default
$request->setLocale($this->defaultLocale);
}
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 25]],
];
}
}
It uses the willdurand/negotiation package, so it needs to be installed first:
composer require willdurand/negotiation
https://packagist.org/packages/willdurand/negotiation

Web Api Help Page XML comments from more than 1 files

I have different plugins in my Web api project with their own XML docs, and have one centralized Help page, but the problem is that Web Api's default Help Page only supports single documentation file
new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/Documentation.xml"))
How is it possible to load config from different files? I wan to do sth like this:
new XmlDocumentationProvider("PluginsFolder/*.xml")
You can modify the installed XmlDocumentationProvider at Areas\HelpPage to do something like following:
Merge multiple Xml document files into a single one:
Example code(is missing some error checks and validation):
using System.Xml.Linq;
using System.Xml.XPath;
XDocument finalDoc = null;
foreach (string file in Directory.GetFiles(#"PluginsFolder", "*.xml"))
{
if(finalDoc == null)
{
finalDoc = XDocument.Load(File.OpenRead(file));
}
else
{
XDocument xdocAdditional = XDocument.Load(File.OpenRead(file));
finalDoc.Root.XPathSelectElement("/doc/members")
.Add(xdocAdditional.Root.XPathSelectElement("/doc/members").Elements());
}
}
// Supply the navigator that rest of the XmlDocumentationProvider code looks for
_documentNavigator = finalDoc.CreateNavigator();
Kirans solution works very well. I ended up using his approach but by creating a copy of XmlDocumentationProvider, called MultiXmlDocumentationProvider, with an altered constructor:
public MultiXmlDocumentationProvider(string xmlDocFilesPath)
{
XDocument finalDoc = null;
foreach (string file in Directory.GetFiles(xmlDocFilesPath, "*.xml"))
{
using (var fileStream = File.OpenRead(file))
{
if (finalDoc == null)
{
finalDoc = XDocument.Load(fileStream);
}
else
{
XDocument xdocAdditional = XDocument.Load(fileStream);
finalDoc.Root.XPathSelectElement("/doc/members")
.Add(xdocAdditional.Root.XPathSelectElement("/doc/members").Elements());
}
}
}
// Supply the navigator that rest of the XmlDocumentationProvider code looks for
_documentNavigator = finalDoc.CreateNavigator();
}
I register the new provider from HelpPageConfig.cs:
config.SetDocumentationProvider(new MultiXmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/")));
Creating a new class and leaving the original one unchanged may be more convenient when upgrading etc...
Rather than create a separate class along the lines of XmlMultiDocumentationProvider, I just added a constructor to the existing XmlDocumentationProvider. Instead of taking a folder name, this takes a list of strings so you can still specify exactly which files you want to include (if there are other xml files in the directory that the Documentation XML are in, it might get hairy). Here's my new constructor:
public XmlDocumentationProvider(IEnumerable<string> documentPaths)
{
if (documentPaths.IsNullOrEmpty())
{
throw new ArgumentNullException(nameof(documentPaths));
}
XDocument fullDocument = null;
foreach (var documentPath in documentPaths)
{
if (documentPath == null)
{
throw new ArgumentNullException(nameof(documentPath));
}
if (fullDocument == null)
{
using (var stream = File.OpenRead(documentPath))
{
fullDocument = XDocument.Load(stream);
}
}
else
{
using (var stream = File.OpenRead(documentPath))
{
var additionalDocument = XDocument.Load(stream);
fullDocument?.Root?.XPathSelectElement("/doc/members").Add(additionalDocument?.Root?.XPathSelectElement("/doc/members").Elements());
}
}
}
_documentNavigator = fullDocument?.CreateNavigator();
}
The HelpPageConfig.cs looks like this. (Yes, it can be fewer lines, but I don't have a line limit so I like splitting it up.)
var xmlPaths = new[]
{
HttpContext.Current.Server.MapPath("~/bin/Path.To.FirstNamespace.XML"),
HttpContext.Current.Server.MapPath("~/bin/Path.To.OtherNamespace.XML")
};
var documentationProvider = new XmlDocumentationProvider(xmlPaths);
config.SetDocumentationProvider(documentationProvider);
I agree with gurra777 that creating a new class is a safer upgrade path. I started with that solution but it involves a fair amount of copy/pasta, which could easily get out of date after a few package updates.
Instead, I am keeping a collection of XmlDocumentationProvider children. For each of the implementation methods, I'm calling into the children to grab the first non-empty result.
public class MultiXmlDocumentationProvider : IDocumentationProvider, IModelDocumentationProvider
{
private IList<XmlDocumentationProvider> _documentationProviders;
public MultiXmlDocumentationProvider(string xmlDocFilesPath)
{
_documentationProviders = new List<XmlDocumentationProvider>();
foreach (string file in Directory.GetFiles(xmlDocFilesPath, "*.xml"))
{
_documentationProviders.Add(new XmlDocumentationProvider(file));
}
}
public string GetDocumentation(System.Reflection.MemberInfo member)
{
return _documentationProviders
.Select(x => x.GetDocumentation(member))
.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
}
//and so on...
The HelpPageConfig registration is the same as in gurra777's answer,
config.SetDocumentationProvider(new MultiXmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/")));

Laravel 4 Model Events don't work with PHPUnit

I build a model side validation in Laravel 4 with the creating Model Event :
class User extends Eloquent {
public function isValid()
{
return Validator::make($this->toArray(), array('name' => 'required'))->passes();
}
public static function boot()
{
parent::boot();
static::creating(function($user)
{
echo "Hello";
if (!$user->isValid()) return false;
});
}
}
It works well but I have issues with PHPUnit. The two following tests are exactly the same but juste the first one pass :
class UserTest extends TestCase {
public function testSaveUserWithoutName()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // pass
assertEquals($count, User::all()->count()); // pass
}
public function testSaveUserWithoutNameBis()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // fail
assertEquals($count, User::all()->count()); // fail, the user is created
}
}
If I try to create a user twice in the same test, it works, but it's like if the binding event is present only in the first test of my test class. The echo "Hello"; is printed only one time, during the first test execution.
I simplify the case for my question but you can see the problem : I can't test several validation rules in different unit tests. I try almost everything since hours but I'm near to jump out the windows now ! Any idea ?
The issue is well documented in Github. See comments above that explains it further.
I've modified one of the 'solutions' in Github to automatically reset all model events during the tests. Add the following to your TestCase.php file.
app/tests/TestCase.php
public function setUp()
{
parent::setUp();
$this->resetEvents();
}
private function resetEvents()
{
// Get all models in the Model directory
$pathToModels = '/app/models'; // <- Change this to your model directory
$files = File::files($pathToModels);
// Remove the directory name and the .php from the filename
$files = str_replace($pathToModels.'/', '', $files);
$files = str_replace('.php', '', $files);
// Remove "BaseModel" as we dont want to boot that moodel
if(($key = array_search('BaseModel', $files)) !== false) {
unset($files[$key]);
}
// Reset each model event listeners.
foreach ($files as $model) {
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Reregister them.
call_user_func(array($model, 'boot'));
}
}
I have my models in subdirectories so I edited #TheShiftExchange code a bit
//Get all models in the Model directory
$pathToModels = '/path/to/app/models';
$files = File::allFiles($pathToModels);
foreach ($files as $file) {
$fileName = $file->getFileName();
if (!ends_with($fileName, 'Search.php') && !starts_with($fileName, 'Base')) {
$model = str_replace('.php', '', $fileName);
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Re-register them.
call_user_func(array($model, 'boot'));
}
}

Symfony2 set class variable with init or construct methods

Have recently been using Symfony2 after using ZF for some time.
I am having problems trying to do something relatively simple, I think.
The following code is within a controller:
private $current_setid = "";
public function __construct() {
$current_set = $this->getCurrentSet();
if ($current_set == "") {
return $this->redirect($this->generateUrl('selectset'));
}
$this->current_setid = $current_set;
}
public function getCurrentSet() {
$session = $this->get("session");
$set = $session->get('set');
return $set;
}
public function setCurrentSet($setid) {
$session = $this->get("session");
$session->set('set', "$setid");
}
If I use __construct() I get errors like:
Fatal error: Call to a member function get() on a non-object in
I have tried using __init() and init() both of which do not seem to get called.
Can anyone point me in the right direction? Is there a simple way to do this or do I have to look into event listeners?
Have you tried getting your session like they do in official documentation?
$session = $this->getRequest()->getSession();
$foo = $session->get('foo');
Basically get fetch dependencies from container and container in the Controller is injected using setter dependency injection. You just not have container in the time of __construct yet.
Just ended up opting for placing a check in every method in the class. Seems silly to have to do that but I find I often have to do that in Symfony2 with the lack of init, postDispatch type methods like ZF has.
Even trying to remove the check to another method was counter productive as I still had to check the return from that method as $this->redirect does not seem to work unless it is within an Action method. For example:
public function isSetSet() {
$current_set = $this->getCurrentSet();
if ($current_set == "") {
$url = $this->generateUrl('selectset');
return $this->redirect($url);
}
return TRUE;
}
public function someAction() {
$check = $this->isSetSet();
if($check != TRUE){
return $check;
}
...
}
So each method needs that 4 line check but the whole check can be done in 4 lines anyway so no need for that extra method:
public function anotherAction() {
$current_setid = $this->getCurrentSet();
if ($current_setid == "") {
return $this->redirect($this->generateUrl('selectset'));
}
...
}

Resources