How to regenerate routes cache when the configuration changes in Symfony 2 - php

I'm currently writing a custom route loader in Symfony 2 that will generate routes based on some configuration options defined in the main config file. The problem is that Symfony caches routes generated by custom routes loaders. Is there a way for me to update the cache when that config file changes?
I defined a configuration like this in app/config/config.yml
admin:
entities:
- BlogBundle\Entity\Post
- BlogBundle\Entity\Comment
My route loader read the config file and generates some routes based on the entities. Now the problem is that once those routes are generated and cached by Symfony I can't change them unless I manually call php app/console cache:clear. What I mean is if I add an entity to the config:
admin:
entities:
- BlogBundle\Entity\Post
- BlogBundle\Entity\Comment
- TrainingBundle\Entity\Training
I will have to manually clear the cache again with php app/console cache:clear in order to create and cache the new routes. I want the routes cache to be invalidated if I change the config, so that a new request to the server will force the regeneration of the routes.

Option 1
If your custom loader class can gain access to the kernel or the container (via DI), you could call the console cache clear command from that class.
E.g.
namespace AppBundle\MyLoader;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpFoundation\Response;
class MyLoader
{
private $kernel;
public function __construct($kernel)
{
$this->kernel = $kernel;
}
public function myFunction()
{
$application = new Application($this->kernel);
$application->setAutoExit(false);
$input = new ArrayInput(array(
'command' => 'cache:clear',
'--env' => 'prod',
));
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
// return new Response(""), if you used NullOutput()
return new Response($content);
}
}
Ref: Call a Command from a Controller
Disclaimer before someone points it out; Injection the kernel/conatiner is not considered "best pratice", but can be a solution.
Option 2
You could also write you own console command that extends Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand that just calls the clear cache command.
Ref ; Call Command from Another
Option 3
This answer also gives you another option

Related

how to set cron job if using codeigniter in cpanel?

I have a controller called Buy and it has a live method. How to install a cronjob if using codeigniter? I use codeigniter 4,and l use cpanel.
php - q /home/kuslon/kuslon/app/Controllers/Services/Buy.php
STEPS
Create a command-line route in app/Config/Routes.php. I.e:
$routes->setDefaultNamespace('App\Controllers');
$routes->setAutoRoute(false);
// ...
$routes->cli("cli/ship-product/(:segment)", "Services\Buy::ship/$1");
// ...
Where:
cli/ship-product/(:segment) - Represents your route. The (:segment) is optional depending on if your Controller method requires an argument or not.
Buy - Represents your Controller.
ship - Represents your Controller method.
$1 - Represents the optional (:segment) to be forwarded to the first Controller method's argument if in case it requires one. You may omit it if your Controller method doesn't require any arguments.
Notice the use of CLI-only routing with the help of ->cli(...).
Run your cron job.
/usr/local/bin/php -q /home/kuslon/kuslon/public/index.php cli ship-product "ed053cb1-29a4-42f2-a17e-3109fa4d80fe"
Where:
-q - Represents quiet-mode. Suppress HTTP header output.
/home/kuslon/kuslon/public/index.php - Represents the absolute path to your project's index.php file. This assumes that the first kuslon in the path represents your Cpanel username and the second kuslon represents your project root folder.
cli - Represents the first section of the command-line route you defined earlier.
ship-product - Represents the second section of the command-line route.
"ed053cb1-29a4-42f2-a17e-3109fa4d80fe" - Represents the third optional section of the command-line route that may be required by your Controller method.
Sample Controller.
<?php
namespace App\Controllers\Services;
use App\Controllers\BaseController;
class Buy extends BaseController
{
public function ship(string $uuid)
{
// echo "{$uuid}";
}
}
Resource: Running via the Command Line
Addendum:
If in case all your routes pass through an Authentication filter, you may want to exclude these command-line based routes in app/Config/Filters.php. I.e: As you may notice below, I've excluded all routes starting with cli/* from the authfilter.
<?php
namespace Config;
use App\Filters\AuthenticationFilter;
use CodeIgniter\Config\BaseConfig;
class Filters extends BaseConfig
{
// ...
public $aliases = [
//...
'authfilter' => AuthenticationFilter::class
];
public $globals = [
'before' => [
//...
'authfilter' => [
'except' => ['cli/*']
]
],
// ...
];
}
the Main thing You must do is check if request is from command Line and not from A browser in your controller using this is_cli() function .
then you can set the cron job in Cpanel .
good luck

