[增添]添加了datasource的setting数据库以及默认值

This commit is contained in:
makotocc0107
2024-08-27 09:57:44 +08:00
parent d111dfaea4
commit 72eb990970
10955 changed files with 978898 additions and 0 deletions

15
vendor/spatie/color/.editorconfig vendored Normal file
View File

@@ -0,0 +1,15 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: spatie

View File

@@ -0,0 +1,24 @@
name: Check & fix styling
on: [push]
jobs:
php-cs-fixer:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- name: Run PHP CS Fixer
uses: docker://oskarstark/php-cs-fixer-ga
with:
args: --config=.php_cs.dist.php --allow-risky=yes
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Fix styling

View File

@@ -0,0 +1,36 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.2, 8.1, 8.0, 7.4, 7.3]
stability: [prefer-lowest, prefer-stable]
name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
coverage: none
- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install dependencies
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/pest

View File

@@ -0,0 +1,28 @@
name: "Update Changelog"
on:
release:
types: [released]
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: main
- name: Update Changelog
uses: stefanzweifel/changelog-updater-action@v1
with:
latest-version: ${{ github.event.release.name }}
release-notes: ${{ github.event.release.body }}
- name: Commit updated CHANGELOG
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: main
commit_message: Update CHANGELOG
file_pattern: CHANGELOG.md

40
vendor/spatie/color/.php_cs.dist.php vendored Normal file
View File

@@ -0,0 +1,40 @@
<?php
$finder = Symfony\Component\Finder\Finder::create()
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => [
'elements' => [
'method' => 'one',
],
],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
])
->setFinder($finder);

