Using Laravel with the latest Angular doesn't seem quite straightforward to achieve, but, with the help of this article, you too can use the latest and greatest Angular within your Laravel app without any complex setup.
Background
I recently decided I'm going to learn a new framework. Learning new things and keeping up-to-date is a great way to continuously improve upon your skills. After a lot of research and contemplation, I decided it might be worth it to learn Laravel.
PHP is one of the first languages I ever learned, way back when in the mid-00s, however, I hadn't used it in quite a few years. Things such as deciding to purely focus on front end development, the horror that is WordPress development, and so on really put me off of it. However, I was thoroughly impressed with the features added to it over the years (especially the stuff coming up in PHP 8).
It now seems less of a "Wacky West" language and more of a something mature and clean you'd actually want to use day-to-day. Laravel also seems like a great framework to base your next application on, with lots of accessible documentation, an expansive ecosystem, and an overall decent community.
The problem
I was making a personal Laravel project and I really wanted to use, my personal favourite front end framework, Angular via the Angular CLI, and keep Laravel as a sort of back end API, but couldn't find a straight forward way of achieving this goal without having to utilise a custom Webpack configuration.
In other words - I wanted to keep it all "vanilla" and be able to utilise the CLI to its fullest potential, but not have to host the two apps (front/back) separately. This article is a result of all my research and experimentation to achieve this goal, gathered in one place as a tutorial, so I hope it helps you in your Angular + Laravel endeavours.
Prerequisites
- >= PHP 7.x
- Composer
- Node
- Angular CLI (
npm i -g @angular/cli
)
Setting up the Laravel application
We need to start where all Laravel projects start - with a simple creation Composer command:
composer create-project --prefer-dist laravel/laravel my-angular-cli-laravel-project
Setting up the Angular application
Now that we have a Laravel app up and running, we need to navigate into its root folder and generate our Angular app via:
ng n app-front-end
(Feel free to use any other CLI generation parameters and to select whatever you like here)
Changing our CLI build location
We want the Angular CLI to output code in our Laravel app's public
folder, that way we are going to avoid having to run two servers at the same time when serving our app in production.
Navigate to app-front-end
and open angular.json
. You need to find the following lines:
{
/* ... */
"options": {
"outputPath": "dist/app-front-end",
"index": "src/index.html",
},
/* ... */
}
and change them to:
{
/* ... */
"options": {
"outputPath": "../public/build",
"index": "",
},
/* ... */
}
Our Angular app will now get compiled and output into the root public/build
folder. Setting our index
property to a blank string simply tells the CLI not to output an index.html
file (since we won't be using it anyway).
It's a good idea to add /public/build
to your root .gitignore
file, as you might not necessarily want to push that folder upstream. Especially if you're planning on developing your app seriously.
Changing our CLI build configuration
We need to alter the default CLI build configuration for our builds to work better with our Laravel app. We'll also add a new configuration, development
, which will be utilised when running our front end app in development mode.
Once again, open angular.json
, and find the following lines of code:
{
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
}
change them to:
{
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "bundles",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"deployUrl": "build/",
"statsJson": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"development": {
"sourceMap": true,
"outputHashing": "none",
"watch": true,
"deployUrl": "build/"
}
}
}
We've changed a few things here - outputHashing: "bundles"
, which will set proper cache busting file names for our bundle files, deployUrl: "build/"
which will tell Webpack where to dynamically import modules from (this is super important if you want to lazy load anything in your app!), and, finally, statsJson: true
which will tell the CLI to generate a stats.json
file (more on that later).
Changing our NPM scripts
As you might've guessed from the multiple configuration setup we made above, we won't be using the default ng serve
script whilst we're developing our front end app. It doesn't work very well alongside our Laravel app when you use lazy loaded modules, and, besides, we don't want to double host our front/back end apps, even during development.
We need to change our generated NPM scripts (located inside package.json
) so that we can work on our app without having to rebuild manually all the time.
Find these scripts:
{
"start": "ng serve",
"build": "ng build"
}
and change them to:
{
"start": "ng build --configuration development --prod=false",
"build": "ng build --prod"
}
That's it for our Angular app! Let's get back to Laravel, and prepare it for its front end counterpart.
Making a custom Laravel service
We need to make a custom PHP service that reads our stats.json
file (when available) and extracts the correct hashed file names that correspond to all our front end assets. We'll later provide said values to our primary view.
First off, create a new folder inside of app
called Services
, then create a file NgBuildService.php
inside of it with the following content:
<?php
namespace App\Services;
use Exception;
class NgBuildService
{
// We'll cache our assets hash here, so we don't have to
// constantly extract the values from stats.json
public $assets = array();
public function __construct()
{
// Only extract the values when in production mode
if (config('app.env') === 'production') {
$this->extractAndCache();
}
}
/**
* Extracts all bundle assets from public/build/stats.json
* in the format:
* {
* "assetFileName": "assetHashFileName"
* }
*/
private function extractAndCache()
{
$path = public_path('build') . '/stats.json';
try {
$json = json_decode(file_get_contents($path), true);
if (isset($json['assets']) && count($json['assets'])) {
foreach ($json['assets'] as $asset) {
$name = $asset['name'];
if ($asset['chunkNames'] && count($asset['chunkNames'])) {
$this->assets[$asset['chunkNames'][0]] = $name;
} else {
$this->assets[$name] = $name;
}
}
}
} catch (Exception $e) {
// Feel free to do something with the exception here
// like yell at the dev they forgot to run
// npm run build / yarn build before they deployed
// the Laravel app or something
}
}
}
In production mode, our new service will extract the right asset names from stats.json
in a hash format { 'assetName': 'assetHashedName' }
.
Making a custom Laravel provider
Now we need to make a companion provider for our service. It'll essentially generate a singleton out of it which will be injected wherever we need it (more on that in a bit).
To generate the provider, we need to run:
php artisan make:provider NgBuildServiceProvider
This will generate a new file app/Providers/NgBuildServiceProvider.php
, open it and change its content to:
<?php
namespace App\Providers;
use App\Services\NgBuildService;
use Illuminate\Support\ServiceProvider;
class NgBuildServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
$this->app->bind('App\Services\NgBuildService', function ($app) {
return new NgBuildService();
});
}
}
We've changed the boot
method so it makes a singleton out of our Service that will be injected app-wide, depending on usage.
Making the App controller and view
We need to make a custom Laravel controller from which we'll provide our NgBuildService
's values to our primary view (which we'll also make):
php artisan make:controller AppController
This will generate a new file app/Http/Controllers/AppController.php
, open it and add a new method inside of its class
called index
:
<?php
namespace App\Http\Controllers;
use App\Services\NgBuildService;
class AppController extends Controller
{
/**
* Our custom service provider is going to make sure
* $ng is a singleton
*/
public function index(NgBuildService $ng)
{
// Provide our service's assets as $ngAssets inside
// of app.blade.php
return view('app', ['ngAssets' => $ng->assets]);
}
}
Now we need to create a new app.blade.php
view in resources/views
:
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name') }}</title>
<base href="/" />
{{-- This'll load our extracted and hashed CSS assets here --}}
@env('production')
@if (isset($ngAssets) && count($ngAssets))
<link rel="stylesheet" href="/build/{{ $ngAssets['styles'] }}">
@endif
@endenv
</head>
<body>
@if (Route::has('login')) @auth
<a href="{{ url('/home') }}">Home</a> @else
<a href="{{ route('login') }}">Login</a>
<a href="{{ route('register') }}">Register</a> @endauth @endif
<noscript>You need to enable JavaScript to run this app.</noscript>
<app-root></app-root>
{{-- This'll load our hashed assets when in production --}}
@env('production')
@if (isset($ngAssets) && count($ngAssets))
<script src="/build/{{ $ngAssets['runtime'] }}" defer></script>
<script src="/build/{{ $ngAssets['polyfills'] }}" defer></script>
<script src="/build/{{ $ngAssets['main'] }}" defer></script>
@endif
{{-- This'll load the development assets when in dev mode --}}
@else
<script src="/build/runtime.js" defer></script>
<script src="/build/polyfills.js" defer></script>
<script src="/build/styles.js" defer></script>
<script src="/build/vendor.js" defer></script>
<script src="/build/main.js" defer></script>
@endenv
</body>
</html>
Configuring Laravel to use the new controller and view
Finally, we need to tell Laravel to use our new controller/view combination for all non-API calls (essentially making Angular handle non-API routing).
Open routes/web.php
and change:
Route::get('/', function () {
return view('welcome');
});
to
Route::get('/{any}', 'AppController@index')->where('any', '.*');
And...we're done! You now have Angular and Laravel, happily, working in tandem:
Developing
Run the development script first:
cd app-front-end && npm start
Then serve the Laravel app with php artisan serve
(in another terminal tab/instance).
Deploying
Before deploying, we need to build our Angular app first:
cd app-front-end && npm run build
Then you can deploy it like any other Laravel app.
Caveat
Since we're not using ng serve
to serve our Angular code in development mode, we also don't get hot module replacement. This means you'll need to manually reload your browser window to see your Angular changes.
Source code
You can find the source code for this tutorial's project here.