Symfony run code after response was sent - php

I took a look at this other question. I am looking for a way to do what the OP of that question wants as well, and that is to continue processing php after sending http response, but in Symfony2.
I implemented an event that fires after every kernel termination. So far so good, but what I want is for it to fire after CERTAIN terminations, in specific controller actions, for instance after a form was sent, not every single time at every request. That is because I want to do some heavy tasks at certain times and don't want the end user to wait for the page to load.
Any idea how can I do that?
<?php
namespace MedAppBundle\Event;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\Tag;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use JMS\DiExtraBundle\Annotation\Inject;
/**
* Class MedicListener
* #package MedAppBundle\EventListener
* #Service("medapp_test.listener")
* #Tag(name="kernel.event_subscriber")
*/
class TestListener implements EventSubscriberInterface
{
private $container;
private $logger;
/**
* Constructor.
*
* #param ContainerInterface $container A ContainerInterface instance
* #param LoggerInterface $logger A LoggerInterface instance
* #InjectParams({
* "container" = #Inject("service_container"),
* "logger" = #Inject("logger")
* })
*/
public function __construct(ContainerInterface $container, LoggerInterface $logger = null)
{
$this->container = $container;
$this->logger = $logger;
}
public function onTerminate()
{
$this->logger->notice('fired');
}
public static function getSubscribedEvents()
{
$listeners = array(KernelEvents::TERMINATE => 'onTerminate');
if (class_exists('Symfony\Component\Console\ConsoleEvents')) {
$listeners[ConsoleEvents::TERMINATE] = 'onTerminate';
}
return $listeners;
}
}
So far I've subscribed the event to the kernel.terminate one, but obviously this fires it at each request. I made it similar to Swiftmailer's EmailSenderListener
It feels kind of strange that the kernel must listen each time for this event even when it's not triggered. I'd rather have it fired only when needed, but not sure how to do that.

In the onTerminate callback you get an instance of PostResponseEvent as first parameter. You can get the Request as well as the Response from that object.
Then you should be able to decide if you want to run the actual termination code.
Also you can store custom data in the attributes bag of the Request. See this link: Symfony and HTTP Fundamentals
The Request class also has a public attributes property, which holds special data related to how the application works internally. For the Symfony Framework, the attributes holds the values returned by the matched route, like _controller, id (if you have an {id} wildcard), and even the name of the matched route (_route). The attributes property exists entirely to be a place where you can prepare and store context-specific information about the request.
Your code could look something like this:
// ...
class TestListener implements EventSubscriberInterface
{
// ...
public function onTerminate(PostResponseEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->get('_route') == 'some_route_name') {
// do stuff
}
}
// ...
}
Edit:
The kernel.terminate event is designed to run after the response is sent. But the symfony documentation is saying the following (taken from here):
Internally, the HttpKernel makes use of the fastcgi_finish_request PHP function. This means that at the moment, only the PHP FPM server API is able to send a response to the client while the server's PHP process still performs some tasks. With all other server APIs, listeners to kernel.terminate are still executed, but the response is not sent to the client until they are all completed.
Edit 2:
To use the solution from here, you could either directly edit the web/app.php file to add it there (but this is some kind of "hacking core" imo, even though it would be easier to use than the following). Or you could do it like this:
Add a listener to kernel.request event with a high priority and start output buffering (ob_start).
Add a listener to kernel.response and add the header values to the response.
Add another listener with highest priority to kernel.terminate and do the flushing (ob_flush, flush).
Run your code in a separate listener with lower priority to kernel.terminate
I did not try it, but it should actually work.

To solve this issue for some of my use cases I simply create symfony commands to do the heavy tasks, and call them via exec() to make them run in a separate process.