87
vendor/spatie/color/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,87 @@
# Changelog
All notable changes to `color` will be documented in this file
## 1.5.2 - 2022-06-24
### What's Changed
- Add RegEx fix to Validate.php too... by @jcogs-design in https://github.com/spatie/color/pull/73
- Fix typo in Distance::CIE76. by @Angel5a in https://github.com/spatie/color/pull/76
### New Contributors
- @jcogs-design made their first contribution in https://github.com/spatie/color/pull/73
- @Angel5a made their first contribution in https://github.com/spatie/color/pull/76
**Full Changelog**: https://github.com/spatie/color/compare/1.5.1...1.5.2
## 1.5.1 - 2022-04-12
## What's Changed
- Fix rgba opacity by @AstroCorp in https://github.com/spatie/color/pull/67
## New Contributors
- @AstroCorp made their first contribution in https://github.com/spatie/color/pull/67
**Full Changelog**: https://github.com/spatie/color/compare/1.5.0...1.5.1
## 1.4.0 - 2022-01-05
- Added support for PHP 8
- Added support for CMYK & HSB
- Added support for HEX alpha channel
- Added support for 3-digit HEX values
## 1.3.1 - 2021-09-09
- Fix HEX/HSL conversion bug
## 1.3.0 - 2021-09-06
- Added CIELab and XYZ color formats and `Distance` API
- Added `Contrast` API
## 1.2.4 - 2021-02-18
- Fixed division by zero error on pure white/black convertions ([#42](https://github.com/spatie/color/pull/42))
## 1.2.3 - 2020-12-10
- Added support for PHP 8
## 1.2.2 - 2020-11-18
- Fix transform RGB value to HSL : division by zero (#38)
## 1.2.1 - 2020-07-17
- HSL to RGB fixes
## 1.2.0 - 2020-06-22
- Added HSL & HSLA support
## 1.1.1 - 2017-02-03
- Fixed validation when a color contained redundant characters at the beginning or end of the string
## 1.1.0 - 2017-01-13
- All color formats now implement a `Color` interface
- Added a `Factory` class with a `fromString` static method to guess a format
- `rgb` and `rgba` values can now contain spaces (e.g. `rgb(255, 255, 255)`)
## 1.0.2 - 2016-10-17
- `rgbChannelToHexChannel` now also accepts single single-digit hex values
## 1.0.1 - 2016-09-22
- Bugfix (breaking!): Alpha channel values are now a float between 0 and 1
## 1.0.0 - 2016-09-21
- First release

21
vendor/spatie/color/LICENSE.md vendored Normal file
View File

@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.

286
vendor/spatie/color/README.md vendored Normal file
View File

@@ -0,0 +1,286 @@
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/support-ukraine.svg?t=1" />](https://supportukrainenow.org)
# A little library to handle color conversions and comparisons
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/color.svg?style=flat-square)](https://packagist.org/packages/spatie/color)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
[![Build Status](https://img.shields.io/travis/spatie/color/master.svg?style=flat-square)](https://travis-ci.org/spatie/color)
[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/color.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/color)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/color.svg?style=flat-square)](https://packagist.org/packages/spatie/color)
![Tests](https://github.com/spatie/color/workflows/Tests/badge.svg)
A little library to handle color conversions and comparisons. Currently supports rgb, rgba, hex, hsl, hsla, CIELab, and xyz color formats as well as CIE76, CIE94, and CIEDE2000 color comparison algorithms.
```php
$rgb = Rgb::fromString('rgb(55,155,255)');
echo $rgb->red(); // 55
echo $rgb->green(); // 155
echo $rgb->blue(); // 255
echo $rgb; // rgb(55,155,255)
$rgba = $rgb->toRgba(); // `Spatie\Color\Rgba`
$rgba->alpha(); // 1
echo $rgba; // rgba(55,155,255,1)
$hex = $rgb->toHex(); // `Spatie\Color\Hex`
$rgba->alpha(); // ff
echo $hex; // #379bff
$cmyk = $rgb->toCmyk(); // `Spatie\Color\Cmyk`
echo $cmyk; // cmyk(78,39,0,0)
$hsl = $rgb->toHsl(); // `Spatie\Color\Hsl`
echo $hsl; // hsl(210,100%,100%)
$hsb = $rgb->toHsb(); // `Spatie\Color\Hsb`
echo $hsb; // hsl(210,78.4%,100%)
$lab = $rgb->toCIELab();
echo $lab; // CIELab(62.91,5.34,-57.73)
$xyz = $rgb->toXyz();
echo $xyz; // xyz(31.3469,31.4749,99.0308)
$hex2 = Hex::fromString('#2d78c8');
$ratio = Contrast::ratio(Hex::fromString('#f0fff0'), Hex::fromString('#191970'));
echo $ratio; // 15.0
$cie76_distance = Distance::CIE76($rgb, $hex2);
$cie76_distance = Distance::CIE76('rgba(55,155,255,1)', '#2d78c8'); // Outputs the same thing, Factory is built-in to all comparison functions
echo $cie76_distance; // 55.89468042667388
$cie94_distance = Distance::CIE94($rgb, $hex2);
echo $cie94_distance; // 13.49091942790753
$cie94_textiles_distance = Distance::CIE94($rgb, $hex2, 1); // Third parameter optionally sets the application type (0 = Graphic Arts [Default], 1 = Textiles)
echo $cie94_textiles_distance; // 7.0926538068477
$ciede2000_distance = Distance::CIEDE2000($rgb, $hex2);
echo $ciede2000_distance; // 12.711957696300898
```
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/color.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/color)
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
## Installation
You can install the package via composer:
```bash
composer require spatie/color
```
## Usage
The `Color` package contains a separate class per color format, which each implement a `Color` interface.
There are seven classes which implement the `Color` interface:
- `CIELab`
- `Cmyk`
- `Hex`
- `Hsb`
- `Hsl`
- `Hsla`
- `Rgb`
- `Rgba`
- `Xyz`
### `interface Spatie\Color\Color`
#### `fromString(): Color`
Parses a color string and returns a `Color` implementation, depending on the format of the input string.
```php
Hex::fromString('#000000');
Rgba::fromString('rgba(255, 255, 255, 1)');
Hsla::fromString('hsla(360, 100%, 100%, 1)');
```
Throws an `InvalidColorValue` exception if the string can't be parsed.
> `Rgb`, `Rgba`, `Hsl` and `Hsla` strings are allowed to have spaces. `rgb(0,0,0)` is just as valid as `rgb(0, 0, 0)`.
#### `red(): int|string`
Return the value of the `red` color channel.
```php
Hex::fromString('#ff0000')->red(); // 'ff'
Rgb::fromString('rgb(255, 0, 0)')->red(); // 255
```
#### `green(): int|string`
Return the value of the `green` color channel.
```php
Hex::fromString('#00ff00')->green(); // 'ff'
Rgb::fromString('rgb(0, 255, 0)')->green(); // 255
```
#### `blue(): int|string`
Return the value of the `blue` color channel.
```php
Hex::fromString('#0000ff')->blue(); // 'ff'
Rgb::fromString('rgb(0, 0, 255)')->blue(); // 255
```
#### `toCmyk(): Cmyk`
Convert a color to a `Cmyk` color.
```php
Rgb::fromString('rgb(0, 0, 255)')->toCmyk();
// `Cmyk` instance; 'cmyk(100,100,0,0)'
```
#### `toHex(): Hex`
Convert a color to a `Hex` color.
```php
Rgb::fromString('rgb(0, 0, 255)')->toHex();
// `Hex` instance; '#0000ff'
```
When coming from a color format that doesn't support opacity, it can be added by passing it to the `$alpha` parameter.
#### `toHsb(): Hsb`
Convert a color to a `Hsb` color.
```php
Rgb::fromString('rgb(0, 0, 255)')->toHsb();
// `Hsl` instance; 'hsb(240, 100%, 100%)'
```
#### `toHsl(): Hsl`
Convert a color to a `Hsl` color.
```php
Rgb::fromString('rgb(0, 0, 255)')->toHsl();
// `Hsl` instance; 'hsl(240, 100%, 50%)'
```
When coming from a color format that supports opacity, the opacity will simply be omitted.
```php
Rgba::fromString('rgba(0, 0, 255, .5)')->toHsl();
// `Hsl` instance; 'hsl(240, 100%, 50%)'
```
#### `toHsla(float $alpha = 1): Hsla`
Convert a color to a `Hsla` color.
```php
Rgb::fromString('rgb(0, 0, 255)')->toHsla();
// `Hsla` instance; 'hsla(240, 100%, 50%, 1.0)'
```
When coming from a color format that doesn't support opacity, it can be added by passing it to the `$alpha` parameter.
```php
Rgb::fromString('rgb(0, 0, 255)')->toHsla(.5);
// `Hsla` instance; 'hsla(240, 100%, 50%, 0.5)'
```
#### `toRgb(): Rgb`
Convert a color to an `Rgb` color.
```php
Hex::fromString('#0000ff')->toRgb();
// `Rgb` instance; 'rgb(0, 0, 255)'
```
When coming from a color format that supports opacity, the opacity will simply be omitted.
```php
Rgba::fromString('rgb(0, 0, 255, .5)')->toRgb();
// `Rgb` instance; 'rgb(0, 0, 255)'
```
#### `toRgba(float $alpha = 1): Rgba`
Convert a color to a `Rgba` color.
```php
Rgb::fromString('rgb(0, 0, 255)')->toRgba();
// `Rgba` instance; 'rgba(0, 0, 255, 1)'
```
When coming from a color format that doesn't support opacity, it can be added by passing it to the `$alpha` parameter.
```php
Rgba::fromString('rgb(0, 0, 255)')->toRgba(.5);
// `Rgba` instance; 'rgba(0, 0, 255, .5)'
```
#### `__toString(): string`
Cast the color to a string.
```php
(string) Rgb::fromString('rgb(0, 0, 255)'); // 'rgb(0,0,255)'
(string) Rgba::fromString('rgb(0, 0, 255, .5)'); // 'rgb(0,0,255,0.5)'
(string) Hex::fromString('#0000ff'); // '#0000ff'
(string) Hsla::fromString('hsl(240, 100%, 50%)'); // 'hsl(240, 100%, 50%)'
(string) Hsla::fromString('hsla(240, 100%, 50%, 1.0)'); // 'hsla(240, 100%, 50%, 1.0)'
```
### `Factory::fromString(): Color`
With the `Factory` class, you can create a color instance from any string (it does an educated guess under the hood). If the string isn't a valid color string in any format, it throws an `InvalidColorValue` exception.
```php
Factory::fromString('rgb(0, 0, 255)'); // `Rgb` instance
Factory::fromString('#0000ff'); // `Hex` instance
Factory::fromString('hsl(240, 100%, 50%)'); // `Hsl` instance
Factory::fromString('Hello world!'); // `InvalidColorValue` exception
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
## Testing
``` bash
$ composer test
```
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security
If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker.
## Credits
- [Sebastian De Deyne](https://github.com/sebastiandedeyne)
- [All Contributors](../../contributors)
## About Spatie
Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource).
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

46
vendor/spatie/color/composer.json vendored Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "spatie/color",
"description": "A little library to handle color conversions",
"keywords": [
"spatie",
"color",
"conversion",
"rgb"
],
"homepage": "https://github.com/spatie/color",
"license": "MIT",
"authors": [
{
"name": "Sebastian De Deyne",
"email": "sebastian@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"require": {
"php" : "^7.3|^8.0"
},
"require-dev": {
"pestphp/pest": "^1.22",
"phpunit/phpunit": "^6.5||^9.0"
},
"autoload": {
"psr-4": {
"Spatie\\Color\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\Color\\Test\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/pest"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
}
}

127
vendor/spatie/color/src/CIELab.php vendored Normal file
View File

@@ -0,0 +1,127 @@
<?php
namespace Spatie\Color;
class CIELab implements Color
{
/** @var float */
protected $l;
protected $a;
protected $b;
public function __construct(float $l, float $a, float $b)
{
Validate::CIELabValue($l, 'l');
Validate::CIELabValue($a, 'a');
Validate::CIELabValue($b, 'b');
$this->l = $l;
$this->a = $a;
$this->b = $b;
}
public static function fromString(string $string)
{
Validate::CIELabColorString($string);
$matches = null;
preg_match('/CIELab\( *(\d{1,3}\.?\d* *, *-?\d{1,3}\.?\d* *, *-?\d{1,3}\.?\d*) *\)/i', $string, $matches);
$channels = explode(',', $matches[1]);
[$l, $a, $b] = array_map('trim', $channels);
return new static($l, $a, $b);
}
public function l(): float
{
return $this->l;
}
public function a(): float
{
return $this->a;
}
public function b(): float
{
return $this->b;
}
public function red(): int
{
$rgb = $this->toRgb();
return $rgb->red();
}
public function blue(): int
{
$rgb = $this->toRgb();
return $rgb->blue();
}
public function green(): int
{
$rgb = $this->toRgb();
return $rgb->green();
}
public function toCIELab(): self
{
return new self($this->l, $this->a, $this->b);
}
public function toCmyk(): Cmyk
{
return $this->toRgb()->toCmyk();
}
public function toHex(string $alpha = 'ff'): Hex
{
return $this->toRgb()->toHex($alpha);
}
public function toHsb(): Hsb
{
return $this->toRgb()->toHsb();
}
public function toHsl(): Hsl
{
return $this->toRgb()->toHSL();
}
public function toHsla(float $alpha = 1): Hsla
{
return $this->toRgb()->toHsla($alpha);
}
public function toRgb(): Rgb
{
return $this->toXyz()->toRgb();
}
public function toRgba(float $alpha = 1): Rgba
{
return $this->toRgb()->toRgba($alpha);
}
public function toXyz(): Xyz
{
[$x, $y, $z] = Convert::CIELabValueToXyz(
$this->l,
$this->a,
$this->b
);
return new Xyz($x, $y, $z);
}
public function __toString(): string
{
return "CIELab({$this->l},{$this->a},{$this->b})";
}
}

132
vendor/spatie/color/src/Cmyk.php vendored Normal file
View File

@@ -0,0 +1,132 @@
<?php
namespace Spatie\Color;
class Cmyk implements Color
{
/** @var float */
protected $cyan;
protected $magenta;
protected $yellow;
protected $key;
public function __construct(float $cyan, float $magenta, float $yellow, float $key)
{
Validate::cmykValue($cyan, 'cyan');
Validate::cmykValue($magenta, 'magenta');
Validate::cmykValue($yellow, 'yellow');
Validate::cmykValue($key, 'key (black)');
$this->cyan = $cyan;
$this->magenta = $magenta;
$this->yellow = $yellow;
$this->key = $key;
}
public static function fromString(string $string)
{
Validate::cmykColorString($string);
$matches = null;
preg_match('/cmyk\( *(\d{1,3})%? *, *(\d{1,3})%? *, *(\d{1,3})%? *, *(\d{1,3})%? *\)/i', $string, $matches);
return new static($matches[1] / 100, $matches[2] / 100, $matches[3] / 100, $matches[4] / 100);
}
public function red(): int
{
return Convert::cmykValueToRgb($this->cyan, $this->magenta, $this->yellow, $this->key)[0];
}
public function green(): int
{
return Convert::cmykValueToRgb($this->cyan, $this->magenta, $this->yellow, $this->key)[1];
}
public function blue(): int
{
return Convert::cmykValueToRgb($this->cyan, $this->magenta, $this->yellow, $this->key)[2];
}
public function cyan(): float
{
return $this->cyan;
}
public function magenta(): float
{
return $this->magenta;
}
public function yellow(): float
{
return $this->yellow;
}
public function key(): float
{
return $this->key;
}
public function black(): float
{
return $this->key;
}
public function toCmyk(): Cmyk
{
return new self($this->cyan, $this->magenta, $this->yellow, $this->key);
}
public function toCIELab(): CIELab
{
return $this->toRgb()->toCIELab();
}
public function toHex(string $alpha = 'ff'): Hex
{
return $this->toRgb()->toHex($alpha);
}
public function toHsb(): Hsb
{
return $this->toRgb()->toHsb();
}
public function toHsl(): Hsl
{
return $this->toRgb()->toHsl();
}
public function toHsla(float $alpha = 1): Hsla
{
return $this->toRgb()->toHsla($alpha);
}
public function toRgb(): Rgb
{
list($red, $green, $blue) = Convert::cmykValueToRgb($this->cyan, $this->magenta, $this->yellow, $this->key);
return new Rgb($red, $green, $blue);
}
public function toRgba(float $alpha = 1): Rgba
{
return $this->toRgb()->toRgba($alpha);
}
public function toXyz(): Xyz
{
return $this->toRgba()->toXyz();
}
public function __toString(): string
{
$cyan = round($this->cyan * 100);
$magenta = round($this->magenta * 100);
$yellow = round($this->yellow * 100);
$key = round($this->key * 100);
return "cmyk({$cyan}%,{$magenta}%,{$yellow}%,{$key}%)";
}
}

34
vendor/spatie/color/src/Color.php vendored Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace Spatie\Color;
interface Color
{
public static function fromString(string $string);
public function red();
public function green();
public function blue();
public function toCIELab(): CIELab;
public function toHex(string $alpha = 'ff'): Hex;
public function toHsb(): Hsb;
public function toHsl(): Hsl;
public function toHsla(float $alpha = 1): Hsla;
public function toRgb(): Rgb;
public function toRgba(float $alpha = 1): Rgba;
public function toXyz(): Xyz;
public function toCmyk(): Cmyk;
public function __toString(): string;
}

33
vendor/spatie/color/src/Contrast.php vendored Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace Spatie\Color;
class Contrast
{
public static function ratio(Color $a, Color $b): float
{
if (! $a instanceof Hex) {
$a = $a->toHex();
}
if (! $b instanceof Hex) {
$b = $b->toHex();
}
$l1 =
0.2126 * pow(hexdec($a->red()) / 255, 2.2) +
0.7152 * pow(hexdec($a->green()) / 255, 2.2) +
0.0722 * pow(hexdec($a->blue()) / 255, 2.2);
$l2 =
0.2126 * pow(hexdec($b->red()) / 255, 2.2) +
0.7152 * pow(hexdec($b->green()) / 255, 2.2) +
0.0722 * pow(hexdec($b->blue()) / 255, 2.2);
if ($l1 > $l2) {
return (int) (($l1 + 0.05) / ($l2 + 0.05));
} else {
return (int) (($l2 + 0.05) / ($l1 + 0.05));
}
}
}

389
vendor/spatie/color/src/Convert.php vendored Normal file
View File

@@ -0,0 +1,389 @@
<?php
namespace Spatie\Color;
class Convert
{
public static function CIELabValueToXyz(float $l, float $a, float $b): array
{
$y = ($l + 16) / 116;
$x = $a / 500 + $y;
$z = $y - $b / 200;
if (pow($y, 3) > 0.008856) {
$y = pow($y, 3);
} else {
$y = ($y - 16 / 116) / 7.787;
}
if (pow($x, 3) > 0.008856) {
$x = pow($x, 3);
} else {
$x = ($x - 16 / 116) / 7.787;
}
if (pow($z, 3) > 0.008856) {
$z = pow($z, 3);
} else {
$z = ($z - 16 / 116) / 7.787;
}
$x = round(95.047 * $x, 4);
$y = round(100.000 * $y, 4);
$z = round(108.883 * $z, 4);
if ($x > 95.047) {
$x = 95.047;
}
if ($y > 100) {
$y = 100;
}
if ($z > 108.883) {
$z = 108.883;
}
return [$x, $y, $z];
}
public static function cmykValueToRgb(float $cyan, float $magenta, float $yellow, float $key): array
{
return [
(int) (255 * (1 - $cyan) * (1 - $key)),
(int) (255 * (1 - $magenta) * (1 - $key)),
(int) (255 * (1 - $yellow) * (1 - $key)),
];
}
public static function rgbValueToCmyk($red, $green, $blue): array
{
$red /= 255;
$green /= 255;
$blue /= 255;
$black = 1 - max($red, $green, $blue);
$keyNeg = (1 - $black);
return [
(1 - $red - $black) / ($keyNeg ?: 1),
(1 - $green - $black) / ($keyNeg ?: 1),
(1 - $blue - $black) / ($keyNeg ?: 1),
$black,
];
}
public static function hexChannelToRgbChannel(string $hexValue): int
{
return hexdec($hexValue);
}
public static function rgbChannelToHexChannel(int $rgbValue): string
{
return str_pad(dechex($rgbValue), 2, '0', STR_PAD_LEFT);
}
public static function hsbValueToRgb($hue, $saturation, $brightness)
{
while ($hue > 360) {
$hue -= 360.0;
}
while ($hue < 0) {
$hue += 360.0;
}
$hue /= 360;
$saturation /= 100;
$brightness /= 100;
if ($saturation == 0) {
$R = $G = $B = $brightness * 255;
} else {
$hue = $hue * 6;
$i = floor($hue);
$j = $brightness * (1 - $saturation);
$k = $brightness * (1 - $saturation * ($hue - $i));
$l = $brightness * (1 - $saturation * (1 - ($hue - $i)));
switch ($i) {
case 0:
$red = $brightness;
$green = $l;
$blue = $j;
break;
case 1:
$red = $k;
$green = $brightness;
$blue = $j;
break;
case 2:
$red = $j;
$green = $brightness;
$blue = $l;
break;
case 3:
$red = $j;
$green = $k;
$blue = $brightness;
break;
case 4:
$red = $l;
$green = $j;
$blue = $brightness;
break;
default:
$red = $brightness;
$green = $j;
$blue = $k;
break;
}
$R = $red * 255;
$G = $green * 255;
$B = $blue * 255;
}
return [round($R), round($G), round($B)];
}
public static function hslValueToRgb(float $hue, float $saturation, float $lightness): array
{
$h = intval((360 + (intval($hue) % 360)) % 360); // hue values can be less than 0 and greater than 360. This normalises them into the range 0-360.
$c = (1 - abs(2 * ($lightness / 100) - 1)) * ($saturation / 100);
$x = $c * (1 - abs(fmod($h / 60, 2) - 1));
$m = ($lightness / 100) - ($c / 2);
if ($h >= 0 && $h <= 60) {
return [round(($c + $m) * 255), round(($x + $m) * 255), round($m * 255)];
}
if ($h > 60 && $h <= 120) {
return [round(($x + $m) * 255), round(($c + $m) * 255), round($m * 255)];
}
if ($h > 120 && $h <= 180) {
return [round($m * 255), round(($c + $m) * 255), round(($x + $m) * 255)];
}
if ($h > 180 && $h <= 240) {
return [round($m * 255), round(($x + $m) * 255), round(($c + $m) * 255)];
}
if ($h > 240 && $h <= 300) {
return [round(($x + $m) * 255), round($m * 255), round(($c + $m) * 255)];
}
if ($h > 300 && $h <= 360) {
return [round(($c + $m) * 255), round($m * 255), round(($x + $m) * 255)];
}
}
public static function rgbValueToHsb($red, $green, $blue): array
{
$red /= 255;
$green /= 255;
$blue /= 255;
$min = min($red, $green, $blue);
$max = max($red, $green, $blue);
$delMax = $max - $min;
$brightness = $max;
$hue = 0;
if ($delMax == 0) {
$hue = 0;
$saturation = 0;
} else {
$saturation = $delMax / $max;
$delR = ((($max - $red) / 6) + ($delMax / 2)) / $delMax;
$delG = ((($max - $green) / 6) + ($delMax / 2)) / $delMax;
$delB = ((($max - $blue) / 6) + ($delMax / 2)) / $delMax;
if ($red == $max) {
$hue = $delB - $delG;
} else {
if ($green == $max) {
$hue = (1 / 3) + $delR - $delB;
} else {
if ($blue == $max) {
$hue = (2 / 3) + $delG - $delR;
}
}
}
if ($hue < 0) {
$hue++;
}
if ($hue > 1) {
$hue--;
}
}
return [round($hue, 2) * 360, round($saturation, 2) * 100, round($brightness, 2) * 100];
}
public static function rgbValueToHsl($red, $green, $blue): array
{
$r = $red / 255;
$g = $green / 255;
$b = $blue / 255;
$cmax = max($r, $g, $b);
$cmin = min($r, $g, $b);
$delta = $cmax - $cmin;
$hue = 0;
if ($delta != 0) {
if ($r === $cmax) {
$hue = 60 * fmod(($g - $b) / $delta, 6);
$hue = $hue < 0 ? $hue + 360 : $hue ;
}
if ($g === $cmax) {
$hue = 60 * ((($b - $r) / $delta) + 2);
}
if ($b === $cmax) {
$hue = 60 * ((($r - $g) / $delta) + 4);
}
}
$lightness = ($cmax + $cmin) / 2;
$saturation = 0;
if ($lightness > 0 && $lightness < 1) {
$saturation = $delta / (1 - abs((2 * $lightness) - 1));
}
return [$hue, min($saturation, 1) * 100, min($lightness, 1) * 100];
}
public static function rgbValueToXyz($red, $green, $blue): array
{
$red = $red / 255;
$green = $green / 255;
$blue = $blue / 255;
if ($red > 0.04045) {
$red = pow((($red + 0.055) / 1.055), 2.4);
} else {
$red = $red / 12.92;
}
if ($green > 0.04045) {
$green = pow((($green + 0.055) / 1.055), 2.4);
} else {
$green = $green / 12.92;
}
if ($blue > 0.04045) {
$blue = pow((($blue + 0.055) / 1.055), 2.4);
} else {
$blue = $blue / 12.92;
}
$red = $red * 100;
$green = $green * 100;
$blue = $blue * 100;
$x = round($red * 0.4124 + $green * 0.3576 + $blue * 0.1805, 4);
$y = round($red * 0.2126 + $green * 0.7152 + $blue * 0.0722, 4);
$z = round($red * 0.0193 + $green * 0.1192 + $blue * 0.9505, 4);
if ($x > 95.047) {
$x = 95.047;
}
if ($y > 100) {
$y = 100;
}
if ($z > 108.883) {
$z = 108.883;
}
return [$x, $y, $z];
}
public static function xyzValueToCIELab(float $x, float $y, float $z): array
{
$x = $x / 95.047;
$y = $y / 100.000;
$z = $z / 108.883;
if ($x > 0.008856) {
$x = pow($x, 1 / 3);
} else {
$x = (7.787 * $x) + (16 / 116);
}
if ($y > 0.008856) {
$y = pow($y, 1 / 3);
} else {
$y = (7.787 * $y) + (16 / 116);
}
if ($y > 0.008856) {
$l = (116 * $y) - 16;
} else {
$l = 903.3 * $y;
}
if ($z > 0.008856) {
$z = pow($z, 1 / 3);
} else {
$z = (7.787 * $z) + (16 / 116);
}
$l = round($l, 2);
$a = round(500 * ($x - $y), 2);
$b = round(200 * ($y - $z), 2);
return [$l, $a, $b];
}
public static function xyzValueToRgb(float $x, float $y, float $z): array
{
$x = $x / 100;
$y = $y / 100;
$z = $z / 100;
$r = $x * 3.2406 + $y * -1.5372 + $z * -0.4986;
$g = $x * -0.9689 + $y * 1.8758 + $z * 0.0415;
$b = $x * 0.0557 + $y * -0.2040 + $z * 1.0570;
if ($r > 0.0031308) {
$r = 1.055 * pow($r, (1 / 2.4)) - 0.055;
} else {
$r = 12.92 * $r;
}
if ($g > 0.0031308) {
$g = 1.055 * pow($g, (1 / 2.4)) - 0.055;
} else {
$g = 12.92 * $g;
}
if ($b > 0.0031308) {
$b = 1.055 * pow($b, (1 / 2.4)) - 0.055;
} else {
$b = 12.92 * $b;
}
$r = intval(max(0, min(255, $r * 255)));
$g = intval(max(0, min(255, $g * 255)));
$b = intval(max(0, min(255, $b * 255)));
return [$r, $g, $b];
}
}

155
vendor/spatie/color/src/Distance.php vendored Normal file
View File

@@ -0,0 +1,155 @@
<?php
namespace Spatie\Color;
class Distance
{
public static function CIE76($color1, $color2): float
{
if (gettype($color1) === 'string') {
$color1 = Factory::fromString($color1);
}
if (gettype($color2) === 'string') {
$color2 = Factory::fromString($color2);
}
$lab1 = $color1->toCIELab();
$lab2 = $color2->toCIELab();
if (strval($lab1) === strval($lab2)) {
return 0;
}
$sum = 0;
$sum += pow($lab1->l() - $lab2->l(), 2);
$sum += pow($lab1->a() - $lab2->a(), 2);
$sum += pow($lab1->b() - $lab2->b(), 2);
return max(min(sqrt($sum), 100), 0);
}
public static function CIE94($color1, $color2, $textiles = 0): float
{
if (gettype($color1) === 'string') {
$color1 = Factory::fromString($color1);
}
if (gettype($color2) === 'string') {
$color2 = Factory::fromString($color2);
}
$lab1 = $color1->toCIELab();
$lab2 = $color2->toCIELab();
$l1 = $lab1->l();
$a1 = $lab1->a();
$b1 = $lab1->b();
$l2 = $lab2->l();
$a2 = $lab2->a();
$b2 = $lab2->b();
$delta_l = $l1 - $l2;
$delta_a = $a1 - $a2;
$delta_b = $b1 - $b2;
$c1 = sqrt(pow($a1, 2) + pow($b1, 2));
$c2 = sqrt(pow($a2, 2) + pow($b2, 2));
$delta_c = $c1 - $c2;
$delta_h = pow($delta_a, 2) + pow($delta_b, 2) - pow($delta_c, 2);
$delta_h = $delta_h < 0 ? 0 : sqrt($delta_h);
if ($textiles) {
$kl = 2.0;
$k1 = .048;
$k2 = .014;
} else {
$kl = 1.0;
$k1 = .045;
$k2 = .015;
}
$sc = 1.0 + $k1 * $c1;
$sh = 1.0 + $k2 * $c1;
$i = pow($delta_l / $kl, 2) + pow($delta_c / $sc, 2) + pow($delta_h / $sh, 2);
return $i < 0 ? 0 : sqrt($i);
}
public static function CIEDE2000($color1, $color2): float
{
if (gettype($color1) === 'string') {
$color1 = Factory::fromString($color1);
}
if (gettype($color2) === 'string') {
$color2 = Factory::fromString($color2);
}
$lab1 = $color1->toCIELab();
$lab2 = $color2->toCIELab();
$l1 = $lab1->l();
$a1 = $lab1->a();
$b1 = $lab1->b();
$l2 = $lab2->l();
$a2 = $lab2->a();
$b2 = $lab2->b();
$avg_lp = ($l1 + $l2) / 2;
$c1 = sqrt(pow($a1, 2) + pow($b1, 2));
$c2 = sqrt(pow($a2, 2) + pow($b2, 2));
$avg_c = ($c1 + $c2) / 2;
$g = (1 - sqrt(pow($avg_c, 7) / (pow($avg_c, 7) + pow(25, 7)))) / 2;
$a1p = $a1 * (1 + $g);
$a2p = $a2 * (1 + $g);
$c1p = sqrt(pow($a1p, 2) + pow($b1, 2));
$c2p = sqrt(pow($a2p, 2) + pow($b2, 2));
$avg_cp = ($c1p + $c2p) / 2;
$h1p = rad2deg(atan2($b1, $a1p));
if ($h1p < 0) {
$h1p += 360;
}
$h2p = rad2deg(atan2($b2, $a2p));
if ($h2p < 0) {
$h2p += 360;
}
$avg_hp = abs($h1p - $h2p) > 180 ? ($h1p + $h2p + 360) / 2 : ($h1p + $h2p) / 2;
$t = 1 - 0.17 * cos(deg2rad($avg_hp - 30)) + 0.24 * cos(deg2rad(2 * $avg_hp)) + 0.32 * cos(deg2rad(3 * $avg_hp + 6)) - 0.2 * cos(deg2rad(4 * $avg_hp - 63));
$delta_hp = $h2p - $h1p;
if (abs($delta_hp) > 180) {
if ($h2p <= $h1p) {
$delta_hp += 360;
} else {
$delta_hp -= 360;
}
}
$delta_lp = $l2 - $l1;
$delta_cp = $c2p - $c1p;
$delta_hp = 2 * sqrt($c1p * $c2p) * sin(deg2rad($delta_hp) / 2);
$s_l = 1 + ((0.015 * pow($avg_lp - 50, 2)) / sqrt(20 + pow($avg_lp - 50, 2)));
$s_c = 1 + 0.045 * $avg_cp;
$s_h = 1 + 0.015 * $avg_cp * $t;
$delta_ro = 30 * exp(-(pow(($avg_hp - 275) / 25, 2)));
$r_c = 2 * sqrt(pow($avg_cp, 7) / (pow($avg_cp, 7) + pow(25, 7)));
$r_t = -$r_c * sin(2 * deg2rad($delta_ro));
$kl = $kc = $kh = 1;
$delta_e = sqrt(pow($delta_lp / ($s_l * $kl), 2) + pow($delta_cp / ($s_c * $kc), 2) + pow($delta_hp / ($s_h * $kh), 2) + $r_t * ($delta_cp / ($s_c * $kc)) * ($delta_hp / ($s_h * $kh)));
return $delta_e;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Spatie\Color\Exceptions;
use Exception;
class InvalidColorValue extends Exception
{
public static function CIELabValueNotInRange(float $value, string $name, float $min, float $max): self
{
return new static("CIELab value `{$name}` must be a number between $min and $max");
}
public static function rgbChannelValueNotInRange(int $value, string $channel): self
{
return new static("An rgb values must be an integer between 0 and 255, `{$value}` provided for channel {$channel}.");
}
public static function alphaChannelValueNotInRange(float $value): self
{
return new static("An alpha values must be a float between 0 and 1, `{$value}` provided.");
}
public static function hexChannelValueHasInvalidLength(string $value): self
{
$length = strlen($value);
return new static("Hex values must contain exactly 2 characters, `{$value}` contains {$length} characters.");
}
public static function malformedCIELabColorString(string $string): self
{
return new static("CIELab color string `{$string}` is malformed. A CIELab color contains 3 comma separated values, wrapped in `CIELab()`, e.g. `CIELab(62.91,5.34,-57.73)`.");
}
public static function cmykValueNotInRange(float $value, string $name): self
{
return new static("Cmyk value `{$name}` must be a number between 0 and 1");
}
public static function hexValueContainsInvalidCharacters(string $value): self
{
return new static("Hex values can only contain numbers or letters from A-F, `{$value}` contains invalid characters.");
}
public static function hsbValueNotInRange(float $value, string $name): self
{
return new static("Hsb value `{$name}` must be a number between 0 and 100");
}
public static function hslValueNotInRange(float $value, string $name): self
{
return new static("Hsl value `{$name}` must be a number between 0 and 100");
}
public static function malformedCmykColorString(string $string): self
{
return new static("Cmyk color string `{$string}` is malformed. A cmyk color contains cyan, magenta, yellow and key (black) values, wrapped in `cmyk()`, e.g. `cmyk(100%,100%,100%,100%)`.");
}
public static function malformedHexColorString(string $string): self
{
return new static("Hex color string `{$string}` is malformed. A hex color string starts with a `#` and contains exactly six characters, e.g. `#aabbcc`.");
}
public static function malformedHslColorString(string $string): self
{
return new static("Hsl color string `{$string}` is malformed. An hsl color contains hue, saturation, and lightness values, wrapped in `hsl()`, e.g. `hsl(300,10%,50%)`.");
}
public static function malformedHslaColorString(string $string): self
{
return new static("Hsla color string `{$string}` is malformed. An hsla color contains hue, saturation, lightness and alpha values, wrapped in `hsl()`, e.g. `hsl(300,10%,50%,0.25)`.");
}
public static function malformedRgbColorString(string $string): self
{
return new static("Rgb color string `{$string}` is malformed. An rgb color contains 3 comma separated values between 0 and 255, wrapped in `rgb()`, e.g. `rgb(0,0,255)`.");
}
public static function malformedRgbaColorString(string $string): self
{
return new static("Rgba color string `{$string}` is malformed. An rgba color contains 3 comma separated values between 0 and 255 with an alpha value between 0 and 1, wrapped in `rgba()`, e.g. `rgb(0,0,255,0.5)`.");
}
public static function malformedColorString(string $string): self
{
return new static("Color string `{$string}` doesn't match any of the available colors.");
}
public static function malformedXyzColorString(string $string): self
{
return new static("Xyz color string `{$string}` is malformed. An xyz color contains 3 comma separated values, wrapped in `xyz()`, e.g. `xyz(31.3469,31.4749,99.0308)`.");
}
public static function xyzValueNotInRange(float $value, string $name, float $min, float $max): self
{
return new static("Xyz value `{$name}` must be a number between $min and $max");
}
}

38
vendor/spatie/color/src/Factory.php vendored Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\Color;
use Spatie\Color\Exceptions\InvalidColorValue;
class Factory
{
public static function fromString(string $string): Color
{
$colorClasses = static::getColorClasses();
foreach ($colorClasses as $colorClass) {
try {
return $colorClass::fromString($string);
} catch (InvalidColorValue $e) {
// Catch the exception but never throw it.
}
}
throw InvalidColorValue::malformedColorString($string);
}
protected static function getColorClasses(): array
{
return [
CIELab::class,
Cmyk::class,
Hex::class,
Hsb::class,
Hsl::class,
Hsla::class,
Rgb::class,
Rgba::class,
Xyz::class,
];
}
}

152
vendor/spatie/color/src/Hex.php vendored Normal file
View File

@@ -0,0 +1,152 @@
<?php
namespace Spatie\Color;
class Hex implements Color
{
/** @var string */
protected $red;
protected $green;
protected $blue;
protected $alpha = 'ff';
public function __construct(string $red, string $green, string $blue, string $alpha = 'ff')
{
Validate::hexChannelValue($red, 'red');
Validate::hexChannelValue($green, 'green');
Validate::hexChannelValue($blue, 'blue');
Validate::hexChannelValue($alpha, 'alpha');
$this->red = strtolower($red);
$this->green = strtolower($green);
$this->blue = strtolower($blue);
$this->alpha = strtolower($alpha);
}
public static function fromString(string $string)
{
Validate::hexColorString($string);
$string = ltrim($string, '#');
switch (strlen($string)) {
case 3:
[$red, $green, $blue] = str_split($string);
$red .= $red;
$green .= $green;
$blue .= $blue;
$alpha = 'ff';
break;
case 4:
[$red, $green, $blue, $alpha] = str_split($string);
$red .= $red;
$green .= $green;
$blue .= $blue;
$alpha .= $alpha;
break;
default:
case 6:
[$red, $green, $blue] = str_split($string, 2);
$alpha = 'ff';
break;
case 8:
[$red, $green, $blue, $alpha] = str_split($string, 2);
break;
}
return new static($red, $green, $blue, $alpha);
}
public function red(): string
{
return $this->red;
}
public function green(): string
{
return $this->green;
}
public function blue(): string
{
return $this->blue;
}
public function alpha(): string
{
return $this->alpha;
}
public function toCIELab(): CIELab
{
return $this->toRgb()->toCIELab();
}
public function toCmyk(): Cmyk
{
return $this->toRgb()->toCmyk();
}
public function toHex(string $alpha = 'ff'): self
{
return new self($this->red, $this->green, $this->blue, $alpha);
}
public function toHsb(): Hsb
{
return $this->toRgb()->toHsb();
}
public function toHsl(): Hsl
{
[$hue, $saturation, $lightness] = Convert::rgbValueToHsl(
Convert::hexChannelToRgbChannel($this->red),
Convert::hexChannelToRgbChannel($this->green),
Convert::hexChannelToRgbChannel($this->blue)
);
return new Hsl($hue, $saturation, $lightness);
}
public function toHsla(float $alpha = 1): Hsla
{
[$hue, $saturation, $lightness] = Convert::rgbValueToHsl(
Convert::hexChannelToRgbChannel($this->red),
Convert::hexChannelToRgbChannel($this->green),
Convert::hexChannelToRgbChannel($this->blue)
);
return new Hsla($hue, $saturation, $lightness, $alpha);
}
public function toRgb(): Rgb
{
return new Rgb(
Convert::hexChannelToRgbChannel($this->red),
Convert::hexChannelToRgbChannel($this->green),
Convert::hexChannelToRgbChannel($this->blue)
);
}
public function toRgba(float $alpha = 1): Rgba
{
return $this->toRgb()->toRgba($alpha);
}
public function toXyz(): Xyz
{
return $this->toRgb()->toXyz();
}
public function __toString(): string
{
return "#{$this->red}{$this->green}{$this->blue}" . ($this->alpha !== 'ff' ? $this->alpha : '');
}
}

121
vendor/spatie/color/src/Hsb.php vendored Normal file
View File

@@ -0,0 +1,121 @@
<?php
namespace Spatie\Color;
class Hsb implements Color
{
/** @var float */
protected $hue;
protected $saturation;
protected $brightness;
public function __construct(float $hue, float $saturation, float $brightness)
{
Validate::hsbValue($hue, 'hue');
Validate::hsbValue($saturation, 'saturation');
Validate::hsbValue($brightness, 'brightness');
$this->hue = $hue;
$this->saturation = $saturation;
$this->brightness = $brightness;
}
public static function fromString(string $string)
{
Validate::hsbColorString($string);
$matches = null;
preg_match('/hs[vb]\( *(-?\d{1,3}) *, *(\d{1,3})%? *, *(\d{1,3})%? *\)/i', $string, $matches);
return new static($matches[1], $matches[2], $matches[3]);
}
public function hue(): float
{
return $this->hue;
}
public function saturation(): float
{
return $this->saturation;
}
public function brightness(): float
{
return $this->brightness;
}
public function red(): int
{
return Convert::hsbValueToRgb($this->hue, $this->saturation, $this->brightness)[0];
}
public function green(): int
{
return Convert::hsbValueToRgb($this->hue, $this->saturation, $this->brightness)[1];
}
public function blue(): int
{
return Convert::hsbValueToRgb($this->hue, $this->saturation, $this->brightness)[2];
}
public function toCIELab(): CIELab
{
return $this->toRgb()->toCIELab();
}
public function toCmyk(): Cmyk
{
return $this->toRgb()->toCmyk();
}
public function toHsb(): Hsb
{
return new self($this->hue, $this->saturation, $this->brightness);
}
public function toHex(string $alpha = 'ff'): Hex
{
return new Hex(
Convert::rgbChannelToHexChannel($this->red()),
Convert::rgbChannelToHexChannel($this->green()),
Convert::rgbChannelToHexChannel($this->blue()),
$alpha
);
}
public function toHsl(): Hsl
{
return $this->toRgb()->toHsl();
}
public function toHsla(float $alpha = 1): Hsla
{
return $this->toRgb()->toHsla($alpha);
}
public function toRgb(): Rgb
{
return new Rgb($this->red(), $this->green(), $this->blue());
}
public function toRgba(float $alpha = 1): Rgba
{
return new Rgba($this->red(), $this->green(), $this->blue(), $alpha);
}
public function toXyz(): Xyz
{
return $this->toRgb()->toXyz();
}
public function __toString(): string
{
$hue = round($this->hue);
$saturation = round($this->saturation);
$brightness = round($this->brightness);
return "hsb({$hue},{$saturation}%,{$brightness}%)";
}
}

120
vendor/spatie/color/src/Hsl.php vendored Normal file
View File

@@ -0,0 +1,120 @@
<?php
namespace Spatie\Color;
class Hsl implements Color
{
/** @var float */
protected $hue;
protected $saturation;
protected $lightness;
public function __construct(float $hue, float $saturation, float $lightness)
{
Validate::hslValue($saturation, 'saturation');
Validate::hslValue($lightness, 'lightness');
$this->hue = $hue;
$this->saturation = $saturation;
$this->lightness = $lightness;
}
public static function fromString(string $string)
{
Validate::hslColorString($string);
$matches = null;
preg_match('/hsl\( *(-?\d{1,3}) *, *(\d{1,3})%? *, *(\d{1,3})%? *\)/i', $string, $matches);
return new static($matches[1], $matches[2], $matches[3]);
}
public function hue(): float
{
return $this->hue;
}
public function saturation(): float
{
return $this->saturation;
}
public function lightness(): float
{
return $this->lightness;
}
public function red(): int
{
return Convert::hslValueToRgb($this->hue, $this->saturation, $this->lightness)[0];
}
public function green(): int
{
return Convert::hslValueToRgb($this->hue, $this->saturation, $this->lightness)[1];
}
public function blue(): int
{
return Convert::hslValueToRgb($this->hue, $this->saturation, $this->lightness)[2];
}
public function toCIELab(): CIELab
{
return $this->toRgb()->toCIELab();
}
public function toCmyk(): Cmyk
{
return $this->toRgb()->toCmyk();
}
public function toHex(string $alpha = 'ff'): Hex
{
return new Hex(
Convert::rgbChannelToHexChannel($this->red()),
Convert::rgbChannelToHexChannel($this->green()),
Convert::rgbChannelToHexChannel($this->blue()),
$alpha
);
}
public function toHsb(): Hsb
{
return $this->toRgb()->toHsb();
}
public function toHsl(): self
{
return new self($this->hue(), $this->saturation(), $this->lightness());
}
public function toHsla(float $alpha = 1): Hsla
{
return new Hsla($this->hue(), $this->saturation(), $this->lightness(), $alpha);
}
public function toRgb(): Rgb
{
return new Rgb($this->red(), $this->green(), $this->blue());
}
public function toRgba(float $alpha = 1): Rgba
{
return new Rgba($this->red(), $this->green(), $this->blue(), $alpha);
}
public function toXyz(): Xyz
{
return $this->toRgb()->toXyz();
}
public function __toString(): string
{
$hue = round($this->hue);
$saturation = round($this->saturation);
$lightness = round($this->lightness);
return "hsl({$hue},{$saturation}%,{$lightness}%)";
}
}

134
vendor/spatie/color/src/Hsla.php vendored Normal file
View File

@@ -0,0 +1,134 @@
<?php
namespace Spatie\Color;
class Hsla implements Color
{
/** @var float */
protected $hue;
protected $saturation;
protected $lightness;
protected $alpha;
public function __construct(float $hue, float $saturation, float $lightness, float $alpha = 1.0)
{
Validate::hslValue($saturation, 'saturation');
Validate::hslValue($lightness, 'lightness');
Validate::alphaChannelValue($alpha);
$this->hue = $hue;
$this->saturation = $saturation;
$this->lightness = $lightness;
$this->alpha = $alpha;
}
public static function fromString(string $string)
{
Validate::hslaColorString($string);
$matches = null;
preg_match('/hsla\( *(\d{1,3}) *, *(\d{1,3})%? *, *(\d{1,3})%? *, *([0-1](\.\d{1,2})?) *\)/i', $string, $matches);
return new static($matches[1], $matches[2], $matches[3], $matches[4]);
}
public function hue(): float
{
return $this->hue;
}
public function saturation(): float
{
return $this->saturation;
}
public function lightness(): float
{
return $this->lightness;
}
public function red(): int
{
return Convert::hslValueToRgb($this->hue, $this->saturation, $this->lightness)[0];
}
public function green(): int
{
return Convert::hslValueToRgb($this->hue, $this->saturation, $this->lightness)[1];
}
public function blue(): int
{
return Convert::hslValueToRgb($this->hue, $this->saturation, $this->lightness)[2];
}
public function alpha(): float
{
return $this->alpha;
}
public function contrast(): self
{
return Contrast::make($this->toHex())->toHsla($this->alpha());
}
public function toCIELab(): CIELab
{
return $this->toRgb()->toCIELab();
}
public function toCmyk(): Cmyk
{
return $this->toRgb()->toCmyk();
}
public function toHex(string $alpha = 'ff'): Hex
{
return new Hex(
Convert::rgbChannelToHexChannel($this->red()),
Convert::rgbChannelToHexChannel($this->green()),
Convert::rgbChannelToHexChannel($this->blue()),
$alpha
);
}
public function toHsb(): Hsb
{
return $this->toRgb()->toHsb();
}
public function toHsla(float $alpha = 1): self
{
return new self($this->hue(), $this->saturation(), $this->lightness(), $alpha);
}
public function toHsl(): Hsl
{
return new Hsl($this->hue(), $this->saturation(), $this->lightness());
}
public function toRgb(): Rgb
{
return new Rgb($this->red(), $this->green(), $this->blue());
}
public function toRgba(float $alpha = 1): Rgba
{
return new Rgba($this->red(), $this->green(), $this->blue(), $alpha);
}
public function toXyz(): Xyz
{
return $this->toRgb()->toXyz();
}
public function __toString(): string
{
$hue = round($this->hue);
$saturation = round($this->saturation);
$lightness = round($this->lightness);
$alpha = round($this->alpha, 2);
return "hsla({$hue},{$saturation}%,{$lightness}%,{$alpha})";
}
}

127
vendor/spatie/color/src/Rgb.php vendored Normal file
View File

@@ -0,0 +1,127 @@
<?php
namespace Spatie\Color;
class Rgb implements Color
{
/** @var int */
protected $red;
protected $green;
protected $blue;
public function __construct(int $red, int $green, int $blue)
{
Validate::rgbChannelValue($red, 'red');
Validate::rgbChannelValue($green, 'green');
Validate::rgbChannelValue($blue, 'blue');
$this->red = $red;
$this->green = $green;
$this->blue = $blue;
}
public static function fromString(string $string)
{
Validate::rgbColorString($string);
$matches = null;
preg_match('/rgb\( *(\d{1,3} *, *\d{1,3} *, *\d{1,3}) *\)/i', $string, $matches);
$channels = explode(',', $matches[1]);
[$red, $green, $blue] = array_map('trim', $channels);
return new static($red, $green, $blue);
}
public function red(): int
{
return $this->red;
}
public function green(): int
{
return $this->green;
}
public function blue(): int
{
return $this->blue;
}
public function toCIELab(): CIELab
{
return $this->toXyz()->toCIELab();
}
public function toCmyk(): Cmyk
{
list($cyan, $magenta, $yellow, $key) = Convert::rgbValueToCmyk($this->red, $this->green, $this->blue);
return new Cmyk($cyan, $magenta, $yellow, $key);
}
public function toHex(string $alpha = 'ff'): Hex
{
return new Hex(
Convert::rgbChannelToHexChannel($this->red),
Convert::rgbChannelToHexChannel($this->green),
Convert::rgbChannelToHexChannel($this->blue),
$alpha
);
}
public function toHsb(): Hsb
{
list($hue, $saturation, $brightness) = Convert::rgbValueToHsb($this->red, $this->green, $this->blue);
return new Hsb($hue, $saturation, $brightness);
}
public function toHsl(): Hsl
{
[$hue, $saturation, $lightness] = Convert::rgbValueToHsl(
$this->red,
$this->green,
$this->blue
);
return new Hsl($hue, $saturation, $lightness);
}
public function toHsla(float $alpha = 1): Hsla
{
[$hue, $saturation, $lightness] = Convert::rgbValueToHsl(
$this->red,
$this->green,
$this->blue
);
return new Hsla($hue, $saturation, $lightness, $alpha);
}
public function toRgb(): self
{
return new self($this->red, $this->green, $this->blue);
}
public function toRgba(float $alpha = 1): Rgba
{
return new Rgba($this->red, $this->green, $this->blue, $alpha);
}
public function toXyz(): Xyz
{
[$x, $y, $z] = Convert::rgbValueToXyz(
$this->red,
$this->green,
$this->blue
);
return new Xyz($x, $y, $z);
}
public function __toString(): string
{
return "rgb({$this->red},{$this->green},{$this->blue})";
}
}

124
vendor/spatie/color/src/Rgba.php vendored Normal file
View File

@@ -0,0 +1,124 @@
<?php
namespace Spatie\Color;
class Rgba implements Color
{
/** @var int */
protected $red;
protected $green;
protected $blue;
/** @var float */
protected $alpha;
public function __construct(int $red, int $green, int $blue, float $alpha)
{
Validate::rgbChannelValue($red, 'red');
Validate::rgbChannelValue($green, 'green');
Validate::rgbChannelValue($blue, 'blue');
Validate::alphaChannelValue($alpha);
$this->red = $red;
$this->green = $green;
$this->blue = $blue;
$this->alpha = $alpha;
}
public static function fromString(string $string)
{
Validate::rgbaColorString($string);
$matches = null;
preg_match('/rgba\( *(\d{1,3} *, *\d{1,3} *, *\d{1,3} *, *[0-1]*(\.\d{1,})?) *\)/i', $string, $matches);
$channels = explode(',', $matches[1]);
[$red, $green, $blue, $alpha] = array_map('trim', $channels);
return new static($red, $green, $blue, $alpha);
}
public function red(): int
{
return $this->red;
}
public function green(): int
{
return $this->green;
}
public function blue(): int
{
return $this->blue;
}
public function alpha(): float
{
return $this->alpha;
}
public function toCIELab(): CIELab
{
return $this->toRgb()->toCIELab();
}
public function toCmyk(): Cmyk
{
return $this->toRgb()->toCmyk();
}
public function toHex(string $alpha = 'ff'): Hex
{
return $this->toRgb()->toHex($alpha);
}
public function toHsb(): Hsb
{
return $this->toRgb()->toHsb();
}
public function toHsl(): Hsl
{
[$hue, $saturation, $lightness] = Convert::rgbValueToHsl(
$this->red,
$this->green,
$this->blue
);
return new Hsl($hue, $saturation, $lightness);
}
public function toHsla(float $alpha = 1): Hsla
{
[$hue, $saturation, $lightness] = Convert::rgbValueToHsl(
$this->red,
$this->green,
$this->blue
);
return new Hsla($hue, $saturation, $lightness, $alpha);
}
public function toRgb(): Rgb
{
return new Rgb($this->red, $this->green, $this->blue);
}
public function toRgba(float $alpha = 1): self
{
return new self($this->red, $this->green, $this->blue, $alpha);
}
public function toXyz(): Xyz
{
return $this->toRgb()->toXyz();
}
public function __toString(): string
{
$alpha = number_format($this->alpha, 2);
return "rgba({$this->red},{$this->green},{$this->blue},{$alpha})";
}
}

155
vendor/spatie/color/src/Validate.php vendored Normal file
View File

@@ -0,0 +1,155 @@
<?php
namespace Spatie\Color;
use Spatie\Color\Exceptions\InvalidColorValue;
class Validate
{
public static function CIELabValue(float $value, string $name): void
{
if ($name === 'l' && ($value < 0 || $value > 100)) {
throw InvalidColorValue::CIELabValueNotInRange($value, $name, 0, 100);
}
if (($name === 'a' || $name === 'b') && ($value < -110 || $value > 110)) {
throw InvalidColorValue::CIELabValueNotInRange($value, $name, -110, 110);
}
}
public static function CIELabColorString($string): void
{
if (! preg_match('/^ *CIELab\( *\d{1,3}\.?\d* *, *-?\d{1,3}\.?\d* *, *-?\d{1,3}\.?\d* *\) *$/i', $string)) {
throw InvalidColorValue::malformedCIELabColorString($string);
}
}
public static function cmykValue(float $value, string $name): void
{
if ($value < 0 || $value > 1) {
throw InvalidColorValue::cmykValueNotInRange($value, $name);
}
}
public static function rgbChannelValue(int $value, string $channel): void
{
if ($value < 0 || $value > 255) {
throw InvalidColorValue::rgbChannelValueNotInRange($value, $channel);
}
}
public static function alphaChannelValue(float $value): void
{
if ($value < 0 || $value > 1) {
throw InvalidColorValue::alphaChannelValueNotInRange($value);
}
}
public static function hexChannelValue(string $value): void
{
if (strlen($value) !== 2) {
throw InvalidColorValue::hexChannelValueHasInvalidLength($value);
}
if (! preg_match('/[a-f0-9]{2}/i', $value)) {
throw InvalidColorValue::hexValueContainsInvalidCharacters($value);
}
}
public static function hsbValue(float $value, string $name): void
{
switch ($name) {
case 'hue':
if ($value < 0 || $value > 360) {
throw InvalidColorValue::hsbValueNotInRange($value, $name);
}
break;
default:
if ($value < 0 || $value > 100) {
throw InvalidColorValue::hsbValueNotInRange($value, $name);
}
break;
}
}
public static function hslValue(float $value, string $name): void
{
if ($value < 0 || $value > 100) {
throw InvalidColorValue::hslValueNotInRange($value, $name);
}
}
public static function cmykColorString($string): void
{
if (! preg_match('/^ *cmyk\( *(\d{1,3})%? *, *(\d{1,3})%? *, *(\d{1,3})%? *, *(\d{1,3})%? *\) *$/i', $string)) {
throw InvalidColorValue::malformedCmykColorString($string);
}
}
public static function rgbColorString($string): void
{
if (! preg_match('/^ *rgb\( *\d{1,3} *, *\d{1,3} *, *\d{1,3} *\) *$/i', $string)) {
throw InvalidColorValue::malformedRgbColorString($string);
}
}
public static function rgbaColorString($string): void
{
if (! preg_match('/^ *rgba\( *\d{1,3} *, *\d{1,3} *, *\d{1,3} *, *[0-1]*(\.\d{1,})? *\) *$/i', $string)) {
throw InvalidColorValue::malformedRgbaColorString($string);
}
}
public static function hexColorString($string): void
{
if (! preg_match('/^#(?:[a-f0-9]{3}|[a-f0-9]{4}|[a-f0-9]{6}|[a-f0-9]{8})$/i', $string)) {
throw InvalidColorValue::malformedHexColorString($string);
}
}
public static function hsbColorString($string): void
{
if (! preg_match('/^ *hs[vb]\( *-?\d{1,3} *, *\d{1,3}%? *, *\d{1,3}%? *\) *$/i', $string)) {
throw InvalidColorValue::malformedHslColorString($string);
}
}
public static function hslColorString($string): void
{
if (! preg_match('/^ *hsl\( *-?\d{1,3} *, *\d{1,3}%? *, *\d{1,3}%? *\) *$/i', $string)) {
throw InvalidColorValue::malformedHslColorString($string);
}
}
public static function hslaColorString($string): void
{
if (! preg_match('/^ *hsla\( *\d{1,3} *, *\d{1,3}%? *, *\d{1,3}%? *, *[0-1](\.\d{1,2})? *\) *$/i', $string)) {
throw InvalidColorValue::malformedHslaColorString($string);
}
}
public static function xyzValue(float $value, string $name): void
{
if ($name === 'x' && ($value < 0 || $value > 95.047)) {
throw InvalidColorValue::xyzValueNotInRange($value, $name, 0, 95.047);
}
if ($name === 'y' && ($value < 0 || $value > 100)) {
throw InvalidColorValue::xyzValueNotInRange($value, $name, 0, 100);
}
if ($name === 'z' && ($value < 0 || $value > 108.883)) {
throw InvalidColorValue::xyzValueNotInRange($value, $name, 0, 108.883);
}
}
public static function xyzColorString($string): void
{
if (! preg_match('/^ *xyz\( *\d{1,2}\.?\d+? *, *\d{1,3}\.?\d+? *, *\d{1,3}\.?\d+? *\) *$/i', $string)) {
throw InvalidColorValue::malformedXyzColorString($string);
}
}
}

133
vendor/spatie/color/src/Xyz.php vendored Normal file
View File

@@ -0,0 +1,133 @@
<?php
namespace Spatie\Color;
class Xyz implements Color
{
/** @var float */
protected $x;
protected $y;
protected $z;
public function __construct(float $x, float $y, float $z)
{
Validate::xyzValue($x, 'x');
Validate::xyzValue($y, 'y');
Validate::xyzValue($z, 'z');
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
public static function fromString(string $string)
{
Validate::xyzColorString($string);
$matches = null;
preg_match('/xyz\( *(\d{1,2}\.?\d+? *, *\d{1,3}\.?\d+? *, *\d{1,3}\.?\d+?) *\)/i', $string, $matches);
$channels = explode(',', $matches[1]);
[$x, $y, $z] = array_map('trim', $channels);
return new static($x, $y, $z);
}
public function x(): float
{
return $this->x;
}
public function y(): float
{
return $this->y;
}
public function z(): float
{
return $this->z;
}
public function red(): int
{
$rgb = $this->toRgb();
return $rgb->red();
}
public function blue(): int
{
$rgb = $this->toRgb();
return $rgb->blue();
}
public function green(): int
{
$rgb = $this->toRgb();
return $rgb->green();
}
public function toCIELab(): CIELab
{
[$l, $a, $b] = Convert::xyzValueToCIELab(
$this->x,
$this->y,
$this->z
);
return new CIELab($l, $a, $b);
}
public function toCmyk(): Cmyk
{
return $this->toRgb()->toCmyk();
}
public function toHex(string $alpha = 'ff'): Hex
{
return $this->toRgb()->toHex($alpha);
}
public function toHsb(): Hsb
{
return $this->toRgb()->toHsb();
}
public function toHsl(): Hsl
{
return $this->toRgb()->toHSL();
}
public function toHsla(float $alpha = 1): Hsla
{
return $this->toRgb()->toHsla($alpha);
}
public function toRgb(): Rgb
{
[$red, $green, $blue] = Convert::xyzValueToRgb(
$this->x,
$this->y,
$this->z
);
return new Rgb($red, $green, $blue);
}
public function toRgba(float $alpha = 1): Rgba
{
return $this->toRgb()->toRgba($alpha);
}
public function toXyz(): self
{
return new self($this->x, $this->y, $this->z);
}
public function __toString(): string
{
return "xyz({$this->x},{$this->y},{$this->z})";
}
}

96
vendor/spatie/invade/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,96 @@
# Changelog
All notable changes to `invade` will be documented in this file.
## 2.0.0 - 2023-07-19
### What's Changed
- invade without reflection by @enricodelazzari in https://github.com/spatie/invade/pull/24
### New Contributors
- @enricodelazzari made their first contribution in https://github.com/spatie/invade/pull/24
**Full Changelog**: https://github.com/spatie/invade/compare/1.1.1...2.0.0
## 1.1.1 - 2022-07-05
### What's Changed
- Bump dependabot/fetch-metadata from 1.3.1 to 1.3.3 by @dependabot in https://github.com/spatie/invade/pull/13
- Improve the PHPStan extension by @jrmajor in https://github.com/spatie/invade/pull/14
### New Contributors
- @jrmajor made their first contribution in https://github.com/spatie/invade/pull/14
**Full Changelog**: https://github.com/spatie/invade/compare/1.1.0...1.1.1
## 1.1.0 - 2022-07-02
### What's Changed
- Add phpstan support to not report access to private properties/methods by @tpetry in https://github.com/spatie/invade/pull/12
### New Contributors
- @tpetry made their first contribution in https://github.com/spatie/invade/pull/12
**Full Changelog**: https://github.com/spatie/invade/compare/1.0.3...1.1.0
## 1.0.3 - 2022-06-06
### What's Changed
- Bump dependabot/fetch-metadata from 1.2.0 to 1.2.1 by @dependabot in https://github.com/spatie/invade/pull/8
- Bump dependabot/fetch-metadata from 1.2.1 to 1.3.0 by @dependabot in https://github.com/spatie/invade/pull/9
- Bump dependabot/fetch-metadata from 1.3.0 to 1.3.1 by @dependabot in https://github.com/spatie/invade/pull/11
- Add type checking via phpstan by @olivernybroe in https://github.com/spatie/invade/pull/5
- Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/spatie/invade/pull/10
### New Contributors
- @olivernybroe made their first contribution in https://github.com/spatie/invade/pull/5
**Full Changelog**: https://github.com/spatie/invade/compare/1.0.2...1.0.3
## 1.0.2 - 2022-02-21
## What's Changed
- Bump dependabot/fetch-metadata from 1.1.1 to 1.2.0 by @dependabot in https://github.com/spatie/invade/pull/6
- Add brand new logo to the project by @caneco in https://github.com/spatie/invade/pull/7
## New Contributors
- @dependabot made their first contribution in https://github.com/spatie/invade/pull/6
- @caneco made their first contribution in https://github.com/spatie/invade/pull/7
**Full Changelog**: https://github.com/spatie/invade/compare/1.0.1...1.0.2
## 1.0.1 - 2022-02-11
## What's Changed
- Invador -> Invader spelling by @danharrin in https://github.com/spatie/invade/pull/2
- Only declare function if it does not already exist by @benjam-es in https://github.com/spatie/invade/pull/1
## New Contributors
- @danharrin made their first contribution in https://github.com/spatie/invade/pull/2
- @benjam-es made their first contribution in https://github.com/spatie/invade/pull/1
**Full Changelog**: https://github.com/spatie/invade/compare/1.0.0...1.0.1
## 0.0.2 - 2022-02-11
- experimental release
## 0.0.1 - 2022-02-11
- experimental release
## 1.0.0 - 202X-XX-XX
- initial release

21
vendor/spatie/invade/LICENSE.md vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) spatie <freek@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

127
vendor/spatie/invade/README.md vendored Normal file
View File

@@ -0,0 +1,127 @@
<p align="center"><img src="/art/socialcard.png" alt="Social Card of Invade"></p>
# A PHP function to access private properties and methods
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/invade.svg?style=flat-square)](https://packagist.org/packages/spatie/invade)
[![Tests](https://github.com/spatie/invade/actions/workflows/run-tests.yml/badge.svg?branch=main)](https://github.com/spatie/invade/actions/workflows/run-tests.yml)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/invade.svg?style=flat-square)](https://packagist.org/packages/spatie/invade)
This package offers an `invade` function that will allow you to read/write private properties of an object. It will also allow you to call private methods.
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/invade.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/invade)
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
## Installation
You can install the package via composer:
```bash
composer require spatie/invade
```
## Usage
Imagine you have this class defined which has a private property and method.
```php
class MyClass
{
private string $privateProperty = 'private value';
private function privateMethod(): string
{
return 'private return value';
}
}
$myClass = new Myclass();
```
This is how you can get the value of the private property using the `invade` function.
```php
invade($myClass)->privateProperty; // returns 'private value'
```
The `invade` function also allows you to change private values.
```php
invade($myClass)->privateProperty = 'changed value';
invade($myClass)->privateProperty; // returns 'changed value
```
Using `invade` you can also call private functions.
```php
invade($myClass)->privateMethod(); // returns 'private return value'
```
Further, you can also get and set private static class properties and call private static methods. Imagine having this class:
```php
class MyClass
{
private static string $privateStaticProperty = 'privateValue';
private static function privateStaticMethod(string $string, int $int): string
{
return 'private return value ' . $string . ' ' . $int;
}
}
```
Here is how you get and set private class properties:
```php
invade(MyClass::class)->get('privateStaticProperty'); // returns 'private value'
invade(MyClass::class)->set('privateStaticProperty', 'changedValue');
invade(MyClass::class)->get('privateStaticProperty'); // returns 'changedValue'
```
And this is how you call private static methods:
```php
invade(MyClass::class)
->method('privateStaticMethod')
->call('foo', 123);
// returns 'private return value foo 123'
```
## Testing
```bash
composer test
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Credits
- [Freek Van der Herten](https://github.com/spatie)
- [All Contributors](../../contributors)
And a special thanks to [Caneco](https://twitter.com/caneco) for the logo ✨
The [original idea](https://twitter.com/calebporzio/status/1492141967404371968) for the `invade` function came from [Caleb "string king" Porzio](https://twitter.com/calebporzio). We slightly polished the code that he created in [this commit on Livewire](https://github.com/livewire/livewire/pull/4649/files).
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

51
vendor/spatie/invade/composer.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "spatie/invade",
"description": "A PHP function to work with private properties and methods",
"keywords": [
"spatie",
"invade"
],
"homepage": "https://github.com/spatie/invade",
"license": "MIT",
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"require": {
"php": "^8.0"
},
"require-dev": {
"pestphp/pest": "^1.20",
"phpstan/phpstan": "^1.4",
"spatie/ray": "^1.28"
},
"autoload": {
"psr-4": {
"Spatie\\Invade\\": "src"
},
"files": [
"src/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Spatie\\Invade\\Tests\\": "tests"
}
},
"scripts": {
"analyse": "vendor/bin/phpstan analyse",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

View File

@@ -0,0 +1,9 @@
includes:
- phpstan-baseline.neon
parameters:
level: 4
paths:
- src
tmpDir: build/phpstan
checkMissingIterableValueType: false

33
vendor/spatie/invade/src/Invader.php vendored Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace Spatie\Invade;
/**
* @template T of object
* @mixin T
*/
class Invader
{
/**
* @param T $obj
*/
public function __construct(
public object $obj
) {
}
public function __get(string $name): mixed
{
return (fn () => $this->{$name})->call($this->obj);
}
public function __set(string $name, mixed $value): void
{
(fn () => $this->{$name} = $value)->call($this->obj);
}
public function __call(string $name, array $params = []): mixed
{
return (fn () => $this->{$name}(...$params))->call($this->obj);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Spatie\Invade;
use Exception;
class StaticInvader
{
private ?string $method = null;
/**
* @param class-string $className
*/
public function __construct(
public string $className,
) {
}
public function get(string $name): mixed
{
return (fn () => static::${$name})->bindTo(null, $this->className)();
}
public function set(string $name, mixed $value): void
{
(fn ($value) => static::${$name} = $value)->bindTo(null, $this->className)($value);
}
public function method(string $name): self
{
$this->method = $name;
return $this;
}
/**
* @throws Exception
*/
public function call(...$params): mixed
{
if ($this->method === null) {
throw new Exception(
'No method to be called. Use it like: invadeStatic(Foo::class)->method(\'bar\')->call()'
);
}
return (fn ($method) => static::{$method}(...$params))->bindTo(null, $this->className)($this->method);
}
}

21
vendor/spatie/invade/src/functions.php vendored Normal file
View File

@@ -0,0 +1,21 @@
<?php
use Spatie\Invade\Invader;
use Spatie\Invade\StaticInvader;
if (! function_exists('invade')) {
/**
* @template T of object
*
* @param T|class-string $object
* @return Invader<T>|StaticInvader
*/
function invade(object|string $object): Invader|StaticInvader
{
if (is_object($object)) {
return new Invader($object);
}
return new StaticInvader($object);
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) spatie <freek@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,553 @@
# Tools for creating Laravel packages
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-package-tools.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-package-tools)
![Tests](https://github.com/spatie/laravel-package-tools/workflows/Tests/badge.svg)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-package-tools.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-package-tools)
This package contains a `PackageServiceProvider` that you can use in your packages to easily register config files,
migrations, and more.
Here's an example of how it can be used.
```php
use Spatie\LaravelPackageTools\PackageServiceProvider;
use Spatie\LaravelPackageTools\Package;
use MyPackage\ViewComponents\Alert;
use Spatie\LaravelPackageTools\Commands\InstallCommand;
class YourPackageServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('your-package-name')
->hasConfigFile()
->hasViews()
->hasViewComponent('spatie', Alert::class)
->hasViewComposer('*', MyViewComposer::class)
->sharesDataWithAllViews('downloads', 3)
->hasTranslations()
->hasAssets()
->publishesServiceProvider('MyProviderName')
->hasRoute('web')
->hasMigration('create_package_tables')
->hasCommand(YourCoolPackageCommand::class)
->hasInstallCommand(function(InstallCommand $command) {
$command
->publishConfigFile()
->publishAssets()
->publishMigrations()
->copyAndRegisterServiceProviderInApp()
->askToStarRepoOnGitHub();
});
}
}
```
Under the hood it will do the necessary work to register the necessary things and make all sorts of files publishable.
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-package-tools.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-package-tools)
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can
support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards
on [our virtual postcard wall](https://spatie.be/open-source/postcards).
## Getting started
This package is opinionated on how you should structure your package. To get started easily, consider
using [our package-skeleton repo](https://github.com/spatie/package-skeleton-laravel) to start your package. The
skeleton is structured perfectly to work perfectly with the `PackageServiceProvider` in this package.
## Usage
In your package you should let your service provider extend `Spatie\LaravelPackageTools\PackageServiceProvider`.
```php
use Spatie\LaravelPackageTools\PackageServiceProvider;
use Spatie\LaravelPackageTools\Package;
class YourPackageServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package) : void
{
$package->name('your-package-name');
}
}
```
Passing the package name to `name` is mandatory.
### Working with a config file
To register a config file, you should create a php file with your package name in the `config` directory of your
package. In this example it should be at `<package root>/config/your-package-name.php`.
If your package name starts with `laravel-`, we expect that your config file does not contain that prefix. So if your
package name is `laravel-cool-package`, the config file should be named `cool-package.php`.
To register that config file, call `hasConfigFile()` on `$package` in the `configurePackage` method.
```php
$package
->name('your-package-name')
->hasConfigFile();
```
The `hasConfigFile` method will also make the config file publishable. Users of your package will be able to publish the
config file with this command.
```bash
php artisan vendor:publish --tag=your-package-name-config
```
Should your package have multiple config files, you can pass their names as an array to `hasConfigFile`
```php
$package
->name('your-package-name')
->hasConfigFile(['my-config-file', 'another-config-file']);
```
### Working with views
Any views your package provides, should be placed in the `<package root>/resources/views` directory.
You can register these views with the `hasViews` command.
```php
$package
->name('your-package-name')
->hasViews();
```
This will register your views with Laravel.
If you have a view `<package root>/resources/views/myView.blade.php`, you can use it like
this: `view('your-package-name::myView')`. Of course, you can also use subdirectories to organise your views. A view
located at `<package root>/resources/views/subdirectory/myOtherView.blade.php` can be used
with `view('your-package-name::subdirectory.myOtherView')`.
#### Using a custom view namespace
You can pass a custom view namespace to the `hasViews` method.
```php
$package
->name('your-package-name')
->hasViews('custom-view-namespace');
```
You can now use the views of the package like this:
```php
view('custom-view-namespace::myView');
```
#### Publishing the views
Calling `hasViews` will also make views publishable. Users of your package will be able to publish the views with this
command:
```bash
php artisan vendor:publish --tag=your-package-name-views
```
> **Note:**
>
> If you use custom view namespace then you should change your publish command like this:
```bash
php artisan vendor:publish --tag=custom-view-namespace-views
```
### Sharing global data with views
You can share data with all views using the `sharesDataWithAllViews` method. This will make the shared variable
available to all views.
```php
$package
->name('your-package-name')
->sharesDataWithAllViews('companyName', 'Spatie');
```
### Working with Blade view components
Any Blade view components that your package provides should be placed in the `<package root>/src/Components` directory.
You can register these views with the `hasViewComponents` command.
```php
$package
->name('your-package-name')
->hasViewComponents('spatie', Alert::class);
```
This will register your view components with Laravel. In the case of `Alert::class`, it can be referenced in views
as `<x-spatie-alert />`, where `spatie` is the prefix you provided during registration.
Calling `hasViewComponents` will also make view components publishable, and will be published
to `app/Views/Components/vendor/<package name>`.
Users of your package will be able to publish the view components with this command:
```bash
php artisan vendor:publish --tag=your-package-name-components
```
### Working with view composers
You can register any view composers that your project uses with the `hasViewComposers` method. You may also register a
callback that receives a `$view` argument instead of a classname.
To register a view composer with all views, use an asterisk as the view name `'*'`.
```php
$package
->name('your-package-name')
->hasViewComposer('viewName', MyViewComposer::class)
->hasViewComposer('*', function($view) {
$view->with('sharedVariable', 123);
});
```
### Working with inertia components
Any `.vue` or `.jsx` files your package provides, should be placed in the `<package root>/resources/js/Pages` directory.
You can register these components with the `hasInertiaComponents` command.
```php
$package
->name('your-package-name')
->hasInertiaComponents();
```
This will register your components with Laravel.
The user should publish the inertia components manually or using the [installer-command](#adding-an-installer-command) in order to use them.
If you have an inertia component `<package root>/resources/js/Pages/myComponent.vue`, you can use it like
this: `Inertia::render('YourPackageName/myComponent')`. Of course, you can also use subdirectories to organise your components.
#### Publishing inertia components
Calling `hasInertiaComponents` will also make inertia components publishable. Users of your package will be able to publish the views with this
command:
```bash
php artisan vendor:publish --tag=your-package-name-inertia-components
```
Also, the inertia components are available in a convenient way with your package [installer-command](#adding-an-installer-command)
### Working with translations
Any translations your package provides, should be placed in the `<package root>/resources/lang/<language-code>`
directory.
You can register these translations with the `hasTranslations` command.
```php
$package
->name('your-package-name')
->hasTranslations();
```
This will register the translations with Laravel.
Assuming you save this translation file at `<package root>/resources/lang/en/translations.php`...
```php
return [
'translatable' => 'translation',
];
```
... your package and users will be able to retrieve the translation with:
```php
trans('your-package-name::translations.translatable'); // returns 'translation'
```
If your package name starts with `laravel-` then you should leave that off in the example above.
Coding with translation strings as keys, you should create JSON files
in `<package root>/resources/lang/<language-code>.json`.
For example, creating `<package root>/resources/lang/it.json` file like so:
```json
{
"Hello!": "Ciao!"
}
```
...the output of...
```php
trans('Hello!');
```
...will be `Ciao!` if the application uses the Italian language.
Calling `hasTranslations` will also make translations publishable. Users of your package will be able to publish the
translations with this command:
```bash
php artisan vendor:publish --tag=your-package-name-translations
```
### Working with assets
Any assets your package provides, should be placed in the `<package root>/resources/dist/` directory.
You can make these assets publishable the `hasAssets` method.
```php
$package
->name('your-package-name')
->hasAssets();
```
Users of your package will be able to publish the assets with this command:
```bash
php artisan vendor:publish --tag=your-package-name-assets
```
This will copy over the assets to the `public/vendor/<your-package-name>` directory in the app where your package is
installed in.
### Working with migrations
The `PackageServiceProvider` assumes that any migrations are placed in this
directory: `<package root>/database/migrations`. Inside that directory you can put any migrations.
To register your migration, you should pass its name without the extension to the `hasMigration` table.
If your migration file is called `create_my_package_tables.php.stub` you can register them like this:
```php
$package
->name('your-package-name')
->hasMigration('create_my_package_tables');
```
Should your package contain multiple migration files, you can just call `hasMigration` multiple times or
use `hasMigrations`.
```php
$package
->name('your-package-name')
->hasMigrations(['my_package_tables', 'some_other_migration']);
```
Calling `hasMigration` will also make migrations publishable. Users of your package will be able to publish the
migrations with this command:
```bash
php artisan vendor:publish --tag=your-package-name-migrations
```
Like you might expect, published migration files will be prefixed with the current datetime.
You can also enable the migrations to be registered without needing the users of your package to publish them:
```php
$package
->name('your-package-name')
->hasMigrations(['my_package_tables', 'some_other_migration'])
->runsMigrations();
```
### Working with a publishable service provider
Some packages need an example service provider to be copied into the `app\Providers` directory of the Laravel app. Think
of for instance, the `laravel/horizon` package that copies an `HorizonServiceProvider` into your app with some sensible
defaults.
```php
$package
->name('your-package-name')
->publishesServiceProvider($nameOfYourServiceProvider);
```
The file that will be copied to the app should be stored in your package
in `/resources/stubs/{$nameOfYourServiceProvider}.php.stub`.
When your package is installed into an app, running this command...
```bash
php artisan vendor:publish --tag=your-package-name-provider
```
... will copy `/resources/stubs/{$nameOfYourServiceProvider}.php.stub` in your package
to `app/Providers/{$nameOfYourServiceProvider}.php` in the app of the user.
### Registering commands
You can register any command you package provides with the `hasCommand` function.
```php
$package
->name('your-package-name')
->hasCommand(YourCoolPackageCommand::class);
````
If your package provides multiple commands, you can either use `hasCommand` multiple times, or pass an array
to `hasCommands`
```php
$package
->name('your-package-name')
->hasCommands([
YourCoolPackageCommand::class,
YourOtherCoolPackageCommand::class,
]);
```
### Adding an installer command
Instead of letting your users manually publishing config files, migrations, and other files manually, you could opt to
add an install command that does all this work in one go. Packages like Laravel Horizon and Livewire provide such
commands.
When using Laravel Package Tools, you don't have to write an `InstallCommand` yourself. Instead, you can simply
call, `hasInstallCommand` and configure it using a closure. Here's an example.
```php
use Spatie\LaravelPackageTools\PackageServiceProvider;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\Commands\InstallCommand;
class YourPackageServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('your-package-name')
->hasConfigFile()
->hasMigration('create_package_tables')
->publishesServiceProvider('MyServiceProviderName')
->hasInstallCommand(function(InstallCommand $command) {
$command
->publishConfigFile()
->publishAssets()
->publishMigrations()
->askToRunMigrations()
->copyAndRegisterServiceProviderInApp()
->askToStarRepoOnGitHub('your-vendor/your-repo-name')
});
}
}
```
With this in place, the package user can call this command:
```bash
php artisan your-package-name:install
```
Using the code above, that command will:
- publish the config file
- publish the assets
- publish the migrations
- copy the `/resources/stubs/MyProviderName.php.stub` from your package to `app/Providers/MyServiceProviderName.php`, and also register that
provider in `config/app.php`
- ask if migrations should be run now
- prompt the user to open up `https://github.com/'your-vendor/your-repo-name'` in the browser in order to star it
You can also call `startWith` and `endWith` on the `InstallCommand`. They will respectively be executed at the start and
end when running `php artisan your-package-name:install`. You can use this to perform extra work or display extra
output.
```php
use Spatie\LaravelPackageTools\Commands\InstallCommand;
public function configurePackage(Package $package): void
{
$package
// ... configure package
->hasInstallCommand(function(InstallCommand $command) {
$command
->startWith(function(InstallCommand $command) {
$command->info('Hello, and welcome to my great new package!');
})
->publishConfigFile()
->publishAssets()
->publishMigrations()
->askToRunMigrations()
->copyAndRegisterServiceProviderInApp()
->askToStarRepoOnGitHub('your-vendor/your-repo-name')
->endWith(function(InstallCommand $command) {
$command->info('Have a great day!');
})
});
}
```
### Working with routes
The `PackageServiceProvider` assumes that any route files are placed in this directory: `<package root>/routes`. Inside
that directory you can put any route files.
To register your route, you should pass its name without the extension to the `hasRoute` method.
If your route file is called `web.php` you can register them like this:
```php
$package
->name('your-package-name')
->hasRoute('web');
```
Should your package contain multiple route files, you can just call `hasRoute` multiple times or use `hasRoutes`.
```php
$package
->name('your-package-name')
->hasRoutes(['web', 'admin']);
```
### Using lifecycle hooks
You can put any custom logic your package needs while starting up in one of these methods:
- `registeringPackage`: will be called at the start of the `register` method of `PackageServiceProvider`
- `packageRegistered`: will be called at the end of the `register` method of `PackageServiceProvider`
- `bootingPackage`: will be called at the start of the `boot` method of `PackageServiceProvider`
- `packageBooted`: will be called at the end of the `boot` method of `PackageServiceProvider`
## Testing
```bash
composer test
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Credits
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@@ -0,0 +1,50 @@
{
"name": "spatie/laravel-package-tools",
"description": "Tools for creating Laravel packages",
"keywords": [
"spatie",
"laravel-package-tools"
],
"homepage": "https://github.com/spatie/laravel-package-tools",
"license": "MIT",
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"require": {
"php": "^8.0",
"illuminate/contracts": "^9.28|^10.0|^11.0"
},
"require-dev": {
"mockery/mockery": "^1.5",
"orchestra/testbench": "^7.7|^8.0",
"pestphp/pest": "^1.22",
"phpunit/phpunit": "^9.5.24",
"spatie/pest-plugin-test-time": "^1.1"
},
"autoload": {
"psr-4": {
"Spatie\\LaravelPackageTools\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\LaravelPackageTools\\Tests\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,199 @@
<?php
namespace Spatie\LaravelPackageTools\Commands;
use Closure;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Spatie\LaravelPackageTools\Package;
class InstallCommand extends Command
{
protected Package $package;
public ?Closure $startWith = null;
protected array $publishes = [];
protected bool $askToRunMigrations = false;
protected bool $copyServiceProviderInApp = false;
protected ?string $starRepo = null;
public ?Closure $endWith = null;
public $hidden = true;
public function __construct(Package $package)
{
$this->signature = $package->shortName() . ':install';
$this->description = 'Install ' . $package->name;
$this->package = $package;
parent::__construct();
}
public function handle()
{
if ($this->startWith) {
($this->startWith)($this);
}
foreach ($this->publishes as $tag) {
$name = str_replace('-', ' ', $tag);
$this->comment("Publishing {$name}...");
$this->callSilently("vendor:publish", [
'--tag' => "{$this->package->shortName()}-{$tag}",
]);
}
if ($this->askToRunMigrations) {
if ($this->confirm('Would you like to run the migrations now?')) {
$this->comment('Running migrations...');
$this->call('migrate');
}
}
if ($this->copyServiceProviderInApp) {
$this->comment('Publishing service provider...');
$this->copyServiceProviderInApp();
}
if ($this->starRepo) {
if ($this->confirm('Would you like to star our repo on GitHub?')) {
$repoUrl = "https://github.com/{$this->starRepo}";
if (PHP_OS_FAMILY == 'Darwin') {
exec("open {$repoUrl}");
}
if (PHP_OS_FAMILY == 'Windows') {
exec("start {$repoUrl}");
}
if (PHP_OS_FAMILY == 'Linux') {
exec("xdg-open {$repoUrl}");
}
}
}
$this->info("{$this->package->shortName()} has been installed!");
if ($this->endWith) {
($this->endWith)($this);
}
}
public function publish(string ...$tag): self
{
$this->publishes = array_merge($this->publishes, $tag);
return $this;
}
public function publishConfigFile(): self
{
return $this->publish('config');
}
public function publishAssets(): self
{
return $this->publish('assets');
}
public function publishInertiaComponents(): self
{
return $this->publish('inertia-components');
}
public function publishMigrations(): self
{
return $this->publish('migrations');
}
public function askToRunMigrations(): self
{
$this->askToRunMigrations = true;
return $this;
}
public function copyAndRegisterServiceProviderInApp(): self
{
$this->copyServiceProviderInApp = true;
return $this;
}
public function askToStarRepoOnGitHub($vendorSlashRepoName): self
{
$this->starRepo = $vendorSlashRepoName;
return $this;
}
public function startWith($callable): self
{
$this->startWith = $callable;
return $this;
}
public function endWith($callable): self
{
$this->endWith = $callable;
return $this;
}
protected function copyServiceProviderInApp(): self
{
$providerName = $this->package->publishableProviderName;
if (! $providerName) {
return $this;
}
$this->callSilent('vendor:publish', ['--tag' => $this->package->shortName() . '-provider']);
$namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace());
if (intval(app()->version()) < 11 || ! file_exists(base_path('bootstrap/providers.php'))) {
$appConfig = file_get_contents(config_path('app.php'));
} else {
$appConfig = file_get_contents(base_path('bootstrap/providers.php'));
}
$class = '\\Providers\\' . $providerName . '::class';
if (Str::contains($appConfig, $namespace . $class)) {
return $this;
}
if (intval(app()->version()) < 11 || ! file_exists(base_path('bootstrap/providers.php'))) {
file_put_contents(config_path('app.php'), str_replace(
"{$namespace}\\Providers\\BroadcastServiceProvider::class,",
"{$namespace}\\Providers\\BroadcastServiceProvider::class," . PHP_EOL . " {$namespace}{$class},",
$appConfig
));
} else {
file_put_contents(base_path('bootstrap/providers.php'), str_replace(
"{$namespace}\\Providers\\AppServiceProvider::class,",
"{$namespace}\\Providers\\AppServiceProvider::class," . PHP_EOL . " {$namespace}{$class},",
$appConfig
));
}
file_put_contents(app_path('Providers/' . $providerName . '.php'), str_replace(
"namespace App\Providers;",
"namespace {$namespace}\Providers;",
file_get_contents(app_path('Providers/' . $providerName . '.php'))
));
return $this;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Spatie\LaravelPackageTools\Exceptions;
use Exception;
class InvalidPackage extends Exception
{
public static function nameIsRequired(): self
{
return new static('This package does not have a name. You can set one with `$package->name("yourName")`');
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Spatie\LaravelPackageTools;
use Illuminate\Support\Str;
use Spatie\LaravelPackageTools\Commands\InstallCommand;
class Package
{
public string $name;
public array $configFileNames = [];
public bool $hasViews = false;
public bool $hasInertiaComponents = false;
public ?string $viewNamespace = null;
public bool $hasTranslations = false;
public bool $hasAssets = false;
public bool $runsMigrations = false;
public array $migrationFileNames = [];
public array $routeFileNames = [];
public array $commands = [];
public array $consoleCommands = [];
public array $viewComponents = [];
public array $sharedViewData = [];
public array $viewComposers = [];
public string $basePath;
public ?string $publishableProviderName = null;
public function name(string $name): static
{
$this->name = $name;
return $this;
}
public function hasConfigFile($configFileName = null): static
{
$configFileName ??= $this->shortName();
if (! is_array($configFileName)) {
$configFileName = [$configFileName];
}
$this->configFileNames = $configFileName;
return $this;
}
public function publishesServiceProvider(string $providerName): static
{
$this->publishableProviderName = $providerName;
return $this;
}
public function hasInstallCommand($callable): static
{
$installCommand = new InstallCommand($this);
$callable($installCommand);
$this->consoleCommands[] = $installCommand;
return $this;
}
public function shortName(): string
{
return Str::after($this->name, 'laravel-');
}
public function hasViews(string $namespace = null): static
{
$this->hasViews = true;
$this->viewNamespace = $namespace;
return $this;
}
public function hasInertiaComponents(string $namespace = null): static
{
$this->hasInertiaComponents = true;
$this->viewNamespace = $namespace;
return $this;
}
public function hasViewComponent(string $prefix, string $viewComponentName): static
{
$this->viewComponents[$viewComponentName] = $prefix;
return $this;
}
public function hasViewComponents(string $prefix, ...$viewComponentNames): static
{
foreach ($viewComponentNames as $componentName) {
$this->viewComponents[$componentName] = $prefix;
}
return $this;
}
public function sharesDataWithAllViews(string $name, $value): static
{
$this->sharedViewData[$name] = $value;
return $this;
}
public function hasViewComposer($view, $viewComposer): static
{
if (! is_array($view)) {
$view = [$view];
}
foreach ($view as $viewName) {
$this->viewComposers[$viewName] = $viewComposer;
}
return $this;
}
public function hasTranslations(): static
{
$this->hasTranslations = true;
return $this;
}
public function hasAssets(): static
{
$this->hasAssets = true;
return $this;
}
public function runsMigrations(bool $runsMigrations = true): static
{
$this->runsMigrations = $runsMigrations;
return $this;
}
public function hasMigration(string $migrationFileName): static
{
$this->migrationFileNames[] = $migrationFileName;
return $this;
}
public function hasMigrations(...$migrationFileNames): static
{
$this->migrationFileNames = array_merge(
$this->migrationFileNames,
collect($migrationFileNames)->flatten()->toArray()
);
return $this;
}
public function hasCommand(string $commandClassName): static
{
$this->commands[] = $commandClassName;
return $this;
}
public function hasCommands(...$commandClassNames): static
{
$this->commands = array_merge($this->commands, collect($commandClassNames)->flatten()->toArray());
return $this;
}
public function hasConsoleCommand(string $commandClassName): static
{
$this->consoleCommands[] = $commandClassName;
return $this;
}
public function hasConsoleCommands(...$commandClassNames): static
{
$this->consoleCommands = array_merge($this->consoleCommands, collect($commandClassNames)->flatten()->toArray());
return $this;
}
public function hasRoute(string $routeFileName): static
{
$this->routeFileNames[] = $routeFileName;
return $this;
}
public function hasRoutes(...$routeFileNames): static
{
$this->routeFileNames = array_merge($this->routeFileNames, collect($routeFileNames)->flatten()->toArray());
return $this;
}
public function basePath(string $directory = null): string
{
if ($directory === null) {
return $this->basePath;
}
return $this->basePath . DIRECTORY_SEPARATOR . ltrim($directory, DIRECTORY_SEPARATOR);
}
public function viewNamespace(): string
{
return $this->viewNamespace ?? $this->shortName();
}
public function setBasePath(string $path): static
{
$this->basePath = $path;
return $this;
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Spatie\LaravelPackageTools;
use Carbon\Carbon;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use ReflectionClass;
use Spatie\LaravelPackageTools\Exceptions\InvalidPackage;
abstract class PackageServiceProvider extends ServiceProvider
{
protected Package $package;
abstract public function configurePackage(Package $package): void;
public function register()
{
$this->registeringPackage();
$this->package = $this->newPackage();
$this->package->setBasePath($this->getPackageBaseDir());
$this->configurePackage($this->package);
if (empty($this->package->name)) {
throw InvalidPackage::nameIsRequired();
}
foreach ($this->package->configFileNames as $configFileName) {
$this->mergeConfigFrom($this->package->basePath("/../config/{$configFileName}.php"), $configFileName);
}
$this->packageRegistered();
return $this;
}
public function newPackage(): Package
{
return new Package();
}
public function boot()
{
$this->bootingPackage();
if ($this->package->hasTranslations) {
$langPath = 'vendor/' . $this->package->shortName();
$langPath = (function_exists('lang_path'))
? lang_path($langPath)
: resource_path('lang/' . $langPath);
}
if ($this->app->runningInConsole()) {
foreach ($this->package->configFileNames as $configFileName) {
$this->publishes([
$this->package->basePath("/../config/{$configFileName}.php") => config_path("{$configFileName}.php"),
], "{$this->package->shortName()}-config");
}
if ($this->package->hasViews) {
$this->publishes([
$this->package->basePath('/../resources/views') => base_path("resources/views/vendor/{$this->packageView($this->package->viewNamespace)}"),
], "{$this->packageView($this->package->viewNamespace)}-views");
}
if ($this->package->hasInertiaComponents) {
$packageDirectoryName = Str::of($this->packageView($this->package->viewNamespace))->studly()->remove('-')->value();
$this->publishes([
$this->package->basePath('/../resources/js/Pages') => base_path("resources/js/Pages/{$packageDirectoryName}"),
], "{$this->packageView($this->package->viewNamespace)}-inertia-components");
}
$now = Carbon::now();
foreach ($this->package->migrationFileNames as $migrationFileName) {
$filePath = $this->package->basePath("/../database/migrations/{$migrationFileName}.php");
if (! file_exists($filePath)) {
// Support for the .stub file extension
$filePath .= '.stub';
}
$this->publishes([
$filePath => $this->generateMigrationName(
$migrationFileName,
$now->addSecond()
), ], "{$this->package->shortName()}-migrations");
if ($this->package->runsMigrations) {
$this->loadMigrationsFrom($filePath);
}
}
if ($this->package->hasTranslations) {
$this->publishes([
$this->package->basePath('/../resources/lang') => $langPath,
], "{$this->package->shortName()}-translations");
}
if ($this->package->hasAssets) {
$this->publishes([
$this->package->basePath('/../resources/dist') => public_path("vendor/{$this->package->shortName()}"),
], "{$this->package->shortName()}-assets");
}
}
if (! empty($this->package->commands)) {
$this->commands($this->package->commands);
}
if (! empty($this->package->consoleCommands) && $this->app->runningInConsole()) {
$this->commands($this->package->consoleCommands);
}
if ($this->package->hasTranslations) {
$this->loadTranslationsFrom(
$this->package->basePath('/../resources/lang/'),
$this->package->shortName()
);
$this->loadJsonTranslationsFrom($this->package->basePath('/../resources/lang/'));
$this->loadJsonTranslationsFrom($langPath);
}
if ($this->package->hasViews) {
$this->loadViewsFrom($this->package->basePath('/../resources/views'), $this->package->viewNamespace());
}
foreach ($this->package->viewComponents as $componentClass => $prefix) {
$this->loadViewComponentsAs($prefix, [$componentClass]);
}
if (count($this->package->viewComponents)) {
$this->publishes([
$this->package->basePath('/Components') => base_path("app/View/Components/vendor/{$this->package->shortName()}"),
], "{$this->package->name}-components");
}
if ($this->package->publishableProviderName) {
$this->publishes([
$this->package->basePath("/../resources/stubs/{$this->package->publishableProviderName}.php.stub") => base_path("app/Providers/{$this->package->publishableProviderName}.php"),
], "{$this->package->shortName()}-provider");
}
foreach ($this->package->routeFileNames as $routeFileName) {
$this->loadRoutesFrom("{$this->package->basePath('/../routes/')}{$routeFileName}.php");
}
foreach ($this->package->sharedViewData as $name => $value) {
View::share($name, $value);
}
foreach ($this->package->viewComposers as $viewName => $viewComposer) {
View::composer($viewName, $viewComposer);
}
$this->packageBooted();
return $this;
}
public static function generateMigrationName(string $migrationFileName, Carbon $now): string
{
$migrationsPath = 'migrations/' . dirname($migrationFileName) . '/';
$migrationFileName = basename($migrationFileName);
$len = strlen($migrationFileName) + 4;
if (Str::contains($migrationFileName, '/')) {
$migrationsPath .= Str::of($migrationFileName)->beforeLast('/')->finish('/');
$migrationFileName = Str::of($migrationFileName)->afterLast('/');
}
foreach (glob(database_path("{$migrationsPath}*.php")) as $filename) {
if ((substr($filename, -$len) === $migrationFileName . '.php')) {
return $filename;
}
}
return database_path($migrationsPath . $now->format('Y_m_d_His') . '_' . Str::of($migrationFileName)->snake()->finish('.php'));
}
public function registeringPackage()
{
}
public function packageRegistered()
{
}
public function bootingPackage()
{
}
public function packageBooted()
{
}
protected function getPackageBaseDir(): string
{
$reflector = new ReflectionClass(get_class($this));
return dirname($reflector->getFileName());
}
public function packageView(?string $namespace)
{
return is_null($namespace) ? $this->package->shortName() : $this->package->viewNamespace;
}
}

View File

@@ -0,0 +1,262 @@
# Changelog
All notable changes to `laravel-settings` will be documented in this file
# Unreleased
- Make `spatie/data-transfer-object` dependency optional. (#160)
## 3.3.1 - 2024-03-13
### What's Changed
* fix when base path is app path by @mvenghaus in https://github.com/spatie/laravel-settings/pull/259
**Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.3.0...3.3.1
## 3.3.0 - 2024-02-19
### What's Changed
* Update composer.json to use Larastan Org by @arnebr in https://github.com/spatie/laravel-settings/pull/252
* Add support for laravel 11 by @shuvroroy in https://github.com/spatie/laravel-settings/pull/256
* Added settings driven custom encoder/decoder by @naxvog in https://github.com/spatie/laravel-settings/pull/250
**Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.2.3...3.3.0
## 3.2.3 - 2023-12-04
- Revert "Use Illuminate\Database\Eloquent\Casts\Json if possible" (#249)
## 3.2.2 - 2023-12-01
- Use Illuminate\Database\Eloquent\Casts\Json if possible (#241)
## 3.2.1 - 2023-09-15
- Change provider tag name for config (#233)
## 3.2.0 - 2023-07-05
- Add support for database-less fakes
## 3.1.0 - 2023-05-11
- Add support for nullable enum properties
- Updates to the upgrade guide
## 3.0.0 - 2023-04-28
- Allow repositories to update multiple settings at once (#213 )
- The default location where searching for settings happens is now `app_path('Settings')` instead of `app_path()`
- The default `discovered_settings_cache_path` is changed
## 2.8.3 - 2023-03-30
- Remove doctrine as a dependency
## 2.8.2 - 2023-03-10
- Fix remigration problems with anonymous settings migrations
## 2.8.1 - 2023-03-02
- Show message and target path after setting migration created (#203)
- Follow Laravel's namespace convention in MakeSettingCommand (#200)
- Update MakeSettingsMigrationCommand.php (#205)
- Revert "Add support for structure discoverer"( #207)
## 2.8.0 - 2023-02-10
- Drop Laravel 8 support
- Drop PHP 8.0 support
- Use spatie/structures-discoverer for finding settings
## 2.7.0 - 2023-02-01
- Add Laravel 10 Support (#192)
- Update make:settings migration class as anonymous class (#189)
- Use correct namespace in make:settings command (#190)
## 2.6.1 - 2023-01-06
- Add current date to the settings migration file (#178)
- Add command to make new settings (#181)
## 1.6.1 - 2022-12-21
- create settings migration with current date (#179)
## 2.6.0 - 2022-11-24
- Add support for caching on repository level
## 2.5.0 - 2022-11-10
- Remove deprecated package
- Add laravel data cast
- Add support for PHP 8.2
- Remove PHP 7.4 support
- Remove dto cast from default config
## 2.4.5 - 2022-09-28
- Add deleteIfExists() method to migrator (#154)
## 2.4.4 - 2022-09-07
- cache encrypted settings
Please, be sure to clear your cache since settings classes with encrypted properties will crash due to the cached versions missing a proper encrypted version of the property. Clearing and caching again after installing this version resolves this problem and is something you probably should always do when deploying to production!
## 2.4.3 - 2022-08-10
- add rollback to migration
## 2.4.2 - 2022-06-17
- use Facade imports instead of aliases (#132)
## 2.4.1 - 2022-04-07
- Switch to using scoped instances instead of singletons (#129)
## 2.4.0 - 2022-03-22
## What's Changed
- Add TTL config for settings cache by @AlexVanderbist in https://github.com/spatie/laravel-settings/pull/122
## New Contributors
- @AlexVanderbist made their first contribution in https://github.com/spatie/laravel-settings/pull/122
**Full Changelog**: https://github.com/spatie/laravel-settings/compare/2.3.3...2.4.0
## 2.3.3 - 2022-03-18
- fix debug info method
- convert PHPUnit to Pest (#118)
## 2.3.2 - 2022-02-25
- Allow migrations without a value (#113)
## 2.3.1 - 2022-02-04
- Add support for Laravel 9
- Fix cache implementation with casts
- Remove Psalm
- Add PHPStan
## 2.2.0 - 2021-10-22
- add support for multiple migration paths (#92)
## 2.1.12 - 2021-10-14
- add possibility to check if setting is locked or unlocked (#89)
## 2.1.11 - 2021-08-23
- ignore abstract classes when discovering settings (#84)
## 2.1.10 - 2021-08-17
- add support for `null` in DateTime casts
## 2.1.9 - 2021-07-08
- fix `empty` call not working when properties weren't loaded
## 2.1.8 - 2021-06-21
- fix fake settings not working with `Arrayable`
## 2.1.7 - 2021-06-08
- add support for refreshing settings
## 2.1.6 - 2021-06-03
- add support for defining the database connection table
## 2.1.5 - 2021-05-21
- fix some casting problems
- update php-cs-fixer
## 2.1.4 - 2021-04-28
- added fallback for settings.auto_discover_settings (#63)
- add support for spatie/data-transfer-object v3 (#62)
## 2.1.3 - 2021-04-14
- add support for spatie/temporary-directory v2
## 2.1.2 - 2021-04-08
- skip classes with errors when discovering settings
## 2.1.1 - 2021-04-07
- add better support for nullable types in docblocks
## 2.1.0 - 2021-04-07
- add casts to migrations (#53)
- add original properties to `SavingSettings` event (#57)
## 2.0.1 - 2021-03-05
- add support for lumen
## 2.0.0 - 2021-03-03
- settings classes:
- properties won't be loaded when constructed but when requested
- receive a `SettingsMapper` when constructed
- faking settings will now only request non-given properties from the repository
- rewritten `SettingsMapper` from scratch
- removed `SettingsPropertyData` and `ettingsPropertyDataCollection`
- changed signatures of `SavingSettings` and `LoadingSettings` events
- added support for caching settings
- renamed `cache_path` in settings.php to `discovered_settings_cache_path`
## 1.0.8 - 2021-03-03
- fix for properties without defined type
## 1.0.7 - 2021-02-19
- fix correct 'Event' facade (#30)
## 1.0.6 - 2021-02-05
- add support for restoring settings after a Laravel schema:dump
## 1.0.5 - 2021-01-29
- bump the `doctrine/dbal` dependency
## 1.0.4 - 2021-01-08
- add support for getting the locked settings
## 1.0.3 - 2020-11-26
- add PHP 8 support
## 1.0.2 - 2020-11-26
- fix package namespace within migrations (#9)
## 1.0.1 - 2020-11-18
- fix config file tag (#4)
- fix database migration path exists (#7)
## 1.0.0 - 2020-11-09
- initial release

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1134
vendor/spatie/laravel-settings/README.md vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
# Upgrading
Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide.
## From v2 to v3
This should be a quick update:
- When creating a new project, the default search location for settings classes will be in the `app_path('Settings')` directory. If you want to keep the old location, then you can set the `auto_discover_settings` option to `app_path()`. For applications which already have published their config, nothing changes.
- If you're implementing custom repositories, then update them according to the interface. The method `updatePropertyPayload` is renamed to `updatePropertiesPayload` and should now update multiple properties at once.
- Add a new migration with the following content
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('settings', function (Blueprint $table): void {
$table->boolean('locked')->default(false)->change();
$table->unique(['group', 'name']);
$table->dropIndex(['group']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('settings', function (Blueprint $table): void {
$table->boolean('locked')->default(null)->change();
$table->dropUnique(['group', 'name']);
$table->index('group');
});
}
};
```

