- Laravel Vapor is a serverless deployment platform for Laravel, powered by AWS. Launch your Laravel infrastructure on Vapor and fall in love with the scalable simplicity of serverless.
- Nova Translation is a tool for Laravel Nova which allows you full control over your translations when using Laravel's localization functionality. Recently, I took advantage of the Black Friday sale and, although I didn't have a use for it, I purchased a Laravel Nova license.
- (2018-07-26) Introducing Laravel Nova by Taylor Otwell (2018-08-22) Getting Started With Laravel Nova (2018-07-25) Introducing Laravel Nova: A Tool for Building Admin Panels and Custom CMSes (2018-08-23) Installing Laravel Nova From a Private Repository (2018-08-22) Common problems when setting up Laravel Nova (2018-08-23) Deep Diving Laravel Nova.
Create a simple customer relationship management system with Laravel Nova following this step-by-step tutorial. This is the third of a four-part series about Laravel Nova which covers installation, creating a CMS, creating a CRM, and adding customization.
Getting started with GitHub Actions
GitHub rolled out GitHub Actions for everyone in November 2019. GitHub Actions are free to use (up to 2000 minutes per month) for public and private repositories and execute a workflow every time a defined event is triggered on GitHub. GitHub provides quite a lot of webhook events that can trigger a workflow.
So let's explore how to automate the steps of our development workflow when working with Laravel and Laravel Nova.
Workflow file
To get started, you need to create the basic YAML configuration file. This file is located in the repository in the .github/workflows
folder.
TL;DR: View the whole workflow file on GitHub.
The keyword on
defines when the action will be executed. In the example above, we listen for the [push]
event. It is also possible to chain multiple events [push, pull_request]
or restrict the workflow to a specific branch.
The jobs
section can hold multiple steps
. The first step loads an action actions/checkout@v1
provided by GitHub, imported with the keyword uses
.
Workflows run on Linux, macOS, Windows, and even Docker container images. If you're interested in a setup with a custom Docker container image, read the post Using Github Actions to setup CI/CD with Laravel by Luis Dalmolin.
Services
Before adding additional steps to our workflow we want to include a service.
In most cases, Laravel Nova needs a database. For this reason, we're adding the MySQL service to the workflow. My application is still running on MySQL 5.7 so I'm pulling in the official MySQL Docker image for MySQL 5.7 (the image also supports other tags like MySQL 8) that will spin up a MySQL server in the container. Make sure to define the env
variables and the port
as it will not work without these parameters!
To not confuse the environments I usually create a separate .env
file for the CI environment. Based on the .env.example
file create a new file with the name .env.github
which is holding the MySQL credentials from above.
You can also find the contents of the file on GitHub.
Verifying the MySQL connection
We're ready to add the first step. To make sure we've set up everything correctly we start with a step that tests if the MySQL connection is working. This step is optional and not related to the remaining workflow but I find it very helpful to make sure the MySQL connection is actually working.
If you're having trouble setting up the MySQL connection jump to the troubleshooting section below.
Installing dependencies
Now we're good to go to set up the world of our application. In this step, we're going to download and install all of the required dependencies via Composer. To authenticate Nova in the GitHub CI environment add your NOVA_USERNAME
and NOVA_PASSWORD
to the GitHub Secrets in the repository. The secrets are environment variables that are encrypted and not shared in forked repositories.
You can set your secrets here: GitHub Repository › Settings › Secrets
Booting Laravel
Next, we're going to boot the main Laravel application including Laravel Nova and all other dependencies. Therefore, we copy our CI credentials from the .env.github
file to the .env
file. After generating an encryption key we can run other artisan
commands.
Migrating the database
Run the available migrations to initiate the main structure of the database.Append the --seed
flag in case you'd like to seed the database with default entries e.g. roles or settings. The seed should not be necessary for your unit or feature tests, but may be helpful for other tests.
Running the build process
To make use of HTTP (feature) tests in our test suite we need to have a fully working application. This requires us to have access to all compiled assets in the CI environment. To compile the assets I'm using yarn
but it works with npm
as well.
Running the test suite
Finally, we are ready to run the PHPUnit test suite:
Be aware of the two different environments:
- the CI environment:
ci
defined in the.env.github
file, copied to the.env
file - the testing environment:
testing
defined in thephpunit.xml
file
Laravel 6 uses an in-memory SQLite database for testing (config on GitHub).
If you'd like to run your tests against a MySQL database, set the DB_CONNECTION
to mysql
and add the name of the database to DB_DATABASE
. Make sure that the database actually exists and is empty.
If you're having trouble setting up the MySQL connection jump to the troubleshooting section below.
Adding security checks
To check if the application uses dependencies with known security vulnerabilities we load and run the open source Symfony Security Checker. I like this step in particular as it adds additional value to the automation workflow.
Logs & Caching
We can cache the dependencies from Composer and Yarn. Therefore we use the actions/cache@v1
GitHub Action and a hash of the .lock
Triple 7 slot machine free game. -file as an identifier.
Ruben Van Assche covered these steps quite well in his post Getting started with GitHub Actions and Laravel so I'm not going to illuminate these steps anymore. The steps are also included in the workflow file on GitHub.
In case of an application error, you can download the logfiles from GitHub.
Running the complete workflow
FYI: Download the workflow file from GitHub.
After we commit and push the workflow file to our repository on GitHub the action runs (see on: [push]
above). Booting up the CI server including all dependencies and executing all the steps takes about two to three minutes (up to 2000 minutes per month are free which makes the CI service basically free to use).
Watch the workflow in action in the video below:
Troubleshooting
Failed to initialize, mysql service is unhealthy.
There is a problem with the MySQL port. If not defined, the port number is a random number. You can get the port number from the job service. Use the env
I39 1 6 0. key to assign the dynamic MySQL port number to a step manually.
SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed
The MySQL credentials are incorrect. Make sure to define the MYSQL_USER
, MYSQL_PASSWORD
and MYSQL_ROOT_PASSWORD
in your .env
file.
Read this answer on Stack Overflow for an in-depth explanation.
QueryException: SQLSTATE[HY000] [1045] Access denied for user 'user'@'localhost' (using password: YES)
The database does not exist or the database credentials are invalid. Check the .env
file and make sure to clear the config cache after a change:php artisan config:clear
RuntimeException: No application encryption key has been specified.
This error will pop up if the APP_KEY
is not set. Make sure to generate an APP_KEY
using the php artisan key:generate
command before executing any commands using artisan
.
Where to go from here?
The future of automation is now. Explore the awesome actions repository and use the available actions to create a release, send a notification or run a deployment. Or even better: Create your own actions!
Update: Freek Van der Herten shared and explained the GitHub workflow file for Ignition in his post Using GitHub actions to run the tests of Laravel projects and packages which contains a test matrix (php
, laravel
, dependency-version
and os
) as well as Slack notifications. Make sure to read the post as well!
- Part 3: Creating a CRM
Part 3: Creating a CRM
In the previous post we quickly created CMS features using Laravel Nova. Learning a few more of Nova's features will allow us add customer relationship management (CRM) functionality as well.
Most large companies have a stand-alone CRM system. If you can afford a stand-alone CRM and have the discipline to use it, I highly recommend implementing one. I can hardly count the number of integrations I've built with Salesforce, Hubspot, and pipedrive (several of which have been integrated using Laravel or Lumen).
So why add CRM features to your Laravel web app using Nova?
- You don't have a stand-alone CRM and don't have the time or budget to implement one
- You don't want to invest the time and resources to learn a new CRM
- You decide your web app's data can be siloed
- You only need a few lightweight CRM features
- You want to control and trigger code functionality directly from your web app
In this post we'll put together a lightweight CRM using Laravel Nova for a hypothetical chain car dealership. The dealership has multiple locations. Potential customers (leads) can enter a raffle giveaway at any dealer location. The web app administrator can make any lead a winner which sends them a congratulatory email. These are the Nova topics we'll cover:
You may follow along every step by cloning my repo and checking out the part-2
branch, or you can checkout the part-3
branch to view the final code. If you use the code base, you will still need a Nova license to get it working.
Setup
We'll set up by creating models, migrations, relationships, factories, seeds, and Nova Resources for locations and leads. If you have a good grasp on these topics from the previous post, feel free to skip down to filters.
Models
Let's make models for Locations (dealerships) and for Leads (potential customers). On the command line type:
Add a relationship between the two models. In app/Location.php
add:
and in app/Lead.php
add:
Database
Next, create the underlying database tables.
Edit the migration for locations and add these table columns:
We give each location a name
. We also set address_2
to nullable in case the user doesn't enter a second line for address.
Now add these columns to the leads migration:
Each lead will have a name
, email
, location
, and a flag (is_winner
) for whether they are a winner or not. We'll use a nullable timestamp so we know if they won, and if so when they won.
Run the migrations:
Seeds
In real-world usage, we could assume that the site admin will add locations manually through Nova and that leads will submit their information through a web form. For the sake of this demo, rather than build that web form to populate leads and then manually submitting that form dozens of times to get lead data, let's just create some using seeders.
We'll start with factories for both models.
Edit database/factories/LocationFactory.php
to fill in some locations using Faker.
We'll set the name of the location as the city name for this example.
Edit database/factories/LeadFactory.php
to generate some leads. Let's set about 1 in 20 leads as winners and set each lead to a random location.
Now we can create and run seeds.
Update database/seeds/LocationsTableSeeder
to create 12 locations:
and database/seeds/LeadsTableSeeder
to create a thousand leads.
Now update database/seeds/DatabaseSeeder.php
to run both seeders:
Finally, run the seeder:
This may take a few moments. When it finishes, you'll have 12 locations and 1000 leads in the database.
Nova Resources
Create resources for Locations and Leads.
Open app/Nova/Location.php
. Make sure the model is set to 'AppLocation'
. Set the $title
to 'name'
. For $search
, let's allow Nova users to search by name, address, city, state, and postal code.
We'd like all the fields to be accessible in Nova. Let's use ID, Text, and Place fields
and return this array for the fields()
method:
We haven't used the Place
field before. This is a neat field that uses the Algolia Places API to provide fast address searching and auto-completion. Even better, you do not need an Algolia account to use this field. We chained ->countries(['US', 'CA'])
to the Place field to tell it to only search addresses in the US and Canada.
If you go to add a new Location in Nova, you get this popup while filling the address field:
If you select a suggested option, Nova automatically fills in the remainder of the fields for you.
Now let's update app/Nova/Lead.php
. Set the $title
to 'name'
and $search
to return name
and email
.
For the Lead's fields, let's make all fields available to Nova as well as the Location the Lead belongs to. For the resource index page, let's hide the is_winner
datetime and instead show a calculated boolean indicating whether or not the lead has won. Slots zeus way.
For the fields()
method, return an array:
Be sure to import all the used field types.
Since we want to use is_winner
as a datetime in Nova, let's have Eloquent cast it as a datetime for us. In app/Lead.php
add:
Now if you click on Leads in Nova you should see your randomly generated leads.
Filters
At this point we have Location and Lead models, database tables, seed data, and Nova resources. We're ready to create our first filter. Nova filters are used to narrow the results shown on a resource index page. This is achieved by scoping Eloquent queries with custom conditions.
For example, let's say we want to be able to view our leads by location. Begin by creating a new filter:
This creates a file at app/Nova/Filters/LeadByLocation.php
with two methods. The options()
method lets us specify which select options will be available as filters for the leads. Update this method to return an array of all the locations where the array keys are the location names and the array values are the location IDs.
Remember to include use AppLocation;
.
Now update the apply()
method. This is where the Eloquent $query
is passed in and scoped. For example, change this method to:
Now when a collection of Leads are passed into this filter, and option may be selected from the Locations array, and the selected location ID is passed into the where()
scope on the Leads.
We're ready to register this filter to the Lead resource. Open app/Nova/Lead.php
and edit filters()
to return:
Refresh the Leads index page and open the options in the top-right corner. You'll see all the locations.
Select any location and only the leads attached to that location will show.
Actions
Nova actions let you perform tasks on Eloquent models. Actions can provide a lot of power to Nova users. In this demo let's create an action that makes a lead a winner.
Begin by creating the action:
This generates a file at app/Nova/Actions/MakeLeadAWinner.php
. The handle()
method is passed a collection of Eloquent models, even if only one model was selected by the Nova user. We can mark each model as a winner by importing use CarbonCarbon;
and updating hande()
to:
Let's register this action and see how it works. Open app/Nova/Lead.php
and edit actions()
to return:
Refresh the Leads index listing and check one or more lead checkboxes.
A dropdown titled Select Action appears with one action under Lead called Make Lead A Winner. Select that action and press the blue triangle button. You will see a confirmation box like this:
If you click Run Action you'll get a green notification saying the action ran successfully. The Is Winner
dots also automatically change from red to green.
Action Log
Click on the the eye icon next to one of the leads you just made a winner to view it. It would be nice if we could see a log of all actions run on each resource. We can do this with the LaravelNovaActionsActionable
trait. Edit app/Lead.php
and have it use the Actionable trait:
Refresh the lead detail page and you will now see an action log at the bottom of the page.
Queued Actions
Some actions may take a while to process, in which case we'd like to send them to the job queue. This is accomplished by implementing IlluminateContractsQueueShouldQueue
. Edit app/Nova/Actoins/MakeLeadAWinner.php
and add the contract:
The action will use the default Laravel queue. The documentation seems to be incorrect on how to do this. To set a queue for the action, add this constructor. We'll use Homestead's redis queue for this example by adding this constructor to MakeLeadAWinner
:
Let's also modify the action to do more than just mark the lead as a winner. Let's have the action also send an email to the lead to let them know they have won.
Open app/Mail/CongratulateWinner.php
. Let's make the Lead model public so it is accessible to the view. We could also generate some action URL to place in the email. Our mailable might look something like this:
Now edit the markdown view at resources/views/email/congratulate-winner-content.php
.
Assuming you are using mailtrap as your development mail server and have configured your MAIL_USERNAME
and MAIL_PASSWORD
in .env
, you should be ready to test email.
To use the Redis queue, we need the predis driver.
Now make sure the queue is on by editing your .env
file and setting QUEUE_CONNECTION=redis
. Then listen using
Finally we can test queued actions. We can run actions on multiple leads from the lead index page or on a single lead from the lead details page. To do this, find a lead that is not a winner on the lead index page and click on the View icon to get to the lead detail page. Select Make Lead A Winner, and press the blue triangle button. Click Run Action from the confirm box.
The green notification immediately opens saying This action ran successfully
. This actually means that the action was queued successfully. The action status shows Waiting
.
Looking at the console we see the job is immediately processed.
Watching the lead detail page, the action status changes to Finished in real time without needing to refresh the browser.
Checking in mailtrap, we see the email sent successfully.
Queued actions are extremely useful for running long tasks (actions) and being able to see when tasks are running and when they are finished. This animated gif shows the full process of running a queued action.
Action Responses
Nova gives us the ability to send messages to the UI and to even mark actions as failed. If we were to edit app/Nova/Actions/MakeLeadAWinner.php
and cause it to throw an exception, our exception handler would mark the action as failed. For example, update the hande()
method to:
Kill and restart the queue so the updated MakeLeadAWinner job is loaded. This time let's put a maximum number of retries so our failed job doesn't get re-queued and retried indefinitely.
Now run the action on any lead. We still immediately get the message that the action was successful because the action was successfully queued. However we see that the job fails in the queue three times before aborting.
Then the lead detail page updates with a status of Failed.
For the sake of the demo, be sure to remove throw new Exception('Something went wrong');
and restart the queue after seeing this functionality.
Action Fields
The last topic we'll cover for actions is how to send input into an action using action fields. Let's say we want to customize the subject line of the email we send to the winner. We can add a field to app/Nova/Actions/MakeLeadAWinner.php
in the fields()
method.
Be sure to include use LaravelNovaFieldsText;
for the Nova field.
We could add as many fields as we'd like and use the same validation rules available to us for Nova resources.
In this case, let's take the Subject
input and use it in our email template. In app/Nova/Actions/MakeLeadAWinner.php
notice that the handle()
method passes in ActionFields $fields
. This object contains all the fields passed by the front-end as properties. To access the subject line field, change:
to
Next, open app/Mail/CongratulateWinner.php
and update it to use the subject field like so:
Now, whenever this mailable is invoked, the subject line is set to the action input.
Let's test this out. Be sure to restart the work queue:
Return to the browser, find a lead, and click Make Lead A Winner
. We see our action fields pop open.
Add a subject line and click Run Action
. Assuming the queue is running, an email is sent with the subject line we entered.
The ability to provide input to any action gives us a lot of flexibility in providing CRM-like features.
Lenses
Let's say we want a page where we can see all the contest winners who have won in the past week. Technically, we could add some filters to the Lead
Nova resource to filter by winners and by week. Any time we wanted to see the recent winners we'd have to re-apply these filters. What if we'd also like to show different fields on the resource listing page such as when they won or how long it took to convert the lead to a winner?
This requires a special type of filter called a Lens Filter. A Lens gives us a direct listing to a resource defined by a fully customized Eloquent query. This can be particularly useful when your results require joining multiple database tables together.
Start by creating a lens:
This generates a file at app/Nova/Lenses/RecentWinners.php
. This class looks like a Nova resource class mashed together with a Nova filter class. Before we start to fill it out, let's attach it to the Lead
Nova resource. Open app/Nova/Lead.php
and in the lenses()
method return:
In the browser, navigate to the Leads page. You'll see the special Lens filter dropdown next to the regular filters dropdown.
Select Recent Winners
from the Lens dropdown. You now see a listing of all Leads
by ID.
Let's return to app/Nova/Lenses/RecentWinners.php
and add some customization.
The query()
method allows you to customize the eloquent results shown on this page. We'll change the returned value to:
This selects only leads who have won within the past week, as well as the location they won at.
Next add the fields we'd like to see on the listing page in the fields()
method.
Import the Number
and DateTime
field types:
Now refresh the browser. We see the the winners in reverse chronological order, just for the past week.
It would be nice to be able to sort the results by the number of days it took for the lead to win. Unfortunately, this is a computed field and Nova can only sort data by database columns.
We can, however, add additional filters to our Nova filter. For example, we could apply the same LeadByLocation
filter that is attached to the Leads
Nova resource to the lens. Do this by editing the filters()
method to return:
Now we can filter our recent winners by this filter.
Metrics
Nova metrics are pre-built cards that show data values, trends, and partitions. Let's set up one of each type.
Value Metrics
Starting with a value metric card, generate the class using:
This metric will show how many new leads we've had for a given interval. The generated file app/Nova/Metrics/NewLeads.php
has four methods. calculate()
returns the data for the card. Let's import use AppLead;
and update calculate()
to return:
The card will now show the count of leads created during a given interval. We could also have leveraged built-in average
, sum
, max
, or min
functions, or we could have done our own calculations.
The ranges()
method returns an array where the keys are the number of days and the values are the written labels for those values. Out of the box, the metric comes with the following intervals:
MTD, QTD and YTD are predefined and understood within Nova. For this example, let's add intervals for one week and two weeks to the array.
The cacheFor()
method allows you to cache the results of the metric for given duration, hooking into your Laravel caching driver. This prevents Nova from re-calculating the same metrics on every page refresh. We'll leave this blank.
The uriKey()
method lets us set the URI that the front-end will call to retrieve the data via AJAX.
Now that our metric is done, let's register it to the Nova Lead resource index by opening app/Nova/Lead.php
and updating the cards()
method to return:
Return to the browser and refresh the lead resource index and we see our metric card. In addition to showing the number of new leads in the chosen interval, it compares that number to the number of new leads in an equally-sized previous interval.
Trend Metrics
Let's create a daily trend metric to represent the same data regarding new leads.
Open app/Nova/Metrics/LeadsPerDay.php
, import use AppLead;
, and update calculate()
to return: Loose slot machines las vegas.
Now update ranges()
to include one and two week intervals:
Lastly, register the metric to the lead resource index in the cards()
method:
Refresh the Lead resource index to see the trend metric. Since we added ->showLatestValue()
to the calculate()
return value, the latest trend metric is highlighted as a single number. Hovering over any point reveals tooltips with more details about the data.
Partition Metrics
Partition metrics are used to show pie charts of multiple values that add up to a whole. For example, we could see what percentage of winners have come from each location. Since it's difficult to see 10 locations in a pie chart, let's only consider the first 4 locations for this example.
Begin by generating the metric class:
Open app/Nova/Metrics/WinnersByLocation.php
. For this metric we need to count winners, which is a subset of Lead model objects. This means that Nova's pre-built function for summing the Lead objects won't give us what we need. Fortunately we can calculate this ourselves.
Begin by importing the Eloquent model:
Now enter the following into the calculate()
method:
This takes only 4 locations and creates an array where the keys are the location names and the values are total number of winners for each location.
Lastly, since we can attach this metric to any Nova resource index page, let's attach it to the Locations resource index. Open app/Nova/Location.php
and update cards()
to return:
Navigate to the Locations resource index to see the metric.
Attaching to the Dashboard
Laravel Novo Banco
Perhaps some of the metrics are really important and we'd like to show them on the Nova dashboard. Out of the box, the Nova dashboard shows several help-related cards. We can change this in app/Providers/NovaServiceProvider.php
in the cards()
method. Remove the Help
card and add in our metrics. To change the size of a metric, chain the ->width()
method like so:
Laravel Nova Demo
Refresh the Nova dashboard in your browser and you now see your metrics.
Laravel Nova Free Download
We'll look into attaching other kinds of cards in the next post titled Part 4: Customization (coming soon). We'll also cover Nova customizations by building our own tools, fields, and cards.