I used these answers to write a Response class that has this functionality:
https://stackoverflow.com/a/28738208/1153227
This implementation will work on Apache and not just PHP FPM. However, to make this work we must prevent Apache from using gzip (by using an invalid Content-Encoding) so it makes sense to have a custom Response class to specify exactly when having an early response is more important than compression.
use Symfony\Component\HttpFoundation\Response;
class EarlyResponse extends Response
{
// Functionality adapted from this answer: https://stackoverflow.com/a/7120170/1153227
protected $callback = null;
/**
* Constructor.
*
* #param mixed $content The response content, see setContent()
* #param int $status The response status code
* #param array $headers An array of response headers
*
* #throws \InvalidArgumentException When the HTTP status code is not valid
*/
public function __construct($content = '', $status = 200, $headers = array(), $callback = null)
{
if (null !== $callback) {
$this->setTerminateCallback($callback);
}
parent::__construct($content, $status, $headers);
}
/**
* Sets the PHP callback associated with this Response.
* It will be called after the terminate events fire and thus after we've sent our response and closed the connection
*
* #param callable $callback A valid PHP callback
*
* #throws \LogicException
*/
public function setTerminateCallback($callback)
{
//Copied From Symfony\Component\HttpFoundation\StreamedResponse
if (!is_callable($callback)) {
throw new \LogicException('The Response callback must be a valid PHP callable.');
}
$this->callback = $callback;
}
/**
* #return Current_Class_Name
*/
public function send() {
if (function_exists('fastcgi_finish_request') || 'cli' === PHP_SAPI) { // we don't need the hack when using fast CGI
return parent::send();
}
ignore_user_abort(true);//prevent apache killing the process
if (!ob_get_level()) { // Check if an ob buffer exists already.
ob_start();//start the output buffer
}
$this->sendContent(); //Send the content to the buffer
static::closeOutputBuffers(1, true); //flush all but the last ob buffer level
$this->headers->set('Content-Length', ob_get_length()); // Set the content length using the last ob buffer level
$this->headers->set('Connection', 'close'); // Close the Connection
$this->headers->set('Content-Encoding', 'none');// This invalid header value will make Apache not delay sending the response while it is
// See: https://serverfault.com/questions/844526/apache-2-4-7-ignores-response-header-content-encoding-identity-instead-respect
$this->sendHeaders(); //Now that we have the headers, we can send them (which will avoid the ob buffers)
static::closeOutputBuffers(0, true); //flush the last ob buffer level
flush(); // After we flush the OB buffer to the normal buffer, we still need to send the normal buffer to output
session_write_close();//close session file on server side to avoid blocking other requests
return $this;
}
/**
* #return Current_Class_Name
*/
public function callTerminateCallback() {
if ($this->callback) {
call_user_func($this->callback);
}
return $this;
}
}
You also need to add a method to your AppKernel.php to make this work (don't forget to add a use statement for your EarlyResponse class)
public function terminate(Request $request, Response $response)
{
ob_start();
//Run this stuff before the terminate events
if ($response instanceof EarlyResponse) {
$response->callTerminateCallback();
}
//Trigger the terminate events
parent::terminate($request, $response);
//Optionally, we can output the beffer that will get cleaned to a file before discarding its contents
//file_put_contents('/tmp/process.log', ob_get_contents());
ob_end_clean();
}

Related

How to create global object Laravel

I'm using Laravel 5.6 and Instagram API library.
To work with this Instagram API I need to create object $ig = new \InstagramAPI\Instagram(). And then for getting any user's information I must use $ig->login('username', 'password') every time.
So I don't want to use this function all the time. The first I want to create a global variable which will contain $ig = new \InstagramAPI\Instagram(). However, I don't know how to correctly do it.
I tried to use singleton:
$this->app->singleton(Instagram::class, function ($app) {
Instagram::$allowDangerousWebUsageAtMyOwnRisk = true; // As wiki says
return new Instagram();
});
When I called $ig->login('name', 'pass') in any method all user profile's information changed in this object, but then if I call dd($ig = app(Instagram::class)) in another Controller method I see that previous data did not save. "WTF?" - I said.
Someone tells me that singleton just promise me that there won't be created the same object, but it does not save any changes.
I tried to use sessions:
However, when I tried to set variable with object as value anything did not happen.
$ig = new \InstagramAPI\Instagram();
session(['ig' => $ig]);
I think it's because of I tried to put a large object. And from the other hand it's not secure method!
Just let me know:
How can I create an object which I could use in every method with saving change for the next actions?
When I called $ig->login('name', 'pass') in any method all user profile's information changed in this object, but then if I call dd($ig = app(Instagram::class)) in another Controller method I see that previous data did not save.
That is the correct behavior. When a new request is sent to Laravel, a new instance of the Instagram is created. I'm not sure if you understand the meaning of a singleton but in terms of Laravel, there is one instance per HTTP request.
Since the Instagram API you're using does not contain functionality to relogin, I created a class (that would be place in the app/Classes folder).
<?php
namespace App\Classes;
use InstagramAPI\Instagram;
use InstagramAPI\Response\LoginResponse;
class CustomInstagram extends Instagram {
public function relogin(LoginResponse $response) {
$appRefreshInterval = 1800;
$this->_updateLoginState($response);
$this->_sendLoginFlow(true, $appRefreshInterval);
return $this;
}
}
Change the singleton instance so it uses the App\Classes\CustomInstagram class.
$this->app->singleton(Instagram::class, function ($app) {
Instagram::$allowDangerousWebUsageAtMyOwnRisk = true; // As wiki says
return new App\Classes\CustomInstagram();
});
In order to use the Instagram object with an authenticated user, the login information will need to be persisted some how. This would be placed where the login is occurring.
try {
$response = app(Instagram::class)->login($username, $password);
if ($response->isTwoFactorRequired()) {
// Need to handle if 2fa is needed (we're not completely logged in yet)
}
// Can use session to persist \InstagramAPI\Response\LoginResponse but I'd recommend the database.
session(['igLoginResponse' => serialize($response)]);
} catch (\Exception $e) {
// Login failed
}
Then create a middleware to relogin the user to Instagram (if the login response exists). You need to register this as described here. Then, the Instagram singleton can be used in your controller.
namespace App\Http\Middleware;
use Closure;
use InstagramAPI\Instagram;
class InstagramLogin
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$responseSerialized = session('igLoginResponse');
if (!is_null($responseSerialized)) {
$ig = app(Instagram::class);
$response = unserialize($responseSerialized);
$ig->relogin($response);
}
return $next($request);
}
}

