Mojolicious - how to intercept the incoming request before controller method handles and renders - perl

In Mojolicious app, I need to figure out which controller method will be handling the incoming request so that I can log details of the remote client and add some logic. I want to do it at only one place and not in every controller methods. I have tried some of the HOOKS but could not figure out. Much appreciate any help on this.
Routes are generated/created from several OpenApi Plugin files and also use Oauth2 Plugin for OAuth.
Mojolicious detail:
CORE
Perl (v5.16.3, linux)
Mojolicious (8.22, Supervillain)
OPTIONAL
Cpanel::JSON::XS 4.04+ (n/a)
EV 4.0+ (4.22)
IO::Socket::Socks 0.64+ (n/a)
IO::Socket::SSL 2.009+ (2.060)
Net::DNS::Native 0.15+ (n/a)
Role::Tiny 2.000001+ (2.000005)

I asked in mojolicious mailing list and got the reply from the creator of the Mojolicious, Sebastian Riedel. Thanks.
For everybody's benifit. $c->match->stack in around_action hook has the info I was looking for.
Here is how:
In your application startup method:
sub startup {
my $self = shift;
...
$self->hook(
around_action => sub {
my ($next, $c, $action, $last) = #_;
use DDP;
p $c->match;
# prints all the info about the controller and the method/action
# it is going to call
...
}
);
...
}
DDP - Output
Mojolicious::Routes::Match {
Parents Mojo::Base
public methods (7) : endpoint, find, has, path_for, position, root, stack
private methods (1) : _match
internals: {
endpoint Mojolicious::Routes::Route,
position 0,
root Mojolicious::Routes,
stack [
[0] {
action "controller_method_name",
controller "ControllerClassName",
handler "openapi",
id 3336,
openapi.object Mojolicious::Plugin::OpenAPI,
openapi.op_path [
[0] "paths",
[1] "/api/endpoint/path/{id}/status",
[2] "get"
],
openapi.parameters [
[0] {
description "job id",
in "path",
name "id",
required JSON::PP::Boolean,
type "number"
}
]
}
]
}
}
Here is the info I am looking for in $c->match->stack:
action "controller_method_name",
controller "ControllerClassName",

Related

Zend Expressive Route with optional parameter

I want use a route to get the complete collection and, if available, a filtered collection.
so my route:
$app->get("/companies", \App\Handler\CompanyPageHandler::class, 'companies');
My Handler for this route:
use App\Entity\Company;
use App\Entity\ExposeableCollection;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class CompanyPageHandler extends AbstractHandler
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$categories = new ExposeableCollection();
foreach (['test', 'test1', 'test3'] as $name) {
$category = new Company();
$category->setName($name);
$categories->addToCollection($category);
}
return $this->publish($categories);
}
}
When getting this route /companies, i get the expected collection
[{"name":"test"},{"name":"test1"},{"name":"test3"}]
So now i change the route
$app->get("/companies[/:search]", \App\Handler\CompanyPageHandler::class, 'companies');
It's all fine when i'm browsing to /companies.
But if i try the optional parameter /companies/test1 then i got an error
Cannot GET http://localhost:8080/companies/test1
my composer require section:
"require": {
"php": "^7.1",
"zendframework/zend-component-installer": "^2.1.1",
"zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-diactoros": "^1.7.1 || ^2.0",
"zendframework/zend-expressive": "^3.0.1",
"zendframework/zend-expressive-helpers": "^5.0",
"zendframework/zend-stdlib": "^3.1",
"zendframework/zend-servicemanager": "^3.3",
"zendframework/zend-expressive-fastroute": "^3.0"
},
In Zend Framework 2 and Symfony4 this route definition works fine. So im confused.
Why my optional parameter doesn't work?
That's because you are using https://github.com/nikic/FastRoute router and correct syntax would be:
$app->get("/companies[/{search}]", \App\Handler\CompanyPageHandler::class, 'companies');
or be more strict and validate search param something like this:
$app->get("/companies[/{search:[\w\d]+}]", \App\Handler\CompanyPageHandler::class, 'companies');

