uncgits/ccps-core

There is no license information available for the latest version (3.3.1) of this package.

CCPS Framework Core components

3.3.1 2021-08-17 16:53 UTC

This package is auto-updated.

Last update: 2024-04-17 22:28:01 UTC


README

This package is designed to take a fresh installation of Laravel 8 (version ^8.0) and transform it magically into CCPS Framework, with all core features required to build a UNCG CCPS web app.

Who maintains this?

The UNCG CCPS Developers Group maintains this app - ccps-developers-l@uncg.edu

What's included

Several things are included in this core repository, which are expected to be used / included in every application built off of CCPS Framework:

  • Default views, routes, database migrations
  • Flash Messaging
  • Guzzle HTTP client
  • Breadcrumbs
  • Menu
  • ACL (Roles and Permissions) with custom Role Mapping
  • Socialite login (with Azure and Google integration)
  • App configuration variables (stored in the database)
  • Logging and live Log Viewer
  • App Cache Management (basic)
  • Queue / Job management (database driver)
  • Emails - sent mail history
  • Cron Jobs and Cron history
  • Backups
  • Google Hangouts Chat notification channel
  • Google Hangouts Chat log channel
  • User-customizable per-channel Notifications (Event-based)
  • Easy support for Laravel Dusk
  • Easy support for Laravel Horizon and Redis-based queues
  • Built-in support for simple API keys with Laravel Sanctum

Upgrading the framework

See the instructions in the UPGRADE.md file if you are upgrading to this version of CCPS Core from a previous version.

Version History

Full history is available in the VERSION.md file.

NOTE: version 3.0 of CCPS Core migrates the entire stack away from Bootstrap and jQuery and instead brings in the "TALL Stack" - TailwindCSS, AlpineJS, Livewire, and Laravel. If you need to continue using Bootstrap 4, either use version 2.*, or read below on how to customize your views away from default.

Notes about Context and Environment

This package was designed to be installed inside of a Docker container, on a development machine, for the development of applications. The expectation is that development will take place locally and the resulting application will be contained within a separate repository, and deployed in its entirety to a production VM (using the same Docker container). The flow in mind is this:

1) Create a new Laravel application 2) Install this package 3) Initialize a new repo for your application 4) Commit changes to your own remote repository for your app 5) As available, use composer to update the elements of your app as CCPS Core is updated

Environment Requirements

For the local environment, you will need:

  • A Docker container running PHP version ^7.3+ or ^8.0 and Apache
  • A Docker container running MariaDB 10+
  • OPTIONAL but helpful - these two containers networked together (so you can use the name of the container as DB_HOST in Laravel)
  • Access to the MariaDB container's database (recommended by Sequel Pro or another GUI app, but command line works if you're so inclined)

First-time Installation

If you are installing CCPS Core for the first time on a new Laravel app, keep reading.

If you are deploying an app that you have already built using CCPS Core to a new server, see the section on Deploying a CCPS Core app

Pre-Installation

CCPS Core can be installed in one of two configurations:

  • to the /var/www/html directory if using a TLD to access the app (officially supported method of Laravel framework) - e.g. https://myapp.uncg.edu
  • to the /var/www/apps directory if you plan to use the "subfolder" method (access using the host machine URL and symlink) - e.g. https://myserver.uncg.edu/myapp

TLD method

If you are using the officially-supported method of installation (TLD pointed to /public/ folder):

  • Edit your machine's hosts file to resolve a new dev domain name to 127.0.0.1
  • Edit the PHP/Apache Docker container's vhosts.conf file to point to the /public/ directory of the folder where you intend to install the Laravel app. Example, assuming you use "myApp" as the app's name:
    <VirtualHost *:80>
      DocumentRoot /var/www/html/myApp/public
      ServerName myApp.local
      AllowEncodedSlashes On
    </VirtualHost>
    
  • Restart the PHP/Apache container so that the host configuration is reloaded.
  • Follow instructions below to install a new Laravel app to the html folder in the container.

Symlink / subfolder method

If you are using the symlink method of installation (subfolder install):

  • Edit your machine's hosts file to ensure that your TLD resolves to 127.0.0.1
  • Edit the PHP/Apache Docker container's vhosts.conf file to point to /var/www/html directory at that hostname. Example, assuming you'd configure your app to be accessed at myserver.local/myApp:
    <VirtualHost *:80>
      DocumentRoot /var/www/html
      ServerName myServer.local
      AllowEncodedSlashes On
    </VirtualHost>
    
  • Restart the PHP/Apache container so that the host configuration is reloaded.
  • Follow instructions below to install a new Laravel app to the apps folder in the container.

Installing and Initializing CCPS Core

Please make sure you've followed the instructions in the Pre-Installation section to set up your machine first.

NOTE: Installing this package on anything other than a FRESH Laravel installation may produce unexpected results! Until/unless this README states otherwise, please only install the CCPS Core package on a fresh Laravel installation by following the steps below.

  1. Set up your database and have the information available - you will need it when installing the CCPS Core framework. YOU MUST HAVE A DATABASE SET UP BEFORE PROCEEDING OR THIS INSTALLATION WILL FAIL.
  2. Log into the container, and use laravel new myApp to install a new Laravel 6.* app into the desired location (html or apps folder) - this is preferred to using laravel new myApp because we can specify what version of Laravel to use. laravel new will always pull latest stable version of Laravel.
  3. cd into the app's directory.
  4. Edit composer.json to include the repositories listed below. ("Repositories you need")
  5. Run composer require uncgits/ccps-core to install this package (it will be added as a requirement to composer.json automatically). If you are installing a dev version, your command will look something like: composer require uncgits/ccps-core:dev-[branchname] instead. There is no need to add a minimum-stability: dev clause to composer.json as of Laravel 5.8, as it is included out of the box.
  6. Run php artisan ccps:init and follow the prompts to complete initialization (initial app setup). If you wish to have yes/no prompts at each step, add the -p or --prompt flag.
  7. If you are installing to the apps folder (symlink method), you will need to set up the symlink: ln -s /var/www/apps/myApp/public /var/www/html/myApp.
  8. Assuming that you set up the hosts file properly as in the Pre-Installation section, you're done! Navigate to your app's address and you are set to go.

Repositories you need

Some child packages used in CCPS Core are not on Packagist, and will need to be added to your application's composer.json (Composer will not read these repositories from child repositories, so they need to be inserted into your app manually.)

In composer.json you will need to add the following:

"repositories": [
    {
        "type": "git",
        "url": "https://bitbucket.org/uncg-its/ccps-core"
    },
    {
        "type": "git",
        "url": "https://bitbucket.org/uncg-its/laravel-breadcrumbs"
    },
    {
        "type": "git",
        "url": "https://github.com/tomhatzer/monolog-parser"
    }
],

Deploying a CCPS Core app

After you build your app from this CCPS Core base, and want to deploy it to a test machine, server, etc., you can simply:

  1. pull it from your upstream repository
  2. run composer install to get all dependencies for packages listed in the current composer.lock
  3. run php artisan ccps:deploy - this will skip most of the steps from ccps:init as those changes will be committed to your repo, but will perform the important steps like getting your .env file copied, setting up your application key, and running migrations/seeders. You can also use the --p or --prompt flag here to step through one thing at a time (just like with ccps:init).

Note: When using ccps:deploy, CCPS Core will perform database migrations a bit differently from traditional Laravel. All CCPS Core migrations (located in vendor/uncgits/ccps-core/migrations) will always run before user and other package migrations. Please plan your app migrations accordingly.

