added support for legacy tickets

This commit is contained in:
Mark Gregory
2026-09-28 11:26:27 +01:00
parent 0a689bb11f
commit 83d617a0a7
28 changed files with 2803 additions and 2741 deletions
-2736
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
### Sentinel API Development
This document details the overall development techniques for effective api development
that makes the Sentinel product much more accessible and scalable.
+63
View File
@@ -0,0 +1,63 @@
### AS400 HTTP Server Documentation
To copy an Apache configuration file using an AS400 console, you'll need to use the IBM i operating system's file management tools. This involves creating a member for your HTTP server in a specific library, then using the COPY command to duplicate the configuration file.
You can then edit the new member using the Data File Utility (DFU).
Here's a step-by-step guide:
1. Create the HTTP Server Member:
Go to the QUSRSYS/QATMHINSTC library using the WRKMBRPDM command.
Choose option 3 next to APACHEDFT and press F4 to prompt the COPYcommand.
Change the TOMBR (To member) parameter to a unique name for your new HTTP server (e.g., MYAPACHE). This name cannot exceed 10 characters.
Press Enter to accept the changes.
2. Edit the New Member:
Type 18 next to the newly created member name (e.g., MYAPACHE) and press Enter.
Page down once in the DFU.
Modify the Instance Data value, ensuring it reflects the new configuration you want to use. For example:
Change -apache -d /www/apachedft -f conf/httpd.conf -AutoStartN to a similar line that points to your new configuration files.
Remember to update the paths to your configuration files (e.g., /www/MYAPACHE/conf/httpd.conf).
3. Create Directories and Copy Files:
Use the 5250 command line to create the necessary directories for your new server instance:
MKDIR '/www/MYAPACHE'
MKDIR '/www/MYAPACHE/conf'
MKDIR '/www/MYAPACHE/logs'
MKDIR '/www/MYAPACHE/htdocs'
Copy the configuration files from the source system to your new server instance using the COPY command or by using the IBM i's file management tools.
4. Configure the httpd.conf File (if necessary):
Open the httpd.conf file (or apache2.conf) in a text editor.
Make any necessary changes to the configuration, such as modifying paths, ports, or other settings as per your requirements.
If you are setting up virtual hosts, ensure the virtual host definitions are included in the configuration file.
Example:
Code
// Create a new member for the HTTP server
````
WRKMBRPDM FILE(QUSRSYS/QATMHINSTC)
````
// Select option 3 for APACHEDFT and press F4
````
COPY FROMBR(APACHEDFT) TOMBR(MYAPACHE)
````
// Edit the new member using DFU
````
WRKMBRPDM FILE(QUSRSYS/QATMHINSTC)
````
// Type 18 next to MYAPACHE and press Enter
#### Important Notes:
The TOMBR parameter must be a unique name within the library.
The new member's Instance Data must be correctly configured to point to your new Apache instance.
Ensure you have the necessary permissions to modify the QUSRSYS/QATMHINSTClibrary.
You may need to restart the HTTP server after making changes to the configuration.
This process duplicates an existing HTTP server instance. If you're configuring a new server, you may need to install Apache first, as well as configure the required directories and configuration files.
Binary file not shown.
+141
View File
@@ -0,0 +1,141 @@
# Install and configure Cron Scheduling on IBM i
From IBM: Now available for #IBMi: cron, via the 'cronie' RPM package! Anacron, too!
This package can be a good way to schedule your open source jobs or CL commands to run on a regular basis.
Install cronie yum packages from IBM i ACS Open Source Package Management
```
Package list:
cronie
cronie-anacron
```
# Setting up sample crontab system schedule file
From your favorite bash editor, edit the system crontab file: in ***/QOpenSys/etc/crontab***
**The example listed below sends a message to QSYSOPR via calling an IBMi CL Command**
We also add APPLICATION_ENV=dev where dev is the environment file that is in use to take
advantage of the environment switcher code and target the correct systems
```
SHELL=/QOpenSys/pkgs/bin/bash
PATH=/QOpenSys/pkgs/bin:/QOpenSys/usr/sbin:/QOpenSys/usr/bin
MAILTO=qsecofr
APPLICATION_ENV=DEV
# For details see man 4 crontabs
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * ibmi-user-name command to be executed
# Run a sample IBMi CL command every minute to send message to QSYSOPR
*/1 * * * * QSECOFR system "SNDMSG MSG(CRONJOB) TOUSR(QSYSOPR)"
# Run a sample IBMi CL command every minute to send message to QSYSOPR and log any stdout/stderr messages to custom log file /tmp/ibmicommand.log.
*/1 * * * * QSECOFR system "SNDMSG MSG(CRONJOB) TOUSR(QSYSOPR)" >> /tmp/ibmicommand.log 2>&1
```
```
to monitor cron use crond -n -x proc this will dump its opertion to the console
```
See crontab configuration sample links below for site that can teach you more about setting up your cron jobs via the crontab configration files.
https://github.com/richardschoen/howtostuff/blob/master/installcronie.md#links
***Note: It appears that you must specify the user ID to run the selected job as. In the above example we are running the SNDMSG example as QSECOFR.***
```Only /QOpenSys/etc/crontab and the files in /QOpenSys/etc/cron.d/ have a username field though. Ours on IBM i are prefixed with /QOpenSys obviously.```
Reading link for more info on running as a specific user.
https://serverfault.com/questions/352835/crontab-running-as-a-specific-user
# Make sure crontab file and cron directories are owned by root user for running system jobs
Run the following commands from a SSH session to make sure root user (QSECOFR) owns the files and directories. Otherwise your crontab will not run correctly because of permission errors even though the crond daemon will start up.
```
cd /QOpenSys/etc
chown qsecofr crontab
chown qsecofr cron.d
chown qsecofr cron.hourly
```
# Starting the crond daemon to run schedule jobs
Log in to SSH as IBM i QSECOFR level user. ```QSECOFR or a user with *ALLOBJ authority.```
Type: ```crond``` and press enter.
From a 5250 session, run ***WRKACTJOB SBS(QUSRWRK) JOB(QP0ZSPWP)*** and you should see the active server jobs and threads in the QUSRWRK subsystem.
```
--------------------------------------------------------------------------------
QP0ZSPWP QSECOFR BCI .0 PGM-crond SELW
--------------------------------------------------------------------------------
```
Tip: If your crontab jobs are not running, drill in to this job via ```option 5. Work with``` and then display the job log for the active crond daemon ```10. Display job log, if active, on job queue, or pending ```. Usually there will be an error listed if your crontab command line entries are bad or permissions aren't set right on your crond configuration files and directories.
# Ending the crond daemon job
Log in to SSH as IBM i QSECOFR level user.
Type: ```kill `cat /QOpenSys/etc/crond.pid` ``` and press enter.
# Start crond daemon job bash script - startcrond.sh
This script can be used to start the crond background job as an active job in the QUSRWRK subsystem.
```
#!/QOpenSys/pkgs/bin/bash
crond -s
```
# End crond daemon job bash script - endcrond.sh
This script can be used to end the crond background job if it's active. It used the process id stored in file /QOpenSys/etc/crond.pid to locate and kill the system job.
```
#!/QOpenSys/pkgs/bin/bash
kill `cat /QOpenSys/etc/crond.pid`
```
# Start crond daemon job from QSHEXEC CL command
This CL command example utilizes the QSHEXEC command which is part of the QSHONI library for running QSH/PASE jobs from a regular IBM i job. https://github.com/richardschoen/qshoni
```
QSHONI/QSHEXEC CMDLINE('crond') LOGSTDOUT(*YES)
```
# End crond daemon job from QSHEXEC CL command
This CL command example utilizes the QSHEXEC command which is part of the QSHONI library for running QSH/PASE jobs from a regular IBM i job. https://github.com/richardschoen/qshoni
```
QSHONI/QSHEXEC CMDLINE('kill ```cat /QOpenSys/etc/crond.pid```') LOGSTDOUT(*YES)
```
This document does not cover production best practices. Best practices should be addressed with your Administration team or by researching Cron security best practices.
# Links
Cronie Github Site
https://github.com/cronie-crond/cronie
Crontab configuration examples
https://phoenixnap.com/kb/set-up-cron-job-linux
https://corenominal.org/2016/05/12/howto-setup-a-crontab-file/
https://pimylifeup.com/cron-jobs-and-crontab/
+319
View File
@@ -0,0 +1,319 @@
View Model Implementation
This is how the Laravel view model is used with the spares dashboard to remove all the logic
from the blade view
### The DashboardViewModel
````
<?php
namespace Modules\Spares\ViewModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Modules\Spares\Repositories\API\APIStorageRepository;
use Psr\SimpleCache\InvalidArgumentException;
use Throwable;
final class DashboardViewModel
{
public array $returns = [];
public array $returnsLabels = [];
public array $returnsValues = [];
public float $totalPOValue = 0.0;
public array $inventory = [];
public array $inventoryLabels = [];
public array $inventoryValues = [];
public float $totalInventoryValue = 0.0;
public array $issues = [];
public array $issuesLabels = [];
public array $issuesValues = [];
public float $totalIssuesValue = 0.0;
public array $stock = [];
public array $stockLabels = [];
public array $stockValues = [];
public float $totalStockValue = 0.0;
public float $totalStockValueLastMonth = 0.0;
public float $grandTotal = 0.0;
public array $topTenIssues = [];
public array $topTenInventory = [];
/**
* Logs are normalized for the Blade:
* [
* ['key'=>'inventory','label'=>'<i ...>Adjustments Sync','run_at'=>'14 Oct 2025, 12:30','server_name'=>'maximo01'],
* ...
* ]
*/
public array $logs = [];
private APIStorageRepository $api;
private bool $useCache;
private int $cacheMinutes;
public function __construct(APIStorageRepository $api = null, ?bool $useCache = null, int $cacheMinutes = 60)
{
$this->api = $api ?? new APIStorageRepository();
$this->useCache = $useCache ?? (bool)settings()->group('maximo')->get('dashboard_cache', false);
$this->cacheMinutes = $cacheMinutes;
}
/**
* Build the data ready fro the view
* @return $this
* @throws Throwable
* @throws InvalidArgumentException
*/
public function build(): self
{
$start = microtime(true);
$apiToken = (string)$this->api->getApiToken();
$params = [];
// ------- Returns -------
$this->returns = $this->fetch('/api/v1/returns/index', $apiToken, $params, 'returns');
$returnsByWeek = $this->groupByIsoWeek($this->returns, 'SRRTRDT', 'SRRUCST');
$this->returnsLabels = array_keys($returnsByWeek);
$this->returnsValues = array_values($returnsByWeek);
$this->totalPOValue = array_sum($this->returnsValues);
// ------- Inventory -------
$this->inventory = $this->fetch('/api/v1/inventory/index', $apiToken, $params, 'inventory');
$this->totalInventoryValue = (float)(collect($this->inventory)->sum('SPACOST') ?: 0.0);
$inventoryByWeek = $this->groupByIsoWeek($this->inventory, 'SPATRDT', 'SPACOST');
$this->inventoryLabels = array_keys($inventoryByWeek);
$this->inventoryValues = array_values($inventoryByWeek);
// ------- Issues -------
$this->issues = $this->fetch('/api/v1/issues/index', $apiToken, $params, 'issues');
$issuesByWeek = $this->groupByIsoWeek($this->issues, 'SPRTRDT', 'SPRUCST');
$this->issuesLabels = array_keys($issuesByWeek);
$this->issuesValues = array_values($issuesByWeek);
$this->totalIssuesValue = array_sum($this->issuesValues);
// ------- Stock -------
$stock = $this->fetch('/api/v1/stock/index', $apiToken, $params, 'stock');
$stockLastMonth = $this->fetch('/api/v1/stock/index/last_month', $apiToken, $params, 'stock_last_month');
$totalStockNow = (float)(collect($stock)->sum('SPSRCST') ?: 0.0);
$this->totalStockValueLastMonth = (float)(collect($stockLastMonth)->sum('SPSRCST') ?: 0.0);
$this->totalStockValue = $totalStockNow - $this->totalStockValueLastMonth;
$combinedStock = array_merge(is_array($stock) ? $stock : [], is_array($stockLastMonth) ? $stockLastMonth : []);
$this->stock = $combinedStock;
$stockByMonth = $this->groupByMonth($combinedStock, 'SPSTRDT', 'SPSRCST');
$this->stockLabels = array_keys($stockByMonth);
$this->stockValues = array_values($stockByMonth);
array_multisort($this->stockLabels, SORT_ASC, $this->stockValues);
// ------- Top Tens (defensive) -------
$this->topTenIssues = $this->fetch('/api/v1/issues/index/top_ten', $apiToken, $params, 'top_ten_issues');
$this->topTenInventory = $this->fetch('/api/v1/inventory/index/top_ten', $apiToken, $params,
'top_ten_inventory');
// ------- Logs (with label mapping & formatted timestamps) -------
$labelMap = [
'INVENTORY' => '<i class="fa-fw fas fa-sliders-h mr-2"></i>Adjustments Sync',
'ISSUES' => '<i class="fas fa-exchange-alt mr-2"></i>Item Usage Sync',
'RETURNS' => '<i class="fa-fw fas fa-thermometer-full mr-2"></i>PO Analysis Sync',
'STOCK' => '<i class="fa-fw fas fa-warehouse mr-2"></i>Movement Sync',
];
$this->logs = collect(['RETURNS', 'STOCK', 'ISSUES', 'INVENTORY'])
->map(function (string $type) use ($labelMap) {
$raw = (array)$this->api->getRunLog('/api/v1/runlog/last', $type);
return [
'key' => strtolower($type),
'label' => $labelMap[$type] ?? e($type),
'run_at' => $this->formatDate($raw['run_at'] ?? null),
'server_name' => (string)($raw['server_name'] ?? 'N/A'),
];
})
->values()
->toArray();
$this->grandTotal = $this->totalPOValue + $this->totalIssuesValue + $this->totalInventoryValue + $this->totalStockValue;
if (app()->environment(['local', 'dev', 'qat'])) {
$runtime = round(microtime(true) - $start, 3);
Log::channel('dashboard')->info(emoji('orange') . "Dashboard ViewModel build() runtime: {$runtime} seconds");
}
return $this;
}
/**
* Returns all the variables as an array.
* @return array
*/
public function toArray(): array
{
return [
'totalPOValue' => $this->totalPOValue,
'totalInventoryValue' => $this->totalInventoryValue,
'totalIssuesValue' => $this->totalIssuesValue,
'totalStockValueLastMonth' => $this->totalStockValueLastMonth,
'totalStockValue' => $this->totalStockValue,
'grandTotal' => $this->grandTotal,
'inventory' => $this->inventory,
'inventoryLabels' => $this->inventoryLabels,
'inventoryValues' => $this->inventoryValues,
'stock' => $this->stock,
'stockLabels' => $this->stockLabels,
'stockValues' => $this->stockValues,
'issues' => $this->issues,
'issuesLabels' => $this->issuesLabels,
'issuesValues' => $this->issuesValues,
'returns' => $this->returns,
'returnsLabels' => $this->returnsLabels,
'returnsValues' => $this->returnsValues,
'topTenIssues' => $this->topTenIssues,
'topTenInventory' => $this->topTenInventory,
'logs' => $this->logs,
];
}
// ----------------- helpers -----------------
/**
* A combined log formatter
* @param string $url
* @param string $token
* @param array $params
* @param string $name
* @return array|mixed
*/
private function fetch(string $url, string $token, array $params, string $name)
{
// normalize everything to strings for cache key safety
$url = (string)$url;
$name = (string)$name;
$paramsJson = json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$key = $name . '_' . md5($url . '|' . ($paramsJson ?? '[]'));
if (!$this->useCache) {
return $this->executeApi($token, $url, $params);
}
return Cache::remember($key, now()->addMinutes($this->cacheMinutes), function () use ($token, $url, $params) {
return $this->executeApi($token, $url, $params);
});
}
/**
* A common API executor
* @param string $token
* @param string $url
* @param array $params
* @return array
* @throws InvalidArgumentException
*/
private function executeApi(string $token, string $url, array $params)
{
try {
$data = APIStorageRepository::execute($token, $url, $params);
// Always return an array so downstream code is predictable
return is_array($data) ? $data : [];
} catch (Throwable $e) {
if (app()->environment(['local', 'dev', 'qat'])) {
Log::channel('dashboard')->warning("API fetch failed for {$url}: {$e->getMessage()}");
}
return [];
}
}
/** ISO year-week to avoid cross-year collisions, e.g. '2025-W01'. */
private function groupByIsoWeek(array $data, string $dateField, string $valueField): array
{
return collect($data)
->filter(fn($i) => !empty($i[$dateField]) && is_numeric($i[$valueField] ?? null))
->groupBy(function ($i) use ($dateField) {
$d = Carbon::parse($i[$dateField]);
return $d->format('o-\WW');
})
->map(fn($items) => collect($items)->sum($valueField))
->toArray();
}
private function groupByMonth(array $data, string $dateField, string $valueField): array
{
return collect($data)
->filter(fn($i) => !empty($i[$dateField]) && is_numeric($i[$valueField] ?? null))
->groupBy(fn($i) => Carbon::parse($i[$dateField])->format('Y-m'))
->map(fn($items) => collect($items)->sum($valueField))
->toArray();
}
/**
* Nice helper for format dates
* @param string|null $value
* @return string
*/
private function formatDate(?string $value): string
{
if (empty($value)) {
return 'N/A';
}
try {
return Carbon::parse($value)->format('d M Y, H:i');
} catch (Throwable $e) {
return 'N/A';
}
}
}
````
And the new controller DashBoardController.php
````
<?php
namespace Modules\Spares\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Modules\Spares\ViewModels\DashboardViewModel;
class DashboardController extends Controller
{
/**
* Main View Loader
* now uses a view model
* @return View
*
*/
public function index(): View
{
$dashboardViewModel = (new DashboardViewModel())->build();
if (app()->environment(['local', 'dev', 'qat'])) {
$useCache = (bool)settings()->group('maximo')->get('dashboard_cache', false);
logger()->channel('dashboard')->info(
$useCache
? emoji('green') . 'Using Cached Data'
: emoji('red') . 'Not Using Cached Data'
);
}
return view('spares::Components.dashboard', $dashboardViewModel->toArray());
}
}
````
This abstracts the data from the view and makes it far more maintainable and extendable.
+54
View File
@@ -0,0 +1,54 @@
### Developer Websites
In your how directory you will see a new directory epro, this is the base directory
with the code base living in /home/<username>/epro/htdocs/epro2,
If one has not been created for your please reach out to ``` mark.gregory@mbpa.com```
All of your work should take place in this environment using the branches as defined above
you can switch your own environment with ``` git checkout ``` complete you work and then merge
into the develop branch and switch back to complete testing. its wise also to do a ``` git pull origin develop ```
to collect other developer changes, if at this stage you get merge conflicts you must resolve these and push back to
develop. and merge back into your own issue, feature or hotfix branch.
This will eliminate all of the code overwrites suffered to date
### Defined Hosts
192.168.0.81:10020 Mark Gregory
192.168.0.81:10021 Tomasz
192.168.0.81:10022 Malak
192.168.0.81:10023 Brad
192.168.0.81:10024 Karl
When we have defined the firewall exclusions you will be able to substitute 192.168.0.81 for 164.39.2.139 we will expand this list as
require when new developers are provisioned.
The current MPBA testing URL is ``` http://164.39.2.139:10008/admin ```
and a backup on ``` http://164.39.2.139:10011/admin```
### Production Deployment Process
1. Prior to release day a code freeze takes place
2. A new branch is created release/version-date
3. UAT servers are switched to this branch and retested
4. RELEASE DAY, merge release/version-date into master
5. Seek release approval via slack.
6. Deploy master and then test release and perform any hotfixes as require
7. Push any hot fixes back and merge release/version-date into develop
8. Release complete - alert team and then code freeze removed.
9. Release issues revert the comment and liaise with team.
The release should take place by the selected developer and on a rota if possible so all
developers are upto date with how to release the system.
### Basic System Information
PHP Version: 8.0
Node Version: 16.20.2
Laravel Version: 8.X
### Git Branch Structure
1. MPBA Server /develop
2. MediaDev /stage
3. MediaUat /qa
4. Production /master
+183
View File
@@ -0,0 +1,183 @@
### System Developer Notes
This documents how one should develop using the sentinel laravel project.
#### Git Structure
The project is tracked using git and has several key branches, we also operate a pull request system and
these branches are protected, a pull request must be raised and approved before code can be merged into the
relevant develop, stage, release or main branches. ****** Code will not be merged in any other way *****
#### main
This is the production branch and is protected from direct commits into this branch except
for senior developers, in normal circumstances this branch in never tampered with hot fixes and
releases being the only exception. This key branch provides the code for http://mediuk:10008
it resides upon the mediuk server in a directory /www/laravel/htdocs.
#### stage
This branch is mainly deployed to support user quality testing and reside on http://mediqat:10008
NB from time to time other branches may be in effect on this server when progressing towards a software release
this structure is normally release/RC-(date)-Vx when (Vx) is is the format of "V" followed by a number to indicate the
release candidate version. its location on the mediqat server is /www/laravel/htdocs
#### develop
This is the key branch that runs on http://medidev:10008 this is not really to be considered a true development
branch as historically this has been use as a pseudo user QAT branch.
## Important Note
The medidev server also runs th e Oracle Connectors required to run the Spares (MAXIMO) system and you
should ensure that this server is working at all times, this may well be backed up by an additional server
running on medidev when time permits.
There are also other key branches
#### release
primarily for release candidates as above.
#### bugfix
Branches under this in the format bugfix/details are used to provide fixes for running systems
#### feature
This is for new systems, as we use Laravel modules and all code is self contained with within the module
follows the format feature/module_name
#### hotfix
This branch structure is for hotfixes into the production system ofr emergency updates in the format
hotfix/fix_details
#### support
This is for minor fixes into all of the Sentinel system.
#### Commit messages
An important aspect of commit messages is the ability to search these in good commit messages are vital
to prevent developer confusion and code loss when merging.
these should follow the format [Module_Name] or [System_Name] and then a concise narrative of what the commit contains.
short and non descriptive commit messages will mean that a pull request is rejected with a request to refine the commit
message.
#### Starting a new project
In amy cases developers have been using the medidev server for pure development and this can cause issues
in, medidev should NOT be used for pure development in all cases a separate server should be requested, in the
current DB2 database setup there is only one database instance and this can and will cause you issues.
It's recommended that you develop locally using docker a high performance docker_compose file and a docker directory
has been provided, you should install docker, once installed you should reach to to MPBA staff to get a backup of
the core databases AAMEDIUK, MPDAT10 and ARCHIVE when using docker you will be using MYSQL as a database server
the database backups will be provided and these cna be restored to you local database's
Always we aware of the fact that the target DB on all Abbot Servers is IBM DB2 and guard code should be produced to
ensure that migrations and seeders which are fully support on DB2 , ensure that they only run once as needed
and log all of their actions. This aids other developers.
All permissions and roles should be defined in a seeder so that simple commands can be used to populate the various
systems as needed .
We do use actually Laravel Sail in some instances so you can use
```
docker-compose build --no-cahe
or
sail build --no-cache
```
To download the images and build containers, then once build you can use
```
sail up -d
or
docker-compose up -d
```
This will start the database server, and php webserver using nginx, its recommended that you add
entries to your hosts file for the following
````
127.0.0.1 mysql
127.0.0.1 mailpit
127.0.0.1 redis
127.0.0.1 host.docker.internal
127.0.0.1 medidev.vm
127.0.0.1 mediuk.vm
127.0.0.1 mediqat.vm
127.0.0.1 sonarcube
127.0.0.1 nginx
````
Your browser will then respond on http://medidev.v, for example. You can use local self certified SSL if you desire and
you should reach out to MPBA staff for details.
### Important Apple Silicon Development
With later releases of Apple OSX the file system is case sensitive, where the IBM AS400 Apache server / filesystem is
not. Therefore its not possible to hold the mysql database files on a case sensitive drive this will cause the
server to crash when loading data etc. We use the Linux sub system provided by the container to overcome this
and performance issues with docker and shared volumes.
Beware if you clean your docker system and purge all the volumes you must rebuilt and
re-import the database backups.
One could use a separate case insensitive volume but this will impact docker performance.
When starting a new project the root branch is develop and you should create your new branch as feature/module name
### Development Steps
1. Perform a git pull using the develop branch ```` goit pull origin develop ````
2. Create your new feature branch and switch ```` git branch feature/module_name && git checkout feature/module_name````
3. Create a suitable env file .env
3. Execute docker commands to build system.
4. Restore the 3 database backups
5. Add the host file entries
6. execute ``` docker-compose up -d or sail up -d``` to start the docker system.
7. create your new module with ```` php artisan module:make (name)`
You should now have a running system.
### Module Naming
Think about how you name you module and make it as short as possible with NO spaces
``` php artisan module:make Spares ``` for example. Changing a module name later is hard and will require a
senior developer.
In previous releases we were using an environment switcher so that .env.dev ate were added to the SCM ( git) this
is not now the case and it relies upon the .env file, the .env file SHOULD NOT BE COMMITTED under any circumstances.
The existing modules provide a great starting point and you should review the code under the Modules directory
#### FINAL IMPORTANT NOTES
1. commit and push pull at high frequency
When a piece has been worked upon commit your changes with a suitable commit message and pull al push any changes, its a
good idea to
pull and push frequently.
2. Commit all work at the end of day
This is important. ALL WORK should be committed and PUSHED to GIT at the end of the day NO EXCEPTIONS
3. You are responsible for your own code, if you generate merge conflicts YOU sort them out to ensure no DATA LOSS
4. if yo do not commit frequently do not expect your code to be updated to GIT you MUST COMMIT your work
Do not rely on backups, its your code, own it and ensure its added to the source control system and also ensure you do
not over write
and core code in the process, if in doubt ASK !!
+183
View File
@@ -0,0 +1,183 @@
## Docker Usage for Local Development
This documents hwo to use docker for local development on OSX but can be modified to run under
windows, it uses a multi stage build process and the latest alpine images to keep the container sizes to a minimum.
it fully supports xdebug and a server version of mysql server. This presents a very fast development
experience that does not pollute the host operating system.
### Why I Use Docker
Docker plays a crucial role in my deployment strategy. It allows me to containerize my Laravel applications, ensuring
consistency across different environments and across multiple services that I need to run a production Laravel app -
Queue workers, Schedulers, Nightwatch agent, etc..
1. Consistent Environments: Every deployment maintains the same configurations, reducing bugs and deployment issues.
2. Scalability: Docker’s lightweight containers.
3. The possibility for automated deployment, Integration with Git allows for continuous deployment workflows.
## Docker Compose
1. We assume you have docker installed and running
2. and also docker-compose installed
3. you can also use laravel sail *** the configuration overrides the default sail definitions
```
services:
php-fpm:
build:
context: ./docker/php-fpm
dockerfile: Dockerfile
container_name: laravel-php
working_dir: /var/www/html
volumes:
# ##########################################################
# Source code via bind mount (fast reads on mac:delegated)
# This makes docker much faster
# ##########################################################
- ./:/var/www/html:delegated
# ##########################################################
# Hot paths on named volumes (fast writes, Linux FS)
# This means we have very fast writes as they are not replicated
# to the host file system, this means that they must be recreated
# when built and also you must run composer install as well
# ##########################################################
- vendor:/var/www/html/vendor
- storage:/var/www/html/storage
- cache:/var/www/html/bootstrap/cache
environment:
PHP_OPCACHE_ENABLE: "1"
PHP_OPCACHE_MEMORY: "256"
PHP_FPM_MAX_CHILDREN: "16"
XDEBUG_MODE: "debug,develop"
extra_hosts:
- "host.docker.internal:host-gateway"
expose:
- "9000"
- "9003"
networks:
web:
shared:
aliases: [ api.medidev.vm, nginx, gateway ]
nginx:
image: nginx:alpine
container_name: laravel-nginx
entrypoint: [ "nginx", "-g", "daemon off;" ]
depends_on: [ php-fpm ]
volumes:
- ./:/var/www/html:ro,delegated
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/nginx/logs:/var/log/nginx
- ./docker/nginx/ssl:/etc/nginx/ssl:ro
ports:
- "80:80"
- "443:443"
networks:
web:
shared:
aliases:
- nginx
- gateway
- api.medidev.vm
redis:
image: redis:alpine
container_name: laravel-redis
command: [ "redis-server", "--save", "", "--appendonly", "no", "--loglevel", "warning" ]
ports:
- "6379:6379"
networks: [ web ]
mysql:
image: mysql:9.0
container_name: laravel-mysql
command: [ "mysqld", "--log-error-verbosity=2","--mysqlx=0","--defaults-file=/etc/mysql/conf.d/my.cnf" ]
logging:
driver: "none"
environment:
MYSQL_ROOT_PASSWORD: "987400Kmh"
MYSQL_DATABASE: "AAMEDIUK"
MYSQL_LOG_CONSOLE: "false"
#>/dev/null 2>&1
entrypoint: [ "sh","-lc","exec mysqld --datadir=/var/lib/mysql --user=mysql --mysqlx=0 --log-error-verbosity=2 " ]
ports:
- "3306:3306"
volumes:
- dbdata_medidev:/var/lib/mysql/
# ##########################################################
# Optional custom config / init files:
# ##########################################################
- ./docker/mysql/conf.d:/etc/mysql/conf.d:ro
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "-p987400Kmh" ]
interval: 10s
timeout: 5s
retries: 10
networks: [ web ]
queue:
build:
context: ./docker/php-fpm
dockerfile: Dockerfile
container_name: laravel-queue
command: [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]
working_dir: /var/www/html
volumes:
- ./:/var/www/html:delegated
- vendor:/var/www/html/vendor
- storage:/var/www/html/storage
- cache:/var/www/html/bootstrap/cache
depends_on:
- redis
- mysql
networks: [ web ]
restart: unless-stopped
networks:
web:
driver: bridge
shared:
external: true
name: shared-frontend
volumes:
vendor:
storage:
cache:
dbdata_medidev:
```
You need to create a docker directory from the project root
and add the following files to this directory once downloaded.
[Download docker.zip](assets/docker.zip){: download}
you will also need to contact the author for a copy of the mysql databases;
Once all the files are in place you can execute
```
sail up --build
or
docker-compose build --no-cache
```
Fix invalid cache path after rebuild as were using names paths
You can also use this command
````
docker exec -it laravel-php bash -lc \
"mkdir -p bootstrap/cache storage/framework/{cache,sessions,views} \
&& chown -R www-data:www-data bootstrap storage \
&& chmod -R ug+rwX bootstrap storage"
`````
NB also run composer install
````
docker network create shared-frontend
````
This makes the necessary cache dirs etc
````
mkdir -p bootstrap/cache storage/framework/{cache,sessions,views,testing,cache/data} && chown -R www-data:www-data bootstrap storage
````
@@ -0,0 +1,96 @@
# Laravel Environment Switcher
### Introduction
*** THIS HAS BEEN DEPRECATED AND IS NO LONGER USED
The basic operation of the environment switcher is to permit the use of different environment files
based upon a variable that is added to the web servers configuration to control which one is loaded.
when you make changes to the environment files you need to execute
``php artisan config:clear``
and
``php artisan config:cache``
The various env files are committed to source control with the exception of the .local environment file
this file is used for local development and is developer specific
1. .dev is used on MEDIDEV
2. .qat is used on MEDIQAT
3. .prod is used on MEDIUK
.local is reserved for local development environments
The files in use are
``` /app/Bootstrap/DetectEnviroment.php``` and ``` /app/Bootstrap/LoadEnvironment.php```
These are added to ``` app/Http/Kernel.php```
```
protected $bootstrappers = [
\App\Bootstrap\DetectEnvironment::class,
\App\Bootstrap\LoadEnvironment::class,
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\SetRequestForConsole::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
```
and for console commands These are added to ``` app/Helpers/Kernel.php```
````
protected $bootstrappers = [
\App\Bootstrap\DetectEnvironment::class,
\App\Bootstrap\LoadEnvironment::class,
//\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\SetRequestForConsole::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
````
This permits the console to use the relevant env file also
### Apache Web Server Configuration
For the Apache Web Server a line is added to the configuration file in
`` /www/laravel/conf/httpd.conf``
This line is `` SetEnv APPLICATION_ENV dev`` for example on MEDIDEV and the corresponding env file
created .env.dev
Apache then needs to be restarted for the new environment file to be activated.
### nginx configuration
The config file for valet etc is changed to add these variables
````
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass "unix:/Users/markgregory/.config/valet/valet.sock";
fastcgi_index "/Users/markgregory/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param APPLICATION_ENV "local";
fastcgi_param APP_ENV "local";
fastcgi_param SCRIPT_FILENAME "/Users/markgregory/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
````
Again nginx should be restarted with nginx -s reload or for valet ``` valet restart ```
### Debugging
When setting this up if the variables are not added you will receive a warning and the app will not
start, for debug only you can add normal entries to .env but do not commit these to the SVM .env is only committed
to show it's not in use.
+98
View File
@@ -0,0 +1,98 @@
### GitHub Branching Name Best Practices
On GitHub, branching is a crucial component of collaborative software development. In addition to keeping your project
organized, using consistent and unambiguous branch naming conventions makes it easier for team members to quickly
comprehend each branch's purpose. Some best practices for naming branches in your GitHub projects are listed below.
1. Use Prefixes to Indicate Purpose
Prefixing branch names based on their purpose helps categorize and identify them quickly. Here are some common
prefixes:
feature/: For new features or functionalities.
bugfix/: For fixing bugs in the code.
hotfix/: For urgent patches, usually applied to production.
design/: For user interface or user experience updates.
refactor/: For improving code structure without changing functionality.
test/: For writing or improving automated tests.
doc/: For documentation updates.
### Example Prefix Usage
feature/user-authentication
bugfix/fix-login-error
hotfix/urgent-patch-crash
design/update-navbar
refactor/remove-unused-code
test/add-unit-tests
doc/update-readme
2. Keep Names Short and Descriptive
Branch names should be concise yet informative. A good branch name briefly describes what it is for without being
overly long or vague. Here are some tips:
Use hyphens (-) to separate words for better readability.
Avoid generic terms like update, changes, or stuff.
Focus on the primary task or issue the branch addresses.
### Examples of Clear Branch Names
'''
Feature Branches
feature/add-user-profile
feature/implement-chat-notifications
Bug Fixes
bugfix/correct-date-display
bugfix/fix-404-error
Design Updates
design/improve-dashboard-ui
design/revise-mobile-layout
Refactoring
refactor/optimize-database-queries
refactor/simplify-api-routes
Hotfixes
hotfix/security-patch
hotfix/fix-login-issue
Documentation
doc/add-api-instructions
doc/update-contributor-guidelines
'''
3. Incorporate Ticket Numbers or Identifiers
For teams using project management tools like Jira or Trello, include the ticket number or issue ID in the branch
name. This links the branch to its corresponding task, making it easier to track progress.
## Examples with Ticket Numbers
feature/JIRA-1234-add-login
bugfix/TICKET-567-resolve-crash
hotfix/ISSUE-890-fix-api
4. Collaborate on a Branch Naming Strategy
Every team is different, so it’s crucial to agree on a branching strategy that works for everyone. Document the
naming conventions in your project’s contribution guidelines or README file so that new contributors can easily
follow them.
### Benefits of Consistent Branch Naming
Improved Collaboration: Team members can quickly understand the purpose of a branch without needing extra context.
Easier Navigation: Searching for specific branches becomes straightforward.
Better Automation: Many CI/CD tools can leverage structured branch names for workflows (e.g., deploying hotfix/ branches
automatically to production).
+5
View File
@@ -0,0 +1,5 @@
# MPBA Helpdesk
The MPBA Helpdesk is available at [Helpdesk](https://helpdesk.mpba.dev).
You can create tickets here once you have a valid account and use the ticket numbers in your branches as required.
+89
View File
@@ -0,0 +1,89 @@
# Welcome to Sentinel Docs
## Sentinel EPRO
This document details the Sentinel development process that will prevent collisions and code overwrites
it's a living document and will be updated frequently and as the process is refined.
The MPBA Helpdesk is available at [Helpdesk](https://helpdesk.mpba.dev).
This documentation used mark down files, if you wish to read more please checkout
out [Mark Down Guild](https://www.markdownguide.org/).
One important aspect of this is that in an ideal world one would develop on a local system with a suitable
ODBC driver, if this is not available then we have made a development environment for each developer
This system resides on the MPBA AS400 server, the details of this developer system is detailed below.
This may be in some cases informing developers of how 'git' does its work its not a definitive
guide to git and anyone who is unsure should reach out so that any issues can be quickly dealt with.
On important aspect of this system is to use a proper git branch structure no longer should developers be working on
the master branch, the master branch is reserved for production servers only.
Developers should always create a branch off develop which will contain the latest developer codebase.
Git is a DVCS we are all used to the older file based SCM's like subversion and PVCS a DVCS only records the
changes. its important to understand that in every case its better to go git pull and push
frequently.
Also do a ``` git fetch ``` this will download new branches for you
Please do ``` git pull ``` and ``` git push ``` as frequently as possible.
Also another useful command is ``` git status``` the output form this speaks for itself as does ``` git log ```.
### Commit Messages
Please make you commit messages useful for developers reason being we can search for a commit message
and isolate changed code quickly
so "Module Debug" is not helpful but "Module debug - corrected issie with service provider and updated .gitignore" is
far more useful
### Production Branches
Production servers will only ever use master or main these are considered to be protected
branches there will not be used to make live edits under any circumstances.
the only exception is a Priority One emergency with a system down and in operable,
even in this case the developer must ensure that develop is also upto date.
In the case of such an emergency the assigned developer should create a new branch hotfix/issue-name and
perform the fix task in there and merge back into master and then master though to develop.
```` git branch hotfix\error-500-sparos ````
then
```` git checkout hotfix\error-500-sparos ````
perform your fixes and verify they are ok push all changes to gitlab and
```` git checkout master ````
then
```` git merge hotfix\error-500-sparos ````
then
```` git push origin master ````
then verify fix worked ok finally
```` git checkout develop ````
then
```` git merge master &&& git push origin develop ````
All of your fixes are in place and merged back.
### Branch Model
The following branches are permitted
* 'master' or 'main' for production servers only
* 'develop' main branch for development environments
* 'qat' for UAT servers
* 'stage' pre production servers *** if used
* 'issues' for code fixes
* 'feature' for major improvements and new systems
* 'hotfix' for emergency fixes
* 'archive' this is for copies of various branches for convenience
* 'release' for candidate releases that are production ready and may be used on a pre-prod server
+3
View File
@@ -0,0 +1,3 @@
### Indexing DB2 for speed
This area details why you should add indexes to key fields to increase DB2 lookup performance
+13
View File
@@ -0,0 +1,13 @@
### Developer Knowledge Base
### Laravel Eloquent
to debug an SQL Query you can use
```
$rawSql = vsprintf(str_replace(['?'], ['\'%s\''], $query->toSql()), $query->getBindings());
```
This will hold the rea; SQL call in $rawSQL
+69
View File
@@ -0,0 +1,69 @@
### Sentinel Migrations
This contains important information regarding migrations.
### Migration Errors
NB these migrations should only been run on MEDIDEV
This is an important step, the current DB_USER on mediuk is not as powerful as
a user with QSECOFR, migrations will fail with a resource 904 error, to overcome this
change the active env file DB_USERNAME and DB_PASSWORD and re-run the migration.
When running migrations on production always for this with care
To run a migration
```
php artisan module:migrate Settings
```
to revert the migration
```
php artisan module:migrate-rollback Settings
```
### Writing Migrations
In line with general Laravel Migration creation and Taylor Ortwel we do not create a down action to prevent tables
being erased in error. Also we should check if a table exists and example ois below.
```
/**
* Run the migrations.
*/
public function up(): void
{
if (!Schema::hasTable(Priority::TABLE)) {
Schema::create(Priority::TABLE, function (Blueprint $table) {
$table->id();
$table->integer('is_default')->default(0);
$table->integer('is_protected')->default(0);
$table->integer('sort_order')->default(0);
$table->string('title')->nullable();
$table->string('slug')->nullable();
$table->text('description')->nullable();
$table->char('uuid', 36)->nullable();
$table->timestamp('deleted_at')->nullable();
$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
```
One should never run migrations on production, any tables are to be created using IBM Utilities.
*** and Never run the commands php artisan migrate, php artisan migrate-rollback or php artisan db:wipe.
+147
View File
@@ -0,0 +1,147 @@
---
title: "New page"
language: "en"
previous: "index.html"
---
## Sentinel EPRO
### System Information
PHP Version: 8.0
Node Version: 16.20.2
Laravel Version: 8.X
# Main Development AS400
Development URL: http://164.39.2.139:10011/
Server Path: /www/ADC8/htdocs/epro2/
branch: develop
# Custom Development AS400 ( Malak's Work )
Development URL: http://164.39.2.139:10008/login
Server Path: /www/epro/htdocs/epro2
branch: master
### Git Branch Structure
1. MPBA Server /develop
2. MediaDev /stage
3. MediaUat /qa
4. Production /master
Other possible branch names are:
production
main
staging
*** it is to developers to adopt these as needed.
The Application does not currently use redis nor memcached and has no reliance upon these caching
servers.
### Important files
There are few important files that change from host to host, these are contained in the provision directory
and should be kept upto date with their respective target hosts
the Keepass file will contain urls and credentials for the various hosts systems.
### Local Development
There are several alternatives when it comes to local development of web application
development, One can use vagrant, Laravel Valet or MAMP LAMP etc,one of the best development systems is
docker, this builds containers for the various components and is available
on all major platforms.
The first thing to do on your development machine is to install docker and docker desktop along with support for
docker composer.
#### Image Storage
When running docker the php artisan storage:link does not resolve to the correct path
to overcome this issue do a docker ps and find the container ID
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1dd8f887cebb sail-8.1/app "start-container" 19 hours ago Up 15 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:5173->5173/tcp, :::5173->5173/tcp, 8000/tcp epro2-sentinal-1
4d60252a7927 mailhog/mailhog "MailHog" 3 days ago Up 15 minutes 0.0.0.0:1025->1025/tcp, :::1025->1025/tcp, 0.0.0.0:8025->8025/tcp, :::8025->8025/tcp mailhog
```
these execute
```
docker exec -it 1dd8f887cebb /bin/sh
```
navigate to public and remove the storage link, then execute
``` ln -s ../storage/app/public storage ```
this will link the storage folder correctly
and do not run storage link as this wil use the UPLOAD_DIR flag from the .env file that will contain the an incorrect
path
On AS400 hosts this is normally /tmp/quotations and storage link does work properly
#### SSL Self Signed Certificates
to import your local self signed certs into chrome got to settings then setting privacy and security then
Authorities click import and navigate to the .valet directory then CA then import .valet/CA/LaravelValetCASelfSigned.pem
accepting all the options and then restart chrome, you local site is now using SSL.
#### Introduction
Laravel Sail is a light-weight command-line interface for interacting with Laravel's default Docker
development environment. Sail provides a great starting point for building a Laravel application using PHP, MySQL, and Redis without requiring prior Docker experience.
You can also use laravel valet ( see below)
At its heart, Sail is the docker-compose.yml file and the sail script that is stored at the root of your project. The sail script provides a CLI with convenient methods for interacting with the Docker containers defined by the docker-compose.yml file.
Laravel Sail is supported on macOS, Linux, and Windows (via WSL2).
#### Initial Installation
Laravel Sail is automatically installed with all new Laravel applications so you may start using it
immediately. To learn how to create a new Laravel application, please consult Laravel's installation
documentation for your operating system.
During installation, you will be asked to choose which Sail supported services your application will be
interacting with.
To install Laravel sail use ``` composer require --dev laravel\sail```
The documentation is available at https://laravel.com/docs/10.x/sail
#### Docker Notes
Some important docker commands are as follows:
NB when running these commands ensure that you have no running instances for example executing sail down.
``` docker system prune -a```
This command will remove the build containers and any downloaded containers, this is useful for saving disk space
``` docker volume rm $(docker volume ls -q) ```
Used to delete any of the volumes created, this is where the database is stored etc again great for cleaning up
and saving disk space.
``` sail build --no-cache ```
this is perfect for rebuilding your containers
### Linux ODBC Installs
The ODBC drivers for linux can present a challenge and its important to
install IBM Client Access for Linux, this includes the CLI drivers for many linux distributions
The database.php in the config directory is the place to set the
drivers and the DSN one also add a specific config file if needed.
We have set credentials in the env file to suite, typically to aid switching between databases
we recommend using the same db credentials as 192.168.0.81 ( MPBA AS400 Host)
'odbc_local' => [
'driver' => 'odbc',
'odbc_driver' => '/opt/ibm/iaccess/lib64/libcwbodbc.so',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
'dsn' => 'Driver={IBM i Access ODBC Driver 64-bit};System=192.168.0.81;AlwaysCalculateResultLength=1;CCSID=1208;NAM=1;DBQ=,'.env('DBQ'),
'options' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL, // PDO::CASE_UPPER PDO::CASE_LOWER PDO::CASE_NATURAL
//PDO::ATTR_AUTOCOMMIT => false
],WARN - #c.j.p.b.d.BladeDebugPathMapper
DB2 on docker is presenting more of a challenge as none of the current
ODBC drivers see to work as expected.
#### Laravel Valet Usage Guidelines
+63
View File
@@ -0,0 +1,63 @@
# AS400 Queue Operation
### Overview
There is a core issue with Laravel and IBM Db2 drivers in the current version of sentinel,.
this means that queues will not run properly that have any large payloads teh table struct is not
suitable and jbs will simply fail to load into the table called QUEUE.
### The solution
The easiest way to fix this issue is not to use DB2 for queue operation but switch to REDIS
the REDIS sever has been installed on MEDIDEV and can now be used to Caching, sessions and also more importantly
QUEUES.
The entries in the env file set these to use redis as opposed to database, file system or array.
One further issue when using Maximo with cron is that cron has no concept of the Oracle Environment Variables
we are devising a solution for this issue.
#### current cron entries for MEDIDEV
```
SHELL=/QOpenSys/pkgs/bin/bash
PATH=/QOpenSys/pkgs/bin:/QOpenSys/usr/sbin:/QOpenSys/usr/bin
MAILTO=qsecofr
APPLICATION_ENV=dev
# For details see man 4 crontabs
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
* * * * * pricedx1 php /www/laravel/htdocs/artisan schedule:run
*/1 * * * * pricedx1 php /www/laravel/htdocs/artisan queue:work --max-time=30
```
This means that the cron job runs every minute and then for a maximum of 30 seconds, if there are no jobs to process
then
the queue:work command will quite.
in most cases a maximum of 3 crn jobs will run on the AS400 1, for the schedule and then two for the queue:work command
if the --max-time parameter is removed then the queue:work command does not quit and you end up with lots of cron jobs
running
which is not ideal as this will kill system resources and slow the AS400 server.
+14
View File
@@ -0,0 +1,14 @@
### Redis Server
to start the redis server on AS400 execute
```
redis-server --daemonize yes
```
you can then use
redis-cli ping
to see if it;s working
+665
View File
@@ -0,0 +1,665 @@
# Task Scheduling
- [Introduction](#introduction)
- [Defining Schedules](#defining-schedules)
- [Scheduling Artisan Commands](#scheduling-artisan-commands)
- [Scheduling Queued Jobs](#scheduling-queued-jobs)
- [Scheduling Shell Commands](#scheduling-shell-commands)
- [Schedule Frequency Options](#schedule-frequency-options)
- [Timezones](#timezones)
- [Preventing Task Overlaps](#preventing-task-overlaps)
- [Running Tasks on One Server](#running-tasks-on-one-server)
- [Background Tasks](#background-tasks)
- [Maintenance Mode](#maintenance-mode)
- [Schedule Groups](#schedule-groups)
- [Running the Scheduler](#running-the-scheduler)
- [Sub-Minute Scheduled Tasks](#sub-minute-scheduled-tasks)
- [Running the Scheduler Locally](#running-the-scheduler-locally)
- [Task Output](#task-output)
- [Task Hooks](#task-hooks)
- [Events](#events)
<a name="introduction"></a>
## Introduction
In the past, you may have written a cron configuration entry for each task you needed to schedule on your server.
However, this can quickly become a pain because your task schedule is no longer in source control and you must SSH into
your server to view your existing cron entries or add additional entries.
Laravel's command scheduler offers a fresh approach to managing scheduled tasks on your server. The scheduler allows you
to fluently and expressively define your command schedule within your Laravel application itself. When using the
scheduler, only a single cron entry is needed on your server. Your task schedule is typically defined in your
application's `routes/console.php` file.
<a name="defining-schedules"></a>
## Defining Schedules
You may define all of your scheduled tasks in your application's `routes/console.php` file. To get started, let's take a
look at an example. In this example, we will schedule a closure to be called every day at midnight. Within the closure
we will execute a database query to clear a table:
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->daily();
In addition to scheduling using closures, you may also
schedule [invokable objects](https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke). Invokable objects
are simple PHP classes that contain an `__invoke` method:
Schedule::call(new DeleteRecentUsers)->daily();
If you prefer to reserve your `routes/console.php` file for command definitions only, you may use the `withSchedule`
method in your application's `bootstrap/app.php` file to define your scheduled tasks. This method accepts a closure that
receives an instance of the scheduler:
use Illuminate\Console\Scheduling\Schedule;
->withSchedule(function (Schedule $schedule) {
$schedule->call(new DeleteRecentUsers)->daily();
})
If you would like to view an overview of your scheduled tasks and the next time they are scheduled to run, you may use
the `schedule:list` Artisan command:
```bash
php artisan schedule:list
```
<a name="scheduling-artisan-commands"></a>
### Scheduling Artisan Commands
In addition to scheduling closures, you may also schedule [Artisan commands](/docs/{{version}}/artisan) and system
commands. For example, you may use the `command` method to schedule an Artisan command using either the command's name
or class.
When scheduling Artisan commands using the command's class name, you may pass an array of additional command-line
arguments that should be provided to the command when it is invoked:
use App\Console\Commands\SendEmailsCommand;
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send Taylor --force')->daily();
Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
<a name="scheduling-artisan-closure-commands"></a>
#### Scheduling Artisan Closure Commands
If you want to schedule an Artisan command defined by a closure, you may chain the scheduling related methods after the
command's definition:
Artisan::command('delete:recent-users', function () {
DB::table('recent_users')->delete();
})->purpose('Delete recent users')->daily();
If you need to pass arguments to the closure command, you may provide them to the `schedule` method:
Artisan::command('emails:send {user} {--force}', function ($user) {
// ...
})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();
<a name="scheduling-queued-jobs"></a>
### Scheduling Queued Jobs
The `job` method may be used to schedule a [queued job](/docs/{{version}}/queues). This method provides a convenient way
to schedule queued jobs without using the `call` method to define closures to queue the job:
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new Heartbeat)->everyFiveMinutes();
Optional second and third arguments may be provided to the `job` method which specifies the queue name and queue
connection that should be used to queue the job:
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
// Dispatch the job to the "heartbeats" queue on the "sqs" connection...
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
<a name="scheduling-shell-commands"></a>
### Scheduling Shell Commands
The `exec` method may be used to issue a command to the operating system:
use Illuminate\Support\Facades\Schedule;
Schedule::exec('node /home/forge/script.js')->daily();
<a name="schedule-frequency-options"></a>
### Schedule Frequency Options
We've already seen a few examples of how you may configure a task to run at specified intervals. However, there are many
more task schedule frequencies that you may assign to a task:
```
|------------------------------------|----------------------------------------------------------|
| Method | Description |
|------------------------------------|----------------------------------------------------------|
| `->cron('* * * * *');` | Run the task on a custom cron schedule. |
| `->everySecond();` | Run the task every second. |
| `->everyTwoSeconds();` | Run the task every two seconds. |
| `->everyFiveSeconds();` | Run the task every five seconds. |
| `->everyTenSeconds();` | Run the task every ten seconds. |
| `->everyFifteenSeconds();` | Run the task every fifteen seconds. |
| `->everyTwentySeconds();` | Run the task every twenty seconds. |
| `->everyThirtySeconds();` | Run the task every thirty seconds. |
| `->everyMinute();` | Run the task every minute. |
| `->everyTwoMinutes();` | Run the task every two minutes. |
| `->everyThreeMinutes();` | Run the task every three minutes. |
| `->everyFourMinutes();` | Run the task every four minutes. |
| `->everyFiveMinutes();` | Run the task every five minutes. |
| `->everyTenMinutes();` | Run the task every ten minutes. |
| `->everyFifteenMinutes();` | Run the task every fifteen minutes. |
| `->everyThirtyMinutes();` | Run the task every thirty minutes. |
| `->hourly();` | Run the task every hour. |
| `->hourlyAt(17);` | Run the task every hour at 17 minutes past the hour. |
| `->everyOddHour($minutes = 0);` | Run the task every odd hour. |
| `->everyTwoHours($minutes = 0);` | Run the task every two hours. |
| `->everyThreeHours($minutes = 0);` | Run the task every three hours. |
| `->everyFourHours($minutes = 0);` | Run the task every four hours. |
| `->everySixHours($minutes = 0);` | Run the task every six hours. |
| `->daily();` | Run the task every day at midnight. |
| `->dailyAt('13:00');` | Run the task every day at 13:00. |
| `->twiceDaily(1, 13);` | Run the task daily at 1:00 & 13:00. |
| `->twiceDailyAt(1, 13, 15);` | Run the task daily at 1:15 & 13:15. |
| `->weekly();` | Run the task every Sunday at 00:00. |
| `->weeklyOn(1, '8:00');` | Run the task every week on Monday at 8:00. |
| `->monthly();` | Run the task on the first day of every month at 00:00. |
| `->monthlyOn(4, '15:00');` | Run the task every month on the 4th at 15:00. |
| `->twiceMonthly(1, 16, '13:00');` | Run the task monthly on the 1st and 16th at 13:00. |
| `->lastDayOfMonth('15:00');` | Run the task on the last day of the month at 15:00. |
| `->quarterly();` | Run the task on the first day of every quarter at 00:00. |
| `->quarterlyOn(4, '14:00');` | Run the task every quarter on the 4th at 14:00. |
| `->yearly();` | Run the task on the first day of every year at 00:00. |
| `->yearlyOn(6, 1, '17:00');` | Run the task every year on June 1st at 17:00. |
| `->timezone('America/New_York');` | Set the timezone for the task. |
|------------------------------------|----------------------------------------------------------|
```
These methods may be combined with additional constraints to create even more finely tuned schedules that only run on
certain days of the week. For example, you may schedule a command to run weekly on Monday:
use Illuminate\Support\Facades\Schedule;
// Run once per week on Monday at 1 PM...
Schedule::call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// Run hourly from 8 AM to 5 PM on weekdays...
Schedule::command('foo')
->weekdays()
->hourly()
->timezone('America/Chicago')
->between('8:00', '17:00');
A list of additional schedule constraints may be found below:
```
|------------------------------------------|--------------------------------------------------------|
| Method | Description |
|------------------------------------------|--------------------------------------------------------|
| `->weekdays();` | Limit the task to weekdays. |
| `->weekends();` | Limit the task to weekends. |
| `->sundays();` | Limit the task to Sunday. |
| `->mondays();` | Limit the task to Monday. |
| `->tuesdays();` | Limit the task to Tuesday. |
| `->wednesdays();` | Limit the task to Wednesday. |
| `->thursdays();` | Limit the task to Thursday. |
| `->fridays();` | Limit the task to Friday. |
| `->saturdays();` | Limit the task to Saturday. |
| `->days(array\|mixed);` | Limit the task to specific days. |
| `->between($startTime, $endTime);` | Limit the task to run between start and end times. |
| `->unlessBetween($startTime, $endTime);` | Limit the task to not run between start and end times. |
| `->when(Closure);` | Limit the task based on a truth test. |
| `->environments($env);` | Limit the task to specific environments. |
|------------------------------------------|--------------------------------------------------------|
```
<a name="day-constraints"></a>
#### Day Constraints
The `days` method may be used to limit the execution of a task to specific days of the week. For example, you may
schedule a command to run hourly on Sundays and Wednesdays:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->hourly()
->days([0, 3]);
Alternatively, you may use the constants available on the `Illuminate\Console\Scheduling\Schedule` class when defining
the days on which a task should run:
use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;
Facades\Schedule::command('emails:send')
->hourly()
->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
<a name="between-time-constraints"></a>
#### Between Time Constraints
The `between` method may be used to limit the execution of a task based on the time of day:
Schedule::command('emails:send')
->hourly()
->between('7:00', '22:00');
Similarly, the `unlessBetween` method can be used to exclude the execution of a task for a period of time:
Schedule::command('emails:send')
->hourly()
->unlessBetween('23:00', '4:00');
<a name="truth-test-constraints"></a>
#### Truth Test Constraints
The `when` method may be used to limit the execution of a task based on the result of a given truth test. In other
words, if the given closure returns `true`, the task will execute as long as no other constraining conditions prevent
the task from running:
Schedule::command('emails:send')->daily()->when(function () {
return true;
});
The `skip` method may be seen as the inverse of `when`. If the `skip` method returns `true`, the scheduled task will not
be executed:
Schedule::command('emails:send')->daily()->skip(function () {
return true;
});
When using chained `when` methods, the scheduled command will only execute if all `when` conditions return `true`.
<a name="environment-constraints"></a>
#### Environment Constraints
The `environments` method may be used to execute tasks only on the given environments (as defined by the
`APP_ENV` [environment variable](/docs/{{version}}/configuration#environment-configuration)):
Schedule::command('emails:send')
->daily()
->environments(['staging', 'production']);
<a name="timezones"></a>
### Timezones
Using the `timezone` method, you may specify that a scheduled task's time should be interpreted within a given timezone:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->timezone('America/New_York')
->at('2:00')
If you are repeatedly assigning the same timezone to all of your scheduled tasks, you can specify which timezone should
be assigned to all schedules by defining a `schedule_timezone` option within your application's `app` configuration
file:
'timezone' => env('APP_TIMEZONE', 'UTC'),
'schedule_timezone' => 'America/Chicago',
> [!WARNING]
> Remember that some timezones utilize daylight savings time. When daylight saving time changes occur, your scheduled
> task may run twice or even not run at all. For this reason, we recommend avoiding timezone scheduling when possible.
<a name="preventing-task-overlaps"></a>
### Preventing Task Overlaps
By default, scheduled tasks will be run even if the previous instance of the task is still running. To prevent this, you
may use the `withoutOverlapping` method:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->withoutOverlapping();
In this example, the `emails:send` [Artisan command](/docs/{{version}}/artisan) will be run every minute if it is not
already running. The `withoutOverlapping` method is especially useful if you have tasks that vary drastically in their
execution time, preventing you from predicting exactly how long a given task will take.
If needed, you may specify how many minutes must pass before the "without overlapping" lock expires. By default, the
lock will expire after 24 hours:
Schedule::command('emails:send')->withoutOverlapping(10);
Behind the scenes, the `withoutOverlapping` method utilizes your application's [cache](/docs/{{version}}/cache) to
obtain locks. If necessary, you can clear these cache locks using the `schedule:clear-cache` Artisan command. This is
typically only necessary if a task becomes stuck due to an unexpected server problem.
<a name="running-tasks-on-one-server"></a>
### Running Tasks on One Server
> [!WARNING]
> To utilize this feature, your application must be using the `database`, `memcached`, `dynamodb`, or `redis` cache
> driver as your application's default cache driver. In addition, all servers must be communicating with the same
> central
> cache server.
If your application's scheduler is running on multiple servers, you may limit a scheduled job to only execute on a
single server. For instance, assume you have a scheduled task that generates a new report every Friday night. If the
task scheduler is running on three worker servers, the scheduled task will run on all three servers and generate the
report three times. Not good!
To indicate that the task should run on only one server, use the `onOneServer` method when defining the scheduled task.
The first server to obtain the task will secure an atomic lock on the job to prevent other servers from running the same
task at the same time:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
<a name="naming-unique-jobs"></a>
#### Naming Single Server Jobs
Sometimes you may need to schedule the same job to be dispatched with different parameters, while still instructing
Laravel to run each permutation of the job on a single server. To accomplish this, you may assign each schedule
definition a unique name via the `name` method:
```php
Schedule::job(new CheckUptime('https://laravel.com'))
->name('check_uptime:laravel.com')
->everyFiveMinutes()
->onOneServer();
Schedule::job(new CheckUptime('https://vapor.laravel.com'))
->name('check_uptime:vapor.laravel.com')
->everyFiveMinutes()
->onOneServer();
```
Similarly, scheduled closures must be assigned a name if they are intended to be run on one server:
```php
Schedule::call(fn () => User::resetApiRequestCount())
->name('reset-api-request-count')
->daily()
->onOneServer();
```
<a name="background-tasks"></a>
### Background Tasks
By default, multiple tasks scheduled at the same time will execute sequentially based on the order they are defined in
your `schedule` method. If you have long-running tasks, this may cause subsequent tasks to start much later than
anticipated. If you would like to run tasks in the background so that they may all run simultaneously, you may use the
`runInBackground` method:
use Illuminate\Support\Facades\Schedule;
Schedule::command('analytics:report')
->daily()
->runInBackground();
> [!WARNING]
> The `runInBackground` method may only be used when scheduling tasks via the `command` and `exec` methods.
<a name="maintenance-mode"></a>
### Maintenance Mode
Your application's scheduled tasks will not run when the application is
in [maintenance mode](/docs/{{version}}/configuration#maintenance-mode), since we don't want your tasks to interfere
with any unfinished maintenance you may be performing on your server. However, if you would like to force a task to run
even in maintenance mode, you may call the `evenInMaintenanceMode` method when defining the task:
Schedule::command('emails:send')->evenInMaintenanceMode();
<a name="schedule-groups"></a>
### Schedule Groups
When defining multiple scheduled tasks with similar configurations, you can use Laravel’s task grouping feature to avoid
repeating the same settings for each task. Grouping tasks simplifies your code and ensures consistency across related
tasks.
To create a group of scheduled tasks, invoke the desired task configuration methods, followed by the `group` method. The
`group` method accepts a closure that is responsible for defining the tasks that share the specified configuration:
```php
use Illuminate\Support\Facades\Schedule;
Schedule::daily()
->onOneServer()
->timezone('America/New_York')
->group(function () {
Schedule::command('emails:send --force');
Schedule::command('emails:prune');
});
```
<a name="running-the-scheduler"></a>
## Running the Scheduler
Now that we have learned how to define scheduled tasks, let's discuss how to actually run them on our server. The
`schedule:run` Artisan command will evaluate all of your scheduled tasks and determine if they need to run based on the
server's current time.
So, when using Laravel's scheduler, we only need to add a single cron configuration entry to our server that runs the
`schedule:run` command every minute. If you do not know how to add cron entries to your server, consider using a service
such as [Laravel Forge](https://forge.laravel.com) which can manage the cron entries for you:
```shell
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
```
<a name="sub-minute-scheduled-tasks"></a>
### Sub-Minute Scheduled Tasks
On most operating systems, cron jobs are limited to running a maximum of once per minute. However, Laravel's scheduler
allows you to schedule tasks to run at more frequent intervals, even as often as once per second:
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->everySecond();
When sub-minute tasks are defined within your application, the `schedule:run` command will continue running until the
end of the current minute instead of exiting immediately. This allows the command to invoke all required sub-minute
tasks throughout the minute.
Since sub-minute tasks that take longer than expected to run could delay the execution of later sub-minute tasks, it is
recommended that all sub-minute tasks dispatch queued jobs or background commands to handle the actual task processing:
use App\Jobs\DeleteRecentUsers;
Schedule::job(new DeleteRecentUsers)->everyTenSeconds();
Schedule::command('users:delete')->everyTenSeconds()->runInBackground();
<a name="interrupting-sub-minute-tasks"></a>
#### Interrupting Sub-Minute Tasks
As the `schedule:run` command runs for the entire minute of invocation when sub-minute tasks are defined, you may
sometimes need to interrupt the command when deploying your application. Otherwise, an instance of the `schedule:run`
command that is already running would continue using your application's previously deployed code until the current
minute ends.
To interrupt in-progress `schedule:run` invocations, you may add the `schedule:interrupt` command to your application's
deployment script. This command should be invoked after your application is finished deploying:
```shell
php artisan schedule:interrupt
```
<a name="running-the-scheduler-locally"></a>
### Running the Scheduler Locally
Typically, you would not add a scheduler cron entry to your local development machine. Instead, you may use the
`schedule:work` Artisan command. This command will run in the foreground and invoke the scheduler every minute until you
terminate the command. When sub-minute tasks are defined, the scheduler will continue running within each minute to
process those tasks:
```shell
php artisan schedule:work
```
<a name="task-output"></a>
## Task Output
The Laravel scheduler provides several convenient methods for working with the output generated by scheduled tasks.
First, using the `sendOutputTo` method, you may send the output to a file for later inspection:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->sendOutputTo($filePath);
If you would like to append the output to a given file, you may use the `appendOutputTo` method:
Schedule::command('emails:send')
->daily()
->appendOutputTo($filePath);
Using the `emailOutputTo` method, you may email the output to an email address of your choice. Before emailing the
output of a task, you should configure Laravel's [email services](/docs/{{version}}/mail):
Schedule::command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('taylor@example.com');
If you only want to email the output if the scheduled Artisan or system command terminates with a non-zero exit code,
use the `emailOutputOnFailure` method:
Schedule::command('report:generate')
->daily()
->emailOutputOnFailure('taylor@example.com');
> [!WARNING]
> The `emailOutputTo`, `emailOutputOnFailure`, `sendOutputTo`, and `appendOutputTo` methods are exclusive to the
`command` and `exec` methods.
<a name="task-hooks"></a>
## Task Hooks
Using the `before` and `after` methods, you may specify code to be executed before and after the scheduled task is
executed:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->before(function () {
// The task is about to execute...
})
->after(function () {
// The task has executed...
});
The `onSuccess` and `onFailure` methods allow you to specify code to be executed if the scheduled task succeeds or
fails. A failure indicates that the scheduled Artisan or system command terminated with a non-zero exit code:
Schedule::command('emails:send')
->daily()
->onSuccess(function () {
// The task succeeded...
})
->onFailure(function () {
// The task failed...
});
If output is available from your command, you may access it in your `after`, `onSuccess` or `onFailure` hooks by
type-hinting an `Illuminate\Support\Stringable` instance as the `$output` argument of your hook's closure definition:
use Illuminate\Support\Stringable;
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// The task succeeded...
})
->onFailure(function (Stringable $output) {
// The task failed...
});
<a name="pinging-urls"></a>
#### Pinging URLs
Using the `pingBefore` and `thenPing` methods, the scheduler can automatically ping a given URL before or after a task
is executed. This method is useful for notifying an external service, such as [Envoyer](https://envoyer.io), that your
scheduled task is beginning or has finished execution:
Schedule::command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);
The `pingOnSuccess` and `pingOnFailure` methods may be used to ping a given URL only if the task succeeds or fails. A
failure indicates that the scheduled Artisan or system command terminated with a non-zero exit code:
Schedule::command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);
The `pingBeforeIf`,`thenPingIf`,`pingOnSuccessIf`, and `pingOnFailureIf` methods may be used to ping a given URL only if
a given condition is `true`:
Schedule::command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
Schedule::command('emails:send')
->daily()
->pingOnSuccessIf($condition, $successUrl)
->pingOnFailureIf($condition, $failureUrl);
<a name="events"></a>
## Events
Laravel dispatches a variety of [events](/docs/{{version}}/events) during the scheduling process. You
may [define listeners](/docs/{{version}}/events) for any of the following events:
```
|-------------------------------------------------------------|
| Event Name |
|-------------------------------------------------------------|
| `Illuminate\Console\Events\ScheduledTaskStarting` |
| `Illuminate\Console\Events\ScheduledTaskFinished` |
| `Illuminate\Console\Events\ScheduledBackgroundTaskFinished` |
| `Illuminate\Console\Events\ScheduledTaskSkipped` |
| `Illuminate\Console\Events\ScheduledTaskFailed` |
|-------------------------------------------------------------|
```
+4
View File
@@ -0,0 +1,4 @@
### The Spares Module
This details the operation and key highlights and developer notes for the Maximo ( Spares) module
+8
View File
@@ -0,0 +1,8 @@
### SSHD Server
to restart the SSH Server on AS400 use in 5250 Console
```
STRTCPSVR *SSHD
```
+461
View File
@@ -0,0 +1,461 @@
# Stop Treating Your Blade Files Like Trash Bins. Give Them Contracts And Structure
Are you a Laravel developer? Do you love Blade? It’s simple, expressive, and fits naturally with the framework. But
let’s be honest: most projects eventually turn their Blade files into dumping grounds for random variables. The
Controllers pass loose data, views end up receiving unstructured data, partials rely on implicit assumptions, and the
whole setup becomes fragile and error-prone.
## The Result?
- No autocomplete.
- Runtime errors from typos.
- Partial views with zero contracts.
- A mess that only grows as the app scales.
It doesn’t have to be this way. Blade files deserve the same contracts and structure we give to APIs, database models,
or services. With a few clear practices, you can bring discipline, contracts, and structure to Blade.
This article will show you the complete approach:
1. The problem of passing data as arrays or variables in views — solved by introducing ViewModels as contracts.
2. Enable Autocomplete in Blade so your IDE actually helps.
3. Annotation to views and partials bound to their own ViewModels.
4. Lock down data structures with DTO classes for strict typing.
5. Establish Principles for sustainable Blade development.
**Remember, we are only targeting Blade for now.** Not Twig or Antlr.
Grab a coffee — this is a long read. But by the end, you’ll never look at Blade the same way again.
---
## The Crime Scene: What’s Wrong With “Array Soup” in Blade
Here’s the standard Laravel approach we’ve all seen in controllers:
```php
class ProductController extends Controller
{
public function index()
{
$products = Product::all();
$categories = Category::all();
$brand = Brand::all();
$title = 'Products';
return view('products.index', compact('products','categories','brands','title') );
}
}
```
Or maybe:
```php
return view('products.index', [
'title' => 'Products',
'products' => Product::all(),
'categories' => Category::all(),
'brand' => Brand::all(),
]);
```
Or a slightly cleaner:
```php
$data = [
'title' => 'Products',
'products' => Product::all(),
'categories' => Category::all(),
'brand' => Brand::all(),
];
return view('products.index', $data);
```
And here is how we do it for partials:
```blade
@foreach ($products as $product)
@include('products.partials.product')
@endforeach
// or pass some additional data
@foreach ($products as $product)
@include('products.partials.product', ['editable' => $editable])
@endforeach
```
And finally for components:
```blade
@foreach ($products as $product)
<x-product-row :name="$product->name" :price ="$product->price" />
@endforeach
```
### The Old Lovely Blade
Here’s how it works well:
```blade
<h1>{{ $title }}</h1>
<ul>
@foreach ($products as $product)
<li>{{ $product->name }} ({{ $product->price }})</li>
{{-- or partial --}}
@include('products.partials.product')
{{-- or component --}}
<x-product-row :name="$product->name" :price ="$product->price" />
@endforeach
</ul>
```
But ask yourself:
- Do you remember all the variables passed to your views?
- Do you get autocomplete in Blade to help you catch typos?
- What about partials and includes — are you sure the data they rely on is really there?
- If a teammate edits the controller, how do you even know the view won’t silently break?
- How many times have you opened a Blade file and had no clue what’s in scope?
---
## 1. Introducing ViewModels: Contracts for Your Views
Instead of throwing arrays around, define a ViewModel — a simple DTO class. It declares exactly what data is expected.
```php
namespace App\ViewModels;
class ProductsViewModel
{
public string $title;
public array $products;
public array $categories;
public array $brand;
}
```
And in your controller:
```php
class ProductController extends Controller
{
public function index()
{
$viewModel = new ProductsViewModel();
$viewModel->title = 'Products';
$viewModel->products = Product::all();
$viewModel->categories = Category::all();
$viewModel->brands = Brand::all();
return view('products.index', ['model' => $viewModel]);
}
}
```
> Suggestion: Always pass view‑model with name `model` for consistency. This will help in autocomplete.
Now the Blade file might look like:
```blade
<h1>{{ $model->title }}</h1>
<ul>
@foreach ($model->products as $product)
<li>{{ $product->name }} ({{ $product->price }})</li>
{{-- or partial --}}
@include('products.partials.product',['model' => $product])
{{-- or component --}}
<x-product-row :model="$product" />
@endforeach
</ul>
@include('partials.brands', ['model' => $model->brands])
@include('partials.categories', ['model' => $model->categories])
```
Again, the suggestion: for partials and components, always pass data with name `$model` for consistency to aid
autocomplete.
**Benefits:**
- The contract is explicit.
- Adding or removing data requires updating the ViewModel.
- Each view, partial, or component has a single data access point `$model`.
---
## 2. Installing Extension for Autocomplete
This does not require code setup, but you’ll need to install a plugin/extension in your IDE. The author uses VS Code and
recommends **PHP Tools by DevSense** (also available for other editors).
If you use a different editor, find a similar extension — support for Blade autocomplete is key.
---
## 3. Enabling Autocomplete in Blade
Even with ViewModels, Blade doesn’t automatically know the type of `$model`. The trick is to annotate it in Blade views,
partials, and components:
```blade
{{-- views/products/index.blade.php --}}
@php
/** @var \App\ViewModels\ProductsViewModel $model */
@endphp
<ul>
@foreach ($model->products as $product)
<li>{{ $product->name }} ({{ $product->price }})</li>
{{-- or partial --}}
@include('products.partials.product')
{{-- or component --}}
<x-product-row :name="$product->name" :price="$product->price" />
@endforeach
</ul>
```
In partials or components:
```blade
{{-- views/products/partials/product.blade.php --}}
@php
/** @var \App\Models\Product $model */
@endphp
<span>
{{ $model->name }}
</span>
```
And in components:
```blade
{{-- views/components/product.blade.php --}}
@php
/** @var \App\View\Components\Product $model */
@endphp
<span>
{{ $model->name }}
</span>
```
Now, in an IDE like VS Code, typing `$model->` will give you real autocomplete suggestions.
This is a game-changer:
- No more guessing property names.
- Mistyped variables get caught instantly.
- New developers know exactly what’s available in the view.
---
## 4. Enforce Types at Runtime
Since now we pass ViewModels into views, partials, or components and define annotations, let’s enforce Blade to accept
only specific types. The author adds:
1. **TypedViewFactory** – A custom view factory that parses `@var` declarations at the top of Blade files and enforces
them.
2. **TypedViewServiceProvider** – Wires everything into Laravel automatically.
If you declare a type in a Blade file, Laravel must pass the correct class, array, or collection; otherwise, it throws
an exception.
Example:
```blade
{{-- products/index.blade.php --}}
@php /** @var App\ViewModels\ProductsViewModel $model */ @endphp
<h1>{{ $model->title }}</h1>
```
Only `ProductsViewModel` works—anything else throws an exception.
Similarly for partials and components, the wrong type is no longer silently accepted; it fails loudly.
You can do the same for arrays and collections:
```blade
@php
/** @var App\Models\Product[] $model */
@endphp
{{-- or --}}
@php
/** @var \Illuminate\Support\Collection<App\Models\Product> $model */
@endphp
```
Wrong types cause errors rather than silent bugs.
---
## 5. Going Fully Typed With DTO Classes
So far, we’ve wrapped data in ViewModels. But what about the data itself? Passing raw Eloquent models to views can be
risky — they carry too much baggage and autocomplete may expose a jungle of methods and properties.
Let’s create DTO classes to define exactly what your views need. (Note: components themselves can act as DTOs, so
sometimes you don’t need separate DTOs.)
### BaseDTO
```php
namespace App\DTO;
use InvalidArgumentException;
abstract class BaseDTO
{
public function __construct(array $data = [])
{
foreach ($data as $key => $value) {
if (!property_exists($this, $key)) {
throw new InvalidArgumentException(
"Property '{$key}' is not defined in " . static::class
);
}
$this->$key = $value;
}
}
public function __get($name)
{
throw new InvalidArgumentException(
"Tried to access undefined property '{$name}' on " . static::class
);
}
public function __set($name, $value)
{
throw new InvalidArgumentException(
"Tried to set undefined property '{$name}' on " . static::class
);
}
public static function columns(): array
{
return array_map(
fn($prop) => $prop->getName(),
(new \ReflectionClass(static::class))->getProperties()
);
}
}
```
### SimpleProduct DTO
```php
namespace App\DTO\Product;
use App\DTO\BaseDTO;
class SimpleProduct extends BaseDTO
{
public string $name;
public string $image;
public float $price;
public int $stock;
}
```
In the controller:
```php
class ProductController extends Controller
{
public function index()
{
$viewModel = new ProductsViewModel();
$viewModel->products = Product::select(SimpleProduct::columns())->get();
// other assignments...
return view('products.index', ['model' => $viewModel]);
}
}
```
In the partial:
```blade
{{-- views/products/partials/product.blade.php --}}
@php
/** @var \App\DTO\Product\SimpleProduct $model */
@endphp
<span>
{{ $model->name }}
</span>
```
Autocomplete now shows only DTO properties.
#### Caveats & Tips
- If you only select `DTO::columns()` in your query and still pass raw models to Blade, IDE autocomplete works via
annotations—but at runtime Blade can still access everything.
- For stricter discipline, wrap results into DTO instances.
- Schema Drift Protection: if a DTO includes a property not in the table, MySQL will error, keeping your DTOs in sync.
- You lose Eloquent helpers, relations, and accessors. Do transformations before mapping to DTOs.
- Mapping overhead is minimal: a base DTO + mapper makes it clean and efficient.
Example mapper:
```php
class DTOMapper
{
public static function map(object $source, string $dtoClass): object
{
$dtoReflection = new ReflectionClass($dtoClass);
$properties = $dtoReflection->getProperties();
$args = [];
foreach ($properties as $property) {
$name = $property->getName();
if (isset($source->$name)) {
$args[$name] = $source->$name;
}
}
return new $dtoClass(...$args);
}
}
```
Usage:
```php
$dto = DTOMapper::map($product, SimpleProduct::class);
$dtos = $products->map(fn($p) => DTOMapper::map($p, ProductDTO::class));
```
---
## Principles of Structured Blade
This approach holds only if you enforce ground rules:
1. **No untyped arrays in views** — every view receives a single typed contract.
2. **One ViewModel per Blade file** — keep mental load low.
3. **Partials and components need contracts too** — no freeloaders.
4. **Always annotate with `@var`** — autocomplete is non‑negotiable.
5. **Always pass data as `model` for consistency** — every blade view has a single entry point.
6. **Business logic stays out of Blade views** — data shaping and conditions belong in ViewModels.
7. **Use DTOs to avoid Eloquent jungle** — views see pure, predictable DTOs.
8. **Strict access, fail loudly** — undefined properties in ViewModel should throw, not silently fail.
---
## Final Word
Blade isn’t broken. The way most developers use it is.
Treating Blade files as unstructured trash bins creates brittle, unmaintainable code. Adding ViewModels, autocomplete,
contracts for partials, and DTOs gives your views the same level of professionalism as the rest of your codebase.
This isn’t overkill. It’s discipline. And discipline is what separates fragile projects from sustainable ones.
So stop passing arrays. Start passing contracts. Give your Blade files the structure they deserve.
---
@@ -29,9 +29,24 @@ final class DocumentationServiceProvider extends ServiceProvider
public function boot(): void
{
$this->registerViews();
$this->registerCommands();
$this->loadMigrationsFrom(module_path('Documentation', 'Database/Migrations'));
}
/**
* Register module console commands.
*/
private function registerCommands(): void
{
if (! $this->app->runningInConsole()) {
return;
}
$this->commands([
\Modules\Documentation\Console\ImportDocumentationCommand::class,
]);
}
/**
* Register module views and anonymous components.
*/
+61
View File
@@ -622,3 +622,64 @@ html.dark .doc-content code {
border-radius: 1.375rem;
}
}
/* Documentation reading experience */
.doc-card-body:has(.doc-content) {
padding: clamp(1.5rem, 3vw, 3rem);
}
.doc-content {
max-width: 68rem;
margin: 0 auto;
color: var(--doc-text);
font-size: 1.0625rem;
line-height: 1.8;
overflow-wrap: anywhere;
}
.doc-content > :first-child { margin-top: 0; }
.doc-content > :last-child { margin-bottom: 0; }
.doc-content h1 { font-size: clamp(2rem, 4vw, 2.75rem); margin-top: 0; margin-bottom: 1.25rem; }
.doc-content h2 { font-size: clamp(1.45rem, 2.5vw, 1.8rem); margin-top: 2.4rem; margin-bottom: 1rem; padding-bottom: .55rem; }
.doc-content h3 { font-size: 1.3rem; margin-top: 2rem; margin-bottom: .75rem; }
.doc-content h4 { font-size: 1.1rem; margin: 1.75rem 0 .65rem; }
.doc-content p { margin: 0 0 1.2rem; }
.doc-content ul,
.doc-content ol { margin: 0 0 1.35rem 1.5rem; padding-left: .75rem; }
.doc-content li { margin: .4rem 0; padding-left: .25rem; }
.doc-content li::marker { color: #60a5fa; font-weight: 700; }
.doc-content a { color: #60a5fa; text-decoration: underline; text-decoration-color: rgba(96,165,250,.4); text-underline-offset: .18em; }
.doc-content a:hover { color: #93c5fd; text-decoration-color: currentColor; }
.doc-content pre { margin: 1.4rem 0 1.65rem; border: 1px solid #263244; background: #0b1220; padding: 1.15rem 1.25rem; box-shadow: inset 0 1px 0 rgba(255,255,255,.03); }
.doc-content pre code { font-size: .925rem; line-height: 1.65; white-space: pre; }
.doc-content :not(pre) > code { border: 1px solid rgba(148,163,184,.18); background: rgba(30,41,59,.85); color: #e2e8f0; }
.doc-content blockquote { margin: 1.5rem 0; border-left-width: 3px; border-radius: 0 .6rem .6rem 0; background: rgba(37,99,235,.07); padding: .9rem 1.1rem; }
.doc-content table { display: block; overflow-x: auto; margin: 1.5rem 0; border-radius: .75rem; }
.doc-content th { background: rgba(255,255,255,.035); font-weight: 700; }
.doc-content hr { margin: 2.25rem 0; border: 0; border-top: 1px solid var(--doc-line); }
/* Pagination: never use Laravel's light/white button treatment in the dark Helpdesk. */
.doc-pagination { margin-top: 1.125rem; }
.doc-pagination nav > div:first-child { display: none; }
.doc-pagination nav > div:last-child { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.doc-pagination nav > div:last-child > div:first-child { color: var(--doc-muted); }
.doc-pagination nav span[aria-current="page"] > span,
.doc-pagination nav a,
.doc-pagination nav span[aria-disabled="true"] > span {
background: #171a1f !important;
border-color: #343a46 !important;
color: #d6dae2 !important;
box-shadow: none !important;
}
.doc-pagination nav a:hover {
background: #232831 !important;
border-color: #4b5563 !important;
color: #fff !important;
}
.doc-pagination nav span[aria-current="page"] > span {
background: #2563eb !important;
border-color: #2563eb !important;
color: #fff !important;
}
.doc-pagination nav span[aria-disabled="true"] > span { color: #697180 !important; cursor: not-allowed; }
.doc-pagination nav svg { width: 1rem; height: 1rem; }
+1 -1
View File
@@ -115,7 +115,7 @@
@endforelse
</tbody>
</table>
<div style="margin-top:18px;">{{ $documents->links() }}</div>
<div class="doc-pagination">{{ $documents->links() }}</div>
</div>
</section>
</div>
@@ -144,10 +144,40 @@
@if(Route::has('helpdesk.settings'))<a class="hd-nav-link hd-nav-link--settings {{ request()->routeIs('helpdesk.settings*') ? 'is-active' : '' }}" href="{{ route('helpdesk.settings') }}" wire:navigate><i class="fa-solid fa-gear" aria-hidden="true"></i><span>Settings</span></a>@endif
</div>
@if(Route::has('helpdesk.legacy.tickets'))
<div class="hd-nav-section"><div class="hd-nav-heading">Legacy System</div>
<a class="hd-nav-link {{ request()->routeIs('helpdesk.legacy.tickets*') ? 'is-active' : '' }}" style="--hd-nav-accent:#64748b;--hd-nav-accent-rgb:100,116,139" href="{{ route('helpdesk.legacy.tickets') }}" wire:navigate><i class="fa-solid fa-clock-rotate-left" aria-hidden="true"></i><span>Legacy Tickets</span></a>
<a class="hd-nav-link {{ request()->routeIs('helpdesk.legacy.categories') ? 'is-active' : '' }}" style="--hd-nav-accent:#64748b;--hd-nav-accent-rgb:100,116,139" href="{{ route('helpdesk.legacy.categories') }}" wire:navigate><i class="fa-solid fa-folder-tree" aria-hidden="true"></i><span>Legacy Categories</span></a>
</div>
<div class="hd-nav-section">
<div class="hd-nav-heading">Legacy System</div>
<a
class="hd-nav-link {{ request()->routeIs('helpdesk.legacy.tickets*') ? 'is-active' : '' }}"
style="--hd-nav-accent:#64748b;--hd-nav-accent-rgb:100,116,139"
href="{{ route('helpdesk.legacy.tickets') }}"
wire:navigate
>
<i class="fa-solid fa-clock-rotate-left" aria-hidden="true"></i>
<span>Legacy Tickets</span>
</a>
<a
class="hd-nav-link {{ request()->routeIs('helpdesk.legacy.categories') ? 'is-active' : '' }}"
style="--hd-nav-accent:#64748b;--hd-nav-accent-rgb:100,116,139"
href="{{ route('helpdesk.legacy.categories') }}"
wire:navigate
>
<i class="fa-solid fa-folder-tree" aria-hidden="true"></i>
<span>Legacy Categories</span>
</a>
<a
class="hd-nav-link"
style="--hd-nav-accent:#64748b;--hd-nav-accent-rgb:100,116,139"
href="https://helpdesk.old.mpba.dev"
target="_blank"
rel="noopener noreferrer"
>
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
<span>Old Helpdesk</span>
</a>
</div>
@endif
@if(Route::has('documentation.index') || Route::has('documentation.documents.index'))
<div class="hd-nav-section"><div class="hd-nav-heading">Documentation</div>
+8
View File
@@ -378,3 +378,11 @@ promiseReactionJob@[native code] {"url":"https://helpdesk.vm/dashboard","user_ag
[2026-09-28 09:42:15] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/admin/helpdesk/legacy/tickets","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:42:15.037Z"}
[2026-09-28 09:44:20] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/admin/helpdesk/legacy/tickets","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:44:20.199Z"}
[2026-09-28 09:45:34] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/admin/helpdesk/legacy/tickets?page=18","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:45:34.462Z"}
[2026-09-28 09:48:44] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/documentation/documents","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:48:44.002Z"}
[2026-09-28 09:54:09] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/documentation/documents","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:54:09.118Z"}
[2026-09-28 09:54:15] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/documentation/documents/2","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:54:15.384Z"}
[2026-09-28 09:54:33] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/documentation/documents/3","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:54:33.504Z"}
[2026-09-28 09:57:10] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/documentation/categories","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T09:57:10.048Z"}
[2026-09-28 10:09:51] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/documentation/documents/7","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T10:09:50.895Z"}
[2026-09-28 10:13:18] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/admin/helpdesk/legacy/tickets","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T10:13:18.190Z"}
[2026-09-28 10:26:09] local.INFO: Helpdesk module assets loaded {"url":"https://helpdesk.vm/admin/helpdesk/legacy/tickets","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0","timestamp":"2026-09-28T10:26:09.685Z"}