ZF2 cacheing matched routes - events

in my application i have a route (Literal or segment) for every action. i am not using one global route for everything so as a result the number of routes has grown hugely with 44+ modules (and more in future) .
It is my understanding (from what i have seen in the code) that in every page request zend goes throw all this routes in a array ans searches for a match witch could be bottleneck for the application (am i right?)
So i was thinking why not cache the matched routes in a db table with index to speed up the search ?
FIRST QUESTION : would this make the systems performance better?
so my first problem is skipping the system route matching mechanism. this is what i tried but it did not work :
public function onBootstrap(MvcEvent $e)
{
$em = StaticEventManager::getInstance();
$em->attach('Zend\Mvc\Application', MvcEvent::EVENT_ROUTE, array($this, 'onRoute'), 100);
}
public function onRoute(MvcEvent $e)
{
//var_dump($e->getRouteMatch());//->null routing has not been done yet
/* #var $router \Zend\Mvc\Router\Http\TreeRouteStack */
$router = $e->getRouter();
//-------------------------------------created a dummy route
$routeMatch = new \Zend\Mvc\Router\RouteMatch(array(
'controller' => 'Links\Controller\Items',
'action' => 'view',
'catId' => 0
));
$routeMatch->setMatchedRouteName('app/links');
$e->setRouteMatch($routeMatch);//set the dummy route
//--------------------------------------------PROBLEM HERE
//detach the onRoute event from routeListener
$e->getApplication()
->getServiceManager()
->get('RouteListener')
->detach($e->getApplication()->getEventManager());
}
the detach method is executed but the onRoute event still gets executed and matches the url to the correct route. so how to bypass(skip|detach) route matching ?

The reason you can't detach the listener is because you're already in the route event. By that point all the listeners have been marshalled together and queued for execution.
Why not instead take the listener out of the equation beforehand?
public function onBootstrap(MvcEvent $e)
{
$app = $e->getApplication();
$events = $app->getEventManager();
$shared = $events->getSharedManager();
$services = $app->getServiceManager();
$routeListener = $services->get('RouteListener');
$routeListener->detach($events);
$shared->attach('Zend\Mvc\Application', MvcEvent::EVENT_ROUTE, array($this, 'onRoute'), 100);
}

Related

Can we use multiple second-level domains with multitenancy?