How to get the Mink Selenium 2 Driver to wait for the page to load with Behat

I am in the process of upgrading from the Behat 2.x series to the Behat 3.x series. In the prior version I could load the Selenium 1 driver, which attached to PhantomJS to execute tests. When I did this I was able to hook into a function called waitForPageToLoad().
This function was provided by php-selenium (from Alexandre Salomé). It hooked into selenium and called a driver action by the same name. This worked perfectly for ensuring that Selenium waited for a page to load. At least until a timeout was reached. It made tests to go much faster.
The problem is that the Selenium 1 driver is not compatible with Behat 3.x. It looks like it has been all but abandoned and I don't see that functionality in the Selenium 2 driver for Mink.
Does anyone know of a way to make this work with Behat 3.x and Selenium 2?
Using Selenium (or any other driver for that matter), I've never had to worry about whether the page has loaded or not, with one exception: if the page finishes loading, then loads more content via AJAX.
To handle this, you can use a spin function as documented in the Behat Manual.
http://docs.behat.org/en/v2.5/cookbook/using_spin_functions.html
The benefits of this are:
It doesn't need you to use the selenium driver (for example, you could use PhantomJS if you want speed over looks).
It won't break if you stop using jQuery and switch to something else (such as Angular's $httpProvider)
I wouldn't use theirs though, the back trace is broken and who want's to wait a second between checks anyway. :)
Try this:
Assuming you are using the Mink Context (thanks Mick), you can simply check the page every second or so until the desired
text has either appeared or dissapeared, or a given timeout has expired in which case we'd assume a fail.
/**
* #When I wait for :text to appear
* #Then I should see :text appear
* #param $text
* #throws \Exception
*/
public function iWaitForTextToAppear($text)
{
$this->spin(function(FeatureContext $context) use ($text) {
try {
$context->assertPageContainsText($text);
return true;
}
catch(ResponseTextException $e) {
// NOOP
}
return false;
});
}
/**
* #When I wait for :text to disappear
* #Then I should see :text disappear
* #param $text
* #throws \Exception
*/
public function iWaitForTextToDisappear($text)
{
$this->spin(function(FeatureContext $context) use ($text) {
try {
$context->assertPageContainsText($text);
}
catch(ResponseTextException $e) {
return true;
}
return false;
});
}
/**
* Based on Behat's own example
* #see http://docs.behat.org/en/v2.5/cookbook/using_spin_functions.html#adding-a-timeout
* #param $lambda
* #param int $wait
* #throws \Exception
*/
public function spin($lambda, $wait = 60)
{
$time = time();
$stopTime = $time + $wait;
while (time() < $stopTime)
{
try {
if ($lambda($this)) {
return;
}
} catch (\Exception $e) {
// do nothing
}
usleep(250000);
}
throw new \Exception("Spin function timed out after {$wait} seconds");
}
Selenium2 now has the wait($timeout, $condition) function.
You can use it like:
/**
* #Then /^I wait for the ajax response$/
*/
public function iWaitForTheAjaxResponse()
{
$this->getSession()->wait(5000, '(0 === jQuery.active)');
}
Other conditions that you could test for are:
the appearance of a certain element on the page
the DOM to finish loading
The reason for the change is outlined on the selenium website documentation
In order to help someone else, I added this method in FeatureContext.php :
/**
* #Then I wait :sec
*/
public function wait($sec)
{
sleep($sec);
}
And it's working
Will
/**
* #Then /^I wait for the ajax response$/
*/
public function iWaitForTheAjaxResponse()
{
$this->getSession()->wait(5000, '(0 === jQuery.active)');
}
it's working