You should also ensure that you are NOT using the DebugBar in production after deploying the app - therefore you should set DEBUGBAR_ENABLED to false in the application's .env file. The DebugBar is shown regardless of the value of APP_DEBUG if DEBUGBAR_ENABLED is set to true.

Usage

Logging in for the first time

To log in, the default credentials are:

  • username: admin@admin.com
  • password: admin

The initial password may be different if you opted to change it during ccps:init or ccps:deploy.

You can create additional users and then remove the original user if you wish - there is no need to keep the default user except as a safety.

Setting other authentication methods

UNCG officially supports Google and Azure authentication, and so you will need to add one or both of them as an accepted login method in the app in order to use them. In the application's .env file, you will need to modify the APP_LOGIN_METHODS variable and set your login methods to a combination of local, azure, and google, separated by commas.

  • local will allow local auth only
  • local,azure will allow local or Azure auth
  • google will allow only Google auth

You will also need to set APP_ALLOW_SIGNUPS to true in order to allow registration of new users. Of course, you'll also need to fill in the SOCIALITE_AZURE_* or SOCIALITE_GOOGLE_* information, with the information you get from creating the app in your Azure or Google environment.

Once this is done, users will be able to authenticate / create accounts via 3rd party providers. If you need further restrictions on access to your application, it is recommended to disallow new account registrations by setting APP_ALLOW_SIGNUPS to false, or by inserting other logic into the authentication flow (e.g. AuthController.php, or through additional middleware). This is up to the app developer to implement if/as needed.

Login redirect

By default the application will direct unauthenticated users (who would receive a 403 error) to the login page, and then back to the previous target. You can disable this behavior and instead show a 403 error to those users by adding the following to the .env file:

APP_LOGIN_REDIRECT=false

Extending core functionality

Most controllers, models, events, listeners are provided in a way that lends to being overridden by app developers for their own needs. To this end, the CCPS Core package (in vendor) contains the base classes (namespaced Uncgits\Ccps\*) with full functionality, but also publishes versions of each class into the App\CcpsCore namespace. Throughout the Core code, rather than referring to or using the Uncgits\Ccps\* namespaced classes, the code refers to the App\CcpsCore\* namespaced classes instead. All of these published classes simply extend the Core classes, with no added functionality - so essentially they perform the same function as the Core classes, but can be used to add / override the functionality of the base class easily.

For example, the Uncgits\Ccps\Controllers\CacheController class is the controller for the Cache module. Let's say that you want to add a method to the Cache portion of the app so that you can more easily visually see what files the Cache currently contains. You could just go to App\Http\Controllers\CcpsCore\CacheController, and add a viewCacheItems method in, and write your logic, routes, views, etc. to go with it. All of the functionality of the Core class will be preserved through inheritance, while your new custom method would be implemented as well.

Similarly, if you wanted to change how an existing method worked (for example, CacheController@index), you simply override it in the App\Http\Controllers\CcpsCore\CacheController class object. If you are doing this, pay particular attention to what the original method did / provided, and make sure you either replicate it or call the parent method from the child.

Extending core views

By default, all views for your application are loaded from the vendor/uncgits/ccps-core/src/views folder, by extending the default paths that Laravel searches for view files in config/view.php. This means that ALL view references, including Blade views, components, partials, Livewire files, etc., are provided by CCPS Core without any additional publishing necessary.

Customizing a view

The view configuration provided by Laravel ensures that your local resources/views folder will be checked FIRST for any views. So if you want to override a view provided by CCPS Core, simply place a copy of it at the same path in resources/views, and it will be loaded in place of the default! Once again, this goes for any Blade views, components, partials, Livewire files, etc.

View Component Classes

Version 3.0 uses View Components for the majority of layout-related scaffolding. Most of these are anonymous, but some require a View Component Class for some additional functionality. These are registered individually in CcpsServiceProvider.php, and if you would like to override their functionality, all you need to do is add the following to the boot() method of your AppServiceProvider.php:

\Blade::component('button', \App\View\Components\MyButton::class);

Then, run php artisan view:clear and you should be using your Component Class in place of the default.

Livewire Component Classes

These are the exact same as View Component Classes. To override and use your own, make the same change in your AppServiceProvider.php:

Livewire::component('flash', \App\Livewire\MyFlash::class);

Then, run php artisan view:clear and you should be using your Component Class in place of the default.

Application Logs

Monolog / Laravel Logging

CCPS Core includes a Log Viewer page to more easily read the application logs onscreen.

Logging is simple, and conforms to the possibilities listed on the Laravel documentation:

Log::info('hi there'); // sends to default channel, defined in config/logging.php
Log::channel('general')->info('hi there'); // sends to 'general' channel regardless of default logging settings
Log::stack(['general','slack'])->info('hi there'); // sends to a custom stack of channels regardless of default logging settings
Log::info('hi there', ['stop-crons' => 'all']); // sends to default channel with extra context info (parseable in a Listener, see below)

Log Channels

Adding channels and further customizing the interval and level can be done per Laravel documentation - config is found in config/logging.php. By default, in addition to the Laravel core log, the following channels are set up upon installation of this package:

  • general channel
  • queue channel
  • cron channel
  • access channel
  • database channel
  • acl channel
  • notifications channel
  • backup channel
  • application-snapshots channel
  • exceptions channel

Daily Log Lifespan

You will be prompted upon running ccps:init or ccps:deploy commands to enter a default number of days for which daily logfiles are kept (sets APP_LOG_MAX_FILES in .env file).

Log Events

When a message is written to the logs, Laravel fires an event of class Illuminate\Log\Events\MessageLogged. By default, CCPS Core ships with the PostLogEntry listener, but it is not wired up in the EventServiceProvider (you can do that yourself). This listener could help you to do things there based on the severity of the message, or based on some context info you send with the log calls.

For instance perhaps you want to stop certain app cron jobs if a message is logged above a certain severity. Building on the example above, your Log:: call might look like Log::emergency('The app broke!', ['stop-crons' => 'all']);, the $event->context object that is part of the handle() method of the PostLogEntry listener would contain an array that is ['stop-crons' => 'all']. So you could, in your listener code, sniff for that using something like:

public function handle($event) {
    if (isset($event->context['stop-crons'])) {
        $cronsToStop = $event->context['stop-crons'];
        // other calls here to automatically disable cronjobs
    }
}

Your logic here might then set all cronjobs to disabled status.

Default CCPS Logging

CCPS Core includes logging for certain events as part of its base functionality.

ActionLog File Used
Cache clearedgeneral
App Configuration updatedgeneral
Queue Job successfulqueue
Queue Job failedqueue
Cron Job startedcron
Cron Job endedcron
Stale Lock File detectedcron
Successful user loginaccess
Failed user loginaccess
User logoutaccess
Uncaught QueryExceptiondatabase
User added / removed / editedacl
Permission added / removed / editedacl
Role added / removed / editedacl
Backup completion or failurebackup
Backup debug (manifest and zip generation)backup
Backup cleanup completion or failurebackup
Notification System noticesnotifications
Application Snapshotsapplication-snapshots
Exceptionsexceptions

Logging to Splunk

As of July 2019, CCPS group (and ITS as a whole) uses Splunk, specifically, for data collection / aggregation purposes.