Test a Symfony REST API using Behat / Mink : prb with POST request

My challenge here is to find the best way to test a Symfony (3.4) API application using Behat/Mink for functionnal test, in my CICD platform.
Because my testing processes must be called in a shell script, all the tests must be very linear. I have no way to start a standalone webserver like Apache or the PHP/Symfony webserver. Also, Docker is not an option.
For the moment, I can successfully test the GET verbs of the API using the Mink syntax :
-- file test.feature
#function1
Scenario Outline: Test my api
When I go to "/api/v1/hello"
Then the response is JSON
The "I go to" instruction is implemented by Mink (http://docs.behat.org/en/v2.5/cookbook/behat_and_mink.html) and it emulates a GET request only. When this instruction is called by BeHat, the app Symfony kernel is "spawned" and the "api/v1/hello" method is called internally : there is no network trafic, no TCP connection, there is no need for a dedicated webserver (apache, or the symfony standalone server). It looks like Behat is emulating a webserver and start by itself the Symfony app it its own user space.
Now I want to test the POST verbs of my API, with a json payload, but unfortunally Mink do not have other verbs than GET.
I have read some articles over the web (keyword : behat test post api) but all I have seen is based on a Guzzl/Curl client. So a real client-to-server connection is made to http://localhost and a real webserver have to respond to the request.
I want the Symfony API to be called internally without using an other webserver.
Is there a way to do that ? How to test a Symfony REST API and specially the POST verb without needing a standalone server to reply ?
Thank you.
Here is how I do a functional test of a POST API, with BeHat, without a local running webserver :
test.feature :
#function1
Scenario Outline: Test my api
Given I have the payload
"""
{ "data":"object"}
"""
When I request "POST /api/v1/post"
Then the response is JSON
The featureContext file implement two functions :
"I Have The Payload" : See here https://github.com/philsturgeon/build-apis-you-wont-hate/blob/master/chapter8/app/tests/behat/features/bootstrap/FeatureContext.php
"I request" : based on code provided by philsturgeon just above, I modify it to have something like that :
/**
* #When /^I request "(GET|PUT|POST|DELETE|PATCH) ([^"]*)"$/
*/
public function iRequest($httpMethod, $resource)
{
$this->lastResponse = $this->lastRequest = null;
$this->iAmOnHomepage();
$method = strtoupper($httpMethod);
$components = parse_url($this->getSession()->getCurrentUrl());
$baseUrl = $components['scheme'].'://'.$components['host'];
$this->requestUrl = $baseUrl.$resource;
$formParams = json_decode($this->requestPayload, true);
$formParamsList = [];
foreach($formParams as $param => $value) {
$formParamsList[$param] = json_encode($value);
}
// Construct request
$headers = [
'Accept'=>'application/json',
'Content-Type'=>'application/x-www-form-urlencoded'
];
try {
// Magic is here : allow to simulate any HTTP verb
$client = $this->getSession()->getDriver()->getClient();
$client->request(
$method,
$this->requestUrl,
$formParamsList,
[],
$headers,
null);
} catch (BadResponseException $e) {
$response = $e->getResponse();
// Sometimes the request will fail, at which point we have
// no response at all. Let Guzzle give an error here, it's
// pretty self-explanatory.
if (null === $response) {
throw $e;
}
$this->lastResponse = $e->getResponse();
throw new \Exception('Bad response.');
}
}
If you use Mink then it is quite easy
class FeatureContext extends RawMinkContext
{
/**
* #When make POST request to some Uri
*/
public function makePostRequestToSomeUri(): void
{
$uri = '/some-end-point';
/** #var \Symfony\Component\BrowserKit\Client $client */
$client = $this->getSession()->getDriver()->getClient();
$postParams = [];
$files = [];
$serverParams = [];
$rawContent = '';
$client->request(
\Symfony\Component\HttpFoundation\Request::METHOD_POST,
$uri,
$postParams,
$files,
$serverParams,
$rawContent
);
/** #var \Symfony\Component\HttpFoundation\Response $response */
$response = $client->getResponse();
//...
}
}

All Dingo routes return 404 when phpunit testing

original posted at https://github.com/dingo/api/issues/1472
I'm using Lumen 5.1 and DingoApi 1.0.x to do my api development, and now I'm trying to do some acceptance testing. Following the documentation of Lumen, here is how I do it:
Here is a simplified routes definition in app\Http\routes.php:
$app->get('/', function () use ($app) {
return "Welcome to mysite.com";
});
$api = app('Dingo\Api\Routing\Router');
$api->version('v1', function ($api) {
$api->group([
'prefix' => 'dealer',
'middleware' => 'checkH5ApiSign'
], function ($api) {
$api->get('list', 'App\Http\Controllers\Credit\DealerController#index');
$api->get('staff_list', 'App\Http\Controllers\Credit\DealerController#getStaffList');
});
}
I can access both routes defined using $app or $api(dingo) in browser or via postman, they both can return a 200 response. But whenever I'm trying to access those routes in phpunit, the $app defined route like / is responding okay with 200 code, but all routes defined with $api(dingo) will response with 404 status code. Here is my test code:
class DealerTest extends TestCase
{
public function testTest()
{
$this->get('/')->assertResponseOk();
$this->get('/dealer/list')->assertResponseOk();
$this->get('/dealer/staff_list')->assertResponseOk();
}
}
and ran result:
PHPUnit 5.7.5 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 590 ms, Memory: 6.00MB
There was 1 failure:
1) DealerTest::testTest
Expected status code 200, got 404.
Failed asserting that false is true.
E:\Gitrepos\api.fin.youxinjinrong.com\vendor\laravel\lumen-framework\src\Testing\AssertionsTrait.php:19
E:\Gitrepos\api.fin.youxinjinrong.com\tests\DealerTest.php:8
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.
I tried ran through Dingo package code to find the cause, but failed. All other related issue could not solve my problem either. So please help me.
update
I followed the code flow, and see that FastRoute\DataGenerator\RegexBasedAbstract.php is doing the addRoute() operation, I dumped $this->staticRoutes) in that addRoute() method, see that it's doing okay both inside browser and under phpunit. But weird enough, the following call of ->getData() is behaving differenctly: in browser all static routes are returned, but not in phpunit.
Hope this can somehow be helpful. I'm still digging this problem...
So I got mine to work by doing this;
Using the example in the example used in creating the issue:
class DealerTest extends TestCase
{
public function testTest()
{
$this->get('/')->assertResponseOk();
$this->get('/dealer/list')->assertResponseOk();
$this->get('/dealer/staff_list')->assertResponseOk();
}
}
becomes
class DealerTest extends TestCase
{
public function testTest()
{
$this->get(getenv('API_DOMAIN') . '/v1/')->assertResponseOk();
$this->get(getenv('API_DOMAIN') . '/v1/dealer/list')->assertResponseOk();
$this->get(getenv('API_DOMAIN') . '/v1/dealer/staff_list')->assertResponseOk();
}
}
I hope this helps