Communicating between a C- and a PHP-Program

I'm writing a program for my Raspberry Pi, that consists of two main parts:
A C-Program that uses the Spotify-API "Libspotify" to search for music and for playing it.
A PHP-program, running on an apache2-Webserver, to control the Spotify-program from a PC or Smartphone in my local network.
The communication between these two separate programs works over a couple of files.
The C-part for receiving the user inputs is being called once a second and works like this:
void get_input() {
int i = 0, c;
FILE *file;
struct stat stat;
char buffer[INPUT_BUFFER_SIZE];
file = fopen(PATH_TO_COMMUNICATION, "r");
if (file == NULL) {
perror("Error opening PATH_TO_COMMUNICATION");
return;
}
fstat(fileno(file), &stat);
if (stat.st_size > 1) {
while((c = fgetc(file)) != EOF && i < INPUT_BUFFER_SIZE) {
buffer[i] = c;
++i;
}
buffer[i] = '\0';
parse_input(buffer);
}
fclose(file);
clear_file(PATH_TO_COMMUNICATION);
}
So, via fwrite() in PHP, I can send commands to the C-program.
Afterwards, the C-Program parses the input and writes the results in an "results"-file. Once this is done, I write the last contents of the "communication"-file an "last_query"-file, so in PHP i can see, when the entire results are written in the "results":
function search($query) {
write_to_communication("search ".$query);
do { // Wait for results
$file = fopen("../tmp/last_query", "r");
$line = fgets($file);
fclose($file);
time_nanosleep(0, 100000000);
} while($line != $query);
echo get_result_json();
}
It does work already, but I don't like the way at all. There is a lot of polling and unneccessary opening and closing of different files. Also, in the worst case the program needs more than a second until it starts to do something with the user input. Furthermore, there can be race conditions, when the C-program tries to read the file during the PHP-program writes into it.
So, now to my question: What is the "right" way, to achieve a nice and clean communication between the two program parts? Is there some completely different way without the ugly polling and without race conditions? Or can I improve the existing code, so that it gets nicer?
I suppose that you write both the PHP and the C code yourself, and that you have access to compilers and so on? What I would do is not at all start up a different process and use inter-process-communication, but write a PHP C++ extension that does this all for you.
The extension starts up a new thread, and this thread picks up all instruction from an in-memory instruction queue. When you're ready to pick up the result, the final instruction is sent to the thread (the instruction to close down), and when the thread is finally finished, you can pick up the result.
You could use a program like this:
#include <phpcpp.h>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <unistd.h>
/**
* Class that takes instructions from PHP, and that executes them in CPP code
*/
class InstructionQueue : public Php::Base
{
private:
/**
* Queue with instructions (for simplicity, we store the instructions
* in strings, much cooler implementations are possible)
*
* #var std::queue
*/
std::queue<std::string> _queue;
/**
* The final result
* #var std::string
*/
std::string _result;
/**
* Counter with number of instructions
* #var int
*/
int _counter = 0;
/**
* Mutex to protect shared data
* #var std::mutex
*/
std::mutex _mutex;
/**
* Condition variable that the thread uses to wait for more instructions
* #var std::condition_variable
*/
std::condition_variable _condition;
/**
* Thread that processes the instructions
* #var std::thread
*/
std::thread _thread;
/**
* Procedure to execute one specific instruction
* #param instruction
*/
void execute(const std::string &instruction)
{
// #todo
//
// add your own the implementation, for now we just
// add the instruction to the result, and sleep a while
// to pretend that this is a difficult algorithm
// append the instruction to the result
_result.append(instruction);
_result.append("\n");
// sleep for a while
sleep(1);
}
/**
* Main procedure that runs the thread
*/
void run()
{
// need the mutex to access shared resources
std::unique_lock<std::mutex> lock(_mutex);
// keep looping
while (true)
{
// go wait for instructions
while (_counter == 0) _condition.wait(lock);
// check the number of instructions, leap out when empty
if (_queue.size() == 0) return;
// get instruction from the queue, and reduce queue size
std::string instruction(std::move(_queue.front()));
// remove front item from queue
_queue.pop();
// no longer need the lock
lock.unlock();
// run the instruction
execute(instruction);
// get back the lock for the next iteration of the main loop
lock.lock();
}
}
public:
/**
* C++ constructor
*/
InstructionQueue() : _thread(&InstructionQueue::run, this) {}
/**
* Copy constructor
*
* We just create a brand new queue when it is copied (copy constructor
* is required by the PHP-CPP library)
*
* #param queue
*/
InstructionQueue(const InstructionQueue &queue) : InstructionQueue() {}
/**
* Destructor
*/
virtual ~InstructionQueue()
{
// stop the thread
stop();
}
/**
* Method to add an instruction
* #param params Object representing PHP parameters
*/
void add(Php::Parameters &params)
{
// first parameter holds the instruction
std::string instruction = params[0];
// need a mutex to access shared resources
_mutex.lock();
// add instruction
_queue.push(instruction);
// update instruction counter
_counter++;
// done with shared resources
_mutex.unlock();
// notify the thread
_condition.notify_one();
}
/**
* Method to stop the thread
*/
void stop()
{
// is the thread already finished?
if (!_thread.joinable()) return;
// thread is still running, send instruction to stop (which is the
// same as not sending an instruction at all but just increasing the
// instruction counter, lock mutex to access protected data
_mutex.lock();
// add instruction
_counter++;
// done with shared resources
_mutex.unlock();
// notify the thread
_condition.notify_one();
// wait for the thread to finish
_thread.join();
}
/**
* Retrieve the result
* #return string
*/
Php::Value result()
{
// stop the thread first
stop();
// return the result
return _result;
}
};
/**
* Switch to C context to ensure that the get_module() function
* is callable by C programs (which the Zend engine is)
*/
extern "C" {
/**
* Startup function that is called by the Zend engine
* to retrieve all information about the extension
* #return void*
*/
PHPCPP_EXPORT void *get_module() {
// extension object
static Php::Extension myExtension("InstructionQueue", "1.0");
// description of the class so that PHP knows
// which methods are accessible
Php::Class<InstructionQueue> myClass("InstructionQueue");
// add methods
myClass.method("add", &InstructionQueue::add);
myClass.method("result", &InstructionQueue::result);
// add the class to the extension
myExtension.add(std::move(myClass));
// return the extension
return myExtension;
}
}
You can use this instruction queue from a PHP script like this:
<?php
$queue = new InstructionQueue();
$queue->add("instruction 1");
$queue->add("instruction 2");
$queue->add("instruction 3");
echo($queue->result());
As an example I've only added a silly implementation that sleeps for a while, but you could run your API-calling functions in there.
The extension uses the PHP-CPP library (see http://www.php-cpp.com).

filter methods in a controller

I want to create a filter for my add, update, and delete actions in my controllers to automatically check if they
were called in a POST, as opposed to GET or some other method
and have the pageInstanceIDs which I set in the forms on my views
protects against xss
protects against double submission of a form
from submit button double click
from back button pressed after a submision
from a url being saved or bookmarked
Currently I extended \lithium\action\Controller using an AppController and have my add, update, and delete actions defined in there.
I also have a boolean function in my AppController that checks if the appropriate pageInstanceIDs are in session or not.
Below is my code:
public function isNotPostBack() {
// pull in the session
$pageInstanceIDs = Session::read('pageInstanceIDs');
$pageInstanceID = uniqid('', true);
$this->set(compact('pageInstanceID'));
$pageInstanceIDs[] = $pageInstanceID;
Session::write('pageInstanceIDs', $pageInstanceIDs);
// checks if this is a save operation
if ($this->request->data){
$pageInstanceIDs = Session::read('pageInstanceIDs');
$pageIDIndex = array_search($this->request->data['pageInstanceID'], $pageInstanceIDs);
if ($pageIDIndex !== false) {
// remove the key
unset($pageInstanceIDs[$pageIDIndex]);
Session::write('pageInstanceIDs', $pageInstanceIDs);
return true;
}
else
return false;
} else {
return true;
}
}
public function add() {
if (!$this->request->is('post') && exist($this->request->data())) {
$msg = "Add can only be called with http:post.";
throw new DispatchException($msg);
}
}
Then in my controllers I inherit from AppController and implement the action like so:
public function add() {
parent::add();
if (parent::isNotPostBack()){
//do work
}
return $this->render(array('layout' => false));
}
which will ensure that the form used a POST and was not double submitted (back button or click happy users). This also helps protect against XSS.
I'm aware there is a plugin for this, but I want to implement this as a filter so that my controller methods are cleaner. Implented this way, the only code in my actions are the //do work portion and the return statement.
You should probably start with a filter on lithium\action\Dispatcher::run() here is some pseudo code. Can't help too much without seeing your parent::isNotPostBack() method but this should get you on the right track.
<?php
use lithium\action\Dispatcher;
Dispatcher::applyFilter('run', function($self, $params, $chain) {
$request = $params['request'];
// Request method is in $request->method
// Post data is in $request->data
if($not_your_conditions) {
return new Response(); // set up your custom response
}
return $chain->next($self, $params, $chain); // to continue on the path of execution
});
First of all, use the integrated CSRF (XSRF) protection.
The RequestToken class creates cryptographically-secure tokens and keys that can be used to validate the authenticity of client requests.
— http://li3.me/docs/lithium/security/validation/RequestToken
Check the CSRF token this way:
if ($this->request->data && !RequestToken::check($this->request)) {
/* do your stuff */
}
You can even check the HTTP method used via is()
$this->request->is('post');
The problem of filters (for that use case) is that they are very generic. So if you don't want to write all your actions as filterable code (which might be painful and overkill), you'll have to find a way to define which method blocks what and filter the Dispatcher::_call.
For CSRF protection, I use something similar to greut's suggestion.
I have this in my extensions/action/Controller.php
protected function _init() {
parent::_init();
if ($this->request->is('post') ||
$this->request->is('put') ||
$this->request->is('delete')) {
//on add, update and delete, if the security token exists, we will verify the token
if ('' != Session::read('security.token') && !RequestToken::check($this->request)) {
RequestToken::get(array('regenerate' => true));
throw new DispatchException('There was an error submitting the form.');
}
}
}
Of course, this means you'd have to also add the following to the top of your file:
use \lithium\storage\Session;
use lithium\security\validation\RequestToken;
use lithium\action\DispatchException;
With this, I don't have to repeatedly check for CSRF.
I implemented something similar in a recent project by subclassing \lithium\action\Controller as app\controllers\ApplicationController (abstract) and applying filters to invokeMethod(), as that's how the dispatcher invokes the action methods. Here's the pertinent chunk:
namespace app\controllers;
class ApplicationController extends \lithium\action\Controller {
/**
* Essential because you cannot invoke `parent::invokeMethod()` from within the closure passed to `_filter()`... But it makes me sad.
*
* #see \lithium\action\Controller::invokeMethod()
*
* #param string $method to be invoked with $arguments
* #param array $arguments to pass to $method
*/
public function _invokeMethod($method, array $arguments = array()) {
return parent::invokeMethod($method, $arguments);
}
/**
* Overridden to make action methods filterable with `applyFilter()`
*
* #see \lithium\action\Controller::invokeMethod()
* #see \lithium\core\Object::applyFilter()
*
* #param string $method to be invoked with $arguments
* #param array $arguments to pass to $method
*/
public function invokeMethod($method, array $arguments = array()) {
return $this->_filter(__METHOD__, compact('method', 'arguments'), function($self, $params){
return $self->_invokeMethod($params['method'], $params['arguments']);
});
}
}
Then you can use applyFilter() inside of _init() to run filters on your method. Instead of checking $method in every filter, you can opt to change _filter(__METHOD__, . . .) to _filter($method, . . .), but we chose to keep the more generic filter.

Kohana 3.2. - How can I use hyphens in URIs

Recently I've been doing some research into SEO and how URIs that use hyphens or underscores are treated differently, particularly by Google who view hyphens as separators.
Anyway, eager to adapt my current project to meet this criteria I found that because Kohana uses function names to define pages I was receiving the unexpected '-' warning.
I was wondering whether there was any way to enable the use of URIs in Kohana like:
http://www.mysite.com/controller/function-name
Obviously I could setup a routeHandler for this... but if I was to have user generated content, i.e. news. I'd then have to get all articles from the database, produce the URI, and then do the routing for each one.
Are there any alternative solutions?
Note: This is the same approach as in Laurent's answer, just slightly more OOP-wise. Kohana allows one to very easily overload any system class, so we can use it to save us some typing and also to allow for cleaner updates in the future.
We can plug-in into the request flow in Kohana and fix the dashes in the action part of the URL. To do it we will override Request_Client_Internal system class and it's execute_request() method. There we'll check if request->action has dashes, and if so we'll switch them to underscores to allow php to call our method properly.
Step 1. Open your application/bootstrap.php and add this line:
define('URL_WITH_DASHES_ONLY', TRUE);
You use this constant to quickly disable this feature on some requests, if you need underscores in the url.
Step 2. Create a new php file in: application/classes/request/client/internal.php and paste this code:
<?php defined('SYSPATH') or die('No direct script access.');
class Request_Client_Internal extends Kohana_Request_Client_Internal {
/**
* We override this method to allow for dashes in the action part of the url
* (See Kohana_Request_Client_Internal::execute_request() for the details)
*
* #param Request $request
* #return Response
*/
public function execute_request(Request $request)
{
// Check the setting for dashes (the one set in bootstrap.php)
if (defined('URL_WITH_DASHES_ONLY') and URL_WITH_DASHES_ONLY == TRUE)
{
// Block URLs with underscore in the action to avoid duplicated content
if (strpos($request->action(), '_') !== false)
{
throw new HTTP_Exception_404('The requested URL :uri was not found on this server.', array(':uri' => $request->uri()));
}
// Modify action part of the request: transform all dashes to underscores
$request->action( strtr($request->action(), '-', '_') );
}
// We are done, let the parent method do the heavy lifting
return parent::execute_request($request);
}
} // end_class Request_Client_Internal
What this does is simply replacing all the dashes in the $request->action with underscores, thus if url was /something/foo-bar, Kohana will now happily route it to our action_foo_bar() method.
In the same time we block all the actions with underscores, to avoid the duplicated content problems.
No way to directly map a hyphenated string to a PHP function so you will have to do routing.
As far as user generated content, you could do something like Stack Exchange does. Each time user content is saved to the database, generated a slug for it (kohana-3-2-how-can-i-use-hyphens-in-uris) and save it along with the other information. Then when you need to link to it, use the unique id and append the slug to the end (ex:http://stackoverflow.com/questions/7404646/kohana-3-2-how-can-i-use-hyphens-in-uris) for readability.
You can do this with lambda functions: http://forum.kohanaframework.org/discussion/comment/62581#Comment_62581
You could do something like
Route::set('route', '<controller>/<identifier>', array(
'identifier' => '[a-zA-Z\-]*'
))
->defaults(array(
'controller' => 'Controller',
'action' => 'show',
));
Then receive your content identifier in the function with Request::current()->param('identifier') and parse it manually to find the relating data.
After having tried various solutions, I found that the easiest and most reliable way is to override Kohana_Request_Client_Internal::execute_request. To do so, add a file in your application folder in "application\classes\kohana\request\client\internal.php" then set its content to:
<?php defined('SYSPATH') or die('No direct script access.');
class Kohana_Request_Client_Internal extends Request_Client {
/**
* #var array
*/
protected $_previous_environment;
/**
* Processes the request, executing the controller action that handles this
* request, determined by the [Route].
*
* 1. Before the controller action is called, the [Controller::before] method
* will be called.
* 2. Next the controller action will be called.
* 3. After the controller action is called, the [Controller::after] method
* will be called.
*
* By default, the output from the controller is captured and returned, and
* no headers are sent.
*
* $request->execute();
*
* #param Request $request
* #return Response
* #throws Kohana_Exception
* #uses [Kohana::$profiling]
* #uses [Profiler]
* #deprecated passing $params to controller methods deprecated since version 3.1
* will be removed in 3.2
*/
public function execute_request(Request $request)
{
// Create the class prefix
$prefix = 'controller_';
// Directory
$directory = $request->directory();
// Controller
$controller = $request->controller();
if ($directory)
{
// Add the directory name to the class prefix
$prefix .= str_replace(array('\\', '/'), '_', trim($directory, '/')).'_';
}
if (Kohana::$profiling)
{
// Set the benchmark name
$benchmark = '"'.$request->uri().'"';
if ($request !== Request::$initial AND Request::$current)
{
// Add the parent request uri
$benchmark .= ' « "'.Request::$current->uri().'"';
}
// Start benchmarking
$benchmark = Profiler::start('Requests', $benchmark);
}
// Store the currently active request
$previous = Request::$current;
// Change the current request to this request
Request::$current = $request;
// Is this the initial request
$initial_request = ($request === Request::$initial);
try
{
if ( ! class_exists($prefix.$controller))
{
throw new HTTP_Exception_404('The requested URL :uri was not found on this server.',
array(':uri' => $request->uri()));
}
// Load the controller using reflection
$class = new ReflectionClass($prefix.$controller);
if ($class->isAbstract())
{
throw new Kohana_Exception('Cannot create instances of abstract :controller',
array(':controller' => $prefix.$controller));
}
// Create a new instance of the controller
$controller = $class->newInstance($request, $request->response() ? $request->response() : $request->create_response());
$class->getMethod('before')->invoke($controller);
// Determine the action to use
/* ADDED */ if (strpos($request->action(), '_') !== false) throw new HTTP_Exception_404('The requested URL :uri was not found on this server.', array(':uri' => $request->uri()));
/* MODIFIED */ $action = str_replace('-', '_', $request->action()); /* ORIGINAL: $action = $request->action(); */
$params = $request->param();
// If the action doesn't exist, it's a 404
if ( ! $class->hasMethod('action_'.$action))
{
throw new HTTP_Exception_404('The requested URL :uri was not found on this server.',
array(':uri' => $request->uri()));
}
$method = $class->getMethod('action_'.$action);
$method->invoke($controller);
// Execute the "after action" method
$class->getMethod('after')->invoke($controller);
}
catch (Exception $e)
{
// Restore the previous request
if ($previous instanceof Request)
{
Request::$current = $previous;
}
if (isset($benchmark))
{
// Delete the benchmark, it is invalid
Profiler::delete($benchmark);
}
// Re-throw the exception
throw $e;
}
// Restore the previous request
Request::$current = $previous;
if (isset($benchmark))
{
// Stop the benchmark
Profiler::stop($benchmark);
}
// Return the response
return $request->response();
}
} // End Kohana_Request_Client_Internal
Then to add an action with hyphens, for example, "controller/my-action", create an action called "my_action()".
This method will also throw an error if the user tries to access "controller/my_action" (to avoid duplicate content).
I know some developers don't like this method but the advantage of it is that it doesn't rename the action, so if you check the current action it will be consistently called "my-action" everywhere. With the Route or lambda function method, the action will sometime be called "my_action", sometime "my-action" (since both methods rename the action).

Categories