As such, a Log Formatter was added in version 1.7.0 so that logs can be generated from Laravel in a Splunk-friendly JSON format. To log to Splunk, take advantage of the formatter key for your log channel in logging.php, and point it to Uncgits\Ccps\Components\Log\Formatter\SplunkJsonFormatter:

'splunk' => [
    'driver'    => 'daily',
    'path'      => storage_path('logs/mysplunklog.log'),
    'level'     => 'info',
    'formatter' => \Uncgits\Ccps\Components\Log\Formatter\SplunkJsonFormatter::class,
]

Formatting logs for Splunk

To effectively send logs to Splunk, you should use the \Log:: facade, and pass an array as the second argument (context). This context array can be flexible but for maximum efficiency, format it with the following information:

[
    'category' => '', // general category for the event that triggers the log, such as access, acl, cron, queue, etc.
    'operation' => '', // what was being attempted / done? - for queues, for instance, success or failure; for model events, for instance, create / update / delete, etc.
    'result' => '', // success (operation was successful), failure (operation failed predictably), error (operation failed unexpectedly)
    'data' => '', // any relevant data or models (note for models that the output will be an array, and so the $hidden attribute on the model will be honored)
]

These three keys will help to effectively categorize the occurrence in Splunk for easier searching. You can pass other data if you like; any other keys that you pass in will be collected into an other key in the final logged JSON object.

Exceptions will also be logged in this format, with the event and type keys set to exception and the Exception class (and user ID if included) rolled up into the data key.

Event key values

As of version 1.7.0, the following values are used by CCPS Core for the category key: access, acl, auth, cron, email, error, model, notification, queue

You can choose to latch onto those values as appropriate in your application, or create your own values. It just depends on how you want to see the data in Splunk!

Other features

Cron Jobs

Cron Job files can be created with php artisan ccps:make:cronjob MyCronJobName, and are configurable in the auto-generated class file (located at app/Cronjobs). The Laravel Scheduler is utilized as the driver to execute the Cron Jobs - however, there is an additional layer present (the ccps_cronjob_meta table) that will assist with enabling / disabling jobs, setting schedule, and so on.

Note: ccps:make:cronjob automatically executes composer dump-autoload after finishing.

After creating the Cron Job, you will need to run php artisan migrate so that the metadata is inserted into the database - if this is not done, CCPS Core will not recognize the job; it will not run, and will not appear on the Cron Jobs GUI page.

The crontab for the web server will need to be set to perform php artisan schedule:run every minute, as per Laravel recommendations.

Cron Job objects

By default, each Cronjob class itself controls some basic data about the job:

class SayHello extends Cronjob {
    protected $schedule = '* * * * *'; // default schedule (overridable in database)
    protected $display_name = 'SayHello'; // default display name (overridable in database)
    protected $description = 'No description provided.'; // default description name (overridable in database)

    // etc.
}

You may set $schedule, $display_name, and $description freely on the class itself, if you wish to standardize any of those things across deployments of the app (since Cronjob classes are committed to your app repo). However, you may override any of these in the database via the GUI, and it is the database values that will be honored (if present) over the class values.

Every Cronjob should return an object that implements CronjobResultInterface. Out of the box, that is the Uncgits\Ccps\Helpers\CronjobResult class. This class accepts two arguments: a boolean $success and a string $message. As of CCPS Core 2.0, best practice is as follows:

  • If your job succeeds: return new CronjobResult(true);
  • If your job fails in a predictable and acceptable way, return a false value for $success and provide a message describing the error: return new CronjobResult(false, 'The API was unreachable'); - this will trigger the CronjobFailed event and fire notifications if configured.
  • Any uncaught exceptions should be treated as application errors and should bubble up to the global exception handler to be reported. Be wary of doing a catch-all on \Exception or \Throwable... in general this should be allowed to happen as it represents an application error and not a job failure.

In general, plan for a "normal course of events" - include a happy path and a not-happy-path of expected results. Anything that falls within that scope should generally be returned as a CronjobResult, and anything outside of that should be left as a larger application problem.

Lock Files

Each cronjob will generate a lock file inside of /storage/cron when it is running. If this file is present, the Cron job will not run, as the purpose of the lock file is to prevent a Cron job from running over top of itself. Laravel's withoutOverlapping() method does this to some extent, but lock files will continue to be in place after an irregularly terminated Cron job process.

To clear lock files, visit the Cron Jobs page in the application and do so in the GUI.

If there is a conflict as a result of a lock file, the App\CcpsCore\Events\CronLockFileConflict event will be fired. You may override the base class as needed (as per standard CCPS Core procedure), and attach any listeners you want. If you want to convert this Event into a notifiable event, read the documentation on the Notification system below.

Default Cron Jobs

CCPS Core ships with multiple built-in Cron Job classes:

  • CheckForStaleLockFiles - make sure cron jobs aren't terminating abnormally
  • LogApplicationSnapshot - log the application's current status (see below)
  • PurgeExpiredTokens - clears out expired API tokens
  • RestoreMappedRoles - Restores Mapped Roles to users if using Mapped Role Groups
  • SyncMappedRoleGroupsFromGrouper - Syncs Mapped Role Groups if using Grouper for this purpose

Checking for stale lock files

The first bundled job is is a health-check job to help detect when a lock file for a Cron Job has been around too long (default 60 minutes). This is bundled with a Notification out of the box.

If you would like to override the functionality of this Cron Job class, it is once again structured for this, as are many other things in CCPS Core - the class called is App\Cronjobs\CcpsCore\CheckForStaleLockFiles, which overrides the parent class from vendor. Simply override the parent's execute() method in that class. For instance, if you wish to change the interval for detection, you should be able to change that by copy-pasting the method from the parent class and then changing the interval referenced on the Carbon class.

The bundled Event (App\Events\CcpsCore\StaleLockFileDetected) and Notification (App\Notifications\CcpsCore\StaleLockFileAlert) can also be similarly overridden for additional/different functionality, changes to the messages sent to logs / notifications, etc.

Logging application snapshots

The second bundled job is another health-check of sorts, as it collects information on the application, including: application repository with remote, package requirements, installed packages. The purpose of this is to be able to determine quickly what packages may be out of date, with custom scripting (or perhaps aggregation in a service like Splunk).

Once again, the bundled Cron Job can be overridden as it is supplemented with an app-level version: App\Cronjobs\CcpsCore\LogApplicationSnapshot.

Breadcrumbs

NOTE: This package was forked from davejamesmiller/laravel-breadcrumbs, which is no longer being maintained. Its usage may change in future versions of CCPS Core

The uncgits/laravel-breadcrumbs package is used in CCPS Core. Documentation: https://bitbucket.org/uncg-its/laravel-breadcrumbs

