How do I access the Router through an artisan command? - php

I need access to the RouteCollection that Laravel possesses when it gets ran normally and all ServiceProviders are booted. I need the RouteCollection because a legacy app needs to know if Laravel has the particular route, so if it doesn't, legacy code can take care of the route.
I figure if I can somehow get a hold of Illuminate\Routing\Router in an artisan command, I could simply call the getRoutes() function and output a JSON file containing an array of all the routes. Then when the legacy code needs to determine if Laravel supports the Route, it could read that file.
In order to do that though, I need access to the Router class. Not sure how to accomplish that... I looked at Illuminate\Foundation\Console\RoutesCommand source code and I can't figure out how it works. What's really odd is it looks like the Router class is being injected, but when I do Artisan::resolve('MyCommand'), I get an empty RouteCollection.
EDIT
I never did figure out how to accomplish this question, but for those in a similar situation, I found this works for the most part, although I'm not sure how bad the overhead is starting Laravel each request just to check the routes. Right now it doesn't seem like that much.
// Start laravel, so we can see if it should handle the request
require_once(__DIR__.'/laravel/bootstrap/autoload.php');
$app = require_once(__DIR__.'/laravel/bootstrap/start.php');
$app->boot();
// Create the fake request for laravel router
$request = Request::createFromGlobals();
$request->server->set('PHP_SELF', '/laravel/public/index.php/'.$_REQUEST['modname']);
$request->server->set('SCRIPT_NAME', '/laravel/public/index.php');
$request->server->set('SCRIPT_FILENAME', __DIR__.'/laravel/public/index.php');
$request->server->set('REQUEST_URI', '/laravel/public/'.$_REQUEST['modname']);
$routes = Route::getRoutes();
foreach($routes as $route) {
if ($route->matches($request)) {
$app->run($request);
exit;
}
}

Here is a simplified implementation
JsonRoutes.php
<?php
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Illuminate\Routing\Route;
use Illuminate\Routing\Router;
class JsonRoutes extends Command {
protected $name = 'routes:json';
protected $description = 'Spits route information in JSON format.';
protected $router;
protected $routes;
public function __construct(Router $router) {
parent::__construct();
$this->router = $router;
$this->routes = $router->getRoutes();
}
public function fire() {
$result = [];
foreach ($this->routes as $route) {
$result[] = [
'methods' => implode(',', $route->methods()),
'host' => $route->domain(),
'uri' => $route->uri(),
'name' => $route->getName(),
'action' => $route->getActionName(),
];
}
$file = $this->option('file');
if ($file) {
File::put($file, json_encode($result));
return;
}
$this->info(json_encode($result));
}
protected function getArguments() { return []; }
protected function getOptions() {
return [
['file', 'f', InputOption::VALUE_OPTIONAL, 'File name.'],
];
}
}
You register it in artisan.php:
Artisan::resolve('JsonRoutes');
Sample usage:
Spit it out to stdout
$ artisan routes:json
[{"methods":"GET,HEAD","host":null,"uri":"\/","name":null,"action":"Closure"}]
Write it to a file
$ artisan routes:json -f /tmp/routes.json

Related

*SOLVED* PHPUnit in Laravel returns 404 for simple get request, yet works fine for others