Laravel HTTP test to own API adds last part form APP_URL

Short:
I cannot test my own API, because of the current APP_URL environment variable that I am using.
APP_URL contains http://localhost:8080/foldername, whereas foldername is a random string character that is very important to our dev-environment. If I try to run the test from the snippet below, my backend recieves the path like foldername/api/users instead of api/users - Of course this will not work.
Problem: I cannot change the APP_URL inside the .env file, nor in the phpunit.xml file. I have to solve this programmatically. How can I cut this "foldername" off?
Long:
I wanted to test my own API that I've built in Laravel, but every test failed with "invalid JSON was returned from the route" - which is weird because I know that my API works. (requests with axios do return the correct json)
The test is very basic and looks like this:
namespace Tests\Feature;
use Tests\TestCase;
class UserTest extends TestCase {
public function test_get_users() {
$response = $this->json('GET', url('/api/users')); //falsely tests foldername/api/users
$response->assertJsonCount(1);
}
}
The routes are defined in the routes/api.php file:
Route::resource('users', 'UserController');
I decided to catch and dump the requested route in one of the used middlewares via $request->path()
and got the output mentioned above.
I first tried it without using the built-in url helper, but with even worse results - maybe these errors correlate, but I cannot see how.
$this->json('GET', '/api/users'); //received as '/' instead - no route at all
Adding this line to the phpunit.xml did not do anything, and even if it did, hardcoded "localhost:8080" won't do. We have many different systems with different app_urls, but every single one has a "foldername" at the end of it, which has to be ignored.
<server name="APP_URL" value="http://localhost:8080"/>
I altered APP_URL inside my .env file and cleared all caches, which made the tests work, but break the rest of the program.
We use Laravel 6.5 and php 7.4
Worked around the problem by overriding the setUp method:
namespace Tests\Feature;
use Tests\TestCase;
class UserTest extends TestCase {
use WithFaker;
public function setUp(): void {
parent::setUp();
\URL::forceRootUrl(dirname(env('APP_URL')));
}
public function test_create_user() {
$response = $this->json('GET', url('/api/users'));
// $response->assertJson(...);
}
}
I still do not understand why my backend gets requested with an empty route "/", when I pass the route how the documentation describes it:
$response = $this->json('GET', '/api/users');

Symfony2 http_cache in custom location

In Symfony2, the command for clearing cache ...
php app/console cache:clear --env=prod
removes completely the app/cache/prod folder. I'd like to preserve the content of http_cache. Is there any way to tell Symfony2 to store the http_cache in other location not affected when do a cache:clear?
For example, there is a simple configuration in config.yml to move sessions out of cache folder, in order to not clear all user sessions on every deployment.
framework:
[...]
session:
save_path: %kernel.root_dir%/var/sessions
Is there some similar way to do this with http_cache folder?
In AppCache.php you can use createStore method.
require_once __DIR__.'/AppKernel.php';
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
class AppCache extends HttpCache
{
protected function createStore()
{
// Use custom logic to build the store
}
}
Default content of that function looks like that:
protected function createStore()
{
return new Store($this->cacheDir ?: $this->kernel->getCacheDir().'/http_cache');
}

Prevent Sessions For Routes in Laravel (Custom on-demand session handling)