I have implemented the simplest example using the Spatie docs for multitenancy, that is working perfectly fine. Now, I intend to use multiple second-level domains for each tenant I have.
For example; I have 2 tenants company-a and company-b and they are being served at company-a.localhost and company-b.localhost, now what I want is that when I visit company-a.admin.localhost, it should tell me COMPANY-A ADMIN and If I visit company-a.employee.localhost, it should tell me COMPANY-A EMPLOYEE.
I have tried using subdomain on routes in RouteServiceProvider like the following:
Route::middleware('web')
->group(base_path('routes/security.php'));
Route::domain($this->baseDomain('admin'))
->middleware('web')
->name('admin.')
->group(base_path('routes/admin.php'));
Route::domain($this->baseDomain('employee'))
->middleware('web')
->name('employee.')
->group(base_path('routes/employee.php'));
private function baseDomain(string $subdomain = ''): string
{
if (strlen($subdomain) > 0) {
$subdomain = "{$subdomain}.";
}
return $subdomain . config('app.base_domain');
}
Without subdomain, it works fine, but the routes with second-level domain, it falls to base level domain route and does not get the current tenant.
What am I missing here? Is this even possible to implement.
Thankyou.
Take, for example, the route:
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
The binding fields would be ['subdomain', 'param1', 'param2'], and the compiled route would have it's regexes declared as
regex => "{^/foo/(?P<param1>[^/]++)/(?P<param2>[^/]++)$}sDu",
hostRegex => "{^(?P<subdomain>[^\.]++)\.example\.com$}sDiu"
Where ^(?P<subdomain>[^\.]++)\. will explicitly stop capturing when finding a dot, in this case the delimiter between groups.
However, these regexes are overridable by using the where method. You could declare the above route as
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
})->where('subdomain', '(.*)');
In the compiled route , the hostRegex would be now
hostRegex => "{^(?P<subdomain>(?:.*))\.example\.com$}sDiu"
Meaning it will capture anything preceding .example.com. If you requested company-a.admin.example.com, $subdomain would be company-a.admin.
You could also declare a route with two binding fields in its domain:
Route::domain('{subsubdomain}.{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
Which might be more useful if you wanted subsubdomains to imply a hierarchy.
I have achieved this by using some checks, in RouteServiceProvider, I have not used the actual domain function on Route like we do normally i.e. Route::domain('foo.bar'). The reason was that, the Spatie package use a kind of middleware Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class which runs whenever we hit the domain with tenant comapny-a.localhost. And it gets the tenant from hostname i.e comapny-a.localhost.
public function findForRequest(Request $request):?Tenant
{
$host = $request->getHost();
return $this->getTenantModel()::whereDomain($host)->first();
}
In my RouteServiceProvide:
$this->routes(function () {
$class = 'security';
$middleware = 'web';
if (Str::contains(request()->getHost(), 'admin')) {
$class = 'admin';
} elseif (Str::contains(request()->getHost(), 'employee')) {
$class = 'employee';
} elseif (Str::contains(request()->getHost(), 'api')) {
$class = 'api';
$middleware = 'api';
}
Route::middleware($middleware)
->name("$class.")
->group(base_path("routes/${class}.php"));
});
As In my scenario, I had only these 2 kind of second-level domains and so, I just checked if this particular keyword exists in the hostname and choosing the file and middleware accordingly.
I also overrided the DomainTenantFinder class and in multitenancy.php config file:
public function findForRequest(Request $request): ?Tenant
{
$host = $request->getHost();
$host = str_replace('admin.', '', $host);
$host = str_replace('employee.', '', $host);
$host = str_replace('api.', '', $host);
$tenant = $this->getTenantModel()::whereDomain($host)->first();
if (empty($tenant)) {
abort(404);
}
return $tenant;
}
I have acheived the desired outcome, however, I have a security concern, specially in RouteServiceProvider logic. Thought??

Laravel Routing Query

I've made some bespoke pages in my admin of my site and they as the first segment of the URL.
e.g
/property-hartlepool
I thought of adding a trap all route into my routes file :
Route::get('{any?}', 'PagesController#view');
The problem I have is it's totally overwriting my other routes, I guess that's the name of a trap all route. However I'd like it to skip if it can't find a match.
I had a route for admin
/admin
But now it throws a 404 Error...
My PagesController#view method is :
public function view(Request $request)
{
$route = $request->segment(1); // $request->path();
// get page content
$page = Page::where('route', $route)->firstOrFail();
// If not full width, get four latest properties
//$properties = Property::latest_properties_for_frontend();
//metadata
$meta = get_metadata($page);
//page is Temporary
return view('frontend.'.themeOptions().'.page', [
'route' => $route,
'meta' => $meta,
'page' => $page
]);
}
Is their a better way of doing this, I do have other routes that sit at "top" level too. e.g...
Route::get('/property/{property}/{propertyId}', 'PropertiesController#property');
declare your trap Route as the last route.
Route::get('/admin', 'AdminController#view');
...
...
Route::get('{any?}', 'PagesController#view');

Laravel Validation Request & API route POST parameter

I'm facing a little issue with Form Request Validation and how to handle it with one API route.
The resource that I need to create depends on an other resource.
(Here an EmailSettings belongs to a Tenant)
So the look of my route should be something like : /api/tenants/{id}/email_settings
And my request validation expects several fields including the tenantId :
public function rules() {
return [
'email' => 'bail|required|email|unique:email_settings,email',
'name' => 'bail|required',
'username' => 'bail|required',
'password' => 'bail|required'
'imapHost' => 'bail|required',
'imapPort' => 'bail|required',
'imapEncryption' => 'bail|required',
'imapValidateCert' => 'bail|required',
'smtpHost' => 'bail|required',
'smtpPort' => 'bail|required',
'smtpEncryption' => 'bail|required',
'tenantId' => 'bail|required',
];
}
And I send the request like this :
try {
const response = await this.tenantForm.post('/api/tenants')
let newTenant = helpers.getNewResourceFromResponseHeaderLocation(response)
let tenantId = parseInt(newTenant.id);
try {
await this.emailSettingsForm.post('/api/tenants/' + tenantId + '/email_settings')
this.requestAllTenants()
} catch ({response}) {
$('.second.modal').modal({blurring: true, closable: false}).modal('show');
}
} catch ({response}) {
$('.first.modal').modal({blurring: true}).modal('show');
}
So the tenantId is passed as a parameter and not in the request body to respect the REST convention.
But the problem is in my Controller, when I merge the data to create the resource, the validation has already took place only on body data before the merge.
public function store(EmailSettingValidation $request, $tenant_id) {
$emailSetting = $this->emailSettingService->create(
array_merge($request->all(), compact($tenant_id))
);
return $this->response->created($emailSetting);
}
So what is the best way to handle it properly ?
Pass the id in the body ? Seems messy
Use Validator to validate manually ? I would prefer to keep Form Validation
Remove the tenantId rule and check it manually ?
Any suggestions ?
Thank you
If you define your api route like this:
Roue::post('tenants/{tenant}/emails_settings', 'Controller#store');
and modify your controller method to type-hint the model with a variable name that matches your route definition:
public function store(EmailSettingValidation $request, Tenant $tenant) {}
then Laravel will automatically find the Tenant by ID and inject it into the controller, throwing a ModelNotFoundException (404) if it doesn't exist. That should take care of validating the id.
Authorization is another matter.
So the solution I found to trigger 404 is the following :
Remove the tenantId from EmailSettings Validation
Add a provider to add a custom error when the exception 'ModelNotFoundException' occurs like here
No query results for model in Laravel with Dingo - how to make a RESTful response on failure?
Try to throw this Exception with the findOrFail method if invalid ID :
public function store(EmailSettingValidation $request, $tenant_id) {
Tenant::findOrFail($tenant_id);
$emailSetting = $this->emailSettingService->create(
array_merge($request->all(), ['tenantId' => $tenant_id])
);
return $this->response->created($emailSetting);
}
Travis Britz and Guillaumehanotel each have half of your answer, but you're still missing a detail.
From Travis Britz- Yes, include the tenant_id on the URI so it gets injected into the controller.
From Guillaumehanotel- Also used the Eloquent findOrFail in that Id in your Controller (or whatever class the Controller is leveraging to do this, like a Repository or Service class).
The last piece you're missing though is handling the error. You can do this in the Controller if you like, but I generally like making it a rule for my entire system that the Illuminate\Database\Eloquent\ModelNotFoundException Exceptions that come out of findOrFail() should always result in a 404.
Go to app/Exceptions/Handler.php. I'm pretty sure Laravel auto-generates a meat and potatoes version of this file for you, but if you don't already have one, it should look something like this:
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
/**
* Class Handler
* #package App\Exceptions
*/
class Handler extends ExceptionHandler
{
/**
* Render an exception into an HTTP response.
*
* For our API, we need to override the call
* to the parent.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $error)
{
$exception = [
'title' => 'Internal Error',
'message' => $error->getMessage();
];
$statusCode = 500;
$headers = [
'Content-Type', 'application/json'
];
return response()->json($exception, $statusCode, $headers, JSON_PRETTY_PRINT);
}
}
Laravel basically has a system-wide try/catch that sends all errors through here first. That's how errors get rendered into something the browser can actually interpret when you're in debug-mode, rather than just kill the process outright. This also gives you the opportunity to apply a few special rules.
So all you need to do is tell Handler::render() to change the default error code that occurs when it sees the type of error that can only come from findOrFail(). (This kind of thing is why it's always good to make your own 'named exceptions', even if they do absolutely nothing except inherit the base \Exception class.)
Just add this just before render() returns anything:
if ($error instanceof Illuminate\Database\Eloquent\ModelNotFoundException) {
$statusCode = 404;
}

Laravel 4 : Route to localhost/controller/action

I'm more or less new to Laravel 4. I've never used routes before but normally what I'm used to is url/controller/action and then the backend routing for me. I've read the documentation for routes and controllers a few times as well as read through some tutorials and so, I'm trying to figure out how to get this to work without writing a route for every controller and action.
I tried something like
Route::get('{controller}/{action}', function($controller, $action = 'index'){
return $controller."#".$action;
});
Now then, I know this is wrong since it doesn't work, but what am I missing? On most tutorials and stuff I'm seeing an route for more or less every controller and action like:
Route::get('/controller/action' , 'ControllerName#Action');
Which seems silly and like a waste of time to me.
Is there anyway to achieve what I want?
If you are looking for a more automated routing, this would be the Laravel 4 way:
Route:
Route::controller('users', 'UsersController');
Controller (in this case UsersController.php):
public function getIndex()
{
// routed from GET request to /users
}
public function getProfile()
{
// routed from GET request to /users/profile
}
public function postProfile()
{
// routed from POST request to /users/profile
}
public function getPosts($id)
{
// routed from GET request to: /users/posts/42
}
As The Shift Exchange mentioned, there are some benefits to doing it the verbose way. In addition to the excellent article he linked, you can create a name for each route, for example:
Route::get("users", array(
"as"=>"dashboard",
"uses"=>"UsersController#getIndex"
));
Then when creating urls in your application, use a helper to generate a link to a named route:
$url = URL::route('dashboard');
Links are then future proofed from changes to controllers/actions.
You can also generate links directly to actions which would still work with automatic routing.
$url = URL::action('UsersController#getIndex');
app\
controllers\
Admin\
AdminController.php
IndexController.php
Route::get('/admin/{controller?}/{action?}', function($controller='Index', $action='index'){
$controller = ucfirst($controller);
$action = $action . 'Action';
return App::make("Admin\\{$controller}Controller")->$action();
});
Route::get('/{controller?}/{action?}', function($controller='Index', $action='index'){
$controller = ucfirst($controller);
$action = $action . 'Action';
return App::make("{$controller}Controller")->$action();
});
I come from .Net world and routing is typically done:
/{Controller}/{action}/{id}
Which looks like:
/Products/Show/1 OR /Products/Show/Beverages
In Laravel I accomplish this routing like so:
Route::get('/{controller?}/{action?}/{id?}', function ($controller='Home', $action='index', $id = null) {
$controller = ucfirst($controller);
return APP::make("{$controller}Controller")->$action($id);
});
The controller would look roughly like so:
class ProductsController extends BaseController {
public function Show($id) {
$products = array( 1 => array("Price" => "$600","Item" => "iPhone 6"),
2 => array("Price" => "$700", "Item" => "iPhone 6 Plus") );
if ($id == null) {
echo $products[1]["Item"];
} else {
echo $products[$id]["Item"];
}
}
}

How to approach caching in ZF2

I am just starting to get my head into caching as a whole. I have a simple indexAction() that fetches all given Datasets. My approach is:
check for existing key 'controllername-index-index'
if existing: return the value of the key
if not existing, do the normal action and add the key
The value inside the key should be the ViewModel that will be generated and populated with my data.
Here's what i have done so far:
<?php
public function indexAction()
{
$sl = $this->getServiceLocator();
// $cache = $sl->get('cache');
// $key = 'kennzahlen-index-index';
//
// if ($cache->hasItem($key)) {
// return $cache->getItem($key);
// }
$viewModel = new ViewModel();
$viewModel->setTemplate('kennzahlen/index/index');
$entityService = $sl->get('kennzahlen_referenzwert_service');
$viewModel->setVariable('entities', $entityService->findAll());
// $cache->setItem($key, $viewModel);
return $viewModel;
}
The Caching parts are commented out for testing purposes, but basically this is all that i am doing. The Caching config/service looks like the following:
<?php
'cache' => function () {
return \Zend\Cache\StorageFactory::factory(array(
'adapter' => array(
'name' => 'filesystem',
'options' => array(
'cache_dir' => __DIR__ . '/../../data/cache',
'ttl' => 100
),
),
'plugins' => array(
array(
'name' => 'serializer',
'options' => array(
)
)
)
));
},
The serialization and caching works quite well, but i am surprised by the missing results. Going by what the ZendDevelopersToolbar tells me, the times WITHOUT caching range between 1.8s to 2.5s. Having the caching parts uncommented (enabled) doesn't really improve the loading time of my page at all.
So my question is: Is this approach completely wrong? Are there different, more speedy parts, that can be saved with some neat configuration tricks?
I Feel that a 2 second load time of a page is DEFINITELY too slow. 1s to me is the maximum given a huge amount of data, but certainly not anything more than that :S
All help/hints/suggestions will be greatly appreciated. Thanks in advance!
One option would be to cache the complete output of your page, for example based on the route match. You need to listen between routing and dispatching which route has been found as match and then act accordingly:
namespace MyModule;
use Zend\Mvc\MvcEvent;
class Module
{
public function onBootstrap(MvcEvent $e)
{
// A list of routes to be cached
$routes = array('foo/bar', 'foo/baz');
$app = $e->getApplication();
$em = $app->getEventManager();
$sm = $app->getServiceManager();
$em->attach(MvcEvent::EVENT_ROUTE, function($e) use ($sm) {
$route = $e->getRouteMatch()->getMatchedRouteName();
$cache = $sm->get('cache-service');
$key = 'route-cache-' . $route;
if ($cache->hasItem($key)) {
// Handle response
$content = $cache->getItem($key);
$response = $e->getResponse();
$response->setContent($content);
return $response;
}
}, -1000); // Low, then routing has happened
$em->attach(MvcEvent::EVENT_RENDER, function($e) use ($sm, $routes) {
$route = $e->getRouteMatch()->getMatchedRouteName();
if (!in_array($route, $routes)) {
return;
}
$response = $e->getResponse();
$content = $response->getContent();
$cache = $sm->get('cache-service');
$key = 'route-cache-' . $route;
$cache->setItem($key, $content);
}, -1000); // Late, then rendering has happened
}
}
The second listener checks at the render event. If that happens, the result of the response will be cached.
This system (perhaps not with 100% copy/paste, but the concept) works because if you return a Response during the route or dispatch event, the application will short circuit the application flow and stop further triggering listeners. It will then serve this response as it is.
Bear in mind it will be the complete page (including layout). If you don't want that (only the controller), move the logic to the controller. The first event (now route) will be dispatch of the controller. Listen to that early, so the normal execution of the action will be omitted. To cache the result, check the render event for the view layer to listen to.
/update: I wrote a small module to use this DRY in your app: SlmCache

Resources