Breadcrumbs for each Module are located within the Module itself. As p(er package instructions, breadcrumbs are loaded via routes/breadcrumbs.php - if adding other Breadcrumbs that are NOT part of an addon Module, register them in there, or add a directive to include a separate file from elsewhere in your filesystem that contains your application breadcrumbs.

Queues and Jobs

CCPS Core utilizes the Laravel Queue system for asynchronous background tasks. Specifically, the Queue was designed with email and API calls in mind - things that do not necessarily require the user to be attentive while the action is taking place. The main enhancement that CCPS Core makes over stock Laravel is Queue history. There are four database tables included - one for pending jobs, one for successful jobs, one for failed jobs, and one for job batches (introduced in Laravel 8).

Most of the operation of these items adheres to the Laravel documentation on Queues and Jobs, but it is important to note that CCPS relies (for now) on the local database for Queue operations. Thus, in the stock CCPS .env file, QUEUE_DRIVER is set to database.

Finally, out-of-the-box CCPS Core does not have any queue-running mechanisms in place. So jobs can be queued as normal per Laravel documentation, but no job will be run until the app developer implements the queue workers. This can be done however the developer sees fit (with Laravel Horizon, Supervisor (available starting in version 1.3.0 of ccps-docker-php-apache), or with a Cron Job that runs a certain number of single queue:work operations, etc.).

Laravel Horizon

As of version 1.3.0, CCPS Core supports Redis - however, the queue monitoring piece will not work the same way. It is recommended, therefore, to utilize a tool such as Laravel Horizon to monitor and manage your queues if using the Redis driver.

Laravel Horizon itself does not support running from a subfolder installation of Laravel, as subfolder installations of the Laravel framework are not officially supported or recommended. However, since CCPS Core provides some shims to be able to run in a subfolder, and one of the goals of the framework is to operate in this way using symlinks, version 1.4.0 introduced a separate shim so that Laravel Horizon 3.0 and above will be able to work in a subfolder without modification to any of the Horizon code itself.

To get Horizon running the most quickly with your CCPS Core application, take the following steps:

  1. Require Horizon 3.0 or above: composer require laravel/horizon
  2. Run the required install step for Horizon (php artisan horizon:install)
  3. If running your app in a subfolder: after installation of Horizon, run the CCPS Core shim: php artisan ccps:horizon:shim
  4. If running your app in a subfolder: Add an entry to your .env file for HORIZON_SUBFOLDER - this should list the subfolder that your application uses, including the trailing slash. For example, if your app was installed at "https://myserver.com/my-app", then you would use HORIZON_SUBFOLDER=my-app/.

You should now be able to access the Horizon dashboard at whatever path you have defined in horizon.php.

Note: when you are upgrading your application and a new version of Horizon is pulled in, follow the Horizon documentation's recommendation and use php artisan horizion:publish to publish the new GUI assets.

Access control and GUI integration

Currently it is up to the developer to implement access control and GUI integration. The GUI integration (e.g. clicking on the Queues and Jobs item taking you to Horizon instead of default database-based queue screens) should be easily doable in the config/ccps.php file.

However, for access control, it is STRONGLY recommended to at least apply the permission:queues.* middleware inside the config/horizon.php file - otherwise anybody will be able to log in and view the Horizon dashboard and its components.

Manual installation of the Horizon shim

If you would prefer to install the shim yourself (manually), look at the src/artisan/ShimHorizon.php file in this package to see what is being done, and make the changes as needed to your routes/web.php and HomeController.php files. Essentially the shim overrides the catch-all Horizon route and routes it to a custom method in the HomeController, which injects the subfolder path into the Javascript variables that Horizon will use. No changes to the Horizon Javascript or views is necessary.

Emails and Email Queue

Queued Email

CCPS Core is built with the default behavior in mind of queueing all emails sent. This is beneficial because CCPS Core also has a separate Email "Queue" or "Log", which tracks and records each email sent.

To ensure that email is queued properly, in accordance with Laravel documentation, use the following in your Mailable class's __construct method:

public function __construct() {
    $this->onQueue('email');
}

Also, to ensure that email is always queued, even if you call Mail::send(), your class should implement the ShouldQueue class (Illuminate\Contracts\Queue\ShouldQueue) as follows, in accordance with the Laravel documentation:

use Illuminate\Contracts\Queue\ShouldQueue;

class MyMail extends Mailable implemens ShouldQueue {
  // etc.
}

Following this standard ensures that all email will run through the email queue, and accurate records will be kept of both the message sent, and the job that was run to send it.

Historical Email Log

CCPS Core hooks into the Illuminate\Mail\Events\MessageSent event to log the sent email separately from the normal Queue jobs. This includes a raw version of each email, which can be viewed in-browser.

Database configuration item scaffolding

CCPS Core provides mechanisms for adding database-configurable items, which are useful for exposing configuration items to users who do not have access to the codebase (and therefore the .env file).

The model is the App\CcpsCore\DbConfig model. This is designed to be used out of the box with the Config module, which provides a single page that lists all database configuration items in a running list. If you would like to handle this differently, feel free to disable this module and write your own - or to add a second configuration page, you can use the module's flow as an example (but still use the DbConfig model under the hood if you want).

Adding your configuration items

Since CCPS Core has no database-configurable items out of the box, the default view (published to resources/views/partials/config-form.blade.php ) simply contains a paragraph tag with text.

To generate your configuration items, simply build your form in the resources/views/partials/config-form.blade.php file. Upon submission of this form (it must be a PATCH request), each form field (except submit, CSRF, and method field) will be parsed and either created (if the key does not already exist in the database) or updated (if the key exists in the database) - this behavior is built into the Uncgits\Ccps\Requests\ConfigUpdateRequest custom request object.

Validation

As with many other places in CCPS Core (see Extending Core Functionality), it is anticipated that the developer will need to edit certain pieces of Core code to suit their needs when developing their own app. This holds true here, and as such, the App\Http\Requests\ConfigUpdateRequest class is published after initializing CCPS Core.

In here you can override the rules() method with your validation rules around the database configuration items that you will build.

Custom request handling

Similarly, you could opt to entirely replace the persist() method on the App\Http\Requests\ConfigUpdateRequest class to handle the request your own way.

Notification system

Version 1.4.1 introduced an entire module dedicated to allowing users to configure when / where / about what they will receive notifications. The concept is fairly simple, and hinges on two main ideas:

  1. A Notification Event is an Event for which notifications can be generated.
  2. A Notification Channel is a vehicle for receiving these notifications.

When an Event is fired that is classified as a Notification Event, any channels configured to "subscribe" to that Event will receive the appropriate notification. Users can log in, define their own channels, and decide on the flow that works best for them.

Supported channel types

As of now, CCPS Core supports 4 built-in channel types (currently not extensible): Email, SMS (AT&T, Verizon, T-Mobile, Sprint carriers only), Google Chat webhook, and Slack webhook.

Getting started

The simplest path forward is to use the command ccps:make:notification {NotificationName} {EventName} - this will set up all required scaffolding for a Notification/Event combination that is compatible with this system. Namely, it:

  • generates a Notification class with full scaffolding for all supported channel types
  • generates an Event class with appropriate traits and methods
  • generates a markdown email stub (for 'mail' channel)
  • generates a plaintext email stub (for 'sms' channel, because SMS is actually sent via the emailable mobile number address)
  • creates a migration to add the new event to the ccps_notification_events table

Adding the new Event to the NotificationSubscriber

Part of the installed scaffolding is the app\Listeners\CcpsCore\NotificationSubscriber.php class, which extends the vendor-supplied version of this file (as per typical CCPS Core convention). In this file, you will need to register each of your new events in the subscribe() method in the App\Listeners\CcpsCore\NotificationSubscriber@sendNotifications listener manually. This is not done automatically since doing so could expose the event to all users before you are ready to use it.

So, if you created an Event called SomethingHappened, you would make sure your subscribe() method reads:

public function subscribe($events)
{
    $events->listen(
        'App\Events\SomethingHappened',
        'App\Listeners\CcpsCore\NotificationSubscriber@sendNotifications'
    );

    parent::subscribe($events);
}

Configuring your Event and Notification

The NotifiesChannels trait defines a handful of properties on the Event class that help to connect it to a Notification class. These properties are as follows:

  • $notificationClass - the class name of the Notification that should be fired when this Event is raised.
  • $notificationClassArgs - an array of arguments that the Notification class accepts
  • $userIdsToNotify - optional argument to specify a subset of users to receive the Notification (read below)

As you develop your Notification class, just remember to come back and update these properties on the Event as necessary.

Setting notifiability on a Notification Event

There may be cases where differently-privileged users in your application should see different notification options - that is, you may want your Admins to be able to subscribe to the TheSystemIsDown Event, but not your end-users. Thus, each Event contains a public static function canBeNotified() method, that simply returns a 'truthy value' to reflect whatever logic you want to employ. This is similar to how Laravel's Gates work.

Each of these methods will be evaluated when the user is setting their notification preferences, so a good practice may be to link in to things like auth()->user()->can('something') or auth()->user()->hasRole('something'). This is an easy place to hook into your ACL / roles / permissions... or make the logic as complex as necessary! If you wish, you can pass in an existing User instead of using the currently-logged-in user.

Users who modify their notification channel settings will only see the events that should be shown to them. Each Event is also checked at send (in case a user subscribes and then the Event code is changed to disallow their access), and if there are "orphan" configurations like this, the notification is not sent, and the anomaly is logged.

Note that the default result for the canBeNotified() method is simply true.

Manually selecting a subset of user IDs to qualify for the notification

The same Event can often be fired with different intent or context - for instance, when a model is involved. You want all users to be able to receive notifications if a blog post they "own" receives a comment, and so it makes sense to set up a CommentAdded Event. However, without being selective about which user(s) receive notifications, this would make it so that user A would potentially receive notifications when user B's post received notifications.

Thus, you would want to set the $userIdsToNotify property on your CommentAdded Event class - assign it a new value in the constructor like so:

class CommentAdded
{
    use Dispatchable, InteractsWithSockets, SerializesModels, NotifiesChannels;

    public function __construct(Post $post)
    {
        $this->notificationClass = NotifyAuthorOfComment::class;
        $this->notificationClassArgs = [
            'You have a new comment!',
        ];
        $this->userIdsToNotify = [$post->author->id];
    }

API Tokens via Laravel Sanctum

Lightweight API token functionality is available via Laravel Sanctum starting in CCPS Core 1.16.*. The installation and usage of this feature follows documented procedures, and affords users the ability to manage their own tokens. It adds an expires_at column to the table as well, and a cronjob to manually revoke/destroy expired tokens.

Basic usage

By default only the admin role may use API tokens. There are four permissions: tokens.create, tokens.edit, tokens.revoke, and tokens.admin. The fourth should be reserved for administrative users of the application, as it will grant the ability to see and edit all tokens. So, to start with, ensure that your ACL is configured to allow the role(s) of your choosing to interact with API tokens. Then, the rest is visible / manageable via the Account area.

Token abilities

By default no abilities are granted to new tokens. This is fine as long as you are not using $token->can() statements in your code. However, if you want to start scoping tokens, you can do so via the ACL module. There, you can modify an individual token's abilities with a simple text prompt. In the future it may be possible to do this in a more systematic way but for now it is at least possible at a basic level.

Purging expired tokens

The included cronjob PurgeExpiredTokens will check regularly for expired tokens and revoke them automatically. Alternatively, anyone with tokens.admin will be able to manually revoke a token.

Extending

The controller class is exposed for overriding as you see fit. If you would like to override / extend API token functionality, you should be able to do so in typical CCPS Core fashion.

Deployment script

By default, a barebones deploy.sh file is included in your base application path. This contains several common commands that you can uncomment to basically build up your deployment script. Then it can be triggered to offer a one-command deployment script in your app! This can also be ignored, or even deleted, without consequence.

By default, for security reasons, the file permissions do not allow the file to be executed. You can chmod the file yourself, and/or explore changing the file permission in the Git index as needed (git update-index --chmod=+x deploy.sh).

Encryptable trait on models

Version 1.4.1 introduced the Encryptable trait, which can be used by any Eloquent model. On the model, you can then define a protected $encryptable array that contains a series of attribute keys that should be encrypted before saving to the database. After this, the data will be encrypted upon save, and decrypted upon retrieval.

NOTE: for any fields you encrypt, be sure that your database columns are large enough text types to hold your encrypted data (typically text at minimum, but may need to use mediumtext or longtext depending on the contents of the column). Generally speaking, a string field will not be long enough to contain the encrypted data.

Example:

class Subscriber extends Model
{
    use Encryptable;

    protected $guarded = ['id'];

    protected $encryptable = ['home_address'];

As with any encrypted data stored in the database, a query will not be able to search any encrypted field. It is not recommended to encrypt data if you intend to rely heavily on it for query searches.

The .env value for MODEL_ENCRYPTION can be set to false to override this encryption if needed... however, bear in mind that going back and forth between true and false is not recommended as there will be no way for the app to determine programmatically whether a value is encrypted by looking at the data itself.

Mapped Role Groups

New users are created by default with no role in the application ACL. Once a user logs in, the administrator (or another privileged user) must grant the new user the proper roles in the ACL before the new user can take any action. Mapped Role Groups were introduced in CCPS Core 1.15, and are a way to automate this step.

Basic functionality

A Mapped Role Group defines a role and provider, and a set of email addresses (Role Mappings). When a user logs in, the Role Mappings are checked, and if any matches are found, the matched role(s) are applied to the user automatically. The simplest way to define a Mapped Role Group is to use the ad-hoc setting, where you manage email addresses manually.

Keeping Roles in sync with role mapping methods

Role Mapping will take place automatically upon user creation, no matter what. If you do not wish this to occur, you may override the appropriate methods on the RegisterController and AuthController files, or simply create no Mapped Role Groups.

Aside from this, CCPS Core provides three methods of managing roles in the application after the initial check is performed. In your .env file, you can define ROLE_MAPPING_METHOD as one of the following:

  • none (default) to manage all Users and Roles manually
  • login to re-check a user's roles with the Mapped Role Groups upon each login
  • cronjob to manage roles on a schedule with the RestoreMappedRoles cronjob

IMPORTANT - if you are implementing this feature, you MUST ensure that your admin users are contained in a Mapped Role Group. If you do not do this, then those users will be locked out next time they are checked!

Syncing with Grouper

If you wish, the application can be configured to sync a Mapped Role Group via the Grouper API (requires the Grouper API Laravel Wrapper package, see the end of this README file). All you need to do is define the Mapped Role Group as a synced group, and supply the group name (including stem). Then, when the bundled SyncMappedRoleGroupsFromGrouper cronjob is enabled and run, it will update the membership of all synced Mapped Role Groups.

Google Hangouts Chat Channel (webhook)

CCPS Core can send any notification to a Google Hangouts Chat channel via webhook. To do this:

  1. Get your webhook from the Chat room that you want to send to
  2. Insert that webhook into your .env file under the GOOGLE_CHAT_DEFAULT_CHANNEL entry
  3. create a new notification with php artisan make:notification
  4. In the notification, add use Uncgits\Ccps\Channels\GoogleChatWebhookChannel; and add GoogleChatWebhookChannel::class to your return array in the via() method.
  5. Implement a toGoogleChat() method in your notification class that does the following:
return (new GoogleChatMessage)
   ->content('My message')
   ->to(config('ccps.google_chat_channels.default'));

You can also abstract this out to configure multiple channels within the app. You could do this by creating a notification class that does this:

    public $message;
    public $webhookUrl;

    public function __construct($message, $channel = 'default')
    {
        $this->message = $message;
        $this->webhookUrl = config('ccps.google_chat_channels.' . $channel);
    }

And in the config/ccps.php file, use something like:

'google_chat_channels' => [
    'default' => env('GOOGLE_CHAT_DEFAULT_CHANNEL', null),
    'my-channel' => 'http://something.com/otherthing,
],

Then, when instantiating your notification, pass in the channel as second argument: new SomethingHappened('something happened!', 'my-channel'). This is just an example of how to configure this within the app, but the channel and message are both already part of CCPS Core, so you can use them as needed.

Google Hangouts Chat Log Channel (webhook)

In addition to the manual notification in the section above, you can connect a Google Chat webhook to a Logger:

'google_chat' => [
    'driver' => 'custom',
    'via'    => Uncgits\Ccps\Logging\GoogleChatLogger::class,
    'url'    => env('MY_GOOGLE_CHAT_WEBHOOK_URL'),
    'level'  => 'error'
]

The url and level attributes are yours to configure - just ensure that driver is set to 'custom' and via is set to Uncgits\Ccps\Logging\GoogleChatLogger::class.

CRUD Generation

As of CCPS Core 1.9.0, this package is optional and must be required separately. Please follow instructions on the repository README to install it.

The CCPS Crud Generator package will provide a quick way to deploy a new CRUD model. Documentation can be found at the package repository.

If you wish to install it, please add the following to your repositories entry in composer.json, as the pacakge is not on Packagist:

{
    "type": "git",
    "url": "https://bitbucket.org/uncg-its/ccps-crud"
}

Using the CRUD generator in CCPS Core will automatically give you a controller, model, set of views, database migration for the model, role seeder, permission seeder, database migration to implement the seeders, and routes. Once the CRUD generator is used to generate the model the developer is free to modify any generated component as needed; the package does not retain any knowledge of the CRUD sets that it generates.

Note that CCPS Core does ship with CRUD stubs for this package, which can be adapted for any other CRUD generator you may choose to install.

Flash messaging

CCPS Core 1.9.0+ uses the laracasts/flash package.

Prior to version 1.9.0, the standaniels/flash package was being used; however, due to lack of activity on the repository, the Laracasts version is replacing it.

Guzzle HTTP Client

CCPS Core includes the Guzzle HTTP Client.

Application and database backup

CCPS Core utilizes the Spatie Laravel Backup package to offer a built-in backup functionality. Note that any VM/image that intends to utilize this functionality must have the mysql-client library installed so that the mysqldump binary is available.

Documentation for use of this package can be found on the package homepage, but some CCPS-specific items include:

  • Logging for each Event broadcast by the backup and cleanup processes (via Laravel logs, and through use of custom Listeners, which are already in place in CCPS Core)
  • The config file for this package is already published as part of CCPS Core, and modified from default to turn off automatic emailing, and also to exclude the storage_path() folder from the backup. You are free to edit this as needed to fit your app's needs.

Optional use of Laravel policies vs. Laratrust policies

The Laratrust package currently "hijacks" the can() method that is used by Laravel's default policy / gate behavior. In the Laratrust documentation, there is a workaround provided (by adding a clause to the User object replacing the Laratrust can() with the default). Prior to CCPS Core 1.14.2, this would break because the base User model used the LaratrustUserTrait in full, and so even if implemented correctly in the overriding class, the workaround would fail.

Starting in CCPS Core 1.14.2, two options are available for your base User model. The default App\CcpsCore\User model (as it always has) will extend the User model that uses LaratrustUserTrait in full - Uncgits\Ccps\Models\User. However, an alternative is provided: Uncgits\Ccps\Models\UserWithPolicies - this user implements the workaround method to allow use of Laravel Policies as well as Laratrust (note that you will need to follow the recommendations in the documentation and use hasPermission() or the other suggestions, instead of the can() method, if you want Laratrust checking.) To use this, change your App\CcpsCore\User model definition:


// use Uncgits\Ccps\Models\User as BaseModel; // OLD
use Uncgits\Ccps\Models\UserWithPolicies; // NEW

class User extends BaseModel
{
    // ....
}

Finally, a third option is provided to extend if you want to start "fresh" without either policy in place. This class is Uncgits\Ccps\Models\UserBare:


// use Uncgits\Ccps\Models\User as BaseModel; // OLD
use Uncgits\Ccps\Models\UserBare; // NEW

class User extends BaseModel
{
    // ....
}

Helper functions

This can be handled however the app developer sees fit. One popular convention used in Laravel apps is to add a separate file to the application somewhere and then register it inside of composer.json - here is an example: https://stackoverflow.com/a/43243743. The advantage of this method is that functions are truly global and do not need to be called as part of a class.

The other alternative that is available would be to use Helper classes.

Either method is acceptable, of course, but nothing is provided with CCPS Core out of the box.

Repository Pattern helpers

The provided EloquentCrud trait (which adheres to the built-in EloquentRepositoryInterface contract) will allow you to use Eloquent in a Repository class (if you decide you want to use the Repository Pattern). It provides a basic abstraction on top of Eloquent, with basic functionality to return a group of results from a query. The details can be discerned by looking at the EloquentCrud class, but here are some good things to know when using EloquentCrud:

  • Set the $modelName on the repository (full class name) to tie the repository to an Eloquent model.
  • You can set the $searchable attribute on the class, optionally, if you plan to offer some search functionality (e.g. a search bar based on property). You should structure it as follows:
public $searchable = [
    'filename'       => [
        'operand' => 'like',
    ],
    'created_before' => [
        'operand'  => '<',
        'column'   => 'created_at',
        'cast'     => 'datetime',
    ],
    'created_after'  => [
        'operand'  => '>',
        'column'   => 'created_at',
        'cast'     => 'datetime',
    ],
    'status'         => [
        'operand' => '=',
    ]
];

Each entry in $searchable should at least have the operand key, which should be an Eloquent-compatible operand or the word 'like'. From there, you can add the column key if the request variable that is passed in needs to be mapped to a column other than the name of the variable itself. In the example above, the form field was keyed created_before, but needs to be mapped to the created_at column. Finally, if the value needs to be cast to another format, use the cast key to accomplish this. As of CCPS Core 1.12.1, the only available value for this is 'datetime', which will cast the result to Carbon as you would expect. More options may be added as needs arise.

Stubs and ccps:make commands

CCPS Core contains several custom make commands to help with scaffolding and building your app.

Cronjobs

php artisan ccps:make:cronjob will help you to generate a Cronjob class. See the documentation on Cronjobs for more info on usage.

Upgrades

php artisan ccps:make:upgrade will help CCPS Core developers to generate upgrade scripts. See the documentation on Upgrades for more info on usage.

ACL Seeders/Migration

php artisan ccps:make:acl-seeders will help you to generate seeders and a database migration for the purpose of creating new roles/permissions for your app. If you are generating an entire set of CRUD data using the CRUD generator, this command is unnecessary. the purpose of this command is to help in cases when you are not using the CRUD generator. To use it:

  1. run php artisan ccps:make:acl-seeders MyModel my/package-name - insert the name of your model or prefix for your seeders/migration, and insert your package name to help identify your ACL items in the database.

  2. The generated files are incomplete, and need to be filled in before running the database migration. In your database/seeds folder, you should update the MyModelRolesTableSeeder.php file so that the $roles array is populated, and do likewise in MyModelPermissionsTableSeeder.php. The MyModelPermissionRoleTableSeeder.php contains a $rolePermissionMap that should also be populated.

  3. Finally, in your database/migrations folder should be a new migration. Edit the down() method to provide values for $permissionsToFind and $rolesToFind, so that the down() migration knows what permissions and roles to detach/delete. Note that core (uneditable) roles will not be deleted, so you can include admin in the list of roles from which to remove your privileges.

  4. After editing these items, you may migrate the database using php artisan migrate

Notifications and Events

php artisan ccps:make:notification will scaffold out a notification and event for use with the CCPS Notification system. See the section above on Notifications for more details.

Service Classes

If your coding style utilizes Service classes, you can use php artisan ccps:make:service MyClassService to create a blank Service class - this will create the app/Services folder if it does not exist.

Repository Classes

Like Service classes, if you utilize the Repository Pattern in your coding style and wish to generate a Repository class, you can use php artisan ccps:make:repository MyClass (note that you provide model name here, not the entire class name like MyClassRepository). This will generate a new class in app/Repositories, creating the folder if it does not exist already. Each Repository implements the built-in EloquentRepositoryInterface via the also-built-in EloquentCrud trait.

Modules in CCPS Framework

Many "features" of CCPS Framework (including the Users, ACL, and Config areas in this Core package) are built into constructs called modules. Modules represent segments of the application that will be detected and configured with a preset set of parameters, such as where to find Breadcrumbs, menu items, icons, routes, views, etc. Out of the box, these all work without having to touch anything.

The Modules idea was built for two main purposes:

  1. To allow for easier expansion with future CCPS packages
  2. To give app developers easy access to change the configuration of the modules without having to worry about altering core code, or publishing too many files that would then make future updates to the Core Framework difficult.

Module Configuration

The config/ccps.php file contains a modules array that defines all module-related information. Each entry is keyed by the module's name. When installing more CCPS packages to expand beyond the base functionality of this core package, the user will be responsible for adding module information to this file.

By default, here is the contents of a module entry in the config file:

'users' => [
    'package' => 'ccps-core', // the name of the package that this module came from
    'icon' => 'fas fa-users', // classing for the icon (FontAwesome) that should represent this module on menus / nav panels / etc.
    'title' => 'Users', // the text of this item in Menus and nav panels
    'index' => 'users', // the "homepage" for this module
    'parent' => 'admin', // for nesting - will control where the module shows up in the site hierarchy. set a parent prefix to use it or set to null, false, or '' to have it show up at root level.
    'required_permissions' => 'users.*', // Laratrust permission set that is required to view items in these modules
    'use_custom_routes' => false, // set to 'true' and add your own custom routes into routes/web.php or leave as false and inherit default routes
    'custom_view_path' => false, // set to the folder where you will put custom views for this module. You will also need to vendor:publish using tag 'module-views' (and move those files to your desired location if you wish).
],

Adding modules

To add additional CCPS Modules to this list (from properly-built CCPS packages), modify this array to include the new module information, in the order in which you want it displayed in menus. Then just copy/paste the structure that has been established for the core Modules, making sure to add the correct package name under package.

Module Authoring

When should I consider building a module?

It is important to note that you do not need to make all add-ons to your CCPS Core app into Modules. Modules are conceptualized as "drop-in" packages that can be reused across other CCPS Core apps, and that also include some GUI elements. If you are building, for example, an API library, chances are good that you do not need to make this into a module; a plain composer package would work. However, if you are building something like an "Application Health" dashboard, you may want to make this a module, as it could be something you reuse across several apps.

Shell package

The CCPS Module Shell package is available as a starting point to build your own Module for adding on to CCPS Core. Clone it, remove the remote repo, and add your own. DO NOT OVERWRITE THIS REPO!

Module Structure

If you wish to add a Module as part of your package, here are some guidelines, based on where CCPS Framework will attempt to look for routes, breadcrumbs, views, etc.:

Breadcrumbs

Breadcrumbs should be placed in src/breadcrumbs and named with the name of the module (e.g. mything.php). Inside, the breadcrumb definitions must use ($module).

Your module's "index" breadcrumb should refer to its parent as $breadcrumbs->parent($module['parent'] ?: 'home');

You should also consider using Named Routes (see Best Practices section below), and then your Breadcrumb definitions are fairly simple: $breadcrumbs->push("Roles", route('acl.roles'));

However, if you decide not to use Named Routes, you will need to incorporate the $prefix variable into your definitions (you will use ($module, $prefix)), and then each other breadcrumb should refer to the $prefix variable when showing the URL it should point to, like so: $breadcrumbs->push("Roles", url($prefix . '/acl/roles'));

Routes

Routes should be placed in src/routes and named with the name of the module (e.g. mything.php). Inside, simply place your route definitions. Nothing special needs to be done here. Again, named routes are recommended.

Views

Views can be placed anywhere, as long as you define the $this->loadRoutesFrom() directive inside of the package's Service Provider. You should include the option to publish your views to the application, as this will jive with the module config options for custom_view_path

Assets (CSS, JS)

By default CCPS Core comes with four files that are part of the build process - ccps-core.scss and ccps-core.js, and app.scss and app.js. These files are pre-compiled into the public folder, and already tied into the main layout template.

The app.* files contain key Javascript (Alpine.js) and CSS (Tailwind). These are not kept in the ccps-core.* files as they were in previous versions of CCPS Core. The main reasoning here is that TailwindCSS offers a purge feature to purge unused styles. The compilation of CSS therefore currently includes only the Tailwind classes necessary for a basic CCPS Core application.

Adding app CSS and JS

Your own custom CSS and JS should be added to the app.* files.

To this end, a default tailwind.config.js file will also be published to the application's root directory, and is configured to perform purging based on Blade files contained your application's resources/views folder. So any Tailwind classes you choose to use in your application may not perform correctly until you recompile the CSS; you can decide to remove the purge directive from the Tailwind config file for development, if you wish.

To compile CSS and JS assets, you will need to be able to run Laravel Mix:

  1. On your development machine, from outside the container, ensure that you can run Laravel Mix. Installation instructions are found in official Laravel documentation - requires node and npm.
  2. On your development machine, from your application's main folder, run npm install to install all required dependencies and boilerplate for your project.
  3. Use Laravel Mix according to documentation. Do NOT alter the ccps-core.* files, as they may be overwritten in future versions of CCSP Core. Ensure you add processing directives to the /webpack.mix.js file in your project (which is committed to the repo).
  4. When ready to deploy, simply run the proper npm command (usually npm run dev or npm run production) to get your assets compiled and placed in the proper location. Of course, you will also ensure that you've linked to them in the HTML in your template files.
  5. Commit these files to your app repo and deploy as normal.

There is no need to run any npm commands from the production server - in fact, the Docker image used by the UNCG CCPS group does not include npm.

AGAIN, do NOT add your own CSS and JS to the ccps-core.* files, as they could potentially be overwritten by future versions of these files as updates to CCPS Core are pushed!

Browser testing with Dusk

If you wish to use Laravel Dusk for testing within your app, you can install the package as normal:

composer require --dev laravel/dusk:^3.0

However, Dusk needs a couple of modifications to run on the Docker image that CCPS Core uses. These modifications are wrapped up in a second installer script. So, after installing the package from composer, run:

php artisan ccps:dusk:install

This installer will write the Dusk assets to your tests folder (so there is no need to do the traditional php artisan dusk:install), and will also change permissions on the browser executables that come with Dusk so that tests can be run.

After doing this, ensure that your application's APP_URL is set accurately in your .env file, and then you can run your tests as usual with php artisan dusk.

Upgrading CCPS Core

CCPS Core likely will undergo changes, small and large. Small changes that only touch contents of the vendor folder are not as big of a deal. However, because CCPS Core does publish many files and override many Laravel defaults that are designed to allow for further customization, the path forward gets trickier if some of these published components need to change.

Thus, CCPS Core uses a modular upgrade system, modeled after Laravel's database migrations. When an upgrade involves changes that affect files housed in the app filesystem rather than vendor, then an upgrade script is needed. Upgrade scripts are PHP files that contain instructions on how to alter the published files in the project to make them compliant with the changes made in CCPS Core.

How do upgrade scripts work?

Scripts live in vendor/uncgits/ccps-core/src/upgrades. They are time/date-stamped just like database migrations. When one finishes running successfully, a signature file will be left in the app filesystem (and should be committed with the repo) - in the /upgrades folder.

Upon running the upgrade process, these two folders will be compared at the filesystem level (not database), and CCPS Framework will look for upgrade files found in the CCPS Core package where a corresponding signature file does not exist in the app filesystem. For each file that it finds, it will be executed. This process allows CCPS Core upgrades to be done "in bulk" - e.g. you can upgrade from 1.0.1 to 1.0.6 and get all upgrades contained within that span.

Creating a new upgrade script

You do not need to use upgrade scripts unless you are upgrading CCPS Core itself. If this still applies to you, then...

Simply run php artisan ccps:make:upgrade from a working app, and you will get an upgrade stub that you can modify. Then, copy it into the CCPS Core codebase's upgrades folder, ensuring that it is dated appropriately (like migrations, upgrades will run sequentially based on date).

When should I make an upgrade vs. a migration?

If you are working on your own app's code, then you don't need to worry about this. However, if you are working on CCPS Core code, you will make a migration for operations that need to touch the database, and upgrades for operations that need to touch the app filesystem.

Running the Upgrade process

PRIOR to executing the upgrade

Upgrades touch files that are published into your app and not a part of /vendor. Therefore, changes that are made by the upgrade scripts will be included in commits to your repository. Thus, upgrade scripts should be run in your development environment, and then once they are successful a new commit can be pushed to your application repository. Then, from production, all you should need to do is git pull on the production app to receive these changes.

It is therefore NOT recommended to use ccps:upgrade on a production application. The ccps:upgrade command will warn you about this, but you can proceed if you are insistent.

It is also highly recommended to be comfortable with git. First, ensure that you are up to date with the remote repository, and have a clean working directory. If you have dirty files, commit or stash them to reapply later. The reason for this is that Upgrades do not have up() and down() methods like migrations. They have a single upgrade() method, and if things break, you will want to revert to the last-known-working version of your app code from your repo.

Executing the upgrade

NOTE: you can skip steps 2 and 3 below in favor of running composer require uncgits/ccps-core:version-number --update-with-dependencies

If you feel that your app is sufficiently prepared and in sync with the remote repository, then you can:

  1. log in to the container (this will fail if you are outside)
  2. update your composer.json file to specify the version (or version range) of uncgits/ccps-core that you wish to accept into your app.
  3. execute composer update uncgits/ccps-core to pull the latest version of Core into your app.
  4. execute php artisan ccps:upgrade (recommended: use -v flag for verbose output). This command will scan the vendor/uncgits/ccps-core/src/upgrades folder of the CCPS Core app to search for Upgrade files. It will also search your application's /upgrades folder. Whatever files are present in the vendor upgrades folder but not present in the application's folder will be run in sequence.*
  5. execute php artisan migrate to execute any database migrations that are included with the upgrade
  6. update any information in your .env file as necessary to be in sync with changes from the Core upgrade
  7. execute any php artisan vendor:publish operations as directed by the UPGRADE.md file
  8. clear the application cache via the GUI, or via php artisan cache:clear.
  9. test your app, and when satisfied, commit the changes and push.

*Note: some upgrade scripts may require a reboot of the application, and/or a composer dump-autoload. Properly-coded Upgrade files will either perform these actions automatically, or require another run of php artisan ccps:upgrade.

Deploying the upgraded app

On your remote servers, the process for deploying the upgrade should be simple. You do not need to (and should not) run php artisan ccps:upgrade on your production servers. Instead, the changes are committed to your repo, so the process would be something like:

  1. log into container
  2. git pull to get your updated files
  3. composer install to install updated packages according to composer.lock (do NOT run composer update)
  4. php artisan migrate
  5. clear the cache via GUI or php artisan cache:clear
  6. Restart Laravel Horizon or the queue process if needed (example: supervisorctl restart your-process-name)
  7. update .env file as needed

Will upgrades fix everything for me?

Most likely not. There are some things that an upgrade script cannot fix, and the developer will need to fix manually. It is the goal of the UNCG CCPS Developers team to provide detailed release notes (similar to the ones provided by Laravel itself) on what has been changed in each subsequent version, so that the developer can confidently make any code changes that are not able to be covered by the upgrade scripts.

For information on any manual upgrade steps, check UPGRADE.md. This will describe how to get to the current version of CCPS Core from the version immediately preceding.

Best Practices

Named Routes

Throughout CCPS Core, the use of named routes is employed. This makes development easier and cleaner, and is the recommended convention for app developers. Using the url() helper to refer to internal application paths is discouraged.

Database Transactions

Laravel offers the ability to wrap sets of database actions in "transactions" (outlined in Laravel documentation). Transactions can be used with both the query builder (DB facade) and Eloquent. Best-practice for applications built off of CCPS Core is to wrap all database-touching operations in transactions. This can be done with the DB::transaction() method (which will rollback automatically if an exception is thrown) or the manual DB::beginTransaction(), DB::commit(), and DB::rollBack() directives with try-catch blocks.

Database "seeding"

Traditionally, Laravel considers "seeds" to be for test/fake data. However, CCPS Core uses Seeders to seed actual production data. We control the seeding through database migrations, so the same seeder should not be run twice by accident. Keep this in mind if you use seeders for fake data in development. You should usually always specify a Seeder class to run when you use php artisan db:seed anyway, but be sure to do it when using CCPS Core, just in case.

That said, as you are developing your own app, consider following the CCPS Core model of including database insertions, etc., as part of migrations.

CSS and JS

Use your own files or the included app.js and app.scss to add app-specific CSS / JS rather than adding to ccps-core.* files. If you do the latter, your additions will likely be overwritten in a future update of CCPS Core!

Recommended additional packages

UNCG Theme

Responsive UNCG Website wrapper (Bootstrap 4, unofficial) composer require uncgits/uncgtheme-laravel:^1.7 https://bitbucket.org/uncg-its/uncgtheme-laravel

Grouper API Laravel Wrapper

Wrapper around the Grouper API PHP Library package - required to sync Mapped Role Group populations with Grouper groups. composer require uncgits/grouper-api-wrapper-laravel https://bitbucket.org/uncg-its/grouper-api-wrapper-laravel

Issues, Bugs, Feature Additions

All ongoing issues are tracked in the repository's Issue Tracker.