In a right pickle with phpunit. I currently have a test class for a resource route with 9 tests in it. All but two of these tests pass, ironically what should be the two simplest; the tests for articles.index and articles.show (the last 2 tests in the code below).
<?php
namespace Tests\Feature;
use App\Article;
use Tests\TestCase;
use App\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use DB;
class ArticleTest extends TestCase
{
use RefreshDatabase;
// runs before any of the tests
protected function setUp(): void
{
parent::setUp();
// run tests without language(locale) middleware
$this->withoutMiddleware(\App\Http\Middleware\Language::class);
}
/** #test */
public function unauthenticated_users_can_not_access_create()
{
$this->get('/articles/create')->assertRedirect('/login');
}
/** #test */
public function admin_users_can_access_edit()
{
$article = factory(Article::class)->create();
$record = DB::table('articles')->where('id', $article->id)->first();
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/articles/' . $record->slug . '/edit?locale=en')->assertOK();
}
/** #test */
public function authenticated_but_not_admin_users_can_not_access_create()
{
$this->actingAs(factory(User::class)->create());
$this->get('/articles/create')->assertRedirect('home');
}
/** #test */
public function admin_can_access_create()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/articles/create')->assertOk();
}
/** #test */
public function can_store_an_article()
{
$article = factory(Article::class)->create();
$record = DB::table('articles')->where('id', $article->id)->first();
$this->assertDatabaseHas('articles', ['slug' => $record->slug]);
}
/** #test */
public function admin_can_access_articles_admin_index()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$this->get('/admin/articles')->assertOk();
}
/** #test */
public function admin_can_delete_article_and_it_is_removed_from_the_database()
{
$user = factory(User::class)->create();
$user->isAdmin = 1;
$this->actingAs($user);
$article = factory(Article::class)->create();
$this->assertDatabaseHas('articles', ['slug' => $article->slug]);
$record = DB::table('articles')->where('id', $article->id)->delete();
$this->assertDatabaseMissing('articles', ['slug' => $article->slug]);
}
/** #test */
public function can_see_article_index_page()
{
$this->get('/articles')->assertOK();
}
/** #test */
public function can_only_access_articles_made_visible()
{
$article = factory(Article::class)->create();
$article->displayStatus = 2;
$this->assertDatabaseHas('articles', ['slug' => $article->slug]);
$this->get('/articles/' . $article->slug)->assertRedirect('/articles');
}
}
The test can_see_article_index_page should return a 200 status code yet it 404's and can_only_access_articles_made_visible should be a redirect status code yet 404's as well.
SEEN HERE
My application is multi-lingual, using the spatie/translatable package and I'm unsure if it's that that is interfering (doesn't really make sense for it to be as the withoutMiddleware line in setUp should prevent this) or something else entirely (i.e my stupidity). My app is built with Laravel 5.8, and I am running phpunit 7.5.
EDIT I found out the error was due to some specific rows not existing in my test database, where in my controller it would fail if it couldn't find them. By adding the line $this->withoutExceptionHandling() to the failing tests it told me this information.
Since all your routes are working but /articles that means that something is not ok with your routes.
You added this route lately: Every time you create a new route or make any changes in the route files of laravel you should run php artisan config:cache command.
I am pretty sure it's not a conflict in routes because of a variable {}, since the status code is a clear 404 meaning that no controller is even accessed that's why i won't elaborate further on possible routing conflicting issues.
If the error persists though get your route at the very top of your routes file and see if it works there. If that's the case that means that one of your routes, overlaps it.
A quick fix to that is to order your static naming routes above the routes using variables for example:
/articles
/{articles}
If you reverse above order your /articles route will never be accessed. This is a case of naming route conflicts.

Laravel: How to set globally available default route parameters

I'm trying to set a handful of default route parameters that will work globally in my application regardless of context. In the documentation for URL generation the example given is using middleware which is fine for HTTP, but won't get called during non-HTTP contexts. I also need this to work when called from the CLI.
My first idea is to have a Service Provider that calls the defaults method on boot:
<?php
namespace App\Providers;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaults extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults([
'foo' => 'abc',
'bar' => 'xyz',
]);
}
}
But this does not work for HTTP requests:
Route::get('test', function (\Illuminate\Routing\UrlGenerator $urlGenerator) {
dump($urlGenerator->getDefaultParameters());
});
Outputs []
I believe this is because in the UrlGenerator, the setRequest method unconditionally sets the routeGenerator property to null. My Service Provider's boot method is called during the bootstrapping process, but then the request is set afterwards clobbering my defaults.
//Illuminate/Routing/UrlGenerator.php
public function setRequest(Request $request)
{
$this->request = $request;
$this->cachedRoot = null;
$this->cachedSchema = null;
$this->routeGenerator = null;
}
Dumping the UrlGenerator during boot and then again in my routes file can demonstrate this:
As you can see, the UrlGenerator instance is the same both times, but the RouteUrlGenerator on the routeGenerator property has changed.
I am unsure of a better way to set these defaults.
Not sure why this is getting attention almost a year later, but I ended up finding a solution by myself.
To add a bit more information to the original question, the purpose of this was to allow us to have the same instance of the code powering both our live and sandbox application. There's more involved to get this working, but this issue was just about URL generation for links in views. All links generated always both a subdomain and tld, so this code injects these values always.
These views are rendered both as a response to a HTTP request, e.g. in our client areas, but also as part of a non HTTP request, e.g. a scheduled task generating invoices and emailing them to clients.
Anyway, the solution:
For non HTTP contexts, a service provider can set the defaults:
<?php namespace App\Providers;
use App\Support\UrlDefaults;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaultsServiceProvider extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults(UrlDefaults::getDefaults());
}
}
Since the there's no routing going on to cause the problem I asked originally, this just works.
For HTTP contexts, the RouteMatched event is listened for and the defaults injected then:
<?php namespace App\Listeners;
use App\Support\UrlDefaults;
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;
/**
* Class SetUrlDefaults
*
* This class listeners for the RouteMatched event, and when it fires, injects the route paramaters (subdomain, tld,
* etc) into the defaults of the UrlGenerator
*
* #package App\Listeners
*/
class SetUrlDefaults
{
private $urlGenerator;
private $router;
public function __construct(UrlGenerator $urlGenerator, Router $router)
{
$this->urlGenerator = $urlGenerator;
$this->router = $router;
}
public function handle(): void
{
$paramaters = array_merge(UrlDefaults::getDefaults(), $this->router->current()->parameters);
$this->urlGenerator->defaults($paramaters);
}
}
UrlDefaults is just a simple class that returns an array:
<?php namespace App\Support;
class UrlDefaults
{
public static function getDefaults(): array
{
return [
'tld' => config('app.url.tld'),
'api' => config('app.url.api'),
'foo' => config('app.url.foo'),
'bar' => config('app.url.bar'),
];
}
}
So digging into the source for routing classes a bit more, there’s a defaults() method on the UrlGenerator class, but it’s not a singleton, so any defaults you set in a service provider aren’t persisted.
I seem to have got it working by setting the defaults in some middleware:
Route::domain('{domain}')->middleware('route.domain')->group(function () {
//
});
namespace App\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator;
class SetRouteDomain
{
private $url;
public function __construct(UrlGenerator $url)
{
$this->url = $url;
}
public function handle($request, Closure $next)
{
$this->url->defaults([
'domain' => $request->getHost(),
]);
return $next($request);
}
}

