Using Angular CLI with Laravel

Published on 24 July 2020 6 minute read (1077 words)

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

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:

Angular working inside of Laravel

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.