View File

@@ -0,0 +1,75 @@
{
"name" : "spatie/laravel-settings",
"description" : "Store your application settings",
"keywords" : [
"spatie",
"laravel-settings"
],
"homepage" : "https://github.com/spatie/laravel-settings",
"license" : "MIT",
"authors" : [
{
"name" : "Ruben Van Assche",
"email" : "ruben@spatie.be",
"homepage" : "https://spatie.be",
"role" : "Developer"
}
],
"require" : {
"php" : "^7.4|^8.0",
"ext-json" : "*",
"illuminate/database" : "^8.73|^9.0|^10.0|^11.0",
"phpdocumentor/type-resolver" : "^1.5",
"spatie/temporary-directory" : "^1.3|^2.0"
},
"require-dev" : {
"ext-redis": "*",
"mockery/mockery": "^1.4",
"larastan/larastan": "^2.0",
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0",
"pestphp/pest": "^1.21|^2.0",
"pestphp/pest-plugin-laravel": "^1.2|^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.5|^10.0",
"spatie/laravel-data": "^1.0.0|^2.0.0|^4.0.0",
"spatie/pest-plugin-snapshots": "^1.1|^2.0",
"spatie/phpunit-snapshot-assertions": "^4.2|^5.0",
"spatie/ray": "^1.36"
},
"suggest" : {
"spatie/data-transfer-object" : "Allows for DTO casting to settings. (deprecated)"
},
"autoload" : {
"psr-4" : {
"Spatie\\LaravelSettings\\" : "src"
}
},
"autoload-dev" : {
"psr-4" : {
"Spatie\\LaravelSettings\\Tests\\" : "tests"
}
},
"scripts" : {
"analyse" : "vendor/bin/phpstan analyse",
"test" : "vendor/bin/pest",
"test-coverage" : "vendor/bin/pest --coverage"
},
"config" : {
"sort-packages" : true,
"allow-plugins" : {
"pestphp/pest-plugin" : true,
"phpstan/extension-installer" : true
}
},
"extra" : {
"laravel" : {
"providers" : [
"Spatie\\LaravelSettings\\LaravelSettingsServiceProvider"
]
}
},
"minimum-stability" : "dev",
"prefer-stable" : true
}