Testing API call in laravel controller/view

I'm testing an API call that I have working in Postman and other interfaces built over the API, but I'm trying to actually get my return values within my laravel site.
Should be simple enough, I made a very bare service file that uses guzzle to get to the API url, and I have a function in there for logging in using a test email, password and client ID (all of which work in the api interface such as Postman)
I'm calling the class and function inside my authController and then returning the function call in the view, then dumping that inside the view.
However, currently I'm getting null on my page dump.
Is there something I'm missing here, possibly in my service file. I don't think there should be anything passed here, but maybe I'm overlooking something more obvious.
Authorize.php
<?php
namespace App\Services\restAPI;
use Illuminate\Support\Facades\Auth;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use Session;
use Exception;
class AuthService
{
protected $baseurl;
protected $guzzleClient;
public function __construct() {
$this->baseurl = config('http://ip.address');
$this->baseurl = rtrim($this->baseurl, '/') . '/'; // ensure trailing slash
$this->guzzleClient = new \GuzzleHttp\Client(
['verify' => config('app.ca_pem_file'),
'headers' => [
'Event-ID' => ACCESS_EVENT_ID
],
'base_uri' => $this->baseurl
]
);
}
public function loginGetToken(){
$password = "password";
$email = "test#thisAPI.com";
$client_id = "0";
$payload = (object)[
'password'=>(string)$password,
'email'=>(string)$email
];
$retval = $this->post('login/?client_id='.$client_id,$payload);
return $retval;
}
private function post($endpoint,$payload){
$result = $this->guzzleClient->request('POST', $endpoint, ['json'=>$payload]);
$body = $result->getBody();
return json_decode($body);
}
}
AuthController.php
use Redirect;
use Illuminate\Http\Request;
use App\Services\restAPI\AuthService;
class AuthController extends Controller
{
public function login(Request $request)
{
$authService = new AuthService();
$login = $authService->loginGetToken();
return redirect(route('auth.login'))
->with('login', $login)
}
login.blade.php
<?php
dd($login);
?>
Since you are redirecting, the data is not directly available in the view as a variable. It should be in the session though, so you can try this in your view:
dd(session('login'));
https://laravel.com/docs/5.7/redirects#redirecting-with-flashed-session-data

Laravel – Calling API connections

Repeated Calls – Lets say you need your Application to talk to an API and you're using guzzle or a wrapper or whatever. I find myself having to call the connection in every controller function e.g:
class ExampleController extends Controller
{
public function one()
{
$client = new Client();
$response = $client->get('http://',
[ 'query' => [ 'secret' => env('SECRET')]]);
$json = json_decode($response->getBody());
$data = $json->object;
// do stuff
}
public function two()
{
$client = new Client();
$response = $client->get('http://',
[ 'query' => [ 'secret' => env('SECRET')]]);
$json = json_decode($response->getBody());
$data = $json->object;
// do stuff
}
}
How do I better handle this? Do I use a service Provider? if so, how would I best implement these calls? Should I create another controller and call all my API connections in each function and then include that controller and call upon each function as required? Should I place it in a __construct?
lets try the Dependency inversion principle
Ok this might sound a bit hard at first and my code might have some typos or minor mistakes but try this
You need to create the interface
namespace app\puttherightnamespace; // this deppends on you
interface ExempleRepositoryInterface
{
public function getquery(); // if you passinga variable -> public function getquery('variable1');
}
Now you have to create the repo
class ExempleRepository implements ExempleRepositoryInterface {
public function getquery() {
$client = new Client();
$response = $client->get('http://',
[ 'query' => [ 'secret' => env('SECRET')]]);
$json = json_decode($response->getBody());
return $json->object;
}
Now last step is to bind the interface to the repo in a service provider register method
public function register()
{
$this->app->bind('namespacehere\ExempleRepositoryInterface', 'namespacehere\ExempleRepository');
}
Now everytime you need the result in a controller all you have to do is to ineject
class ExempleController extends Controller {
private $exemple;
public function __construct(ExempleRepositoryInterface $home) {
$this->exemple = $exemple;
}
public function test() {
$data = $this->exemple->getquery(); / you can pass a variable here if you want like this $this->exemple->getquery('variable');
// do stuff
}
this is not the simplest way but this is the best way i guess

Routing tests in ZF2 / ZF2 equivalent of Zend_Test_PHPUnit_Controller_TestCase?

Thus far I've been testing my ZF2 controllers as follows:
namespace Application\Controller;
use Application\Controller\IndexController;
use Zend\Http\Request;
use Zend\Http\Response;
use Zend\Mvc\MvcEvent;
use Zend\Mvc\Router\RouteMatch;
use PHPUnit_Framework_TestCase;
class IndexControllerTest extends PHPUnit_Framework_TestCase
{
public function testIndexActionCanBeAccessed()
{
$this->routeMatch->setParam('action', 'index');
$result = $this->controller->dispatch($this->request);
$response = $this->controller->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$this->assertInstanceOf('Zend\View\Model\ViewModel', $result);
}
protected function setUp()
{
\Zend\Mvc\Application::init(include 'config/application.config.php');
$this->controller = new IndexController();
$this->request = new Request();
$this->routeMatch = new RouteMatch(array('controller' => 'index'));
$this->event = new MvcEvent();
$this->event->setRouteMatch($this->routeMatch);
$this->controller->setEvent($this->event);
}
protected $controller = null;
protected $event = null;
protected $request = null;
protected $response = null;
protected $routeMatch = null;
}
This allows me to test that the ViewModel is having the correct data (if any) assigned to it before the view is rendered. This serves that purpose just fine, but what it doesn't do is test that my routing is working correctly like the ZF1 Zend_Test_PHPUnit_Controller_TestCase tests would.
In those, I'd kick off the test by running $this->dispatch('/some/relative/url') and only get positive test results if the routes were set up correctly. With these ZF2 tests, I'm specifically telling it which route to use, which doesn't necessarily mean that a real request will be routed correctly.
How do I test that my routing is working correctly in ZF2?
I'm late to the party, but it still could be useful for newcomers. The solution nowadays would be to inherit from \Zend\Test\PHPUnit\Controller\AbstractControllerTestCase, so the usage would be very similar to ZF1:
class IndexControllerTest extends \Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase
{
public function setUp()
{
$this->setApplicationConfig(
include __DIR__ . '/../../../../../config/application.config.php'
);
parent::setUp();
}
public function testIndexActionCanBeAccessed()
{
$this->dispatch('/');
$this->assertResponseStatusCode(200);
$this->assertModuleName('application');
$this->assertControllerName('application\controller\index');
$this->assertControllerClass('IndexController');
$this->assertMatchedRouteName('home');
$this->assertQuery('html > head');
}
}
Note: This uses \Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase which includes assertQuery($path) as well as other web related methods.
EDIT: ZF2 has been updated since I self-answered this. PowerKiki's answer is better.

Categories