I am building APIs for my Android app using laravel and default session driver set to REDIS.
I found a good article here http://dor.ky/laravel-prevent-sessions-for-routes-via-a-filter/ which sort of serves the purpose.
However when ever I hit the url it also hits the redis and generates the key which is empty. Now I want avoid creating empty session keys in redis. Ideally it should not hit the redis How can I do that?
Can we customise sessios in a way so that sessions are generated only for specific routes (or disable for specific routes)?
I can explain more with specific use case, please let me know.
Its really easy using the middleware in Laravel 5, I needed any request with an API key not to have a session and I simply did :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Session\Middleware\StartSession as BaseStartSession;
class StartSession extends BaseStartSession
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(\Request::has('api_key'))
{
\Config::set('session.driver', 'array');
}
return parent::handle($request, $next);
}
}
Also you will need to extend the SessionServiceProvider as follows:
<?php namespace App\Providers;
use Illuminate\Session\SessionServiceProvider as BaseSessionServiceProvider;
class SessionServiceProvider extends BaseSessionServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton('App\Http\Middleware\StartSession');
}
}
and place in your config/app.php under providers:
'App\Providers\SessionServiceProvider',
Also you must change it in your kernel file: App/Http/Kernel.php, in the $middlewareGroups section change the default entry, \Illuminate\Session\Middleware\StartSession::class, to your new class \App\Http\Middleware\StartSession::class,.
In Laravel 5, just don't use the StartSession, ShareErrorsFromSession, and VerifyCsrfToken middlewares.
In my application I've moved these three middlewares from the web group to a new stateful group, and then I have included this stateful group on routes which need to know about the session (in addition to web in all cases, in my app at least). The other routes belong to either the web or api groups.
Now when making requests to the routes which are not using the stateful middleware group session cookies are not sent back.
The simplest way to achieve this is to Make your own AppStartSession middleware that subclasses Illuminate\Session\Middleware\StartSession and the replace the class being used in kernel.php. The only method you need to override in your subclass is sessionConfigured() for which you can return false to disable the session or parent::sessionConfigured() to allow it.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Session\Middleware\StartSession;
class AppStartSession extends StartSession
{
protected function sessionConfigured(){
if(!\Request::has('api_key')){
return false;
}else{
return parent::sessionConfigured();
}
}
}
kernel.php (see *** comment for where the change is done)
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* #var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// *** Replace start session class
// \Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\AppStartSession::class,
// *** Also comment these ones that depend on there always being a session.
//\Illuminate\View\Middleware\ShareErrorsFromSession::class,
//\App\Http\Middleware\VerifyCsrfToken::class,
];
/**
* The application's route middleware.
*
* #var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
];
}
Don't fight the framework, embrace it!
Since Laravel 5.2, when middleware groups were introduced, you may disable session for certain routes by defining them outside of the "web" middleware group (which includes the StartSession middleware responsible for session handling). As on latest 5.2.x versions the whole default routes.php file is wrapped with "web" middleware group, you need to make some modification in app/Providers/RouteServiceProvider.php file, as described here.
There appears to be a way to accomplish this using a session reject callback.
Relevant sources...
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Foundation/Application.php#L655
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Foundation/Application.php#L660
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Session/Middleware.php#L60
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Session/Middleware.php#L97
I can't find many references to this around the web, but reading more through the source it appears that if the session reject callback returns a truthy value, the session will be forced to use an array driver for the request rather than whatever is configured. Your callback also gets the current request injected so you can do some logic based on the request parameters.
I've only tested this on a local Laravel 4.2 install but it seems to work. You just need to bind a function to session.reject.
First, create a SessionRejectServiceProvider (or something like that)
<?php
use \Illuminate\Support\ServiceProvider;
class SessionRejectServiceProvider extends ServiceProvider {
public function register()
{
$me = $this;
$this->app->bind('session.reject', function($app)use($me){
return function($request)use($me){
return call_user_func_array(array($me, 'reject'), array($request));
};
});
}
// Put the guts of whatever you want to do in here, in this case I've
// disabled sessions for every request that is an Ajax request, you
// could do something else like check the path against a list and
// selectively return true if there's a match.
protected function reject($request)
{
return $request->ajax();
}
}
Then add it to your providers in your app/config/app.php
<?php
return array(
// ... other stuff
'providers' => array(
// ... existing stuff...
'SessionRejectServiceProvider',
),
);
Edit / More Info
The net result is that the reject() method is called on every request to your application, before the session is started. If your reject() method returns true, sessions will be set to the array driver and basically do nothing. You can find a lot of useful info the $request parameter to determine this, here's the API reference for the request object in 4.2.
http://laravel.com/api/4.2/Illuminate/Http/Request.html
I've been trying to accomplish a similar feature.
Our API is stateless except for 1 route - the version 1 cart.
I ended up with setting 'driver' in the app/config/session.php like this ...
'driver' => 'v1/cart' === Request::getDecodedPath() ? 'native' : 'array',
Nothing magic. Initially we though of using a before filter, but that wasn't happening early enough.
It seems a simple way to do things, but I may be missing something.
Putting the switch in the config seems an easy place for other developers to see what the driver is whereas putting it in a service provider is so tucked out of the way, without knowing what service providers are installed and what they interact with, it would be far harder to debug.
Anyway. Hope this is of some use.
As pointed out below ... DO NOT CACHE YOUR CONFIG IF IT IS DYNAMIC.
Which does lead to it being of limited use. As soon as we no longer need to support v1/cart, we will be dropping this route and then be back on a static config.
Laravel default have two routes group called web and api, the api routes group default without session.
So, we can write any route role to routes/api.php, will not use session default.
If not want to use the api prefix url, we can modify app\Providers\RouteServiceProvider add a new group like this:
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/static.php'));
Now you can place any routes into routes/static.php file will not to use session.
Hope helpful.
Laravel 5x
In the App\Providers\RouteServiceProvider file, just copy the mapApiRoutes() method to a new method called mapStaticRoutes(), remove the prefix('api') call, and add "routes/static.php" (you will need to create this file). This will use the same stateless "api" middleware and not have an /api prefix assigned to the routes.
protected function mapStaticRoutes()
{
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/static.php'));
}
Just update the "map()" method to call "$this->mapStaticRoutes();" so that it knows of your new file. And any route added there should now be stateless and it wasn't much work.....
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
// Static Routes (stateless, no /api prefix)
$this->mapStaticRoutes();
}
static.php
// Health Check / Status Route (No Auth)
Route::get('/status', function() {
return response()->json([
'app' => 'My Awesome App',
'status' => 'OK'
]);
});

Caching View Output in Laravel 4

I know that Blade already caches the compiled PHP for all blade views, but I would like to take this a step further. A website that I'm working on is modularized into component views and then pieced together in the default controller. Each of the "widgets" has its own view, which rarely changes content (with the exception of a few frequently updating ones). So, I'd like to cache the HTML output of these rarely-changing views to prevent them from being evaluated on every page load.
In Laravel 3 we could do something like so (credit Laravel forums):
Event::listen(View::loader, function($bundle, $view)
{
return Cache::get($bundle.'::'.$view, View::file($bundle, $view,
Bundle::path($bundle).'view'));
});
Unfortunately, View::loader has entirely disappeared in Laravel 4. When digging through \Illuminate\View\View and \Illuminate\View\Environment, I discovered that each view dispatches an event named "composing: {view_name}". Listening for this event provides the view name and data being passed to it on each view render, however returning from the callback does not have the same effect as it did in Laravel 3:
Event::listen('composing: *', function($view) {
if(!in_array($view->getName(), Config::get('view.alwaysFresh'))) {
// Hacky way of removing data that we didn't pass in
// that have nasty cyclic references (like __env, app, and errors)
$passedData = array_diff_key($view->getData(), $view->getEnvironment()
->getShared());
return Cache::forever($view->getName() . json_encode($passedData), function() {
return 'test view data -- this should appear in the browser';
});
}, 99);
The above does not circumvent the normal view including and rendering process.
So how can you circumvent normal view rendering and return cached content from this composing event? Is it possible currently in Laravel without some ugly hackery?
Quick and Dirty
Well, one option, as I'm sure you know, is to cache the items inside of controllers as the View is being rendered. I suspect you don't want to do that, as it's less maintainable in the long-run.
More maintainable(?) method
However, if the View loader/renderer doesn't fire an event where you want, you can create one. Because every package/library in Laravel 4 is set in the App container, you can actually replace the View library with your own.
The steps I would take is:
Create a library/package. The goal is to create a class which extends Laravel's view logic. After taking a look, you might want to extend this one - This is the View facade
If you extended the View facade with your own (aka if my assumption on the file in step 1 is correct), you'll then just need to replace the alias for View in app/config/app.php with your own.
Edit- I played with this a bit. Although I don't necessarily agree with caching a View result, vs caching sql queries or the "heavier lifts", here is how I'd go about doing this in Laravel 4:
The View rendering in Laravel 4 doesn't fire an event that let's us cache the result of a view. Here's how I've added in that functionality to cache a view's result.
You may want to consider the ramifications of caching a view's result. For instance, this doesn't get around doing the hard work of talking to a datbase to get the data needed for the view. In any case, this gives a good overview on extending or replacing core items.
First, create a package and set up its autoloading. I'll use the namespace Fideloper\View. It's autoloading in composer.json will looks like this:
"autoload": {
"classmap": [
"app/commands",
"app/controllers",
"app/models",
"app/database/migrations",
"app/database/seeds",
"app/tests/TestCase.php"
],
"psr-0": {
"Fideloper": "app/"
}
},
Next, create a class to replace the View facade. In our case, that means we'll be extending Illuminate\View\Environment.
In this class, we'll take the result of the View being rendered and add some logic to cache (or not cache) it. Here is Fideloper/View/Environment.php:
<?php namespace Fideloper\View;
use Illuminate\View\Environment as BaseEnvironment;
use Illuminate\View\View;
class Environment extends BaseEnvironment {
/**
* Get a evaluated view contents for the given view.
*
* #param string $view
* #param array $data
* #param array $mergeData
* #return \Illuminate\View\View
*/
public function make($view, $data = array(), $mergeData = array())
{
$path = $this->finder->find($view);
$data = array_merge($mergeData, $this->parseData($data));
$newView = new View($this, $this->getEngineFromPath($path), $view, $path, $data);
// Cache Logic Here
return $newView;
}
}
So, that's where the bulk of your work will be - filling out that // Cache Logic Here. However, we have some plumbing left to do.
Next, we need to set up our new Environment class to work as a Facade. I have a blog post about creating Laravel facades. Here's how to accomplish that in this case:
Create the facade for our new Environment. We'll name it fideloper.view in code.
<?php namespace Fideloper\View;
use Illuminate\Support\Facades\Facade;
class ViewFacade extends Facade {
/**
* Get the registered name of the component.
*
* #return string
*/
protected static function getFacadeAccessor() { return 'fideloper.view'; }
}
Then, create the Service Provider which will tell Laravel what to create when fideloper.view is called. Note that this needs to mimic functionality of the Illuminate\View\ViewServiceProvider for creating the extended Environment class.
<?php namespace Fideloper\View;
use Illuminate\Support\ServiceProvider;
class ViewServiceProvider extends ServiceProvider {
public function register()
{
$this->app['fideloper.view'] = $this->app->share(function($app)
{
// Next we need to grab the engine resolver instance that will be used by the
// environment. The resolver will be used by an environment to get each of
// the various engine implementations such as plain PHP or Blade engine.
$resolver = $app['view.engine.resolver'];
$finder = $app['view.finder'];
$env = new Environment($resolver, $finder, $app['events']);
// We will also set the container instance on this view environment since the
// view composers may be classes registered in the container, which allows
// for great testable, flexible composers for the application developer.
$env->setContainer($app);
$env->share('app', $app);
return $env;
});
}
}
Lastly, we need to hook this all together and tell Laravel to load our Service Provider and replace Illuminate's View facade with our own. Edit app/config/app.php:
Add the Service Provider:
'providers' => array(
// Other providers
'Fideloper\View\ViewServiceProvider',
),
Replace the View facade with our own:
'aliases' => array(
// Other Aliases
//'View' => 'Illuminate\Support\Facades\View',
'View' => 'Fideloper\View\ViewFacade',
),
You'll then be able to use whatever logic you wish in the View::make() method!
Finally
It's worth noting that there are some patterns to load in multiple "requests" per web request. Symfony, for instance, let's you define controllers as servers. Zend has (had?) a concept of Action Stacks, which let you
... effectively help you create a queue of [controller] actions to execute during the request.
Perhaps you'd like to explore that possibility within Laravel, and cache the results of those "actions" (vs caching a view directly).
Just a thought, not a recommendation.
There is a library for caching views/parts in Laravel(and not only) - Flatten.
It is a powerful cache system for caching pages at runtime. What it does is quite simple : you tell him which page are to be cached, when the cache is to be flushed, and from there Flatten handles it all. It will quietly flatten your pages to plain HTML and store them. That whay if an user visit a page that has already been flattened, all the PHP is highjacked to instead display a simple HTML page. This will provide an essential boost to your application's speed, as your page's cache only gets refreshed when a change is made to the data it displays.
To cache all authorized pages in your application via the artisan flatten:build command. It will crawl your application and go from page to page, caching all the pages you allowed him to.
Flushing
Sometimes you may want to flush a specific page or pattern. If per example you cache your users's profiles, you may want to flush those when the user edit its informations. You can do so via the following methods :
// Manual flushing
Flatten::flushAll();
Flatten::flushPattern('users/.+');
Flatten::flushUrl('http://localhost/users/taylorotwell');
// Flushing via an UrlGenerator
Flatten::flushRoute('user', 'taylorotwell');
Flatten::flushAction('UsersController#user', 'taylorotwell');
// Flushing template sections (see below)
Flatten::flushSection('articles');
link to - https://github.com/Anahkiasen/flatten

Categories