How to fix error "Cannot dispatch middleware Application\Middleware\IndexMiddleware"?

I've set up a Zend Application as normal, except in my case the difference is that I set it up over an existing legacy web application.
I still want to call my existing legacy application over the ZF3 app. It was suggested I can do so using Middleware. I went over https://docs.zendframework.com/zend-mvc/middleware/ and set up my routing as described there.
However, when I run the application, I am greeted by this:
Cannot dispatch middleware Application\Middleware\IndexMiddleware
#0 zend-mvc\src\MiddlewareListener.php(146):
Zend\Mvc\Exception\InvalidMiddlewareException::fromMiddlewareName('Application\\Mid...')
Here is where the exception happens:
https://github.com/zendframework/zend-mvc/blob/release-3.1.0/src/MiddlewareListener.php#L146
Just to note:
$middlewareToBePiped; //'Application\Middleware\IndexMiddleware'
is_string($middlewareToBePiped); // true
$serviceLocator->has($middlewareToBePiped);//false
$middlewareToBePiped instanceof MiddlewareInterface; //false
is_callable($middlewareToBePiped);//false
My class is:
namespace Application\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Zend\Http\Response;
class IndexMiddleware implements MiddlewareInterface
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
{}
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{}
}
I am thinking that my issue is that my IndexMiddleware class is not being found in ServiceLocator... (line 142 of linked API). How do I get it in there?
I put this into my application.config.php file:
'service_manager' => [
'invokables' => array(
'middleware' => IndexMiddleware::class
)
]
onto the next error it is.. (Last middleware executed did not return a response.)
but looks like it has executed)