View File

@@ -0,0 +1,94 @@
<?php
return [
/*
* Each settings class used in your application must be registered, you can
* put them (manually) here.
*/
'settings' => [
],
/*
* The path where the settings classes will be created.
*/
'setting_class_path' => app_path('Settings'),
/*
* In these directories settings migrations will be stored and ran when migrating. A settings
* migration created via the make:settings-migration command will be stored in the first path or
* a custom defined path when running the command.
*/
'migrations_paths' => [
database_path('settings'),
],
/*
* When no repository was set for a settings class the following repository
* will be used for loading and saving settings.
*/
'default_repository' => 'database',
/*
* Settings will be stored and loaded from these repositories.
*/
'repositories' => [
'database' => [
'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class,
'model' => null,
'table' => null,
'connection' => null,
],
'redis' => [
'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class,
'connection' => null,
'prefix' => null,
],
],
/*
* The encoder and decoder will determine how settings are stored and
* retrieved in the database. By default, `json_encode` and `json_decode`
* are used.
*/
'encoder' => null,
'decoder' => null,
/*
* The contents of settings classes can be cached through your application,
* settings will be stored within a provided Laravel store and can have an
* additional prefix.
*/
'cache' => [
'enabled' => env('SETTINGS_CACHE_ENABLED', false),
'store' => null,
'prefix' => null,
'ttl' => null,
],
/*
* These global casts will be automatically used whenever a property within
* your settings class isn't a default PHP type.
*/
'global_casts' => [
DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class,
DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class,
// Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class,
Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class,
],
/*
* The package will look for settings in these paths and automatically
* register them.
*/
'auto_discover_settings' => [
app_path('Settings'),
],
/*
* Automatically discovered settings classes can be cached, so they don't
* need to be searched each time the application boots up.
*/
'discovered_settings_cache_path' => base_path('bootstrap/cache'),
];

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('settings', function (Blueprint $table): void {
$table->id();
$table->string('group');
$table->string('name');
$table->boolean('locked')->default(false);
$table->json('payload');
$table->timestamps();
$table->unique(['group', 'name']);
});
}
};

