longman/laravel-lodash
Laravel Lodash
This package adds lot of useful functionality to the Laravel >=13.0 project.
Compatibility
The package major does not track the Laravel major. It did for some releases (lodash 11 targets Laravel 11, lodash 13 targets Laravel 13), but a package major is cut for this package's own breaking changes, so lodash 14 and 15 both target Laravel 13. Read the table rather than inferring from the version number.
| laravel-lodash | Laravel framework | PHP |
|---|---|---|
^15.0 |
^13.0 |
^8.4 |
^14.0 |
^13.0 |
^8.4 |
^13.0 |
^13.0 |
^8.4 |
^12.0 |
^11.0 or ^12.0 |
^8.2 or ^8.4 |
^11.0 |
^11.0 |
^8.2 |
^10.0 |
^10.0 |
^8.2 |
^9.0 |
^10.0 |
^8.1 |
^8.0 |
^8.0 or ^9.0 |
^8.1 |
^7.0 |
^9.0 |
^8.1 |
^6.0 |
^8.0 |
^8.0 |
^5.0 |
^8.0 |
^8.0 |
^4.0 |
^8.0 |
^7.4 |
^3.0 |
^7.3 |
^7.3 |
^2.0 |
^6.3 |
^7.2 |
^1.0 |
5.5 to 5.8 |
>=7.1.3 |
The 12.x line moved framework support mid-major: 12.0 targets Laravel 11, 12.1 accepts Laravel 11 or 12, and 12.2 requires Laravel 12 and PHP 8.4. Prefer ^12.2 when targeting Laravel 12.
If your application is still on an older Laravel major, install the matching tag of laravel-lodash from the table above.
Table of Contents
- Installation
- Usage
- General
- Enable Debug Mode depending on visitor's IP Address
- Add created_by, updated_by and deleted_by to the eloquent models
- Use UUID in the Eloquent Models
- Eager loading of limited many to many relations via subquery or union
- Redis using igbinary
- Redis client side sharding
- AWS SQS Fifo Queue
- Elasticsearch Integration
- Check if installed packages are in sync with composer.lock
- Helper Functions
- Extended Classes
- Artisan Commands
- Middleware List
- Misc
- Testing Helpers
- General
- TODO
- Troubleshooting
- Contributing
- License
- Credits
Installation
Install this package through Composer.
Run a command in your command line:
composer require longman/laravel-lodash
Longman\LaravelLodash\ServiceProvider is registered automatically through Laravel's package discovery, so there is no provider to add by hand.
The opt-in feature providers are a separate matter. Cache, Redis, Queue, Elasticsearch and Debug are deliberately not auto-discovered, because each one replaces or extends a Laravel provider. Register those explicitly, as documented in the feature section that covers each.
Copy the package config and translations to your application with the publish command:
php artisan vendor:publish --provider="Longman\LaravelLodash\ServiceProvider"
Usage
General
Enable Debug Mode depending on visitor's IP Address
Add Longman\LaravelLodash\Debug\DebugServiceProvider::class in to config/app.php
and specify debug IP's in your config/lodash.php config file:
. . .
'debug' => [
'ips' => [ // IP list for enabling debug mode
//'127.0.0.1',
],
],
. . .
Add created_by, updated_by and deleted_by to the eloquent models
Sometimes we need to know who created, updated or deleted entry in the database.
For this just add Longman\LaravelLodash\Eloquent\UserIdentities trait to your model and also
update migration file adding necessary columns:
$table->unsignedInteger('created_by')->nullable();
$table->unsignedInteger('updated_by')->nullable();
$table->unsignedInteger('deleted_by')->nullable();
Use UUID in the Eloquent Models
For this just add Longman\LaravelLodash\Eloquent\UuidAsPrimary trait to your model and also
update related migration file:
$table->uuid('id')->primary();
Also there is possible to specify uuid version via defining uuidVersion property in the model class.
Eager loading of limited many to many relations via subquery or union
Eager load many to many relations with limit via subquery or union.
For using this feature, add Longman\LaravelLodash\Eloquent\ManyToManyPreload trait to the models.
After that you can use methods limitPerGroupViaUnion() and limitPerGroupViaSubQuery().
For example you want to select users and 3 related user photos per user.
$items = (new User)->with([
'photos' => function (BelongsToMany $builder) {
// Select via union. There you should pass pivot table fields array
$builder->limitPerGroupViaUnion(3, ['user_id', 'photo_id']);
// or
// Select via subquery
$builder->limitPerGroupViaSubQuery(3);
}, 'other.relation1', 'other.relation2'
]);
$items = $items->get();
Now each user model have 3 photos model selected via one query. You can specify additional where clauses or order by fields before the group method call.
Redis using igbinary
Igbinary is a drop in replacement for the standard php serializer. Igbinary stores php data structures in compact binary form. Savings are significant when using Redis or similar memory based storages for serialized data. Via Igbinary repetitive strings are stored only once. Collections of Eloquent objects benefit significantly from this.
By default Laravel does not provide an option to enable igbinary serializer for PhpRedis connection and you have to use LaravelLodash implementation for this.
First of all, make sure you enabled PhpRedis driver by this guide https://laravel.com/docs/5.5/redis#phpredis
After that include Cache and Redis service providers in the app.php before your App providers:
. . .
Longman\LaravelLodash\Cache\CacheServiceProvider::class,
Longman\LaravelLodash\Redis\RedisServiceProvider::class,
. . .
You can remove Laravel's Cache and Redis service providers from the config, because LaravelLodash providers are extended from them and therefore implements entire functional.
Now you can specify the serializer in your database.php under config folder:
Also, you can specify other options like scan or etc. See https://github.com/phpredis/phpredis#setoption
Redis client side sharding
PhpRedis extension along with native Redis Cluster, also supports client-side sharding. This feature is very useful, when you want distribute your data between multiple servers, but do not want use native Redis Cluster.
Its not implemented in the Laravel by default. We tried to fix this :smile:
Config example:
. . .
'redis' => [
'client' => 'phpredis',
'clusters' => [
'options' => [
'lazy_connect' => true,
'connect_timeout' => 1,
'read_timeout' => 3,
'password' => env('REDIS_PASSWORD', null),
'database' => env('REDIS_DATABASE', 0),
'prefix' => env('REDIS_PREFIX'),
'serializer' => Redis::SERIALIZER_IGBINARY,
'compression' => Redis::COMPRESSION_ZSTD,
'compression_level' => Redis::COMPRESSION_ZSTD_DEFAULT,
],
'default' => [
[
'host' => env('REDIS_SHARD1_HOST', '127.0.0.1'),
'port' => env('REDIS_SHARD1_PORT', 6379),
],
[
'host' => env('REDIS_SHARD2_HOST', '127.0.0.2'),
'port' => env('REDIS_SHARD2_PORT', 6379),
],
. . .
],
],
],
. . .
AWS SQS Fifo Queue
Laravel by default does not supports AWS FIFO queues and this package fixes it.
You have to add QueueServiceProvider service provider in the app.php before your App providers:
. . .
Longman\LaravelLodash\Queue\QueueServiceProvider::class,
. . .
You can remove Laravel's Queue service provider from the config, because LaravelLodash provider are extended from that and therefore implements entire functional.
Now you can add the new connection in the queue.php under config folder:
. . .
'sqs_fifo' => [
'driver' => 'sqs.fifo',
'version' => 'latest',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('AWS_SQS_URL'),
'queue' => env('AWS_SQS_DEFAULT_QUEUE'),
'region' => env('AWS_DEFAULT_REGION'),
'options' => [
'type' => 'fifo', // fifo, normal
'polling' => 'long', // long, short
'wait_time' => 20,
],
],
. . .
Elasticsearch Integration
First of all you have to install official elasticsearch php sdk:
composer require elasticsearch/elasticsearch
After add ElasticsearchServiceProvider service provider in the app.php before your App providers:
. . .
Longman\LaravelLodash\Elasticsearch\ElasticsearchServiceProvider::class,
. . .
Now you can add the configuration in the services.php under config folder:
. . .
'elasticsearch' => [
'enabled' => env('ELASTICSEARCH_ENABLED', false),
'log_channel' => ['daily'],
'hosts' => [
[
'host' => env('ELASTICSEARCH_HOST', 'localhost'),
'port' => env('ELASTICSEARCH_PORT', 9200),
],
],
'connectionParams' => [
'client' => [
'timeout' => env('ELASTICSEARCH_TIMEOUT', 3),
'connect_timeout' => env('ELASTICSEARCH_TIMEOUT', 3),
],
],
],
. . .
You can use Elasticsearch integration via
$elasticsearch_manager = app(ElasticsearchManagerContract::class);
// Call wrapped methods
$elasticsearch_manager->createIndex('some-index');
// Or get native client and access their methods
$client = $elasticsearch_manager->getClient();
$client->indices()->create($params);
Also you can perform search via searchable query object. Just create class and
implement ElasticsearchQueryContract and you can pass object to performSearch method
$elasticsearch_manager = app(ElasticsearchManagerContract::class);
$results = $elasticsearch_manager->performSearch($query);
Check if installed packages are in sync with composer.lock
For development purposes, it is recommended to check if vendor folder is in sync with composer.lock file.
For this, in composer.json you have to add script ComposerScripts::createPackageHash:
. . .
"post-autoload-dump": [
"Longman\\LaravelLodash\\Composer\\ComposerScripts::createPackageHash",
. . .
],
. . .
And in the AppServiceProvider::boot add these lines:
. . .
if (config('app.debug')) {
$checker = new ComposerChecker(base_path());
$checker->checkHash();
}
. . .
Helper Functions
| Function | Description |
|---|---|
p(...$values): void |
Add debug messages to the debugbar |
get_db_query(): ?string |
Get last executed database query |
get_db_queries(): ?array |
Get all executed database queries |
Extended Classes
The service provider is auto-discovered, but these macros are off by default. Publish the config and set register.request_macros to true in config/lodash.php to enable them.
There is an extended classes via Laravel's builtin macros functionality
Request class
| Method | Description |
|---|---|
getInt(string $name, int $default = 0): int |
Return request field value as a integer |
getBool(string $name, bool $default = false): bool |
Return request field value as a boolean |
getFloat(string $name, float $default = 0): float |
Return request field value as a float |
getString(string $name, string $default = ''): string |
Return request field value as a string |
API Resources and Relationships
Longman\LaravelLodash\Http\Resources\JsonResource exposes each relationship as an include<Name>() method and renders only the relationships the client requested.
A populated relationship carries its own data wrapper, one level deeper than the relationship key:
{
"data": {
"id": "1",
"type": "User",
"attributes": {"name": "Alice"},
"relationships": {
"avatar": {"data": {"id": "9", "type": "Avatar", "attributes": {"url": "..."}}}
}
}
}
A requested relationship that resolves to nothing renders as null at the relationship key itself, one level shallower:
{
"data": {
"id": "1",
"type": "User",
"attributes": {"name": "Alice"},
"relationships": {"avatar": null}
}
}
The key is always present once the client asked for the relationship, so an absent key means only that the relationship was not requested. Both ways of writing an empty relationship produce the same output:
public function includeAvatar(): ?AvatarResource
{
return $this->resource->avatar ? new AvatarResource($this->resource->avatar) : null;
}
public function includeAvatar(): AvatarResource
{
return new AvatarResource($this->resource->avatar);
}
Requesting a relationship with no matching include<Name>() method throws, so a misspelled include stays loud rather than silently resolving to an empty value.
Include paths compose as a set, not an ordered list of instructions. A parent and a path nested under it may both be requested, in either order, and both are honoured; a repeated path is harmless; and the rendered tree is the union of every path requested. That makes the usual controller pattern safe:
private const array DEFAULT_INCLUDES = ['criteria', 'criteria.courseComponent'];
$includes = array_merge(self::DEFAULT_INCLUDES, $request->getValidatedIncludes());
A client sending ?include=criteria yields ['criteria', 'criteria.courseComponent', 'criteria'], and the nested courseComponent still renders. The order in which keys appear inside the relationships object is not part of the contract, so compare payloads by structure rather than byte-for-byte across differently-ordered include lists.
Artisan Commands
The service provider is auto-discovered and register.commands defaults to true, so these commands are available out of the box. Set it to false in config/lodash.php to leave them unregistered.
| Command | Description |
|---|---|
php artisan clear-all |
Clear entire cache and all cached routes, views, etc. |
php artisan db:clear |
Drop all tables from database. Options:--database= : The database connection to use.--force : Force the operation to run when in production.--pretend : Dump the SQL queries that would be run. |
php artisan db:dump |
Dump database to sql file using mysqldump CLI utility. Options:--database= : The database connection to use.--path= : Folder path for store database dump files. |
php artisan db:restore {file} |
Restore database from sql file using mysqldump CLI utility. Options:--database= : The database connection to use.--force : Force the operation to run when in production |
php artisan log:clear |
Clear log files from storage/logs recursively. Options:--force : Force the operation to run when in production. |
php artisan user:add {email} {password?} |
Create a new user. Options:--guard= : The guard to use. |
php artisan user:password {email} {password?} |
Update/reset user password. Options:--guard= : The guard to use. |
Middleware List
XssSecurity
Sets XSS Security headers. Can be configured excluded URI-s, etc in the config/lodash.php .
SimpleBasicAuth
Add simple basic auth to a route.
In the config/auth.php you have to add:
. . .
'simple' => [
'enabled' => env('SIMPLE_AUTH_ENABLED', true),
'user' => env('SIMPLE_AUTH_USER', 'user'),
'password' => env('SIMPLE_AUTH_PASS', 'secret'),
],
. . .
Misc
SelfDiagnosis Checks
For using this checks, you have to install the package: laravel-self-diagnosis
Available Disk Space Check
...
\Longman\LaravelLodash\SelfDiagnosis\Checks\AvailableDiskSpace::class => [
'paths' => [
'/' => '100G', // At least 100G should be available for the path "/"
'/var/www' => '5G',
],
],
...
Filesystem Disks Are Available
...
\Longman\LaravelLodash\SelfDiagnosis\Checks\FilesystemsAreAvailable::class => [
'disks' => [
'local',
's3',
],
],
...
Elasticsearch Health Check
...
\Longman\LaravelLodash\SelfDiagnosis\Checks\ElasticsearchCanBeAccessed::class => [
'client' => ElasticSearchClient::class,
],
...
Php Ini Options Check
...
\Longman\LaravelLodash\SelfDiagnosis\Checks\PhpIniOptions::class => [
'options' => [
'upload_max_filesize' => '>=128M',
'post_max_size' => '>=128M',
'memory_limit' => '>=128M',
'max_input_vars' => '>=10000',
'file_uploads' => '1',
'disable_functions' => '',
],
],
...
Php Ini Options Check
...
\Longman\LaravelLodash\SelfDiagnosis\Checks\RedisCanBeAccessed::class => [
'default_connection' => true,
'connections' => ['sessions'],
],
...
Servers Are Pingable Check
...
\Longman\LaravelLodash\SelfDiagnosis\Checks\ServersArePingable::class => [
'servers' => [
[
'host' => config('app.url'),
'port' => 80,
'timeout' => 1,
],
[
'host' => 'sqs.eu-west-1.amazonaws.com',
'port' => 443,
'timeout' => 3,
],
[
'host' => 'www.googleapis.com',
'port' => 443,
'timeout' => 3,
],
],
],
...
Horizon is running
...
\Longman\LaravelLodash\SelfDiagnosis\Checks\HorizonIsRunning::class,
...
Testing Helpers
The Longman\LaravelLodash\Testing\Response class extends Laravel's TestResponse with assertions for JSON envelopes of the shape {status, message, data, meta}. Wire it into your base test case by overriding createTestResponse():
use Longman\LaravelLodash\Testing\Response;
protected function createTestResponse($response, $request)
{
return Response::fromBaseResponse($response, $request);
}
Register your application's envelope shapes once (for example in the base test case's setUp()):
Response::setSuccessResponseStructure(['status', 'message', 'data']);
Response::setErrorResponseStructure(['status', 'message']);
Structure assertions validate data as a single item or as a collection. Collection assertions validate every row and fail with readable messages when data is missing, empty, or not a list. With exact: true any missing or unexpected extra key inside data (and inside meta.pagination when pager or cursor meta is included) fails the test, while envelope keys outside those subtrees stay loosely checked:
$response->assertJsonDataItemStructure(['id', 'type', 'attributes' => ['name']], exact: true);
$response->assertJsonDataCollectionStructure($structure, includePagerMeta: true, exact: true);
Reusable structures live in a DataStructuresProvider subclass, one static property per structure:
use Longman\LaravelLodash\Testing\DataStructuresProvider;
class AppStructures extends DataStructuresProvider
{
protected static array $userStructure = ['id', 'type', 'attributes' => ['name']];
protected static array $roleStructure = ['id', 'type', 'attributes' => ['title']];
}
$structure = AppStructures::getUserStructure(['roles[]']);
Relation includes accept dots for linear chains and nested arrays for branching. Each segment is name or name[] (collection), optionally followed by :StructureName when the structure name differs from the relation key. Overlapping declarations merge, regardless of order:
AppStructures::getUserStructure([
'program:AdminProgram' => [
'faculty:AdminFaculty',
'department:AdminDepartment',
],
'roles[]',
'roles[].admins[].item',
]);
The legacy '[roles]' wrapper syntax is not supported; declarations like it throw an InvalidArgumentException with a migration hint (write 'roles[]' instead).
A single relation that may render as null is declared with a ? suffix. One structure then covers both the populated and the empty case, under exact and non-exact matching, while a missing relationship key still fails with a message naming the relation:
AppStructures::getUserStructure(['avatar?']); // null or the populated subtree
AppStructures::getUserStructure(['avatar?:AdminAvatar']); // with an explicit structure name
AppStructures::getUserStructure(['accounts?.degree']); // nullable parent in a chain
The marker cannot be combined with the collection marker ('roles[]?' and 'roles?[]' both throw), because a relationship collection never renders as null. Structures containing a ? marker must be asserted through Testing\Response, which resolves the marker against the actual payload; passing one to Laravel's raw assertJsonStructure() would assert against a key that does not exist in the response. Under a collection relation the rows must agree: the marker resolves to the empty form only when every observed row has that relationship null.
The combined resource assertions tie a response to the exact records it should contain. Register the provider once, then a single call checks the structure (exact by default), the wire type (defaults to the structure name, override with type:), and the record identity. Expected ids derive from (string) $model->getKey(), or getUidString() for UUID-primary models:
Response::setDataStructuresProvider(AppStructures::class);
$response->assertJsonDataResource('User', $user, relations: ['roles[]']);
$response->assertJsonDataResources('User', $users); // exactly these ids, any order
$response->assertJsonDataResources('User', $users, ordered: true); // and in this sequence
$response->assertJsonDataResources('User', []); // proves the response is empty
On paginated endpoints the id comparison covers the current page. Resources with conditional fields can opt out of exactness with exact: false.
TODO
write more tests and add more features
Troubleshooting
If you like living on the edge, please report any bugs you find on the laravel-lodash issues page.
Contributing
Pull requests are welcome. See CONTRIBUTING.md for information.
License
Please see the LICENSE included in this repository for a full copy of the MIT license, which this project is licensed under.
Credits
Full credit list in Contributors
Related Packages
Custom SQS connector for Laravel that supports custom format JSON
Run Laravel (or Lumen) tasks and queue listeners inside of AWS Elastic Beanstalk...