Zend Error: Registry is already initialized

So here's the situation:
-I have a forum software, XenForo for customers to frolick about in
-I have a membership software, aMember, to handle customer payments and deliver digital products. (Both on the same website)
aMember has a template system that allows you to (ideally) easily customize the script to look like it's naturally part of your website.
XenForo has a script addon that lets you use the customized XenForo Header and footer using a PHP "include" function.
So essentially, I can take a regular php file, call the XenForo header and footer using php include, and make that page look like it's part of the forum software (almost like a wordpress header/footer). So far, everything I've mentioned is tested and working outside the aMember system, so I currently have an index.php file that calls the XenForo header and footer using include and it works great.
Here's where it gets nasty, I tried to use the PHP include script inside the aMember template system. I got it mostly working, but then the following Error is thrown with the resulting fun batch of code afterwards:
Error: Registry is already initialized
Exception Zend_Exception
Zend_Registry::setClassName [ /home/content/p/p/o/ppowers/html/forum/library/XenForo/Application.php : 244 ]
XenForo_Application::initialize [ /home/content/p/p/o/ppowers/html/forum/library/Dark/Kotomi/KotomiHeader.php : 5 ]
include_once [ /home/content/p/p/o/ppowers/html/header.php : 6 ]
include_once [ library/Am/View.php : 419 ]
Am_View->printLayoutHead [ application/default/themes/sample/layout.phtml : 8 ]
include [ library/Am/View.php : 352 ]
Am_View->_run [ library/Zend/View/Abstract.php : 888 ]
Zend_View_Abstract->render [ library/Am/View.php : 326 ]
Am_View->display [ application/default/controllers/IndexController.php : 7 ]
IndexController->indexAction [ library/Am/Controller.php : 139 ]
Am_Controller->_runAction [ library/Am/Controller.php : 116 ]
Am_Controller->dispatch [ library/Zend/Controller/Dispatcher/Standard.php : 295 ]
Zend_Controller_Dispatcher_Standard->dispatch [ library/Zend/Controller/Front.php : 954 ]
Zend_Controller_Front->dispatch [ library/Am/App.php : 1372 ]
Am_App->run [ index.php : 41 ]
From what I can tell and my limited programming knowledge, it looks like aMember and XenForo are having a fight over who gets to use the Zend Registry.
Is there anyway I can make them play well together without hiring a full time programmer for 6 months? Thanks so much!
........................................................Response to comment:
The add comment didn't have enough charecters, so here's some of the code.
From what I can tell XenForo uses it as it's primary..well...everything, here's the START of Application.php, part of XenForo's source code. This file is over 1,000 lines, all of which make up the class that start's at the top... This seems to be the only file that uses the Zend_Registry that isn't part of the Zend source itself.
class XenForo_Application extends Zend_Registry
{
const URL_ID_DELIMITER = '.';
public static $version = '1.1.0';
public static $versionId = 1010070; // abbccde = a.b.c d (alpha: 1, beta: 3, RC: 5, stable: 7, PL: 9) e
public static $jsVersion = '';
public static $jQueryVersion = '1.5.2';
protected $_configDir = '.';
protected $_rootDir = '.';
protected $_initialized = false;
protected $_lazyLoaders = array();
protected static $_handlePhpError = true;
protected static $_debug;
protected static $_randomData = '';
protected static $_classCache = array();
public static $time = 0;
public static $host = 'localhost';
aMember uses it across several files, here are a few examples:
This is inside form.php...
public function findRuleMessage(HTML_QuickForm2_Rule $rule, HTML_QuickForm2_Node $el)
{
$strings = array(
'rule.required' => ___('This is a required field'),
);
$type = lcfirst(preg_replace('/^.+rule_/i', '', get_class($rule)));
$tr = Zend_Registry::get('Zend_Translate');
$fuzzy = sprintf('rule.%s', $type);
if (array_key_exists($fuzzy, $strings))
return $strings[$fuzzy];
}
And this is inside app.php....
function amDate($string) {
if ($string == null) return '';
return date(Zend_Registry::get('Am_Locale')->getDateFormat(), amstrtotime($string));
}
function amDatetime($string) {
if ($string == null) return '';
return date(Zend_Registry::get('Am_Locale')->getDateTimeFormat(), amstrtotime($string));
}
function amTime($string) {
if ($string == null) return '';
return date(Zend_Registry::get('Am_Locale')->getTimeFormat(), amstrtotime($string));
}
Alright, I better not post any more source code or they'll send the men in black after me.
It looks like it might be easier to program it out of aMember, but this is quickly looking like an insurmountable task, especially at my (lack of) skill level.
Additional Info:
public static function initialize($configDir = '.', $rootDir = '.', $loadDefaultData = true)
{
(244)self::setClassName(__CLASS__);
self::getInstance()->beginApplication($configDir, $rootDir, $loadDefaultData);
}
Commenting out Line 244 produced the following error:
Fatal error: Call to undefined method Zend_Registry::beginApplication() in /home/content/p/p/o/ppowers/html/forum/library/XenForo/Application.php on line 245
And adding the code you suggested into the aMember index.php file produced this error:
Fatal error: Class 'XenForo_Application' not found in /home/content/p/p/o/ppowers/html/amember/index.php on line 40
What is on line 244 of /home/content/p/p/o/ppowers/html/forum/library/XenForo/Application.php? If it's just Zend_Registry which is the default class name and if you are not afraid of modifying the sources, just comment out the call on line 244. But this is not advised as you would have problems if you wanted to update XenForo in the future.
Check if whatever is passed to setClassName() on line 244 can be configured somehow. Paste some more code. Get some more answers.
UPDATE
Fighting with XenForo would probably require quite a lot of coding, so I'd suggest a rather different approach. Since aMemeber seems to use vanilla Zend_Registry, you can try to make sure XenForo's extended version of Zend_Registry instantiates first. In your index.php (probably located in public directory) locate the line with $application->bootstrap(); or similar, and before this line add something like this:
XenForo_Application::setClassName("XenForo_Application");
And comment out line 244 of /home/content/p/p/o/ppowers/html/forum/library/XenForo/Application.php.
If this works, remember to comment on the change, cross-referencing both modified files.
I build my website I am using Zend framework v1.5: http://www.panpic.vn (**) |
Forum using xenforo: http://www.panpic.vn/forum (*)
at homepage (**) I am Authentication User Xenforo
My Coding:
define('XF_ROOT', '/home/www/lighttpd/my_web/forum'); // set this (absolute path)!
define('STARTTIME', microtime(true) );
define('SESSION_BYPASS', false); // if true: logged in user info and sessions are not needed
require_once(XF_ROOT . '/library/XenForo/Autoloader.php');
XenForo_Autoloader::getInstance()->setupAutoloader(XF_ROOT . '/library');
XenForo_Application::initialize(XF_ROOT . '/library', XF_ROOT);
XenForo_Application::set('page_start_time', STARTTIME );
XenForo_Application::setDebugMode(false);
if (!SESSION_BYPASS)
{
$dependencies = new XenForo_Dependencies_Public();
$dependencies->preLoadData();
$session = XenForo_Session::startPublicSession(new Zend_Controller_Request_Http);
XenForo_Visitor::setup($session->get('user_id'));
$visitor = XenForo_Visitor::getInstance();
if ($visitor->getUserId())
{
$userModel = XenForo_Model::create('XenForo_Model_User');
$userinfo = $userModel->getFullUserById($visitor->getUserId());
}
}
Error: Registry is already initialized
Could you how do I fix ?