View File

@@ -0,0 +1,36 @@
<?php
namespace Spatie\LaravelSettings\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Spatie\LaravelSettings\SettingsContainer;
class CacheDiscoveredSettingsCommand extends Command
{
protected $signature = 'settings:discover';
protected $description = 'Cache all auto discovered settings';
public function handle(SettingsContainer $container, Filesystem $files): void
{
$this->info('Caching registered settings...');
$container
->clearCache()
->getSettingClasses()
->pipe(function (Collection $settingClasses) use ($files) {
$cachePath = config('settings.discovered_settings_cache_path');
$files->makeDirectory($cachePath, 0755, true, true);
$files->put(
$cachePath . '/settings.php',
'<?php return ' . var_export($settingClasses->toArray(), true) . ';'
);
});
$this->info('All done!');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Spatie\LaravelSettings\Console;
use Illuminate\Console\Command;
use Spatie\LaravelSettings\Support\SettingsCacheFactory;
class ClearCachedSettingsCommand extends Command
{
protected $signature = 'settings:clear-cache';
protected $description = 'Clear cached settings';
public function handle(SettingsCacheFactory $settingsCacheFactory): void
{
foreach ($settingsCacheFactory->all() as $settingsCache) {
$settingsCache->clear();
}
$this->info('Cached settings cleared!');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Spatie\LaravelSettings\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class ClearDiscoveredSettingsCacheCommand extends Command
{
protected $signature = 'settings:clear-discovered';
protected $description = 'Clear cached auto discovered registered settings classes';
public function handle(Filesystem $files): void
{
$files->delete(config('settings.discovered_settings_cache_path') . '/settings.php');
$this->info('Cached discovered settings cleared!');
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace Spatie\LaravelSettings\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use InvalidArgumentException;
class MakeSettingCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:setting {name : The name of the setting class} {--group=default : The group name} {--path= : Path to write the setting class file to}';
/**
* The console command name.
*
* @var string
*/
protected $name = 'make:setting';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new Settings Class';
/**
* @var Filesystem
*/
protected Filesystem $files;
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
public function handle()
{
$name = trim($this->input->getArgument('name'));
$group = trim($this->input->getOption('group'));
$path = trim($this->input->getOption('path'));
if (empty($path)) {
$path = $this->resolveSettingsPath();
}
$this->ensureSettingClassDoesntAlreadyExist($name, $path);
$this->files->ensureDirectoryExists($path);
$this->files->put(
$this->getPath($name, $path),
$this->getContent($name, $group, $path)
);
}
protected function getStub(): string
{
return <<<EOT
<?php
namespace {{ namespace }};
use Spatie\LaravelSettings\Settings;
class {{ class }} extends Settings
{
public static function group(): string
{
return '{{ group }}';
}
}
EOT;
}
protected function getContent($name, $group, $path)
{
return str_replace(
['{{ namespace }}', '{{ class }}', '{{ group }}'],
[$this->getNamespace($path), $name, $group],
$this->getStub()
);
}
protected function ensureSettingClassDoesntAlreadyExist($name, $path): void
{
if ($this->files->exists($this->getPath($name, $path))) {
throw new InvalidArgumentException(sprintf('%s already exists!', $name));
}
}
protected function resolveSettingsPath(): string
{
return config('settings.setting_class_path', app_path('Settings'));
}
protected function getPath($name, $path): string
{
return $path . '/' . $name . '.php';
}
protected function getNamespace($path): string
{
$path = preg_replace(
[
'/^(' . preg_quote(base_path(), '/') . ')/',
'/\//',
],
[
'',
'\\',
],
$path
);
$namespace = implode('\\', array_map(fn ($directory) => ucfirst($directory), explode('\\', $path)));
// Remove leading backslash if present
if (substr($namespace, 0, 1) === '\\') {
$namespace = substr($namespace, 1);
}
return $namespace;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Spatie\LaravelSettings\Console;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use InvalidArgumentException;
class MakeSettingsMigrationCommand extends Command
{
protected $signature = 'make:settings-migration {name : The name of the migration} {path? : Path to write migration file to}';
protected $description = 'Create a new settings migration file';
protected Filesystem $files;
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
public function handle(): void
{
$name = trim($this->input->getArgument('name'));
$path = trim($this->input->getArgument('path'));
// If path is still empty we get the first path from new settings.migrations_paths config
if (empty($path)) {
$path = $this->resolveMigrationPaths()[0];
}
$this->ensureMigrationDoesntAlreadyExist($name, $path);
$this->files->ensureDirectoryExists($path);
$this->files->put(
$file = $this->getPath($name, $path),
$this->getStub()
);
$this->info(sprintf('Setting migration [%s] created successfully.', $file));
}
protected function getStub(): string
{
return <<<EOT
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
}
};
EOT;
}
protected function ensureMigrationDoesntAlreadyExist($name, $migrationPath = null): void
{
if (! empty($migrationPath)) {
$migrationFiles = $this->files->glob($migrationPath . '/*.php');
foreach ($migrationFiles as $migrationFile) {
$this->files->requireOnce($migrationFile);
}
}
if (class_exists($className = Str::studly($name))) {
throw new InvalidArgumentException("A {$className} class already exists.");
}
}
protected function getPath($name, $path): string
{
return $path . '/' . Carbon::now()->format('Y_m_d_His') . '_' . Str::snake($name) . '.php';
}
protected function resolveMigrationPaths(): array
{
return ! empty(config('settings.migrations_path'))
? [config('settings.migrations_path')]
: config('settings.migrations_paths');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelSettings\Events;
use Illuminate\Support\Collection;
class LoadingSettings
{
public string $settingsClass;
public Collection $properties;
public function __construct(string $settingsClass, Collection $properties)
{
$this->settingsClass = $settingsClass;
$this->properties = $properties;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Spatie\LaravelSettings\Events;
use Illuminate\Support\Collection;
use Spatie\LaravelSettings\Settings;
class SavingSettings
{
public Settings $settings;
public Collection $properties;
public ?Collection $originalValues;
public function __construct(
Collection $properties,
?Collection $originalValues,
Settings $settings
) {
$this->properties = $properties;
$this->originalValues = $originalValues;
$this->settings = $settings;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Spatie\LaravelSettings\Events;
use Spatie\LaravelSettings\Settings;
class SettingsLoaded
{
public Settings $settings;
public function __construct(Settings $settings)
{
$this->settings = $settings;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Spatie\LaravelSettings\Events;
use Spatie\LaravelSettings\Settings;
class SettingsSaved
{
public Settings $settings;
public function __construct(Settings $settings)
{
$this->settings = $settings;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Spatie\LaravelSettings\Exceptions;
use Exception;
use ReflectionProperty;
class CouldNotResolveDocblockType extends Exception
{
public static function create(
string $type,
ReflectionProperty $reflectionProperty
): self {
return new self("Could not resolve type in docblock: `{$type}` of property `{$reflectionProperty->getDeclaringClass()->getName()}::{$reflectionProperty->getName()}`");
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Spatie\LaravelSettings\Exceptions;
use Exception;
class CouldNotUnserializeSettings extends Exception
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Spatie\LaravelSettings\Exceptions;
use Exception;
class InvalidSettingName extends Exception
{
public static function create(string $property): self
{
return new self("Setting {$property} is invalid, it should be formatted as such: group.name");
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Spatie\LaravelSettings\Exceptions;
use Exception;
class MissingSettings extends Exception
{
public static function create(string $settingsClass, array $missingProperties, string $operation): self
{
$missing = implode(', ', $missingProperties);
return new self("Tried {$operation} settings '{$settingsClass}', and following properties were missing: {$missing}");
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelSettings\Exceptions;
use Exception;
class SettingAlreadyExists extends Exception
{
public static function whenAdding(string $property): self
{
throw new self("Could not create setting {$property} because it already exists");
}
public static function whenRenaming(string $from, string $to): self
{
return new self("Could not rename setting {$from} to {$to} because it already exists");
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Spatie\LaravelSettings\Exceptions;
use Exception;
class SettingDoesNotExist extends Exception
{
public static function whenDeleting(string $property): self
{
return new self("Could not delete setting {$property} because it does not exist");
}
public static function whenEditing(string $property): self
{
return new self("Could not edit setting {$property} because it does not exist");
}
public static function whenRenaming(string $from, string $to): self
{
return new self("Could not rename setting {$from} to {$to} because it does not exist");
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Spatie\LaravelSettings\Exceptions;
use Exception;
class SettingsCacheDisabled extends Exception
{
public static function create(): self
{
return new self('Settings cache is not enabled');
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Spatie\LaravelSettings\Factories;
use Illuminate\Support\Str;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\AbstractList;
use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\String_;
use ReflectionProperty;
use Spatie\LaravelSettings\SettingsCasts\ArraySettingsCast;
use Spatie\LaravelSettings\SettingsCasts\EnumCast;
use Spatie\LaravelSettings\SettingsCasts\SettingsCast;
use Spatie\LaravelSettings\Support\PropertyReflector;
class SettingsCastFactory
{
public static function resolve(
ReflectionProperty $reflectionProperty,
array $localCasts
): ?SettingsCast {
$name = $reflectionProperty->getName();
$reflectedType = PropertyReflector::resolveType($reflectionProperty);
if (array_key_exists($name, $localCasts)) {
return self::createLocalCast($localCasts[$name], $reflectedType);
}
if ($reflectedType === null) {
return null;
}
return self::createDefaultCast($reflectedType);
}
/**
* @param string|SettingsCast $castDefinition
* @param \phpDocumentor\Reflection\Type|null $type
*
* @return \Spatie\LaravelSettings\SettingsCasts\SettingsCast
*/
protected static function createLocalCast(
$castDefinition,
?Type $type
): SettingsCast {
if ($castDefinition instanceof SettingsCast) {
return $castDefinition;
}
$castClass = Str::before($castDefinition, ':');
$arguments = Str::contains($castDefinition, ':')
? explode(',', Str::after($castDefinition, ':'))
: [];
$reflectedType = self::getLocalCastReflectedType($type);
if ($reflectedType) {
array_push($arguments, $reflectedType);
}
return new $castClass(...$arguments);
}
protected static function createDefaultCast(
Type $type
): ?SettingsCast {
$noCastRequired = self::isTypeWithNoCastRequired($type)
|| ($type instanceof AbstractList && self::isTypeWithNoCastRequired($type->getValueType()))
|| ($type instanceof Nullable && self::isTypeWithNoCastRequired($type->getActualType()));
if ($noCastRequired) {
return null;
}
if ($type instanceof AbstractList) {
return new ArraySettingsCast(self::createDefaultCast($type->getValueType()));
}
if ($type instanceof Nullable) {
return self::createDefaultCast($type->getActualType());
}
if (! $type instanceof Object_) {
return null;
}
$className = self::getObjectClassName($type);
if (enum_exists($className)) {
return new EnumCast($className);
}
foreach (config('settings.global_casts', []) as $base => $cast) {
if (self::shouldCast($className, $base)) {
return new $cast($className);
}
}
return null;
}
protected static function isTypeWithNoCastRequired(Type $type): bool
{
return $type instanceof Integer
|| $type instanceof Boolean
|| $type instanceof Float_
|| $type instanceof String_;
}
protected static function shouldCast(string $type, string $base): bool
{
return $type === $base
|| in_array($type, class_implements($base))
|| is_subclass_of($type, $base);
}
protected static function getLocalCastReflectedType(?Type $type): ?string
{
if ($type instanceof Object_) {
return self::getObjectClassName($type);
}
if ($type instanceof Nullable) {
return self::getLocalCastReflectedType($type->getActualType());
}
return null;
}
protected static function getObjectClassName(Object_ $type): string
{
return ltrim((string ) $type->getFqsen(), '\\');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Spatie\LaravelSettings\Factories;
use Exception;
use Spatie\LaravelSettings\SettingsRepositories\SettingsRepository;
class SettingsRepositoryFactory
{
public static function create(?string $name = null): SettingsRepository
{
$name ??= config('settings.default_repository');
if (! array_key_exists($name, config('settings.repositories'))) {
throw new Exception("Tried to create unknown settings repository: {$name}");
}
$config = config("settings.repositories.{$name}");
return app($config['type'], [
'config' => $config,
]);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Spatie\LaravelSettings;
use Illuminate\Database\Events\SchemaLoaded;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Spatie\LaravelSettings\Console\CacheDiscoveredSettingsCommand;
use Spatie\LaravelSettings\Console\ClearCachedSettingsCommand;
use Spatie\LaravelSettings\Console\ClearDiscoveredSettingsCacheCommand;
use Spatie\LaravelSettings\Console\MakeSettingCommand;
use Spatie\LaravelSettings\Console\MakeSettingsMigrationCommand;
use Spatie\LaravelSettings\Factories\SettingsRepositoryFactory;
use Spatie\LaravelSettings\Migrations\SettingsMigration;
use Spatie\LaravelSettings\SettingsRepositories\SettingsRepository;
use Spatie\LaravelSettings\Support\SettingsCacheFactory;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
class LaravelSettingsServiceProvider extends ServiceProvider
{
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/settings.php' => config_path('settings.php'),
], 'config');
if (! class_exists('CreateSettingsTable')) {
$this->publishes([
__DIR__ . '/../database/migrations/create_settings_table.php.stub' => database_path('migrations/2022_12_14_083707_create_settings_table.php'),
], 'migrations');
}
$this->commands([
MakeSettingCommand::class,
MakeSettingsMigrationCommand::class,
CacheDiscoveredSettingsCommand::class,
ClearDiscoveredSettingsCacheCommand::class,
ClearCachedSettingsCommand::class,
]);
}
Event::subscribe(SettingsEventSubscriber::class);
Event::listen(SchemaLoaded::class, fn ($event) => $this->removeMigrationsWhenSchemaLoaded($event));
$this->loadMigrationsFrom($this->resolveMigrationPaths());
}
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../config/settings.php', 'settings');
$this->app->bind(SettingsRepository::class, fn () => SettingsRepositoryFactory::create());
$this->app->bind(SettingsCacheFactory::class, fn () => new SettingsCacheFactory(
config('settings'),
));
$this->app->scoped(SettingsMapper::class);
$settingsContainer = app(SettingsContainer::class);
$settingsContainer->registerBindings();
}
private function removeMigrationsWhenSchemaLoaded(SchemaLoaded $event)
{
$files = Finder::create()
->files()
->ignoreDotFiles(true)
->in($this->resolveMigrationPaths())
->depth(0);
$migrations = collect(iterator_to_array($files))
->map(function (SplFileInfo $file) {
$contents = file_get_contents($file->getRealPath());
if (
str_contains($contents, 'return new class extends '.SettingsMigration::class)
|| str_contains($contents, 'return new class extends SettingsMigration')
) {
return $file->getBasename('.php');
}
preg_match('/class\s*(?P<className>\w*)\s*extends/', $contents, $found);
if (empty($found['className'])) {
return null;
}
require_once $file->getRealPath();
if (! is_subclass_of($found['className'], SettingsMigration::class)) {
return null;
}
return $file->getBasename('.php');
})
->filter()
->values();
$migrationsConfig = config()->get('database.migrations');
$migrationsTable = is_array($migrationsConfig) ? ($migrationsConfig['table'] ?? null) : $migrationsConfig;
$event->connection
->table($migrationsTable)
->useWritePdo()
->whereIn('migration', $migrations)
->delete();
}
protected function resolveMigrationPaths(): array
{
return ! empty(config('settings.migrations_path'))
? [config('settings.migrations_path')]
: config('settings.migrations_paths');
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Spatie\LaravelSettings\Migrations;
use Closure;
class SettingsBlueprint
{
protected string $group;
protected SettingsMigrator $migrator;
public function __construct(string $group, SettingsMigrator $migrator)
{
$this->group = $group;
$this->migrator = $migrator;
}
public function rename(string $from, string $to): void
{
$this->migrator->rename(
$this->prependWithGroup($from),
$this->prependWithGroup($to)
);
}
public function add(string $name, $value = null, bool $encrypted = false): void
{
$this->migrator->add($this->prependWithGroup($name), $value, $encrypted);
}
public function delete(string $name): void
{
$this->migrator->delete($this->prependWithGroup($name));
}
public function update(string $name, Closure $closure, bool $encrypted = false): void
{
$this->migrator->update($this->prependWithGroup($name), $closure, $encrypted);
}
public function addEncrypted(string $name, $value = null): void
{
$this->migrator->addEncrypted($this->prependWithGroup($name), $value);
}
public function updateEncrypted(string $name, Closure $closure): void
{
$this->migrator->updateEncrypted($this->prependWithGroup($name), $closure);
}
public function encrypt(string $name): void
{
$this->migrator->encrypt($this->prependWithGroup($name));
}
public function decrypt(string $name): void
{
$this->migrator->decrypt($this->prependWithGroup($name));
}
protected function prependWithGroup(string $name): string
{
return "{$this->group}.{$name}";
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Spatie\LaravelSettings\Migrations;
use Illuminate\Database\Migrations\Migration;
abstract class SettingsMigration extends Migration
{
protected SettingsMigrator $migrator;
abstract public function up();
public function __construct()
{
$this->migrator = app(SettingsMigrator::class);
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Spatie\LaravelSettings\Migrations;
use Closure;
use Illuminate\Support\Collection;
use Spatie\LaravelSettings\Exceptions\InvalidSettingName;
use Spatie\LaravelSettings\Exceptions\SettingAlreadyExists;
use Spatie\LaravelSettings\Exceptions\SettingDoesNotExist;
use Spatie\LaravelSettings\Factories\SettingsRepositoryFactory;
use Spatie\LaravelSettings\SettingsCasts\SettingsCast;
use Spatie\LaravelSettings\SettingsConfig;
use Spatie\LaravelSettings\SettingsContainer;
use Spatie\LaravelSettings\SettingsRepositories\SettingsRepository;
use Spatie\LaravelSettings\Support\Crypto;
class SettingsMigrator
{
protected SettingsRepository $repository;
public function __construct(SettingsRepository $connection)
{
$this->repository = $connection;
}
public function repository(string $name): self
{
$this->repository = SettingsRepositoryFactory::create($name);
return $this;
}
public function rename(string $from, string $to): void
{
if (! $this->checkIfPropertyExists($from)) {
throw SettingDoesNotExist::whenRenaming($from, $to);
}
if ($this->checkIfPropertyExists($to)) {
throw SettingAlreadyExists::whenRenaming($from, $to);
}
$this->createProperty(
$to,
$this->getPropertyPayload($from)
);
$this->deleteProperty($from);
}
public function add(string $property, $value = null, bool $encrypted = false): void
{
if ($this->checkIfPropertyExists($property)) {
throw SettingAlreadyExists::whenAdding($property);
}
if ($encrypted) {
$value = Crypto::encrypt($value);
}
$this->createProperty($property, $value);
}
public function delete(string $property): void
{
if (! $this->checkIfPropertyExists($property)) {
throw SettingDoesNotExist::whenDeleting($property);
}
$this->deleteProperty($property);
}
public function deleteIfExists(string $property): void
{
if ($this->checkIfPropertyExists($property)) {
$this->deleteProperty($property);
}
}
public function update(string $property, Closure $closure, bool $encrypted = false): void
{
if (! $this->checkIfPropertyExists($property)) {
throw SettingDoesNotExist::whenEditing($property);
}
$originalPayload = $encrypted
? Crypto::decrypt($this->getPropertyPayload($property))
: $this->getPropertyPayload($property);
$updatedPayload = $encrypted
? Crypto::encrypt($closure($originalPayload))
: $closure($originalPayload);
$this->updatePropertyPayload($property, $updatedPayload);
}
public function addEncrypted(string $property, $value = null): void
{
$this->add($property, $value, true);
}
public function updateEncrypted(string $property, Closure $closure): void
{
$this->update($property, $closure, true);
}
public function encrypt(string $property): void
{
$this->update($property, fn ($payload) => Crypto::encrypt($payload));
}
public function decrypt(string $property): void
{
$this->update($property, fn ($payload) => Crypto::decrypt($payload));
}
public function inGroup(string $group, Closure $closure): void
{
$closure(new SettingsBlueprint($group, $this));
}
protected function getPropertyParts(string $property): array
{
$propertyParts = explode('.', $property);
if (count($propertyParts) !== 2) {
throw InvalidSettingName::create($property);
}
return ['group' => $propertyParts[0], 'name' => $propertyParts[1]];
}
protected function checkIfPropertyExists(string $property): bool
{
['group' => $group, 'name' => $name] = $this->getPropertyParts($property);
return $this->repository->checkIfPropertyExists($group, $name);
}
protected function getPropertyPayload(string $property)
{
['group' => $group, 'name' => $name] = $this->getPropertyParts($property);
$payload = $this->repository->getPropertyPayload($group, $name);
return optional($this->getCast($group, $name))->get($payload) ?: $payload;
}
protected function createProperty(string $property, $payload): void
{
['group' => $group, 'name' => $name] = $this->getPropertyParts($property);
if (is_object($payload)) {
$payload = optional($this->getCast($group, $name))->set($payload) ?: $payload;
}
$this->repository->createProperty($group, $name, $payload);
}
protected function updatePropertyPayload(string $property, $payload): void
{
['group' => $group, 'name' => $name] = $this->getPropertyParts($property);
if (is_object($payload)) {
$payload = optional($this->getCast($group, $name))->set($payload) ?: $payload;
}
$this->repository->updatePropertiesPayload($group, [$name => $payload]);
}
protected function deleteProperty(string $property): void
{
['group' => $group, 'name' => $name] = $this->getPropertyParts($property);
$this->repository->deleteProperty($group, $name);
}
protected function getCast(string $group, string $name): ?SettingsCast
{
return optional($this->settingsGroups()->get($group))->getCast($name);
}
protected function settingsGroups(): Collection
{
return app(SettingsContainer::class)
->getSettingClasses()
->mapWithKeys(fn (string $settingsClass) => [
$settingsClass::group() => new SettingsConfig($settingsClass),
]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Spatie\LaravelSettings\Models;
use Illuminate\Database\Eloquent\Model;
class SettingsProperty extends Model
{
protected $table = 'settings';
protected $guarded = [];
protected $casts = [
'locked' => 'boolean',
];
public static function get(string $property)
{
[$group, $name] = explode('.', $property);
$setting = self::query()
->where('group', $group)
->where('name', $name)
->first('payload');
return json_decode($setting->getAttribute('payload'));
}
}

View File

@@ -0,0 +1,301 @@
<?php
namespace Spatie\LaravelSettings;
use Exception;
use Illuminate\Container\Container;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Support\Collection;
use ReflectionProperty;
use Spatie\LaravelSettings\Events\SavingSettings;
use Spatie\LaravelSettings\Events\SettingsLoaded;
use Spatie\LaravelSettings\Events\SettingsSaved;
use Spatie\LaravelSettings\Exceptions\MissingSettings;
use Spatie\LaravelSettings\SettingsRepositories\SettingsRepository;
use Spatie\LaravelSettings\Support\Crypto;
abstract class Settings implements Arrayable, Jsonable, Responsable
{
private SettingsMapper $mapper;
private SettingsConfig $config;
private bool $loaded = false;
private bool $configInitialized = false;
protected ?Collection $originalValues = null;
abstract public static function group(): string;
public static function repository(): ?string
{
return null;
}
public static function casts(): array
{
return [];
}
public static function encrypted(): array
{
return [];
}
/**
* @param array $values
*
* @return static
*/
public static function fake(array $values, bool $loadMissingValues = true): self
{
$settingsMapper = app(SettingsMapper::class);
$propertiesToLoad = $settingsMapper->initialize(static::class)
->getReflectedProperties()
->keys()
->reject(fn (string $name) => array_key_exists($name, $values));
if ($propertiesToLoad->isEmpty()) {
return app(Container::class)->instance(static::class, new static(
$values
));
}
if($propertiesToLoad->isNotEmpty() && $loadMissingValues === false) {
throw MissingSettings::create(static::class, $propertiesToLoad->toArray(), 'loading fake');
}
$mergedValues = $settingsMapper
->fetchProperties(static::class, $propertiesToLoad)
->merge($values)
->all();
return app(Container::class)->instance(static::class, new static(
$mergedValues
));
}
final public function __construct(array $values = [])
{
$this->ensureConfigIsLoaded();
foreach ($this->config->getReflectedProperties() as $name => $property) {
if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) {
continue;
}
unset($this->{$name});
}
if (! empty($values)) {
$this->loadValues($values);
}
}
public function __get($name)
{
$this->loadValues();
return $this->{$name};
}
public function __set($name, $value)
{
$this->loadValues();
$this->{$name} = $value;
}
public function __debugInfo(): array
{
try {
$this->loadValues();
return $this->toArray();
} catch (Exception $exception) {
return [
'Could not load values',
];
}
}
public function __isset($name)
{
$this->loadValues();
return isset($this->{$name});
}
public function __serialize(): array
{
/** @var Collection $encrypted */
/** @var Collection $nonEncrypted */
[$encrypted, $nonEncrypted] = $this->toCollection()->partition(
fn ($value, string $name) => $this->config->isEncrypted($name)
);
return array_merge(
$encrypted->map(fn ($value) => Crypto::encrypt($value))->all(),
$nonEncrypted->all()
);
}
public function __unserialize(array $data): void
{
$this->loaded = false;
$this->ensureConfigIsLoaded();
/** @var Collection $encrypted */
/** @var Collection $nonEncrypted */
[$encrypted, $nonEncrypted] = collect($data)->partition(
fn ($value, string $name) => $this->config->isEncrypted($name)
);
$data = array_merge(
$encrypted->map(fn ($value) => Crypto::decrypt($value))->all(),
$nonEncrypted->all()
);
$this->loadValues($data);
}
/**
* @param \Illuminate\Support\Collection|array $properties
*
* @return $this
*/
public function fill($properties): self
{
foreach ($properties as $name => $payload) {
$this->{$name} = $payload;
}
return $this;
}
public function save(): self
{
$properties = $this->toCollection();
event(new SavingSettings($properties, $this->originalValues, $this));
$values = $this->mapper->save(static::class, $properties);
$this->fill($values);
$this->originalValues = $values;
event(new SettingsSaved($this));
return $this;
}
public function lock(string ...$properties)
{
$this->ensureConfigIsLoaded();
$this->config->lock(...$properties);
}
public function unlock(string ...$properties)
{
$this->ensureConfigIsLoaded();
$this->config->unlock(...$properties);
}
public function isLocked(string $property): bool
{
return in_array($property, $this->getLockedProperties());
}
public function isUnlocked(string $property): bool
{
return ! $this->isLocked($property);
}
public function getLockedProperties(): array
{
$this->ensureConfigIsLoaded();
return $this->config->getLocked()->toArray();
}
public function toCollection(): Collection
{
$this->ensureConfigIsLoaded();
return $this->config
->getReflectedProperties()
->mapWithKeys(fn (ReflectionProperty $property) => [
$property->getName() => $this->{$property->getName()},
]);
}
public function toArray(): array
{
return $this->toCollection()->toArray();
}
public function toJson($options = 0): string
{
return json_encode($this->toArray(), $options);
}
public function toResponse($request)
{
return response()->json($this->toJson());
}
public function getRepository(): SettingsRepository
{
$this->ensureConfigIsLoaded();
return $this->config->getRepository();
}
public function refresh(): self
{
$this->config->clearCachedLockedProperties();
$this->loaded = false;
$this->loadValues();
return $this;
}
private function loadValues(?array $values = null): self
{
if ($this->loaded) {
return $this;
}
$values ??= $this->mapper->load(static::class);
$this->loaded = true;
$this->fill($values);
$this->originalValues = collect($values);
event(new SettingsLoaded($this));
return $this;
}
private function ensureConfigIsLoaded(): self
{
if ($this->configInitialized) {
return $this;
}
$this->mapper = app(SettingsMapper::class);
$this->config = $this->mapper->initialize(static::class);
$this->configInitialized = true;
return $this;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Spatie\LaravelSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Spatie\LaravelSettings\Exceptions\CouldNotUnserializeSettings;
use Spatie\LaravelSettings\Exceptions\SettingsCacheDisabled;
class SettingsCache
{
private bool $enabled;
private ?string $store;
private ?string $prefix;
/** @var \DateTimeInterface|\DateInterval|int|null */
private $ttl;
public function __construct(
bool $enabled,
?string $store,
?string $prefix,
$ttl = null
) {
$this->enabled = $enabled;
$this->store = $store;
$this->prefix = $prefix;
$this->ttl = $ttl;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function has(string $settingsClass): bool
{
if ($this->enabled === false) {
return false;
}
return Cache::store($this->store)->has($this->resolveCacheKey($settingsClass));
}
public function get(string $settingsClass): Settings
{
if ($this->enabled === false) {
throw SettingsCacheDisabled::create();
}
$serialized = Cache::store($this->store)->get($this->resolveCacheKey($settingsClass));
$settings = unserialize($serialized);
if (! $settings instanceof Settings) {
throw new CouldNotUnserializeSettings();
}
return $settings;
}
public function put(Settings $settings): void
{
if ($this->enabled === false) {
return;
}
$serialized = serialize($settings);
Cache::store($this->store)->put(
$this->resolveCacheKey(get_class($settings)),
$serialized,
$this->ttl
);
}
public function clear(): void
{
app(SettingsContainer::class)
->getSettingClasses()
->map(fn (string $class) => $this->resolveCacheKey($class))
->pipe(fn (Collection $keys) => Cache::store($this->store)->deleteMultiple($keys));
}
private function resolveCacheKey(string $settingsClass): string
{
$prefix = $this->prefix ? "{$this->prefix}." : '';
return "{$prefix}settings.{$settingsClass}";
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
class ArraySettingsCast implements SettingsCast
{
protected SettingsCast $cast;
public function __construct(SettingsCast $cast)
{
$this->cast = $cast;
}
public function getCast(): ?SettingsCast
{
return $this->cast;
}
public function get($payload): array
{
return array_map(
fn ($data) => $this->cast->get($data),
$payload
);
}
public function set($payload)
{
return array_map(
fn ($data) => $this->cast->set($data),
$payload
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
use Illuminate\Support\Collection;
class CollectionCast implements SettingsCast
{
public function get($payload): Collection
{
return collect($payload);
}
public function set($payload): array
{
return $payload->toArray();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
use Exception;
use Spatie\LaravelData\Data;
class DataCast implements SettingsCast
{
protected string $type;
public function __construct(?string $type)
{
$this->type = $this->ensureDataTypeExists($type);
}
public function get($payload): Data
{
return $this->type::from($payload);
}
/**
* @param Data $payload
*
* @return array
*/
public function set($payload): array
{
return $payload->toArray();
}
protected function ensureDataTypeExists(?string $type): string
{
if ($type === null) {
throw new Exception('Cannot create a data cast because no data class was given');
}
if (! class_exists($type)) {
throw new Exception("Cannot create a data cast for `{$type}` because the data does not exist");
}
return $type;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
class DateTimeInterfaceCast implements SettingsCast
{
protected string $type;
public function __construct(?string $type)
{
$this->type = $type ?? DateTime::class;
}
public function get($payload): ?DateTimeInterface
{
if ($payload === null) {
return null;
}
if ($this->type === Carbon::class) {
return new Carbon($payload);
}
if ($this->type === CarbonImmutable::class) {
return new CarbonImmutable($payload);
}
if ($this->type === DateTimeImmutable::class) {
return new DateTimeImmutable($payload);
}
if ($this->type === DateTime::class) {
return new DateTime($payload);
}
throw new Exception("Could not cast DateTime type `{$this->type}`");
}
/** @param DateTimeInterface|null $payload */
public function set($payload): ?string
{
return $payload !== null
? $payload->format(DATE_ATOM)
: null;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
use DateTimeZone;
class DateTimeZoneCast implements SettingsCast
{
protected string $type;
public function __construct(?string $type)
{
$this->type = $type ?? DateTimeZone::class;
}
public function get($payload): ?DateTimeZone
{
return $payload !== null
? new DateTimeZone($payload)
: null;
}
/**
* @param DateTimeZone|null $payload
*
* @return string
*/
public function set($payload): ?string
{
return $payload !== null
? $payload->getName()
: null;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
use Exception;
use Spatie\DataTransferObject\DataTransferObject;
/** @deprecated */
class DtoCast implements SettingsCast
{
protected string $type;
public function __construct(?string $type)
{
$this->type = $this->ensureDtoTypeExists($type);
}
public function get($payload): DataTransferObject
{
return new $this->type($payload);
}
/**
* @param \Spatie\DataTransferObject\DataTransferObject $payload
*
* @return array
*/
public function set($payload): array
{
return $payload->toArray();
}
protected function ensureDtoTypeExists(?string $type): string
{
if ($type === null) {
throw new Exception('Cannot create a DTO cast because no DTO class was given');
}
if (! class_exists($type)) {
throw new Exception("Cannot create a DTO cast for `{$type}` because the DTO does not exist");
}
return $type;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
use BackedEnum;
use Exception;
use UnitEnum;
class EnumCast implements SettingsCast
{
private string $enum;
public function __construct(string $enum)
{
$this->enum = $enum;
}
public function get($payload): ?UnitEnum
{
if($payload === null) {
return null;
}
if (is_a($this->enum, BackedEnum::class, true)) {
return $this->enum::from($payload);
}
if (is_a($this->enum, UnitEnum::class, true)) {
foreach ($this->enum::cases() as $enum) {
if ($enum->name === $payload) {
return $enum;
}
}
}
throw new Exception('Invalid enum');
}
public function set($payload): string|int|null
{
if($payload === null) {
return null;
}
if ($payload instanceof BackedEnum) {
return $payload->value;
}
if ($payload instanceof UnitEnum) {
return $payload->name;
}
throw new Exception('Invalid enum');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelSettings\SettingsCasts;
interface SettingsCast
{
/**
* Will be used to when retrieving a value from the repository, and
* inserting it into the settings class.
*/
public function get($payload);
/**
* Will be used to when retrieving a value from the settings class, and
* inserting it into the repository.
*/
public function set($payload);
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Spatie\LaravelSettings;
use Exception;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionProperty;
use Spatie\LaravelSettings\Factories\SettingsCastFactory;
use Spatie\LaravelSettings\Factories\SettingsRepositoryFactory;
use Spatie\LaravelSettings\SettingsCasts\SettingsCast;
use Spatie\LaravelSettings\SettingsRepositories\SettingsRepository;
class SettingsConfig
{
/** @var class-string<\Spatie\LaravelSettings\Settings> */
private string $settingsClass;
/** @var Collection<string, ?\Spatie\LaravelSettings\SettingsCasts\SettingsCast> */
private Collection $casts;
/** @var Collection<string, \ReflectionProperty> */
private Collection $reflectionProperties;
/** @var string[]|\Illuminate\Support\Collection */
private Collection $encrypted;
/** @var string[]|\Illuminate\Support\Collection */
private Collection $locked;
private SettingsRepository $repository;
public function __construct(string $settingsClass)
{
if (! is_subclass_of($settingsClass, Settings::class)) {
throw new Exception("Tried decorating {$settingsClass} which is not extending `Spatie\LaravelSettings\Settings::class`");
}
$this->settingsClass = $settingsClass;
$this->reflectionProperties = collect(
(new ReflectionClass($settingsClass))->getProperties(ReflectionProperty::IS_PUBLIC)
)->mapWithKeys(fn (ReflectionProperty $property) => [$property->getName() => $property]);
$this->casts = $this->reflectionProperties
->map(fn (ReflectionProperty $reflectionProperty) => SettingsCastFactory::resolve(
$reflectionProperty,
$this->settingsClass::casts()
));
$this->encrypted = collect($this->settingsClass::encrypted());
$this->repository = SettingsRepositoryFactory::create($this->settingsClass::repository());
}
public function getName(): string
{
return $this->settingsClass;
}
public function getReflectedProperties(): Collection
{
return $this->reflectionProperties;
}
public function getRepository(): SettingsRepository
{
return $this->repository;
}
public function getGroup(): string
{
return $this->settingsClass::group();
}
public function isEncrypted(string $name): bool
{
return $this->encrypted->contains($name);
}
public function isLocked(string $name): bool
{
return $this->getLocked()->contains($name);
}
public function getCast(string $name): ?SettingsCast
{
return $this->casts->get($name);
}
public function lock(string ...$names): self
{
$this->locked = $this->getLocked()->merge($names);
$this->repository->lockProperties(
$this->getGroup(),
$names
);
return $this;
}
public function unlock(string ...$names): self
{
$this->locked = $this->getLocked()->diff($names);
$this->repository->unlockProperties(
$this->getGroup(),
$names
);
return $this;
}
public function getLocked(): Collection
{
if (! empty($this->locked)) {
return $this->locked;
}
return $this->locked = collect(
$this->repository->getLockedProperties($this->settingsClass::group())
);
}
public function clearCachedLockedProperties(): self
{
unset($this->locked);
return $this;
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Spatie\LaravelSettings;
use Illuminate\Container\Container;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Spatie\LaravelSettings\Exceptions\CouldNotUnserializeSettings;
use Spatie\LaravelSettings\Support\Composer;
use Spatie\LaravelSettings\Support\DiscoverSettings;
use Spatie\LaravelSettings\Support\SettingsCacheFactory;
class SettingsContainer
{
protected Container $container;
protected static ?Collection $settingsClasses = null;
public function __construct(Container $container)
{
$this->container = $container;
}
public function registerBindings(): void
{
$cacheFactory = $this->container->make(SettingsCacheFactory::class);
$this->getSettingClasses()->each(function (string $settingClass) use ($cacheFactory) {
$this->container->scoped($settingClass, function () use ($cacheFactory, $settingClass) {
$cache = $cacheFactory->build($settingClass::repository());
if ($cache->isEnabled() && $cache->has($settingClass)) {
try {
return $cache->get($settingClass);
} catch (CouldNotUnserializeSettings $exception) {
Log::error("Could not unserialize settings class: `{$settingClass}` from cache");
}
}
return new $settingClass();
});
});
}
public function getSettingClasses(): Collection
{
if (self::$settingsClasses !== null) {
return self::$settingsClasses;
}
$cachedDiscoveredSettings = config('settings.discovered_settings_cache_path') . '/settings.php';
if (file_exists($cachedDiscoveredSettings)) {
$classes = require $cachedDiscoveredSettings;
return self::$settingsClasses = collect($classes);
}
/** @var \Spatie\LaravelSettings\Settings[] $settings */
$settings = array_merge(
$this->discoverSettings(),
config('settings.settings', [])
);
return self::$settingsClasses = collect($settings)->unique();
}
public function clearCache(): self
{
self::$settingsClasses = null;
return $this;
}
protected function discoverSettings(): array
{
return (new DiscoverSettings())
->within(config('settings.auto_discover_settings', []))
->useBasePath(base_path())
->ignoringFiles(Composer::getAutoloadedFiles(base_path('composer.json')))
->discover();
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Spatie\LaravelSettings;
use Illuminate\Events\Dispatcher;
use Spatie\LaravelSettings\Events\SettingsLoaded;
use Spatie\LaravelSettings\Events\SettingsSaved;
use Spatie\LaravelSettings\Support\SettingsCacheFactory;
class SettingsEventSubscriber
{
private SettingsCacheFactory $settingsCacheFactory;
public function __construct(SettingsCacheFactory $settingsCacheFactory)
{
$this->settingsCacheFactory = $settingsCacheFactory;
}
public function subscribe(Dispatcher $dispatcher)
{
$dispatcher->listen(
SettingsSaved::class,
function (SettingsSaved $event) {
$cache = $this->settingsCacheFactory->build(
$event->settings::repository()
);
if ($cache->isEnabled()) {
$cache->put($event->settings);
}
}
);
$dispatcher->listen(
SettingsLoaded::class,
function (SettingsLoaded $event) {
$cache = $this->settingsCacheFactory->build(
$event->settings::repository()
);
if ($cache->has(get_class($event->settings))) {
return;
}
$cache->put($event->settings);
}
);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Spatie\LaravelSettings;
use Illuminate\Support\Collection;
use Spatie\LaravelSettings\Events\LoadingSettings;
use Spatie\LaravelSettings\Exceptions\MissingSettings;
use Spatie\LaravelSettings\Support\Crypto;
class SettingsMapper
{
/** @var array<string, \Spatie\LaravelSettings\SettingsConfig> */
private array $configs = [];
public function initialize(string $settingsClass): SettingsConfig
{
if ($this->has($settingsClass)) {
return $this->configs[$settingsClass];
}
$config = new SettingsConfig($settingsClass);
return $this->configs[$settingsClass] = $config;
}
public function has(string $settingsClass): bool
{
return array_key_exists($settingsClass, $this->configs);
}
public function load(string $settingsClass): Collection
{
$config = $this->getConfig($settingsClass);
$properties = $this->fetchProperties(
$settingsClass,
$config->getReflectedProperties()->keys()
);
event(new LoadingSettings($settingsClass, $properties));
$this->ensureNoMissingSettings($config, $properties, 'loading');
return $properties;
}
public function save(
string $settingsClass,
Collection $properties
): Collection {
$config = $this->getConfig($settingsClass);
$this->ensureNoMissingSettings($config, $properties, 'saving');
$notRejectedProperties = $properties
->reject(fn ($payload, string $name) => $config->isLocked($name));
$changedProperties = $notRejectedProperties
->map(function ($payload, string $name) use ($config) {
if ($cast = $config->getCast($name)) {
$payload = $cast->set($payload);
}
if ($config->isEncrypted($name)) {
$payload = Crypto::encrypt($payload);
}
return $payload;
})
->toArray();
$config->getRepository()->updatePropertiesPayload(
$config->getGroup(),
$changedProperties
);
return $this
->fetchProperties($settingsClass, $config->getLocked())
->merge($notRejectedProperties);
}
public function fetchProperties(string $settingsClass, Collection $names): Collection
{
$config = $this->getConfig($settingsClass);
return collect($config->getRepository()->getPropertiesInGroup($config->getGroup()))
->filter(fn ($payload, string $name) => $names->contains($name))
->map(function ($payload, string $name) use ($config) {
if ($config->isEncrypted($name)) {
$payload = Crypto::decrypt($payload);
}
if ($cast = $config->getCast($name)) {
$payload = $cast->get($payload);
}
return $payload;
});
}
private function getConfig(string $settingsClass): SettingsConfig
{
if (! $this->has($settingsClass)) {
$this->initialize($settingsClass);
}
return $this->configs[$settingsClass];
}
private function ensureNoMissingSettings(
SettingsConfig $config,
Collection $properties,
string $operation
): void {
$missingSettings = $config
->getReflectedProperties()
->keys()
->diff($properties->keys())
->toArray();
if (! empty($missingSettings)) {
throw MissingSettings::create($config->getName(), $missingSettings, $operation);
}
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Spatie\LaravelSettings\SettingsRepositories;
use Illuminate\Database\Eloquent\Builder;
use Spatie\LaravelSettings\Models\SettingsProperty;
class DatabaseSettingsRepository implements SettingsRepository
{
/** @var class-string<\Illuminate\Database\Eloquent\Model> */
protected string $propertyModel;
protected ?string $connection;
protected ?string $table;
public function __construct(array $config)
{
$this->propertyModel = $config['model'] ?? SettingsProperty::class;
$this->connection = $config['connection'] ?? null;
$this->table = $config['table'] ?? null;
}
public function getPropertiesInGroup(string $group): array
{
return $this->getBuilder()
->where('group', $group)
->get(['name', 'payload'])
->mapWithKeys(function (object $object) {
return [$object->name => $this->decode($object->payload, true)];
})
->toArray();
}
public function checkIfPropertyExists(string $group, string $name): bool
{
return $this->getBuilder()
->where('group', $group)
->where('name', $name)
->exists();
}
public function getPropertyPayload(string $group, string $name)
{
$setting = $this->getBuilder()
->where('group', $group)
->where('name', $name)
->first('payload')
->toArray();
return $this->decode($setting['payload']);
}
public function createProperty(string $group, string $name, $payload): void
{
$this->getBuilder()->create([
'group' => $group,
'name' => $name,
'payload' => $this->encode($payload),
'locked' => false,
]);
}
public function updatePropertiesPayload(string $group, array $properties): void
{
$propertiesInBatch = collect($properties)->map(function ($payload, $name) use ($group) {
return [
'group' => $group,
'name' => $name,
'payload' => $this->encode($payload),
];
})->values()->toArray();
$this->getBuilder()
->where('group', $group)
->upsert($propertiesInBatch, ['group', 'name'], ['payload']);
}
public function deleteProperty(string $group, string $name): void
{
$this->getBuilder()
->where('group', $group)
->where('name', $name)
->delete();
}
public function lockProperties(string $group, array $properties): void
{
$this->getBuilder()
->where('group', $group)
->whereIn('name', $properties)
->update(['locked' => true]);
}
public function unlockProperties(string $group, array $properties): void
{
$this->getBuilder()
->where('group', $group)
->whereIn('name', $properties)
->update(['locked' => false]);
}
public function getLockedProperties(string $group): array
{
return $this->getBuilder()
->where('group', $group)
->where('locked', true)
->pluck('name')
->toArray();
}
public function getBuilder(): Builder
{
$model = new $this->propertyModel;
if ($this->connection) {
$model->setConnection($this->connection);
}
if ($this->table) {
$model->setTable($this->table);
}
return $model->newQuery();
}
/**
* @param mixed $value
* @return mixed
*/
protected function encode($value)
{
$encoder = config('settings.encoder') ?? fn ($value) => json_encode($value);
return $encoder($value);
}
/**
* @return mixed
*/
protected function decode(string $payload, bool $associative = false)
{
$decoder = config('settings.decoder') ?? fn ($payload, $associative) => json_decode($payload, $associative);
return $decoder($payload, $associative);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Spatie\LaravelSettings\SettingsRepositories;
use Illuminate\Redis\RedisManager;
class RedisSettingsRepository implements SettingsRepository
{
/** @var \Redis */
protected $connection;
protected string $prefix;
public function __construct(array $config, RedisManager $connection)
{
$this->connection = $connection
->connection($config['connection'] ?? null)
->client();
$this->prefix = array_key_exists('prefix', $config)
? "{$config['prefix']}."
: '';
}
public function getPropertiesInGroup(string $group): array
{
return collect($this->connection->hGetAll($this->prefix . $group))
->mapWithKeys(function ($payload, string $name) {
return [$name => json_decode($payload, true)];
})->toArray();
}
public function checkIfPropertyExists(string $group, string $name): bool
{
return $this->connection->hExists($this->prefix . $group, $name);
}
public function getPropertyPayload(string $group, string $name)
{
return json_decode($this->connection->hGet($this->prefix . $group, $name));
}
public function createProperty(string $group, string $name, $payload): void
{
$this->connection->hSet($this->prefix . $group, $name, json_encode($payload));
}
public function updatePropertiesPayload(string $group, array $properties): void
{
$properties = collect($properties)->mapWithKeys(function ($payload, $name) {
return [$name => json_encode($payload)];
})->toArray();
$this->connection->hmset($this->prefix . $group, $properties);
}
public function deleteProperty(string $group, string $name): void
{
$this->connection->hDel($this->prefix . $group, $name);
}
public function lockProperties(string $group, array $properties): void
{
$this->connection->sAdd($this->getLocksSetKey($group), ...$properties);
}
public function unlockProperties(string $group, array $properties): void
{
$this->connection->sRem($this->getLocksSetKey($group), ...$properties);
}
public function getLockedProperties(string $group): array
{
return $this->connection->sMembers($this->getLocksSetKey($group));
}
protected function getLocksSetKey(string $group): string
{
return $this->prefix . 'locks.' . $group;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Spatie\LaravelSettings\SettingsRepositories;
interface SettingsRepository
{
/**
* Get all the properties in the repository for a single group
*/
public function getPropertiesInGroup(string $group): array;
/**
* Check if a property exists in a group
*/
public function checkIfPropertyExists(string $group, string $name): bool;
/**
* Get the payload of a property
*/
public function getPropertyPayload(string $group, string $name);
/**
* Create a property within a group with a payload
*/
public function createProperty(string $group, string $name, $payload): void;
/**
* Update the payloads of properties within a group.
*/
public function updatePropertiesPayload(string $group, array $properties): void;
/**
* Delete a property from a group
*/
public function deleteProperty(string $group, string $name): void;
/**
* Lock a set of properties for a specific group
*/
public function lockProperties(string $group, array $properties): void;
/**
* Unlock a set of properties for a group
*/
public function unlockProperties(string $group, array $properties): void;
/**
* Get all the locked properties within a group
*/
public function getLockedProperties(string $group): array;
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Spatie\LaravelSettings\Support;
use Illuminate\Support\Str;
class Composer
{
public static function getAutoloadedFiles($composerJsonPath): array
{
if (! file_exists($composerJsonPath)) {
return [];
}
$basePath = Str::before($composerJsonPath, 'composer.json');
$composerContents = json_decode(file_get_contents($composerJsonPath), true);
$paths = array_merge(
$composerContents['autoload']['files'] ?? [],
$composerContents['autoload-dev']['files'] ?? []
);
return array_map(fn (string $path) => realpath($basePath.$path), $paths);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Spatie\LaravelSettings\Support;
class Crypto
{
public static function encrypt($payload): ?string
{
return $payload !== null
? encrypt($payload)
: $payload;
}
public static function decrypt(?string $payload)
{
return $payload !== null
? decrypt($payload)
: null;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Spatie\LaravelSettings\Support;
use Illuminate\Support\Str;
use ReflectionClass;
use Spatie\LaravelSettings\Settings;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
use Throwable;
class DiscoverSettings
{
protected array $directories = [];
protected string $basePath = '';
protected string $rootNamespace = '';
protected array $ignoredFiles = [];
public function __construct()
{
$this->basePath = app_path();
}
public function within(array $directories): self
{
$this->directories = array_values(
array_filter($directories, fn (string $directory) => is_dir($directory))
);
return $this;
}
public function useBasePath(string $basePath): self
{
$this->basePath = $basePath;
return $this;
}
public function useRootNamespace(string $rootNamespace): self
{
$this->rootNamespace = $rootNamespace;
return $this;
}
public function ignoringFiles(array $ignoredFiles): self
{
$this->ignoredFiles = $ignoredFiles;
return $this;
}
public function discover(): array
{
if (empty($this->directories)) {
return [];
}
$files = (new Finder())->files()->in($this->directories);
return collect($files)
->reject(fn (SplFileInfo $file) => in_array($file->getPathname(), $this->ignoredFiles))
->map(fn (SplFileInfo $file) => $this->fullQualifiedClassNameFromFile($file))
->filter(function (string $settingsClass) {
try {
return is_subclass_of($settingsClass, Settings::class) &&
(new ReflectionClass($settingsClass))->isInstantiable();
} catch (Throwable $e) {
return false;
}
})
->flatten()
->toArray();
}
protected function fullQualifiedClassNameFromFile(SplFileInfo $file): string
{
$class = trim(Str::replaceFirst($this->basePath, '', $file->getRealPath()), DIRECTORY_SEPARATOR);
$class = str_replace(
[DIRECTORY_SEPARATOR, 'App\\'],
['\\', app()->getNamespace()],
ucfirst(Str::replaceLast('.php', '', $class))
);
return $this->rootNamespace.$class;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Spatie\LaravelSettings\Support;
use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\FqsenResolver;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\AbstractList;
use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\ContextFactory;
use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\String_;
use ReflectionNamedType;
use ReflectionProperty;
use Spatie\LaravelSettings\Exceptions\CouldNotResolveDocblockType;
class PropertyReflector
{
public static function resolveType(
ReflectionProperty $reflectionProperty
): ?Type {
$reflectionType = $reflectionProperty->getType();
$docblock = $reflectionProperty->getDocComment();
if ($reflectionType === null && empty($docblock)) {
return null;
}
if ($docblock) {
preg_match('/@var ((?:(?:[\w?|\\\\<>,\s])+(?:\[])?)+)/', $docblock, $output_array);
return count($output_array) === 2
? self::reflectDocblock($reflectionProperty, $output_array[1])
: null;
}
if (! $reflectionType instanceof ReflectionNamedType) {
return null;
}
$builtInTypes = [
'int',
'string',
'float',
'bool',
'mixed',
'array',
];
if (in_array($reflectionType->getName(), $builtInTypes)) {
return null;
}
$type = new Object_(new Fqsen('\\' . $reflectionType->getName()));
return $reflectionType->allowsNull()
? new Nullable($type)
: $type;
}
protected static function reflectDocblock(
ReflectionProperty $reflectionProperty,
string $type
): Type {
$resolvedType = (new TypeResolver())->resolve($type);
$isValidPrimitive = $resolvedType instanceof Boolean
|| $resolvedType instanceof Float_
|| $resolvedType instanceof Integer
|| $resolvedType instanceof String_;
if ($isValidPrimitive) {
return $resolvedType;
}
if ($resolvedType instanceof Object_) {
return self::reflectObject($reflectionProperty, $resolvedType);
}
if ($resolvedType instanceof Compound) {
return self::reflectCompound($reflectionProperty, $resolvedType);
}
if ($resolvedType instanceof Nullable) {
return new Nullable(self::reflectDocblock($reflectionProperty, (string) $resolvedType->getActualType()));
}
if ($resolvedType instanceof AbstractList) {
$listType = get_class($resolvedType);
return new $listType(
self::reflectDocblock($reflectionProperty, (string) $resolvedType->getValueType()),
$resolvedType->getKeyType()
);
}
throw CouldNotResolveDocblockType::create($type, $reflectionProperty);
}
private static function reflectCompound(
ReflectionProperty $reflectionProperty,
Compound $compound
): Nullable {
if ($compound->getIterator()->count() !== 2 || ! $compound->contains(new Null_())) {
throw CouldNotResolveDocblockType::create((string) $compound, $reflectionProperty);
}
$other = current(array_filter(
iterator_to_array($compound->getIterator()),
fn (Type $type) => ! $type instanceof Null_
));
return new Nullable(self::reflectDocblock($reflectionProperty, (string) $other));
}
private static function reflectObject(
ReflectionProperty $reflectionProperty,
Object_ $object
): Object_ {
if (class_exists((string) $object->getFqsen())) {
return $object;
}
$context = (new ContextFactory)->createFromReflector($reflectionProperty);
$className = ltrim((string) $object->getFqsen(), '\\');
$fqsen = (new FqsenResolver)->resolve($className, $context);
return new Object_($fqsen);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Spatie\LaravelSettings\Support;
use Spatie\LaravelSettings\SettingsCache;
class SettingsCacheFactory
{
private array $config;
private SettingsCache $defaultCache;
private array $repositoryCaches = [];
public function __construct(array $settingsConfig)
{
$this->config = $settingsConfig;
$this->initializeCaches();
}
public function build(?string $repository = null): SettingsCache
{
if ($repository === null) {
return $this->defaultCache;
}
if (array_key_exists($repository, $this->repositoryCaches)) {
return $this->repositoryCaches[$repository];
}
return $this->defaultCache;
}
/** @return array<SettingsCache> */
public function all(): array
{
return array_merge(
['default' => $this->defaultCache],
$this->repositoryCaches
);
}
protected function initializeCaches(): void
{
$this->defaultCache = $this->initializeCache($this->config['cache']);
foreach ($this->config['repositories'] as $name => $repositoryConfig) {
if (array_key_exists('cache', $repositoryConfig)) {
$this->repositoryCaches[$name] = $this->initializeCache($repositoryConfig['cache']);
}
}
}
protected function initializeCache(array $config): SettingsCache
{
return new SettingsCache(
$config['enabled'] ?? false,
$config['store'] ?? null,
$config['prefix'] ?? null,
$config['ttl'] ?? null,
);
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,180 @@
# Quickly create, use and delete temporary directories
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/temporary-directory.svg?style=flat-square)](https://packagist.org/packages/spatie/temporary-directory)
![Tests](https://github.com/spatie/temporary-directory/workflows/run-tests/badge.svg?label=tests)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/temporary-directory.svg?style=flat-square)](https://packagist.org/packages/spatie/temporary-directory)
This package allows you to quickly create, use and delete a temporary directory in the system's temporary directory.
Here's a quick example on how to create a temporary directory and delete it:
```php
use Spatie\TemporaryDirectory\TemporaryDirectory;
$temporaryDirectory = (new TemporaryDirectory())->create();
// Get a path inside the temporary directory
$temporaryDirectory->path('temporaryfile.txt');
// Delete the temporary directory and all the files inside it
$temporaryDirectory->delete();
```
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/temporary-directory.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/temporary-directory)
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
## Installation
You can install the package via composer:
```bash
composer require spatie/temporary-directory
```
## Usage
### Creating a temporary directory
To create a temporary directory simply call the `create` method on a `TemporaryDirectory` object.
```php
(new TemporaryDirectory())->create();
```
Alternatively, use the static `make` method on a `TemporaryDirectory` object.
```php
TemporaryDirectory::make();
```
By default, the temporary directory will be created in a timestamped directory in your system's temporary directory (usually `/tmp`).
### Naming your temporary directory
If you want to use a custom name for your temporary directory instead of the timestamp call the `name` method with a string `$name` argument before the `create` method.
```php
(new TemporaryDirectory())
->name($name)
->create();
```
By default an exception will be thrown if a directory already exists with the given argument. You can override this behaviour by calling the `force` method in combination with the `name` method.
```php
(new TemporaryDirectory())
->name($name)
->force()
->create();
```
### Setting a custom location for a temporary directory
You can set a custom location in which your temporary directory will be created by passing a string `$location` argument to the `TemporaryDirectory` constructor.
```php
(new TemporaryDirectory($location))
->create();
```
The `make` method also accepts a `$location` argument.
```php
TemporaryDirectory::make($location);
```
Finally, you can call the `location` method with a `$location` argument.
```php
(new TemporaryDirectory())
->location($location)
->create();
```
### Determining paths within the temporary directory
You can use the `path` method to determine the full path to a file or directory in the temporary directory:
```php
$temporaryDirectory = (new TemporaryDirectory())->create();
$temporaryDirectory->path('dumps/datadump.dat'); // return /tmp/1485941876276/dumps/datadump.dat
```
### Emptying a temporary directory
Use the `empty` method to delete all the files inside the temporary directory.
```php
$temporaryDirectory->empty();
```
### Deleting a temporary directory
Once you're done processing your temporary data you can delete the entire temporary directory using the `delete` method. All files inside of it will be deleted.
```php
$temporaryDirectory->delete();
```
### Deleting a temporary directory when the object is destroyed
If you want to automatically have the filesystem directory deleted when the object instance has no more references in
its defined scope, you can enable `deleteWhenDestroyed()` on the TemporaryDirectory object.
```php
function handleTemporaryFiles()
{
$temporaryDirectory = (new TemporaryDirectory())
->deleteWhenDestroyed()
->create();
// ... use the temporary directory
return; // no need to manually call $temporaryDirectory->delete()!
}
handleTemporaryFiles();
```
You can also call `unset()` on an object instance.
## Testing
```bash
composer test
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Postcardware
You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium.
We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards).
## Credits
- [Alex Vanderbist](https://github.com/AlexVanderbist)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@@ -0,0 +1,44 @@
{
"name": "spatie/temporary-directory",
"description": "Easily create, use and destroy temporary directories",
"license": "MIT",
"keywords": [
"spatie",
"php",
"temporary-directory"
],
"authors": [
{
"name": "Alex Vanderbist",
"email": "alex@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"homepage": "https://github.com/spatie/temporary-directory",
"require": {
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Spatie\\TemporaryDirectory\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\TemporaryDirectory\\Test\\": "tests"
}
},
"config": {
"sort-packages": true
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Spatie\TemporaryDirectory\Exceptions;
class InvalidDirectoryName extends \Exception
{
public static function create(string $directoryName): static
{
return new static("The directory name `{$directoryName}` contains invalid characters.");
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Spatie\TemporaryDirectory\Exceptions;
class PathAlreadyExists extends \Exception
{
public static function create(string $path): static
{
return new static("Path `{$path}` already exists.");
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Spatie\TemporaryDirectory;
use FilesystemIterator;
use Spatie\TemporaryDirectory\Exceptions\InvalidDirectoryName;
use Spatie\TemporaryDirectory\Exceptions\PathAlreadyExists;
use Throwable;
class TemporaryDirectory
{
protected string $location;
protected string $name = '';
protected bool $forceCreate = false;
protected bool $deleteWhenDestroyed = false;
public function __construct(string $location = '')
{
$this->location = $this->sanitizePath($location);
}
public static function make(string $location = ''): self
{
return (new self($location))->create();
}
public function create(): self
{
if (empty($this->location)) {
$this->location = $this->getSystemTemporaryDirectory();
}
if (empty($this->name)) {
$this->name = mt_rand().'-'.str_replace([' ', '.'], '', microtime());
}
if ($this->forceCreate && file_exists($this->getFullPath())) {
$this->deleteDirectory($this->getFullPath());
}
if ($this->exists()) {
throw PathAlreadyExists::create($this->getFullPath());
}
mkdir($this->getFullPath(), 0777, true);
return $this;
}
public function force(): self
{
$this->forceCreate = true;
return $this;
}
public function name(string $name): self
{
$this->name = $this->sanitizeName($name);
return $this;
}
public function location(string $location): self
{
$this->location = $this->sanitizePath($location);
return $this;
}
public function path(string $pathOrFilename = ''): string
{
if (empty($pathOrFilename)) {
return $this->getFullPath();
}
$path = $this->getFullPath().DIRECTORY_SEPARATOR.trim($pathOrFilename, '/');
$directoryPath = $this->removeFilenameFromPath($path);
if (! file_exists($directoryPath)) {
mkdir($directoryPath, 0777, true);
}
return $path;
}
public function empty(): self
{
$this->deleteDirectory($this->getFullPath());
mkdir($this->getFullPath(), 0777, true);
return $this;
}
public function delete(): bool
{
return $this->deleteDirectory($this->getFullPath());
}
public function exists(): bool
{
return file_exists($this->getFullPath());
}
protected function getFullPath(): string
{
return $this->location.(! empty($this->name) ? DIRECTORY_SEPARATOR.$this->name : '');
}
protected function isValidDirectoryName(string $directoryName): bool
{
return strpbrk($directoryName, '\\/?%*:|"<>') === false;
}
protected function getSystemTemporaryDirectory(): string
{
return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR);
}
protected function sanitizePath(string $path): string
{
$path = rtrim($path);
return rtrim($path, DIRECTORY_SEPARATOR);
}
protected function sanitizeName(string $name): string
{
if (! $this->isValidDirectoryName($name)) {
throw InvalidDirectoryName::create($name);
}
return trim($name);
}
protected function removeFilenameFromPath(string $path): string
{
if (! $this->isFilePath($path)) {
return $path;
}
return substr($path, 0, strrpos($path, DIRECTORY_SEPARATOR));
}
protected function isFilePath(string $path): bool
{
return str_contains($path, '.');
}
protected function deleteDirectory(string $path): bool
{
try {
if (is_link($path)) {
return unlink($path);
}
if (! file_exists($path)) {
return true;
}
if (! is_dir($path)) {
return unlink($path);
}
foreach (new FilesystemIterator($path) as $item) {
if (! $this->deleteDirectory((string) $item)) {
return false;
}
}
/*
* By forcing a php garbage collection cycle using gc_collect_cycles() we can ensure
* that the rmdir does not fail due to files still being reserved in memory.
*/
gc_collect_cycles();
return rmdir($path);
} catch (Throwable) {
return false;
}
}
public function deleteWhenDestroyed(bool $deleteWhenDestroyed = true): self
{
$this->deleteWhenDestroyed = $deleteWhenDestroyed;
return $this;
}
public function __destruct()
{
if ($this->deleteWhenDestroyed) {
$this->delete();
}
}
}