upgrade to laravel 8.0

This commit is contained in:
Attila Kerekes 2022-11-13 17:05:03 +01:00 committed by Attila Jozsef Kerekes
parent 43f894b58d
commit 27f58c0866
No known key found for this signature in database
GPG key ID: E1121565A016ADFD
3910 changed files with 155144 additions and 129050 deletions

View file

@ -1,37 +1,44 @@
{
"name": "laravel/laravel",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"type": "project",
"require": {
"php": ">=7.2.5",
"php": ">=7.3.0",
"facade/ignition": "^2.3.6",
"fideloper/proxy": "^4.0",
"graham-campbell/github": "^10.5",
"guzzlehttp/guzzle": "^7.4",
"laravel/framework": "^7.0",
"laravel/framework": "^8.0",
"laravel/tinker": "^2.0",
"laravel/ui": "^2.4",
"laravel/ui": "^3.0",
"laravelcollective/html": "^6.0",
"nunomaduro/collision": "^5.0",
"symfony/yaml": "^5.4"
},
"require-dev": {
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~6.0",
"phpunit/phpunit": "~9.0",
"symfony/thanks": "^1.0"
},
"autoload": {
"classmap": [
"database/seeds",
"database/seeders",
"database/factories"
],
"files": [
"app/Helper.php"
],
"psr-4": {
"App\\": "app/"
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {

2975
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,39 @@
<?php
use Faker\Generator as Faker;
namespace Database\Factories;
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
$factory->define(App\User::class, function (Faker $faker) {
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
'remember_token' => str_random(10),
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
});
}
/**
* Indicate that the model's email address should be unverified.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
public function unverified()
{
return $this->state(function (array $attributes) {
return [
'email_verified_at' => null,
];
});
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$this->call(SettingsSeeder::class);
$this->call(UsersSeeder::class);
}
}

View file

@ -0,0 +1,269 @@
<?php
namespace Database\Seeders;
use App\Setting;
use App\SettingGroup;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class SettingsSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Groups
if (! $setting_group = SettingGroup::find(1)) {
$setting_group = new SettingGroup;
$setting_group->id = 1;
$setting_group->title = 'app.settings.system';
$setting_group->order = 0;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.system';
$setting_group->save();
}
if (! $setting_group = SettingGroup::find(2)) {
$setting_group = new SettingGroup;
$setting_group->id = 2;
$setting_group->title = 'app.settings.appearance';
$setting_group->order = 1;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.appearance';
$setting_group->save();
}
if (! $setting_group = SettingGroup::find(3)) {
$setting_group = new SettingGroup;
$setting_group->id = 3;
$setting_group->title = 'app.settings.miscellaneous';
$setting_group->order = 2;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.miscellaneous';
$setting_group->save();
}
if (! $setting_group = SettingGroup::find(4)) {
$setting_group = new SettingGroup;
$setting_group->id = 4;
$setting_group->title = 'app.settings.advanced';
$setting_group->order = 3;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.advanced';
$setting_group->save();
}
if ($version = Setting::find(1)) {
$version->label = 'app.settings.version';
$version->value = config('app.version');
$version->save();
} else {
$setting = new Setting;
$setting->id = 1;
$setting->group_id = 1;
$setting->key = 'version';
$setting->type = 'text';
$setting->label = 'app.settings.version';
$setting->value = config('app.version');
$setting->system = true;
$setting->save();
}
if (! $setting = Setting::find(2)) {
$setting = new Setting;
$setting->id = 2;
$setting->group_id = 2;
$setting->key = 'background_image';
$setting->type = 'image';
$setting->label = 'app.settings.background_image';
$setting->save();
} else {
$setting->label = 'app.settings.background_image';
$setting->save();
}
if (! $setting = Setting::find(3)) {
$setting = new Setting;
$setting->id = 3;
$setting->group_id = 3;
$setting->key = 'homepage_search';
$setting->type = 'boolean';
$setting->label = 'app.settings.homepage_search';
$setting->save();
} else {
$setting->label = 'app.settings.homepage_search';
$setting->save();
}
$options = json_encode([
'none' => 'app.options.none',
'google' => 'app.options.google',
'ddg' => 'app.options.ddg',
'qwant' => 'app.options.qwant',
'bing' => 'app.options.bing',
'startpage' => 'app.options.startpage',
]);
if (! $setting = Setting::find(4)) {
$setting = new Setting;
$setting->id = 4;
$setting->group_id = 3;
$setting->key = 'search_provider';
$setting->type = 'select';
$setting->options = $options;
$setting->label = 'app.settings.search_provider';
$setting->save();
} else {
$setting->options = $options;
$setting->label = 'app.settings.search_provider';
$setting->save();
}
$language_options = json_encode([
'de' => 'Deutsch (German)',
'en' => 'English',
'cn' => '简体中文 (Simplified Chinese)',
'fi' => 'Suomi (Finnish)',
'fr' => 'Français (French)',
'el' => 'Ελληνικά (Greek)',
'it' => 'Italiano (Italian)',
'no' => 'Norsk (Norwegian)',
'pl' => 'Polski (Polish)',
'sv' => 'Svenska (Swedish)',
'es' => 'Español (Spanish)',
'tr' => 'Türkçe (Turkish)',
]);
if ($languages = Setting::find(5)) {
$languages->options = $language_options;
$languages->save();
} else {
$setting = new Setting;
$setting->id = 5;
$setting->group_id = 1;
$setting->key = 'language';
$setting->type = 'select';
$setting->label = 'app.settings.language';
$setting->options = $language_options;
$setting->value = 'en';
$setting->save();
}
$window_target_options = json_encode([
'current' => 'app.settings.window_target.current',
'heimdall' => 'app.settings.window_target.one',
'_blank' => 'app.settings.window_target.new',
]);
if (! $setting = Setting::find(7)) {
$setting = new Setting;
$setting->id = 7;
$setting->group_id = 3;
$setting->key = 'window_target';
$setting->type = 'select';
$setting->options = $window_target_options;
$setting->label = 'app.settings.window_target';
$setting->value = 'heimdall';
$setting->save();
} else {
$setting->options = $window_target_options;
$setting->label = 'app.settings.window_target';
$setting->save();
}
if ($support = Setting::find(8)) {
$support->label = 'app.settings.support';
$support->value = '<a rel="noopener" target="_blank" href="https://discord.gg/CCjHKn4">Discord</a> | <a rel="noopener" target="_blank" href="https://github.com/linuxserver/Heimdall">Github</a> | <a rel="noopener" target="_blank" href="https://blog.heimdall.site/">Blog</a>';
$support->save();
} else {
$setting = new Setting;
$setting->id = 8;
$setting->group_id = 1;
$setting->key = 'support';
$setting->type = 'text';
$setting->label = 'app.settings.support';
$setting->value = '<a rel="noopener" target="_blank" href="https://discord.gg/CCjHKn4">Discord</a> | <a rel="noopener" target="_blank" href="https://github.com/linuxserver/Heimdall">Github</a> | <a rel="noopener" target="_blank" href="https://blog.heimdall.site/">Blog</a>';
$setting->system = true;
$setting->save();
}
if ($donate = Setting::find(9)) {
$donate->label = 'app.settings.donate';
$donate->value = '<a rel="noopener" target="_blank" href="https://www.paypal.me/heimdall">Paypal</a>';
$donate->save();
} else {
$setting = new Setting;
$setting->id = 9;
$setting->group_id = 1;
$setting->key = 'donate';
$setting->type = 'text';
$setting->label = 'app.settings.donate';
$setting->value = '<a rel="noopener" target="_blank" href="https://www.paypal.me/heimdall">Paypal</a>';
$setting->system = true;
$setting->save();
}
if (! $setting = Setting::find(10)) {
$setting = new Setting;
$setting->id = 10;
$setting->group_id = 4;
$setting->key = 'custom_css';
$setting->type = 'textarea';
$setting->label = 'app.settings.custom_css';
$setting->value = '';
$setting->save();
} else {
$setting->type = 'textarea';
$setting->group_id = 4;
$setting->label = 'app.settings.custom_css';
$setting->save();
}
if (! $setting = Setting::find(11)) {
$setting = new Setting;
$setting->id = 11;
$setting->group_id = 4;
$setting->key = 'custom_js';
$setting->type = 'textarea';
$setting->label = 'app.settings.custom_js';
$setting->value = '';
$setting->save();
} else {
$setting->type = 'textarea';
$setting->group_id = 4;
$setting->label = 'app.settings.custom_js';
$setting->save();
}
if (! $home_tag = \App\Item::find(0)) {
$home_tag = new \App\Item;
$home_tag->id = 0;
$home_tag->title = 'app.dashboard';
$home_tag->pinned = 0;
$home_tag->url = '';
$home_tag->type = 1;
$home_tag->user_id = 0;
$home_tag->save();
$home_tag_id = $home_tag->id;
if($home_tag_id != 0) {
Log::info("Home Tag returned with id $home_tag_id from db! Changing to 0.");
DB::update('update items set id = 0 where id = ?', [$home_tag_id]);
}
$homeapps = \App\Item::withoutGlobalScope('user_id')->doesntHave('parents')->get();
foreach ($homeapps as $app) {
if ($app->id === 0) {
continue;
}
$app->parents()->attach(0);
}
}
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Database\Seeders;
use App\User;
use Illuminate\Database\Seeder;
class UsersSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Groups
if (! $user = User::find(1)) {
$user = new User;
$user->username = 'admin';
$user->email = 'admin@test.com';
$user->password = null;
$user->save();
$user_id = $user->id;
if($user_id != 1) {
Log::info("First User returned with id $user_id from db! Changing to 1.");
DB::update('update users set id = 1 where id = ?', [$user_id]);
}
} else {
//$user->save();
}
}
}

View file

@ -1,17 +0,0 @@
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$this->call(SettingsSeeder::class);
$this->call(UsersSeeder::class);
}
}

View file

@ -1,267 +0,0 @@
<?php
use App\Setting;
use App\SettingGroup;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class SettingsSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Groups
if (! $setting_group = SettingGroup::find(1)) {
$setting_group = new SettingGroup;
$setting_group->id = 1;
$setting_group->title = 'app.settings.system';
$setting_group->order = 0;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.system';
$setting_group->save();
}
if (! $setting_group = SettingGroup::find(2)) {
$setting_group = new SettingGroup;
$setting_group->id = 2;
$setting_group->title = 'app.settings.appearance';
$setting_group->order = 1;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.appearance';
$setting_group->save();
}
if (! $setting_group = SettingGroup::find(3)) {
$setting_group = new SettingGroup;
$setting_group->id = 3;
$setting_group->title = 'app.settings.miscellaneous';
$setting_group->order = 2;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.miscellaneous';
$setting_group->save();
}
if (! $setting_group = SettingGroup::find(4)) {
$setting_group = new SettingGroup;
$setting_group->id = 4;
$setting_group->title = 'app.settings.advanced';
$setting_group->order = 3;
$setting_group->save();
} else {
$setting_group->title = 'app.settings.advanced';
$setting_group->save();
}
if ($version = Setting::find(1)) {
$version->label = 'app.settings.version';
$version->value = config('app.version');
$version->save();
} else {
$setting = new Setting;
$setting->id = 1;
$setting->group_id = 1;
$setting->key = 'version';
$setting->type = 'text';
$setting->label = 'app.settings.version';
$setting->value = config('app.version');
$setting->system = true;
$setting->save();
}
if (! $setting = Setting::find(2)) {
$setting = new Setting;
$setting->id = 2;
$setting->group_id = 2;
$setting->key = 'background_image';
$setting->type = 'image';
$setting->label = 'app.settings.background_image';
$setting->save();
} else {
$setting->label = 'app.settings.background_image';
$setting->save();
}
if (! $setting = Setting::find(3)) {
$setting = new Setting;
$setting->id = 3;
$setting->group_id = 3;
$setting->key = 'homepage_search';
$setting->type = 'boolean';
$setting->label = 'app.settings.homepage_search';
$setting->save();
} else {
$setting->label = 'app.settings.homepage_search';
$setting->save();
}
$options = json_encode([
'none' => 'app.options.none',
'google' => 'app.options.google',
'ddg' => 'app.options.ddg',
'qwant' => 'app.options.qwant',
'bing' => 'app.options.bing',
'startpage' => 'app.options.startpage',
]);
if (! $setting = Setting::find(4)) {
$setting = new Setting;
$setting->id = 4;
$setting->group_id = 3;
$setting->key = 'search_provider';
$setting->type = 'select';
$setting->options = $options;
$setting->label = 'app.settings.search_provider';
$setting->save();
} else {
$setting->options = $options;
$setting->label = 'app.settings.search_provider';
$setting->save();
}
$language_options = json_encode([
'de' => 'Deutsch (German)',
'en' => 'English',
'cn' => '简体中文 (Simplified Chinese)',
'fi' => 'Suomi (Finnish)',
'fr' => 'Français (French)',
'el' => 'Ελληνικά (Greek)',
'it' => 'Italiano (Italian)',
'no' => 'Norsk (Norwegian)',
'pl' => 'Polski (Polish)',
'sv' => 'Svenska (Swedish)',
'es' => 'Español (Spanish)',
'tr' => 'Türkçe (Turkish)',
]);
if ($languages = Setting::find(5)) {
$languages->options = $language_options;
$languages->save();
} else {
$setting = new Setting;
$setting->id = 5;
$setting->group_id = 1;
$setting->key = 'language';
$setting->type = 'select';
$setting->label = 'app.settings.language';
$setting->options = $language_options;
$setting->value = 'en';
$setting->save();
}
$window_target_options = json_encode([
'current' => 'app.settings.window_target.current',
'heimdall' => 'app.settings.window_target.one',
'_blank' => 'app.settings.window_target.new',
]);
if (! $setting = Setting::find(7)) {
$setting = new Setting;
$setting->id = 7;
$setting->group_id = 3;
$setting->key = 'window_target';
$setting->type = 'select';
$setting->options = $window_target_options;
$setting->label = 'app.settings.window_target';
$setting->value = 'heimdall';
$setting->save();
} else {
$setting->options = $window_target_options;
$setting->label = 'app.settings.window_target';
$setting->save();
}
if ($support = Setting::find(8)) {
$support->label = 'app.settings.support';
$support->value = '<a rel="noopener" target="_blank" href="https://discord.gg/CCjHKn4">Discord</a> | <a rel="noopener" target="_blank" href="https://github.com/linuxserver/Heimdall">Github</a> | <a rel="noopener" target="_blank" href="https://blog.heimdall.site/">Blog</a>';
$support->save();
} else {
$setting = new Setting;
$setting->id = 8;
$setting->group_id = 1;
$setting->key = 'support';
$setting->type = 'text';
$setting->label = 'app.settings.support';
$setting->value = '<a rel="noopener" target="_blank" href="https://discord.gg/CCjHKn4">Discord</a> | <a rel="noopener" target="_blank" href="https://github.com/linuxserver/Heimdall">Github</a> | <a rel="noopener" target="_blank" href="https://blog.heimdall.site/">Blog</a>';
$setting->system = true;
$setting->save();
}
if ($donate = Setting::find(9)) {
$donate->label = 'app.settings.donate';
$donate->value = '<a rel="noopener" target="_blank" href="https://www.paypal.me/heimdall">Paypal</a>';
$donate->save();
} else {
$setting = new Setting;
$setting->id = 9;
$setting->group_id = 1;
$setting->key = 'donate';
$setting->type = 'text';
$setting->label = 'app.settings.donate';
$setting->value = '<a rel="noopener" target="_blank" href="https://www.paypal.me/heimdall">Paypal</a>';
$setting->system = true;
$setting->save();
}
if (! $setting = Setting::find(10)) {
$setting = new Setting;
$setting->id = 10;
$setting->group_id = 4;
$setting->key = 'custom_css';
$setting->type = 'textarea';
$setting->label = 'app.settings.custom_css';
$setting->value = '';
$setting->save();
} else {
$setting->type = 'textarea';
$setting->group_id = 4;
$setting->label = 'app.settings.custom_css';
$setting->save();
}
if (! $setting = Setting::find(11)) {
$setting = new Setting;
$setting->id = 11;
$setting->group_id = 4;
$setting->key = 'custom_js';
$setting->type = 'textarea';
$setting->label = 'app.settings.custom_js';
$setting->value = '';
$setting->save();
} else {
$setting->type = 'textarea';
$setting->group_id = 4;
$setting->label = 'app.settings.custom_js';
$setting->save();
}
if (! $home_tag = \App\Item::find(0)) {
$home_tag = new \App\Item;
$home_tag->id = 0;
$home_tag->title = 'app.dashboard';
$home_tag->pinned = 0;
$home_tag->url = '';
$home_tag->type = 1;
$home_tag->user_id = 0;
$home_tag->save();
$home_tag_id = $home_tag->id;
if($home_tag_id != 0) {
Log::info("Home Tag returned with id $home_tag_id from db! Changing to 0.");
DB::update('update items set id = 0 where id = ?', [$home_tag_id]);
}
$homeapps = \App\Item::withoutGlobalScope('user_id')->doesntHave('parents')->get();
foreach ($homeapps as $app) {
if ($app->id === 0) {
continue;
}
$app->parents()->attach(0);
}
}
}
}

View file

@ -1,34 +0,0 @@
<?php
use App\User;
use Illuminate\Database\Seeder;
class UsersSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Groups
if (! $user = User::find(1)) {
$user = new User;
$user->username = 'admin';
$user->email = 'admin@test.com';
$user->password = null;
$user->save();
$user_id = $user->id;
if($user_id != 1) {
Log::info("First User returned with id $user_id from db! Changing to 1.");
DB::update('update users set id = 1 where id = ?', [$user_id]);
}
} else {
//$user->save();
}
}
}

View file

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
@ -12,11 +13,6 @@
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>

View file

@ -34,7 +34,7 @@ Supported applications are recognized by the title of the application as entered
[![foundationapps](https://img.shields.io/badge/dynamic/json.svg?label=Foundation%20Apps&url=https%3A%2F%2Fapps.heimdall.site%2Fstats&query=foundation_apps&colorB=3f8483&style=for-the-badge&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAjCAMAAACw/5reAAAAnFBMVEUAAADu7u7u7u7u7u7u7u7x8fHu7u7u7u7u7u7u7u7u7u7u7u7r6+vu7u7v7+/u7u7t7e3v7+/v7+/u7u7u7u7u7u7u7u7u7u7u7u7u7u7v7+/u7u7p6ent7e3v7+/v7+/v7+/u7u7u7u7u7u7u7u7t7e3////u7u7u7u7u7u7u7u7w8PDw8PDt7e3u7u7t7e3s7Ozu7u7t7e3u7u4TnCP6AAAAM3RSTlMA+9n3phHw3czC088M5Y5zG6mflWdJFumyfj4sB2NeTi7hiWlDOQPGt5lsMiG9hFQntpFqxQJtAAABnElEQVQoz2WRh3KrQAxFtYWO6ZhucItrynv6/3/LFnA24c6wurpnYBkJZvXduNix6+GXTo8qWnxUPU4m2w0O1ktTozPsftiZpejGlm7C2MWUnRcWOohIo36+PaKyDZdLUOgDXvqQfaT9kwkfvP3AN18E7Kl8hkJHMHSXSSadxaTtTNjJhMkfjFHKMqGlolg4T7mtCbcq8gBCotxkwklFLIQSlQoTHnVWQqzNxYQuzpfmqGVMc5ijHK5yAuIhxbZ5p/S92RZkjv5BKs6aosSIr0JrcXBo1FtICVINKRKK6u0GnraoN84O5KbhjRwYzxCJnQCMtotkdNxjq2F7dJ2RoGuXIBTvc3ROthdmat6hZ7cOyfcxKGV+wTxBkxQxTQTzWOFny/7qS2nzx37T7nbtZj9xu7zUr/323nVy0sQnhwMJktSZrl5v7CjgSQmWi+haUCY8sH4tyc/FGSKGouS+WqBJm8U2NIE/+nLu2tzpF/xVNGy02QzRClafC/ysVpDzQJuA8xXsKl8bv+pgpXz57H9Yy3J1lQNY62wUrW+mdzrylWS0QwAAAABJRU5ErkJggg==)](https://apps.heimdall.site/applications/foundation)
## Installing
Apart from the Laravel dependencies, namely PHP >= 7.2.5, BCMath PHP Extension, Ctype PHP Extension, Fileinfo PHP extension, JSON PHP Extension, Mbstring PHP Extension, OpenSSL PHP Extension, PDO PHP Extension, Tokenizer PHP Extension, XML PHP Extension, the only other thing Heimdall needs is sqlite support and zip support (php-zip).
Apart from the Laravel 8 dependencies, namely PHP >= 7.3.0, BCMath PHP Extension, Ctype PHP Extension, Fileinfo PHP extension, JSON PHP Extension, Mbstring PHP Extension, OpenSSL PHP Extension, PDO PHP Extension, Tokenizer PHP Extension, XML PHP Extension, the only other thing Heimdall needs is sqlite support and zip support (php-zip).
If you find you can't change the background make sure `php_fileinfo` is enabled in your php.ini. I believe it should be by default, but one user came across the issue on a windows system.

File diff suppressed because one or more lines are too long

18
vendor/autoload.php vendored
View file

@ -2,6 +2,24 @@
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit4b6fb9210a1ea37c2db27b8ff53a1ecf::getLoader();

1
vendor/bin/carbon vendored
View file

@ -1 +0,0 @@
../nesbot/carbon/bin/carbon

120
vendor/bin/carbon vendored Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../nesbot/carbon/bin/carbon)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
exit(0);
}
}
include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';

117
vendor/bin/commonmark vendored
View file

@ -1,117 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../league/commonmark/bin/commonmark)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/league/commonmark/bin/commonmark');
exit(0);
}
}
include __DIR__ . '/..'.'/league/commonmark/bin/commonmark';

View file

@ -1,5 +0,0 @@
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/commonmark
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
php "%BIN_TARGET%" %*

View file

@ -1 +0,0 @@
../symfony/error-handler/Resources/bin/patch-type-declarations

120
vendor/bin/patch-type-declarations vendored Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/error-handler/Resources/bin/patch-type-declarations)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
exit(0);
}
}
include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';

View file

@ -1 +0,0 @@
../nikic/php-parser/bin/php-parse

120
vendor/bin/php-parse vendored Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
exit(0);
}
}
include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';

5
vendor/bin/phpunit vendored
View file

@ -111,7 +111,10 @@ if (PHP_VERSION_ID < 80000) {
}
}
if (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) {
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
exit(0);
}

1
vendor/bin/psysh vendored
View file

@ -1 +0,0 @@
../psy/psysh/bin/psysh

120
vendor/bin/psysh vendored Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../psy/psysh/bin/psysh)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
exit(0);
}
}
include __DIR__ . '/..'.'/psy/psysh/bin/psysh';

View file

@ -1 +0,0 @@
../symfony/var-dumper/Resources/bin/var-dump-server

120
vendor/bin/var-dump-server vendored Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
exit(0);
}
}
include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';

View file

@ -1 +0,0 @@
../symfony/yaml/Resources/bin/yaml-lint

120
vendor/bin/yaml-lint vendored Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
exit(0);
}
}
include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';

View file

@ -149,7 +149,7 @@ class ClassLoader
/**
* @return string[] Array of classname => path
* @psalm-var array<string, string>
* @psalm-return array<string, string>
*/
public function getClassMap()
{

View file

@ -21,12 +21,14 @@ use Composer\Semver\VersionParser;
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
@ -37,7 +39,7 @@ class InstalledVersions
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
@ -241,7 +243,7 @@ class InstalledVersions
/**
* @return array
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
@ -255,7 +257,7 @@ class InstalledVersions
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@ -278,7 +280,7 @@ class InstalledVersions
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
@ -301,7 +303,7 @@ class InstalledVersions
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
@ -311,7 +313,7 @@ class InstalledVersions
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{

File diff suppressed because it is too large Load diff

View file

@ -2,36 +2,41 @@
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php',
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
'2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'9cdd7b9056abc3081735233ba9dd9c7f' => $vendorDir . '/facade/flare-client-php/src/helpers.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'538ca81a9a966a6716601ecf48f4eaef' => $vendorDir . '/opis/closure/functions.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
'ed962a97bd972bc82007176b647d4e36' => $vendorDir . '/facade/ignition/src/helpers.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php',
'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php',
'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
'58571171fd5812e6e447dce228f52f4d' => $vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
'f18cc91337d49233e5754e93f3ed9ec3' => $vendorDir . '/laravelcollective/html/src/helpers.php',
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
'e617b14322a074392076a2f38eaf6115' => $baseDir . '/app/Helper.php',
);

View file

@ -2,7 +2,7 @@
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(

View file

@ -2,12 +2,11 @@
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/reflection-docblock/src', $vendorDir . '/phpdocumentor/type-resolver/src'),
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
@ -54,16 +53,19 @@ return array(
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'Prophecy\\' => array($vendorDir . '/phpspec/prophecy/src/Prophecy'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'Opis\\Closure\\' => array($vendorDir . '/opis/closure/src'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),
'League\\Config\\' => array($vendorDir . '/league/config/src'),
'League\\CommonMark\\' => array($vendorDir . '/league/commonmark/src'),
'Laravel\\Ui\\' => array($vendorDir . '/laravel/ui/src'),
'Laravel\\Tinker\\' => array($vendorDir . '/laravel/tinker/src'),
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
'Illuminate\\Support\\' => array($vendorDir . '/laravel/framework/src/Illuminate/Macroable', $vendorDir . '/laravel/framework/src/Illuminate/Collections'),
'Illuminate\\Foundation\\Auth\\' => array($vendorDir . '/laravel/ui/auth-backend'),
'Illuminate\\' => array($vendorDir . '/laravel/framework/src/Illuminate'),
'Http\\Promise\\' => array($vendorDir . '/php-http/promise/src'),
@ -76,18 +78,25 @@ return array(
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
'GrahamCampbell\\Manager\\' => array($vendorDir . '/graham-campbell/manager/src'),
'GrahamCampbell\\GitHub\\' => array($vendorDir . '/graham-campbell/github/src'),
'GrahamCampbell\\BoundedCache\\' => array($vendorDir . '/graham-campbell/bounded-cache/src'),
'Github\\' => array($vendorDir . '/knplabs/github-api/lib/Github'),
'Fideloper\\Proxy\\' => array($vendorDir . '/fideloper/proxy/src'),
'Faker\\' => array($vendorDir . '/fzaninotto/faker/src/Faker'),
'Facade\\Ignition\\' => array($vendorDir . '/facade/ignition/src'),
'Facade\\IgnitionContracts\\' => array($vendorDir . '/facade/ignition-contracts/src'),
'Facade\\FlareClient\\' => array($vendorDir . '/facade/flare-client-php/src'),
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib/Doctrine/Inflector'),
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib/Doctrine/Common/Lexer'),
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
'Database\\Seeders\\' => array($baseDir . '/database/seeders'),
'Database\\Factories\\' => array($baseDir . '/database/factories'),
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
'Collective\\Html\\' => array($vendorDir . '/laravelcollective/html/src'),
'Clue\\StreamFilter\\' => array($vendorDir . '/clue/stream-filter/src'),

View file

@ -25,38 +25,15 @@ class ComposerAutoloaderInit4b6fb9210a1ea37c2db27b8ff53a1ecf
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit4b6fb9210a1ea37c2db27b8ff53a1ecf', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit4b6fb9210a1ea37c2db27b8ff53a1ecf', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit4b6fb9210a1ea37c2db27b8ff53a1ecf::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit4b6fb9210a1ea37c2db27b8ff53a1ecf::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
$includeFiles = \Composer\Autoload\ComposerStaticInit4b6fb9210a1ea37c2db27b8ff53a1ecf::$files;
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire4b6fb9210a1ea37c2db27b8ff53a1ecf($fileIdentifier, $file);
}
@ -65,11 +42,16 @@ class ComposerAutoloaderInit4b6fb9210a1ea37c2db27b8ff53a1ecf
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire4b6fb9210a1ea37c2db27b8ff53a1ecf($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [3.0.2] - 2022-10-27
### Fixed
- Added missing return types to docblocks (#44, #45)
## [3.0.1] - 2021-08-13
### Added
- Adds ReturnTypeWillChange to suppress PHP 8.1 warnings (#40)
## [3.0.0] - 2021-01-01
### Added
- Added support for both `.` and `/`-delimited key paths (#24)
- Added parameter and return types to everything; enabled strict type checks (#18)
- Added new exception classes to better identify certain types of errors (#20)
- `Data` now implements `ArrayAccess` (#17)
- Added ability to merge non-associative array values (#31, #32)
### Changed
- All thrown exceptions are now instances or subclasses of `DataException` (#20)
- Calling `get()` on a missing key path without providing a default will throw a `MissingPathException` instead of returning `null` (#29)
- Bumped supported PHP versions to 7.1 - 8.x (#18)
### Fixed
- Fixed incorrect merging of array values into string values (#32)
- Fixed `get()` method behaving as if keys with `null` values didn't exist
## [2.0.0] - 2017-12-21
### Changed
- Bumped supported PHP versions to 7.0 - 7.4 (#12)
- Switched to PSR-4 autoloading
## [1.1.0] - 2017-01-20
### Added
- Added new `has()` method to check for the existence of the given key (#4, #7)
## [1.0.1] - 2015-08-12
### Added
- Added new optional `$default` parameter to the `get()` method (#2)
## [1.0.0] - 2012-07-17
**Initial release!**
[Unreleased]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.2...main
[3.0.2]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.1...v3.0.2
[3.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v2.0.0...v3.0.0
[2.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.1.0...v2.0.0
[1.1.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/dflydev/dflydev-dot-access-data/releases/tag/v1.0.0

19
vendor/dflydev/dot-access-data/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2012 Dragonfly Development Inc.
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.

158
vendor/dflydev/dot-access-data/README.md vendored Normal file
View file

@ -0,0 +1,158 @@
Dot Access Data
===============
[![Latest Version](https://img.shields.io/packagist/v/dflydev/dot-access-data.svg?style=flat-square)](https://packagist.org/packages/dflydev/dot-access-data)
[![Total Downloads](https://img.shields.io/packagist/dt/dflydev/dot-access-data.svg?style=flat-square)](https://packagist.org/packages/dflydev/dot-access-data)
[![Software License](https://img.shields.io/badge/License-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/github/workflow/status/dflydev/dflydev-dot-access-data/Tests/main.svg?style=flat-square)](https://github.com/dflydev/dflydev-dot-access-data/actions?query=workflow%3ATests+branch%3Amain)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/dflydev/dflydev-dot-access-data.svg?style=flat-square)](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data/code-structure/)
[![Quality Score](https://img.shields.io/scrutinizer/g/dflydev/dflydev-dot-access-data.svg?style=flat-square)](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data)
Given a deep data structure, access data by dot notation.
Requirements
------------
* PHP (7.1+)
> For PHP (5.3+) please refer to version `1.0`.
Usage
-----
Abstract example:
```php
use Dflydev\DotAccessData\Data;
$data = new Data;
$data->set('a.b.c', 'C');
$data->set('a.b.d', 'D1');
$data->append('a.b.d', 'D2');
$data->set('a.b.e', ['E0', 'E1', 'E2']);
// C
$data->get('a.b.c');
// ['D1', 'D2']
$data->get('a.b.d');
// ['E0', 'E1', 'E2']
$data->get('a.b.e');
// true
$data->has('a.b.c');
// false
$data->has('a.b.d.j');
// 'some-default-value'
$data->get('some.path.that.does.not.exist', 'some-default-value');
// throws a MissingPathException because no default was given
$data->get('some.path.that.does.not.exist');
```
A more concrete example:
```php
use Dflydev\DotAccessData\Data;
$data = new Data([
'hosts' => [
'hewey' => [
'username' => 'hman',
'password' => 'HPASS',
'roles' => ['web'],
],
'dewey' => [
'username' => 'dman',
'password' => 'D---S',
'roles' => ['web', 'db'],
'nick' => 'dewey dman',
],
'lewey' => [
'username' => 'lman',
'password' => 'LP@$$',
'roles' => ['db'],
],
],
]);
// hman
$username = $data->get('hosts.hewey.username');
// HPASS
$password = $data->get('hosts.hewey.password');
// ['web']
$roles = $data->get('hosts.hewey.roles');
// dewey dman
$nick = $data->get('hosts.dewey.nick');
// Unknown
$nick = $data->get('hosts.lewey.nick', 'Unknown');
// DataInterface instance
$dewey = $data->getData('hosts.dewey');
// dman
$username = $dewey->get('username');
// D---S
$password = $dewey->get('password');
// ['web', 'db']
$roles = $dewey->get('roles');
// No more lewey
$data->remove('hosts.lewey');
// Add DB to hewey's roles
$data->append('hosts.hewey.roles', 'db');
$data->set('hosts.april', [
'username' => 'aman',
'password' => '@---S',
'roles' => ['web'],
]);
// Check if a key exists (true to this case)
$hasKey = $data->has('hosts.dewey.username');
```
`Data` may be used as an array, since it implements `ArrayAccess` interface:
```php
// Get
$data->get('name') === $data['name']; // true
$data['name'] = 'Dewey';
// is equivalent to
$data->set($name, 'Dewey');
isset($data['name']) === $data->has('name');
// Remove key
unset($data['name']);
```
`/` can also be used as a path delimiter:
```php
$data->set('a/b/c', 'd');
echo $data->get('a/b/c'); // "d"
$data->get('a/b/c') === $data->get('a.b.c'); // true
```
License
-------
This library is licensed under the MIT License - see the LICENSE file
for details.
Community
---------
If you have questions or want to help out, join us in the
[#dflydev](irc://irc.freenode.net/#dflydev) channel on irc.freenode.net.

View file

@ -0,0 +1,67 @@
{
"name": "dflydev/dot-access-data",
"type": "library",
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": ["dot", "access", "data", "notation"],
"license": "MIT",
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.0.0"
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Dflydev\\DotAccessData\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"scripts": {
"phpcs": "phpcs",
"phpstan": "phpstan analyse",
"phpunit": "phpunit --no-coverage",
"psalm": "psalm",
"test": [
"@phpcs",
"@phpstan",
"@psalm",
"@phpunit"
]
}
}

View file

@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData;
use ArrayAccess;
use Dflydev\DotAccessData\Exception\DataException;
use Dflydev\DotAccessData\Exception\InvalidPathException;
use Dflydev\DotAccessData\Exception\MissingPathException;
/**
* @implements ArrayAccess<string, mixed>
*/
class Data implements DataInterface, ArrayAccess
{
private const DELIMITERS = ['.', '/'];
/**
* Internal representation of data data
*
* @var array<string, mixed>
*/
protected $data;
/**
* Constructor
*
* @param array<string, mixed> $data
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* {@inheritdoc}
*/
public function append(string $key, $value = null): void
{
$currentValue =& $this->data;
$keyPath = self::keyToPathArray($key);
$endKey = array_pop($keyPath);
foreach ($keyPath as $currentKey) {
if (! isset($currentValue[$currentKey])) {
$currentValue[$currentKey] = [];
}
$currentValue =& $currentValue[$currentKey];
}
if (!isset($currentValue[$endKey])) {
$currentValue[$endKey] = [];
}
if (!is_array($currentValue[$endKey])) {
// Promote this key to an array.
// TODO: Is this really what we want to do?
$currentValue[$endKey] = [$currentValue[$endKey]];
}
$currentValue[$endKey][] = $value;
}
/**
* {@inheritdoc}
*/
public function set(string $key, $value = null): void
{
$currentValue =& $this->data;
$keyPath = self::keyToPathArray($key);
$endKey = array_pop($keyPath);
foreach ($keyPath as $currentKey) {
if (!isset($currentValue[$currentKey])) {
$currentValue[$currentKey] = [];
}
if (!is_array($currentValue[$currentKey])) {
throw new DataException(sprintf('Key path "%s" within "%s" cannot be indexed into (is not an array)', $currentKey, self::formatPath($key)));
}
$currentValue =& $currentValue[$currentKey];
}
$currentValue[$endKey] = $value;
}
/**
* {@inheritdoc}
*/
public function remove(string $key): void
{
$currentValue =& $this->data;
$keyPath = self::keyToPathArray($key);
$endKey = array_pop($keyPath);
foreach ($keyPath as $currentKey) {
if (!isset($currentValue[$currentKey])) {
return;
}
$currentValue =& $currentValue[$currentKey];
}
unset($currentValue[$endKey]);
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function get(string $key, $default = null)
{
/** @psalm-suppress ImpureFunctionCall */
$hasDefault = \func_num_args() > 1;
$currentValue = $this->data;
$keyPath = self::keyToPathArray($key);
foreach ($keyPath as $currentKey) {
if (!is_array($currentValue) || !array_key_exists($currentKey, $currentValue)) {
if ($hasDefault) {
return $default;
}
throw new MissingPathException($key, sprintf('No data exists at the given path: "%s"', self::formatPath($keyPath)));
}
$currentValue = $currentValue[$currentKey];
}
return $currentValue === null ? $default : $currentValue;
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function has(string $key): bool
{
$currentValue = $this->data;
foreach (self::keyToPathArray($key) as $currentKey) {
if (
!is_array($currentValue) ||
!array_key_exists($currentKey, $currentValue)
) {
return false;
}
$currentValue = $currentValue[$currentKey];
}
return true;
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function getData(string $key): DataInterface
{
$value = $this->get($key);
if (is_array($value) && Util::isAssoc($value)) {
return new Data($value);
}
throw new DataException(sprintf('Value at "%s" could not be represented as a DataInterface', self::formatPath($key)));
}
/**
* {@inheritdoc}
*/
public function import(array $data, int $mode = self::REPLACE): void
{
$this->data = Util::mergeAssocArray($this->data, $data, $mode);
}
/**
* {@inheritdoc}
*/
public function importData(DataInterface $data, int $mode = self::REPLACE): void
{
$this->import($data->export(), $mode);
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function export(): array
{
return $this->data;
}
/**
* {@inheritdoc}
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
{
return $this->has($key);
}
/**
* {@inheritdoc}
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->get($key, null);
}
/**
* {@inheritdoc}
*
* @param string $key
* @param mixed $value
*
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
{
$this->set($key, $value);
}
/**
* {@inheritdoc}
*
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
{
$this->remove($key);
}
/**
* @param string $path
*
* @return string[]
*
* @psalm-return non-empty-list<string>
*
* @psalm-pure
*/
protected static function keyToPathArray(string $path): array
{
if (\strlen($path) === 0) {
throw new InvalidPathException('Path cannot be an empty string');
}
$path = \str_replace(self::DELIMITERS, '.', $path);
return \explode('.', $path);
}
/**
* @param string|string[] $path
*
* @return string
*
* @psalm-pure
*/
protected static function formatPath($path): string
{
if (is_string($path)) {
$path = self::keyToPathArray($path);
}
return implode(' » ', $path);
}
}

View file

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData;
use Dflydev\DotAccessData\Exception\DataException;
use Dflydev\DotAccessData\Exception\InvalidPathException;
interface DataInterface
{
public const PRESERVE = 0;
public const REPLACE = 1;
public const MERGE = 2;
/**
* Append a value to a key (assumes key refers to an array value)
*
* If the key does not yet exist it will be created.
* If the key references a non-array it's existing contents will be added into a new array before appending the new value.
*
* @param string $key
* @param mixed $value
*
* @throws InvalidPathException if the given key is empty
*/
public function append(string $key, $value = null): void;
/**
* Set a value for a key
*
* If the key does not yet exist it will be created.
*
* @param string $key
* @param mixed $value
*
* @throws InvalidPathException if the given key is empty
* @throws DataException if the given key does not target an array
*/
public function set(string $key, $value = null): void;
/**
* Remove a key
*
* No exception will be thrown if the key does not exist
*
* @param string $key
*
* @throws InvalidPathException if the given key is empty
*/
public function remove(string $key): void;
/**
* Get the raw value for a key
*
* If the key does not exist, an optional default value can be returned instead.
* If no default is provided then an exception will be thrown instead.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*
* @throws InvalidPathException if the given key is empty
* @throws InvalidPathException if the given key does not exist and no default value was given
*
* @psalm-mutation-free
*/
public function get(string $key, $default = null);
/**
* Check if the key exists
*
* @param string $key
*
* @return bool
*
* @throws InvalidPathException if the given key is empty
*
* @psalm-mutation-free
*/
public function has(string $key): bool;
/**
* Get a data instance for a key
*
* @param string $key
*
* @return DataInterface
*
* @throws InvalidPathException if the given key is empty
* @throws DataException if the given key does not reference an array
*
* @psalm-mutation-free
*/
public function getData(string $key): DataInterface;
/**
* Import data into existing data
*
* @param array<string, mixed> $data
* @param self::PRESERVE|self::REPLACE|self::MERGE $mode
*/
public function import(array $data, int $mode = self::REPLACE): void;
/**
* Import data from an external data into existing data
*
* @param DataInterface $data
* @param self::PRESERVE|self::REPLACE|self::MERGE $mode
*/
public function importData(DataInterface $data, int $mode = self::REPLACE): void;
/**
* Export data as raw data
*
* @return array<string, mixed>
*
* @psalm-mutation-free
*/
public function export(): array;
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData\Exception;
/**
* Base runtime exception type thrown by this library
*/
class DataException extends \RuntimeException
{
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData\Exception;
/**
* Thrown when trying to access an invalid path in the data array
*/
class InvalidPathException extends DataException
{
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData\Exception;
use Throwable;
/**
* Thrown when trying to access a path that does not exist
*/
class MissingPathException extends DataException
{
/** @var string */
protected $path;
public function __construct(string $path, string $message = '', int $code = 0, Throwable $previous = null)
{
$this->path = $path;
parent::__construct($message, $code, $previous);
}
public function getPath(): string
{
return $this->path;
}
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData;
class Util
{
/**
* Test if array is an associative array
*
* Note that this function will return true if an array is empty. Meaning
* empty arrays will be treated as if they are associative arrays.
*
* @param array<mixed> $arr
*
* @return bool
*
* @psalm-pure
*/
public static function isAssoc(array $arr): bool
{
return !count($arr) || count(array_filter(array_keys($arr), 'is_string')) == count($arr);
}
/**
* Merge contents from one associtative array to another
*
* @param mixed $to
* @param mixed $from
* @param DataInterface::PRESERVE|DataInterface::REPLACE|DataInterface::MERGE $mode
*
* @return mixed
*
* @psalm-pure
*/
public static function mergeAssocArray($to, $from, int $mode = DataInterface::REPLACE)
{
if ($mode === DataInterface::MERGE && self::isList($to) && self::isList($from)) {
return array_merge($to, $from);
}
if (is_array($from) && is_array($to)) {
foreach ($from as $k => $v) {
if (!isset($to[$k])) {
$to[$k] = $v;
} else {
$to[$k] = self::mergeAssocArray($to[$k], $v, $mode);
}
}
return $to;
}
return $mode === DataInterface::PRESERVE ? $to : $from;
}
/**
* @param mixed $value
*
* @return bool
*
* @psalm-pure
*/
private static function isList($value): bool
{
return is_array($value) && array_values($value) === $value;
}
}

View file

@ -16,12 +16,12 @@
"php": "^7.2 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^8.2",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"vimeo/psalm": "^4.10"
"doctrine/coding-standard": "^10",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-strict-rules": "^1.3",
"phpunit/phpunit": "^8.5 || ^9.5",
"vimeo/psalm": "^4.25"
},
"autoload": {
"psr-4": {
@ -32,5 +32,10 @@
"psr-4": {
"Doctrine\\Tests\\Inflector\\": "tests/Doctrine/Tests/Inflector"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View file

@ -11,9 +11,7 @@ use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getSingular(): iterable
{
yield new Transformation(new Pattern('(s)tatuses$'), '\1\2tatus');
@ -56,12 +54,12 @@ class Inflectible
yield new Transformation(new Pattern('(f)eet$'), '\1oot');
yield new Transformation(new Pattern('(n)ews$'), '\1\2ews');
yield new Transformation(new Pattern('eaus$'), 'eau');
yield new Transformation(new Pattern('^tights$'), 'tights');
yield new Transformation(new Pattern('^shorts$'), 'shorts');
yield new Transformation(new Pattern('s$'), '');
}
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getPlural(): iterable
{
yield new Transformation(new Pattern('(s)tatus$'), '\1\2tatuses');
@ -91,14 +89,13 @@ class Inflectible
yield new Transformation(new Pattern('$'), 's');
}
/**
* @return Substitution[]
*/
/** @return Substitution[] */
public static function getIrregular(): iterable
{
yield new Substitution(new Word('atlas'), new Word('atlases'));
yield new Substitution(new Word('axe'), new Word('axes'));
yield new Substitution(new Word('beef'), new Word('beefs'));
yield new Substitution(new Word('blouse'), new Word('blouses'));
yield new Substitution(new Word('brother'), new Word('brothers'));
yield new Substitution(new Word('cafe'), new Word('cafes'));
yield new Substitution(new Word('chateau'), new Word('chateaux'));
@ -151,6 +148,7 @@ class Inflectible
yield new Substitution(new Word('runner-up'), new Word('runners-up'));
yield new Substitution(new Word('safe'), new Word('safes'));
yield new Substitution(new Word('sex'), new Word('sexes'));
yield new Substitution(new Word('sieve'), new Word('sieves'));
yield new Substitution(new Word('soliloquy'), new Word('soliloquies'));
yield new Substitution(new Word('son-in-law'), new Word('sons-in-law'));
yield new Substitution(new Word('syllabus'), new Word('syllabi'));

View file

@ -8,9 +8,7 @@ use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getSingular(): iterable
{
yield from self::getDefault();
@ -30,9 +28,7 @@ final class Uninflected
yield new Pattern('utopia');
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getPlural(): iterable
{
yield from self::getDefault();
@ -43,9 +39,7 @@ final class Uninflected
yield new Pattern('media');
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
private static function getDefault(): iterable
{
yield new Pattern('\w+media');
@ -64,6 +58,7 @@ final class Uninflected
yield new Pattern('butter');
yield new Pattern('cantus');
yield new Pattern('carp');
yield new Pattern('cattle');
yield new Pattern('chassis');
yield new Pattern('clippers');
yield new Pattern('clothing');
@ -111,6 +106,7 @@ final class Uninflected
yield new Pattern('jackanapes');
yield new Pattern('jeans');
yield new Pattern('jedi');
yield new Pattern('kin');
yield new Pattern('kiplingese');
yield new Pattern('knowledge');
yield new Pattern('kongoese');

View file

@ -11,9 +11,7 @@ use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getSingular(): iterable
{
yield new Transformation(new Pattern('/(b|cor|ém|gemm|soupir|trav|vant|vitr)aux$/'), '\1ail');
@ -23,9 +21,7 @@ class Inflectible
yield new Transformation(new Pattern('/s$/'), '');
}
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getPlural(): iterable
{
yield new Transformation(new Pattern('/(s|x|z)$/'), '\1');
@ -38,9 +34,7 @@ class Inflectible
yield new Transformation(new Pattern('/$/'), 's');
}
/**
* @return Substitution[]
*/
/** @return Substitution[] */
public static function getIrregular(): iterable
{
yield new Substitution(new Word('monsieur'), new Word('messieurs'));

View file

@ -8,25 +8,19 @@ use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getSingular(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getPlural(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
private static function getDefault(): iterable
{
yield new Pattern('');

View file

@ -11,18 +11,14 @@ use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getSingular(): iterable
{
yield new Transformation(new Pattern('/re$/i'), 'r');
yield new Transformation(new Pattern('/er$/i'), '');
}
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getPlural(): iterable
{
yield new Transformation(new Pattern('/e$/i'), 'er');
@ -30,9 +26,7 @@ class Inflectible
yield new Transformation(new Pattern('/$/'), 'er');
}
/**
* @return Substitution[]
*/
/** @return Substitution[] */
public static function getIrregular(): iterable
{
yield new Substitution(new Word('konto'), new Word('konti'));

View file

@ -8,25 +8,19 @@ use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getSingular(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getPlural(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
private static function getDefault(): iterable
{
yield new Pattern('barn');

View file

@ -11,9 +11,7 @@ use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getSingular(): iterable
{
yield new Transformation(new Pattern('/^(g|)ases$/i'), '\1ás');
@ -34,9 +32,7 @@ class Inflectible
yield new Transformation(new Pattern('/([^ê])s$/i'), '\1');
}
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getPlural(): iterable
{
yield new Transformation(new Pattern('/^(alem|c|p)ao$/i'), '\1aes');
@ -58,9 +54,7 @@ class Inflectible
yield new Transformation(new Pattern('/$/'), 's');
}
/**
* @return Substitution[]
*/
/** @return Substitution[] */
public static function getIrregular(): iterable
{
yield new Substitution(new Word('abdomen'), new Word('abdomens'));

View file

@ -8,25 +8,19 @@ use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getSingular(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getPlural(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
private static function getDefault(): iterable
{
yield new Pattern('tórax');

View file

@ -11,9 +11,7 @@ use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getSingular(): iterable
{
yield new Transformation(new Pattern('/ereses$/'), 'erés');
@ -23,9 +21,7 @@ class Inflectible
yield new Transformation(new Pattern('/s$/'), '');
}
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getPlural(): iterable
{
yield new Transformation(new Pattern('/ú([sn])$/i'), 'u\1es');
@ -39,9 +35,7 @@ class Inflectible
yield new Transformation(new Pattern('/$/'), 's');
}
/**
* @return Substitution[]
*/
/** @return Substitution[] */
public static function getIrregular(): iterable
{
yield new Substitution(new Word('el'), new Word('los'));

View file

@ -8,25 +8,19 @@ use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getSingular(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getPlural(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
private static function getDefault(): iterable
{
yield new Pattern('lunes');

View file

@ -11,26 +11,20 @@ use Doctrine\Inflector\Rules\Word;
class Inflectible
{
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getSingular(): iterable
{
yield new Transformation(new Pattern('/l[ae]r$/i'), '');
}
/**
* @return Transformation[]
*/
/** @return Transformation[] */
public static function getPlural(): iterable
{
yield new Transformation(new Pattern('/([eöiü][^aoıueöiü]{0,6})$/u'), '\1ler');
yield new Transformation(new Pattern('/([aoıu][^aoıueöiü]{0,6})$/u'), '\1lar');
}
/**
* @return Substitution[]
*/
/** @return Substitution[] */
public static function getIrregular(): iterable
{
yield new Substitution(new Word('ben'), new Word('biz'));

View file

@ -8,25 +8,19 @@ use Doctrine\Inflector\Rules\Pattern;
final class Uninflected
{
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getSingular(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
public static function getPlural(): iterable
{
yield from self::getDefault();
}
/**
* @return Pattern[]
*/
/** @return Pattern[] */
private static function getDefault(): iterable
{
yield new Pattern('lunes');

View file

@ -1,13 +0,0 @@
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
parameters:
level: 7
paths:
- lib
- tests
excludes_analyse:
- %rootDir%/../../../tests/Doctrine/Tests/Common/*

View file

@ -1,15 +0,0 @@
<?xml version="1.0"?>
<psalm
errorLevel="7"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="lib/Doctrine/Inflector" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

View file

@ -1,16 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_style = space
indent_size = 2

View file

@ -1,5 +1,149 @@
# Change Log
## [3.3.2] - 2022-09-19
### Added
- N/A
### Changed
- Skip some daylight savings time tests for PHP 8.1 daylight savings time weirdness (#146)
### Fixed
- Changed string interpolations to work better with PHP 8.2 (#142)
## [3.3.1] - 2022-01-18
### Added
- N/A
### Changed
- N/A
### Fixed
- Fixed issue when timezones had no transition, which can occur over very short timespans (#134)
## [3.3.0] - 2022-01-13
### Added
- Added ability to register your own expression aliases (#132)
### Changed
- Changed how Day of Week and Day of Month resolve when one or the other is `*` or `?`
### Fixed
- PHPStan should no longer error out
## [3.2.4] - 2022-01-12
### Added
- N/A
### Changed
- Changed how Day of Week increment/decrement to help with DST changes (#131)
### Fixed
- N/A
## [3.2.3] - 2022-01-05
### Added
- N/A
### Changed
- Changed how minutes and hours increment/decrement to help with DST changes (#131)
### Fixed
- N/A
## [3.2.2] - 2022-01-05
### Added
- N/A
### Changed
- Marked some methods `@internal` (#124)
### Fixed
- Fixed issue with small ranges and large steps that caused an error with `range()` (#88)
- Fixed issue where wraparound logic incorrectly considered high bound on range (#89)
## [3.2.1] - 2022-01-04
### Added
- N/A
### Changed
- Added PHP 8.1 to testing (#125)
### Fixed
- Allow better mixture of ranges, steps, and lists (#122)
- Fixed return order when multiple dates are requested and inverted (#121)
- Better handling over DST (#115)
- Fixed PHPStan tests (#130)
## [3.2.0] - 2022-01-04
### Added
- Added alias for `@midnight` (#117)
### Changed
- Improved testing for instance of field in tests (#105)
- Optimization for determining multiple run dates (#75)
- `CronExpression` properties changed from private to protected (#106)
### Fixed
- N/A
## [3.1.0] - 2020-11-24
### Added
- Added `CronExpression::getParts()` method to get parts of the expression as an array (#83)
### Changed
- Changed to Interfaces for some type hints (#97, #86)
- Dropped minimum PHP version to 7.2
- Few syntax changes for phpstan compatibility (#93)
### Fixed
- N/A
### Deprecated
- Deprecated `CronExpression::factory` in favor of the constructor (#56)
- Deprecated `CronExpression::YEAR` as a formality, the functionality is already removed (#87)
## [3.0.1] - 2020-10-12
### Added
- Added support for PHP 8 (#92)
### Changed
- N/A
### Fixed
- N/A
## [3.0.0] - 2020-03-25
**MAJOR CHANGE** - In previous versions of this library, setting both a "Day of Month" and a "Day of Week" would be interpreted as an `AND` statement, not an `OR` statement. For example:
`30 0 1 * 1`
would evaluate to "Run 30 minutes after the 0 hour when the Day Of Month is 1 AND a Monday" instead of "Run 30 minutes after the 0 hour on Day Of Month 1 OR a Monday", where the latter is more inline with most cron systems. This means that if your cron expression has both of these fields set, you may see your expression fire more often starting with v3.0.0.
### Added
- Additional docblocks for IDE and documentation
- Added phpstan as a development dependency
- Added a `Cron\FieldFactoryInterface` to make migrations easier (#38)
### Changed
- Changed some DI testing during TravisCI runs
- `\Cron\CronExpression::determineTimezone()` now checks for `\DateTimeInterface` instead of just `\DateTime`
- Errors with fields now report a more human-understandable error and are 1-based instead of 0-based
- Better support for `\DateTimeImmutable` across the library by typehinting for `\DateTimeInterface` now
- Literals should now be less case-sensative across the board
- Changed logic for when both a Day of Week and a Day of Month are supplied to now be an OR statement, not an AND
### Fixed
- Fixed infinite loop when determining last day of week from literals
- Fixed bug where single number ranges were allowed (ex: `1/10`)
- Fixed nullable FieldFactory in CronExpression where no factory could be supplied
- Fixed issue where logic for dropping seconds to 0 could lead to a timezone change
## [2.3.1] - 2020-10-12
### Added
- Added support for PHP 8 (#92)

View file

@ -1,7 +1,7 @@
PHP Cron Expression Parser
==========================
[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression)
[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) [![StyleCI](https://github.styleci.io/repos/103715337/shield?branch=master)](https://github.styleci.io/repos/103715337)
The PHP cron expression parser can parse a CRON expression, determine if it is
due to run, calculate the next run date of the expression, and calculate the previous
@ -32,21 +32,21 @@ Usage
require_once '/vendor/autoload.php';
// Works with predefined scheduling definitions
$cron = Cron\CronExpression::factory('@daily');
$cron = new Cron\CronExpression('@daily');
$cron->isDue();
echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s');
// Works with complex expressions
$cron = Cron\CronExpression::factory('3-59/15 6-12 */15 1 2-5');
$cron = new Cron\CronExpression('3-59/15 6-12 */15 1 2-5');
echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
// Calculate a run date two iterations into the future
$cron = Cron\CronExpression::factory('@daily');
$cron = new Cron\CronExpression('@daily');
echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s');
// Calculate a run date relative to a specific time
$cron = Cron\CronExpression::factory('@monthly');
$cron = new Cron\CronExpression('@monthly');
echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s');
```
@ -65,10 +65,18 @@ A CRON expression is a string representing the schedule for a particular command
| +-------------------- hour (0 - 23)
+------------------------- min (0 - 59)
This library also supports a few macros:
* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - `0 0 1 1 *`
* `@monthly` - Run once a month, midnight, first of month - `0 0 1 * *`
* `@weekly` - Run once a week, midnight on Sun - `0 0 * * 0`
* `@daily`, `@midnight` - Run once a day, midnight - `0 0 * * *`
* `@hourly` - Run once an hour, first minute - `0 * * * *`
Requirements
============
- PHP 7.0+
- PHP 7.2+
- PHPUnit is required to run the unit tests
- Composer is required to run the unit tests
@ -76,3 +84,4 @@ Projects that Use cron-expression
=================================
* Part of the [Laravel Framework](https://github.com/laravel/framework/)
* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle)
* Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/lavary/crunz)

View file

@ -5,11 +5,6 @@
"keywords": ["cron", "schedule"],
"license": "MIT",
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Chris Tankersley",
"email": "chris@ctankersley.com",
@ -17,10 +12,14 @@
}
],
"require": {
"php": "^7.0|^8.0"
"php": "^7.2|^8.0",
"webmozart/assert": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0"
"phpstan/phpstan": "^1.0",
"phpunit/phpunit": "^7.0|^8.0|^9.0",
"phpstan/phpstan-webmozart-assert": "^1.0",
"phpstan/extension-installer": "^1.0"
},
"autoload": {
"psr-4": {
@ -29,12 +28,20 @@
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/Cron/"
"Cron\\Tests\\": "tests/Cron/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.3-dev"
"replace": {
"mtdowling/cron-expression": "^1.0"
},
"scripts": {
"phpstan": "./vendor/bin/phpstan analyze",
"test": "phpunit"
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"phpstan/extension-installer": true
}
}
}

View file

@ -0,0 +1,15 @@
parameters:
checkMissingIterableValueType: false
ignoreErrors:
- '#Call to an undefined method DateTimeInterface::add\(\)#'
- '#Call to an undefined method DateTimeInterface::modify\(\)#'
- '#Call to an undefined method DateTimeInterface::setDate\(\)#'
- '#Call to an undefined method DateTimeInterface::setTime\(\)#'
- '#Call to an undefined method DateTimeInterface::setTimezone\(\)#'
- '#Call to an undefined method DateTimeInterface::sub\(\)#'
level: max
paths:
- src/

View file

@ -1,33 +1,41 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
/**
* Abstract CRON expression field
* Abstract CRON expression field.
*/
abstract class AbstractField implements FieldInterface
{
/**
* Full range of values that are allowed for this field type
* Full range of values that are allowed for this field type.
*
* @var array
*/
protected $fullRange = [];
/**
* Literal values we need to convert to integers
* Literal values we need to convert to integers.
*
* @var array
*/
protected $literals = [];
/**
* Start value of the full range
* @var integer
* Start value of the full range.
*
* @var int
*/
protected $rangeStart;
/**
* End value of the full range
* @var integer
* End value of the full range.
*
* @var int
*/
protected $rangeEnd;
@ -40,98 +48,107 @@ abstract class AbstractField implements FieldInterface
}
/**
* Check to see if a field is satisfied by a value
* Check to see if a field is satisfied by a value.
*
* @param string $dateValue Date value to check
* @internal
* @param int $dateValue Date value to check
* @param string $value Value to test
*
* @return bool
*/
public function isSatisfied($dateValue, $value)
public function isSatisfied(int $dateValue, string $value): bool
{
if ($this->isIncrementsOfRanges($value)) {
return $this->isInIncrementsOfRanges($dateValue, $value);
} elseif ($this->isRange($value)) {
}
if ($this->isRange($value)) {
return $this->isInRange($dateValue, $value);
}
return $value == '*' || $dateValue == $value;
return '*' === $value || $dateValue === (int) $value;
}
/**
* Check if a value is a range
* Check if a value is a range.
*
* @internal
* @param string $value Value to test
*
* @return bool
*/
public function isRange($value)
public function isRange(string $value): bool
{
return strpos($value, '-') !== false;
return false !== strpos($value, '-');
}
/**
* Check if a value is an increments of ranges
* Check if a value is an increments of ranges.
*
* @internal
* @param string $value Value to test
*
* @return bool
*/
public function isIncrementsOfRanges($value)
public function isIncrementsOfRanges(string $value): bool
{
return strpos($value, '/') !== false;
return false !== strpos($value, '/');
}
/**
* Test if a value is within a range
* Test if a value is within a range.
*
* @param string $dateValue Set date value
* @internal
* @param int $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInRange($dateValue, $value)
public function isInRange(int $dateValue, $value): bool
{
$parts = array_map(function($value) {
$parts = array_map(
function ($value) {
$value = trim($value);
$value = $this->convertLiterals($value);
return $value;
return $this->convertLiterals($value);
},
explode('-', $value, 2)
);
return $dateValue >= $parts[0] && $dateValue <= $parts[1];
}
/**
* Test if a value is within an increments of ranges (offset[-to]/step size)
* Test if a value is within an increments of ranges (offset[-to]/step size).
*
* @param string $dateValue Set date value
* @internal
* @param int $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInIncrementsOfRanges($dateValue, $value)
public function isInIncrementsOfRanges(int $dateValue, string $value): bool
{
$chunks = array_map('trim', explode('/', $value, 2));
$range = $chunks[0];
$step = isset($chunks[1]) ? $chunks[1] : 0;
$step = $chunks[1] ?? 0;
// No step or 0 steps aren't cool
if (is_null($step) || '0' === $step || 0 === $step) {
/** @phpstan-ignore-next-line */
if (null === $step || '0' === $step || 0 === $step) {
return false;
}
// Expand the * to a full range
if ('*' == $range) {
if ('*' === $range) {
$range = $this->rangeStart . '-' . $this->rangeEnd;
}
// Generate the requested small range
$rangeChunks = explode('-', $range, 2);
$rangeStart = $rangeChunks[0];
$rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart;
$rangeStart = (int) $rangeChunks[0];
$rangeEnd = $rangeChunks[1] ?? $rangeStart;
$rangeEnd = (int) $rangeEnd;
if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) {
throw new \OutOfRangeException('Invalid range start requested');
@ -141,82 +158,93 @@ abstract class AbstractField implements FieldInterface
throw new \OutOfRangeException('Invalid range end requested');
}
// Steps larger than the range need to wrap around and be handled slightly differently than smaller steps
if ($step >= $this->rangeEnd) {
$thisRange = [$this->fullRange[$step % count($this->fullRange)]];
// Steps larger than the range need to wrap around and be handled
// slightly differently than smaller steps
// UPDATE - This is actually false. The C implementation will allow a
// larger step as valid syntax, it never wraps around. It will stop
// once it hits the end. Unfortunately this means in future versions
// we will not wrap around. However, because the logic exists today
// per the above documentation, fixing the bug from #89
if ($step > $this->rangeEnd) {
$thisRange = [$this->fullRange[$step % \count($this->fullRange)]];
} else {
$thisRange = range($rangeStart, $rangeEnd, $step);
if ($step > ($rangeEnd - $rangeStart)) {
$thisRange[$rangeStart] = (int) $rangeStart;
} else {
$thisRange = range($rangeStart, $rangeEnd, (int) $step);
}
}
return in_array($dateValue, $thisRange);
return \in_array($dateValue, $thisRange, true);
}
/**
* Returns a range of values for the given cron expression
* Returns a range of values for the given cron expression.
*
* @param string $expression The expression to evaluate
* @param int $max Maximum offset for range
*
* @return array
*/
public function getRangeForExpression($expression, $max)
public function getRangeForExpression(string $expression, int $max): array
{
$values = array();
$values = [];
$expression = $this->convertLiterals($expression);
if (strpos($expression, ',') !== false) {
if (false !== strpos($expression, ',')) {
$ranges = explode(',', $expression);
$values = [];
foreach ($ranges as $range) {
$expanded = $this->getRangeForExpression($range, $this->rangeEnd);
$values = array_merge($values, $expanded);
}
return $values;
}
if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) {
if (!$this->isIncrementsOfRanges($expression)) {
list ($offset, $to) = explode('-', $expression);
[$offset, $to] = explode('-', $expression);
$offset = $this->convertLiterals($offset);
$to = $this->convertLiterals($to);
$stepSize = 1;
}
else {
} else {
$range = array_map('trim', explode('/', $expression, 2));
$stepSize = isset($range[1]) ? $range[1] : 0;
$stepSize = $range[1] ?? 0;
$range = $range[0];
$range = explode('-', $range, 2);
$offset = $range[0];
$to = isset($range[1]) ? $range[1] : $max;
$to = $range[1] ?? $max;
}
$offset = $offset == '*' ? $this->rangeStart : $offset;
$offset = '*' === $offset ? $this->rangeStart : $offset;
if ($stepSize >= $this->rangeEnd) {
$values = [$this->fullRange[$stepSize % count($this->fullRange)]];
$values = [$this->fullRange[$stepSize % \count($this->fullRange)]];
} else {
for ($i = $offset; $i <= $to; $i += $stepSize) {
$values[] = (int)$i;
$values[] = (int) $i;
}
}
sort($values);
}
else {
$values = array($expression);
} else {
$values = [$expression];
}
return $values;
}
/**
* Convert literal
* Convert literal.
*
* @param string $value
*
* @return string
*/
protected function convertLiterals($value)
protected function convertLiterals(string $value): string
{
if (count($this->literals)) {
$key = array_search($value, $this->literals);
if ($key !== false) {
if (\count($this->literals)) {
$key = array_search(strtoupper($value), $this->literals, true);
if (false !== $key) {
return (string) $key;
}
}
@ -225,12 +253,13 @@ abstract class AbstractField implements FieldInterface
}
/**
* Checks to see if a value is valid for the field
* Checks to see if a value is valid for the field.
*
* @param string $value
*
* @return bool
*/
public function validate($value)
public function validate(string $value): bool
{
$value = $this->convertLiterals($value);
@ -239,22 +268,29 @@ abstract class AbstractField implements FieldInterface
return true;
}
if (strpos($value, '/') !== false) {
list($range, $step) = explode('/', $value);
return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
}
// Validate each chunk of a list individually
if (strpos($value, ',') !== false) {
if (false !== strpos($value, ',')) {
foreach (explode(',', $value) as $listItem) {
if (!$this->validate($listItem)) {
return false;
}
}
return true;
}
if (strpos($value, '-') !== false) {
if (false !== strpos($value, '/')) {
[$range, $step] = explode('/', $value);
// Don't allow numeric ranges
if (is_numeric($range)) {
return false;
}
return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
}
if (false !== strpos($value, '-')) {
if (substr_count($value, '-') > 1) {
return false;
}
@ -263,7 +299,7 @@ abstract class AbstractField implements FieldInterface
$chunks[0] = $this->convertLiterals($chunks[0]);
$chunks[1] = $this->convertLiterals($chunks[1]);
if ('*' == $chunks[0] || '*' == $chunks[1]) {
if ('*' === $chunks[0] || '*' === $chunks[1]) {
return false;
}
@ -274,13 +310,37 @@ abstract class AbstractField implements FieldInterface
return false;
}
if (is_float($value) || strpos($value, '.') !== false) {
if (false !== strpos($value, '.')) {
return false;
}
// We should have a numeric by now, so coerce this into an integer
$value = (int) $value;
return in_array($value, $this->fullRange, true);
return \in_array($value, $this->fullRange, true);
}
protected function timezoneSafeModify(DateTimeInterface $dt, string $modification): DateTimeInterface
{
$timezone = $dt->getTimezone();
$dt = $dt->setTimezone(new \DateTimeZone("UTC"));
$dt = $dt->modify($modification);
$dt = $dt->setTimezone($timezone);
return $dt;
}
protected function setTimeHour(DateTimeInterface $date, bool $invert, int $originalTimestamp): DateTimeInterface
{
$date = $date->setTime((int)$date->format('H'), ($invert ? 59 : 0));
// setTime caused the offset to change, moving time in the wrong direction
$actualTimestamp = $date->format('U');
if ((! $invert) && ($actualTimestamp <= $originalTimestamp)) {
$date = $this->timezoneSafeModify($date, "+1 hour");
} elseif ($invert && ($actualTimestamp >= $originalTimestamp)) {
$date = $this->timezoneSafeModify($date, "-1 hour");
}
return $date;
}
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTime;
@ -8,7 +10,9 @@ use DateTimeInterface;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use LogicException;
use RuntimeException;
use Webmozart\Assert\Assert;
/**
* CRON expression parser that can determine whether or not a CRON expression is
@ -20,83 +24,147 @@ use RuntimeException;
* minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week
* [1-7|MON-SUN], and an optional year.
*
* @link http://en.wikipedia.org/wiki/Cron
* @see http://en.wikipedia.org/wiki/Cron
*/
class CronExpression
{
const MINUTE = 0;
const HOUR = 1;
const DAY = 2;
const MONTH = 3;
const WEEKDAY = 4;
const YEAR = 5;
public const MINUTE = 0;
public const HOUR = 1;
public const DAY = 2;
public const MONTH = 3;
public const WEEKDAY = 4;
/**
* @var array CRON expression parts
*/
private $cronParts;
/** @deprecated */
public const YEAR = 5;
/**
* @var FieldFactory CRON field factory
*/
private $fieldFactory;
/**
* @var int Max iteration count when searching for next run date
*/
private $maxIterationCount = 1000;
/**
* @var array Order in which to test of cron parts
*/
private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE);
/**
* Factory method to create a new CronExpression.
*
* @param string $expression The CRON expression to create. There are
* several special predefined values which can be used to substitute the
* CRON expression:
*
* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 *
* `@monthly` - Run once a month, midnight, first of month - 0 0 1 * *
* `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0
* `@daily` - Run once a day, midnight - 0 0 * * *
* `@hourly` - Run once an hour, first minute - 0 * * * *
* @param FieldFactory|null $fieldFactory Field factory to use
*
* @return CronExpression
*/
public static function factory($expression, FieldFactory $fieldFactory = null)
{
$mappings = array(
public const MAPPINGS = [
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@hourly' => '0 * * * *'
);
'@midnight' => '0 0 * * *',
'@hourly' => '0 * * * *',
];
if (isset($mappings[$expression])) {
$expression = $mappings[$expression];
/**
* @var array CRON expression parts
*/
protected $cronParts;
/**
* @var FieldFactoryInterface CRON field factory
*/
protected $fieldFactory;
/**
* @var int Max iteration count when searching for next run date
*/
protected $maxIterationCount = 1000;
/**
* @var array Order in which to test of cron parts
*/
protected static $order = [
self::YEAR,
self::MONTH,
self::DAY,
self::WEEKDAY,
self::HOUR,
self::MINUTE,
];
/**
* @var array<string, string>
*/
private static $registeredAliases = self::MAPPINGS;
/**
* Registered a user defined CRON Expression Alias.
*
* @throws LogicException If the expression or the alias name are invalid
* or if the alias is already registered.
*/
public static function registerAlias(string $alias, string $expression): void
{
try {
new self($expression);
} catch (InvalidArgumentException $exception) {
throw new LogicException("The expression `$expression` is invalid", 0, $exception);
}
return new static($expression, $fieldFactory ?: new FieldFactory());
$shortcut = strtolower($alias);
if (1 !== preg_match('/^@\w+$/', $shortcut)) {
throw new LogicException("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");
}
if (isset(self::$registeredAliases[$shortcut])) {
throw new LogicException("The alias `$alias` is already registered.");
}
self::$registeredAliases[$shortcut] = $expression;
}
/**
* Unregistered a user defined CRON Expression Alias.
*
* @throws LogicException If the user tries to unregister a built-in alias
*/
public static function unregisterAlias(string $alias): bool
{
$shortcut = strtolower($alias);
if (isset(self::MAPPINGS[$shortcut])) {
throw new LogicException("The alias `$alias` is a built-in alias; it can not be unregistered.");
}
if (!isset(self::$registeredAliases[$shortcut])) {
return false;
}
unset(self::$registeredAliases[$shortcut]);
return true;
}
/**
* Tells whether a CRON Expression alias is registered.
*/
public static function supportsAlias(string $alias): bool
{
return isset(self::$registeredAliases[strtolower($alias)]);
}
/**
* Returns all registered aliases as an associated array where the aliases are the key
* and their associated expressions are the values.
*
* @return array<string, string>
*/
public static function getAliases(): array
{
return self::$registeredAliases;
}
/**
* @deprecated since version 3.0.2, use __construct instead.
*/
public static function factory(string $expression, FieldFactoryInterface $fieldFactory = null): CronExpression
{
/** @phpstan-ignore-next-line */
return new static($expression, $fieldFactory);
}
/**
* Validate a CronExpression.
*
* @param string $expression The CRON expression to validate.
* @param string $expression the CRON expression to validate
*
* @return bool True if a valid CRON expression was passed. False if not.
* @see \Cron\CronExpression::factory
*/
public static function isValidExpression($expression)
public static function isValidExpression(string $expression): bool
{
try {
self::factory($expression);
new CronExpression($expression);
} catch (InvalidArgumentException $e) {
return false;
}
@ -105,29 +173,36 @@ class CronExpression
}
/**
* Parse a CRON expression
* Parse a CRON expression.
*
* @param string $expression CRON expression (e.g. '8 * * * *')
* @param FieldFactory|null $fieldFactory Factory to create cron fields
* @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields
*/
public function __construct($expression, FieldFactory $fieldFactory = null)
public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null)
{
$this->fieldFactory = $fieldFactory;
$shortcut = strtolower($expression);
$expression = self::$registeredAliases[$shortcut] ?? $expression;
$this->fieldFactory = $fieldFactory ?: new FieldFactory();
$this->setExpression($expression);
}
/**
* Set or change the CRON expression
* Set or change the CRON expression.
*
* @param string $value CRON expression (e.g. 8 * * * *)
*
* @return CronExpression
* @throws \InvalidArgumentException if not a valid CRON expression
*
* @return CronExpression
*/
public function setExpression($value)
public function setExpression(string $value): CronExpression
{
$this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
if (count($this->cronParts) < 5) {
$split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
Assert::isArray($split);
$this->cronParts = $split;
if (\count($this->cronParts) < 5) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
@ -141,15 +216,16 @@ class CronExpression
}
/**
* Set part of the CRON expression
* Set part of the CRON expression.
*
* @param int $position The position of the CRON expression to set
* @param string $value The value to set
*
* @return CronExpression
* @throws \InvalidArgumentException if the value is not valid for the part
*
* @return CronExpression
*/
public function setPart($position, $value)
public function setPart(int $position, string $value): CronExpression
{
if (!$this->fieldFactory->getField($position)->validate($value)) {
throw new InvalidArgumentException(
@ -163,13 +239,13 @@ class CronExpression
}
/**
* Set max iteration count for searching next run dates
* Set max iteration count for searching next run dates.
*
* @param int $maxIterationCount Max iteration count when searching for next run date
*
* @return CronExpression
*/
public function setMaxIterationCount($maxIterationCount)
public function setMaxIterationCount(int $maxIterationCount): CronExpression
{
$this->maxIterationCount = $maxIterationCount;
@ -191,16 +267,18 @@ class CronExpression
* it matches the cron expression.
* @param null|string $timeZone TimeZone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
* @throws \Exception
*
* @return \DateTime
*/
public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
}
/**
* Get a previous run date relative to the current date or a specific date
* Get a previous run date relative to the current date or a specific date.
*
* @param string|\DateTimeInterface $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
@ -208,20 +286,23 @@ class CronExpression
* current date if it matches the cron expression
* @param null|string $timeZone TimeZone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
* @throws \Exception
*
* @return \DateTime
*
* @see \Cron\CronExpression::getNextRunDate
*/
public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone);
}
/**
* Get multiple run dates starting at the current date or a specific date
* Get multiple run dates starting at the current date or a specific date.
*
* @param int $total Set the total number of dates to calculate
* @param string|\DateTimeInterface $currentTime Relative calculation date
* @param string|\DateTimeInterface|null $currentTime Relative calculation date
* @param bool $invert Set to TRUE to retrieve previous dates
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
@ -229,48 +310,80 @@ class CronExpression
*
* @return \DateTime[] Returns an array of run dates
*/
public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null)
public function getMultipleRunDates(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array
{
$matches = array();
for ($i = 0; $i < max(0, $total); $i++) {
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
Assert::isInstanceOf($currentTime, DateTime::class);
$currentTime->setTimezone(new DateTimeZone($timeZone));
$matches = [];
for ($i = 0; $i < $total; ++$i) {
try {
$matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone);
$result = $this->getRunDate($currentTime, 0, $invert, $allowCurrentDate, $timeZone);
} catch (RuntimeException $e) {
break;
}
$allowCurrentDate = false;
$currentTime = clone $result;
$matches[] = $result;
}
return $matches;
}
/**
* Get all or part of the CRON expression
* Get all or part of the CRON expression.
*
* @param string $part Specify the part to retrieve or NULL to get the full
* cron schedule string.
* @param int|string|null $part specify the part to retrieve or NULL to get the full
* cron schedule string
*
* @return string|null Returns the CRON expression, a part of the
* @return null|string Returns the CRON expression, a part of the
* CRON expression, or NULL if the part was specified but not found
*/
public function getExpression($part = null)
public function getExpression($part = null): ?string
{
if (null === $part) {
return implode(' ', $this->cronParts);
} elseif (array_key_exists($part, $this->cronParts)) {
}
if (array_key_exists($part, $this->cronParts)) {
return $this->cronParts[$part];
}
return null;
}
/**
* Gets the parts of the cron expression as an array.
*
* @return string[]
* The array of parts that make up this expression.
*/
public function getParts()
{
return $this->cronParts;
}
/**
* Helper method to output the full expression.
*
* @return string Full CRON expression
*/
public function __toString()
public function __toString(): string
{
return $this->getExpression();
return (string) $this->getExpression();
}
/**
@ -283,23 +396,25 @@ class CronExpression
*
* @return bool Returns TRUE if the cron is due to run or FALSE if not
*/
public function isDue($currentTime = 'now', $timeZone = null)
public function isDue($currentTime = 'now', $timeZone = null): bool
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
//
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} else {
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
$currentTime->setTimeZone(new DateTimeZone($timeZone));
Assert::isInstanceOf($currentTime, DateTime::class);
$currentTime->setTimezone(new DateTimeZone($timeZone));
// drop the seconds to 0
$currentTime = DateTime::createFromFormat('Y-m-d H:i', $currentTime->format('Y-m-d H:i'));
$currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0);
try {
return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp();
@ -309,19 +424,21 @@ class CronExpression
}
/**
* Get the next or previous run date of the expression relative to a date
* Get the next or previous run date of the expression relative to a date.
*
* @param string|\DateTimeInterface $currentTime Relative calculation date
* @param string|\DateTimeInterface|null $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $invert Set to TRUE to go backwards in time
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param string|null $timeZone TimeZone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
* @throws Exception
*
* @return \DateTime
*/
protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null)
protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
@ -329,18 +446,26 @@ class CronExpression
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentDate = new DateTime($currentTime);
} else {
$currentDate = new DateTime($currentTime ?: 'now');
$currentDate = new DateTime('now');
}
$currentDate->setTimeZone(new DateTimeZone($timeZone));
$currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
Assert::isInstanceOf($currentDate, DateTime::class);
$currentDate->setTimezone(new DateTimeZone($timeZone));
// Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074
$currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone());
if ($currentDate === false) {
throw new \RuntimeException('Unable to create date from format');
}
$currentDate->setTimezone(new DateTimeZone($timeZone));
$nextRun = clone $currentDate;
$nth = (int) $nth;
// We don't have to satisfy * or null fields
$parts = array();
$fields = array();
$parts = [];
$fields = [];
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
@ -350,20 +475,49 @@ class CronExpression
$fields[$position] = $this->fieldFactory->getField($position);
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; $i++) {
if (isset($parts[self::DAY]) && isset($parts[self::WEEKDAY])) {
$domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3));
$dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4));
$domExpression = new self($domExpression);
$dowExpression = new self($dowExpression);
$domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
$dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
if ($parts[self::DAY] === '?' || $parts[self::DAY] === '*') {
$domRunDates = [];
}
if ($parts[self::WEEKDAY] === '?' || $parts[self::WEEKDAY] === '*') {
$dowRunDates = [];
}
$combined = array_merge($domRunDates, $dowRunDates);
usort($combined, function ($a, $b) {
return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s');
});
if ($invert) {
$combined = array_reverse($combined);
}
return $combined[$nth];
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; ++$i) {
foreach ($parts as $position => $part) {
$satisfied = false;
// Get the field object used to validate this part
$field = $fields[$position];
// Check if this is singular or a list
if (strpos($part, ',') === false) {
$satisfied = $field->isSatisfiedBy($nextRun, $part);
if (false === strpos($part, ',')) {
$satisfied = $field->isSatisfiedBy($nextRun, $part, $invert);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart)) {
if ($field->isSatisfiedBy($nextRun, $listPart, $invert)) {
$satisfied = true;
break;
}
}
@ -372,13 +526,14 @@ class CronExpression
// If the field is not satisfied, then start over
if (!$satisfied) {
$field->increment($nextRun, $invert, $part);
continue 2;
}
}
// Skip this match if needed
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
$this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null);
$this->fieldFactory->getField(self::MINUTE)->increment($nextRun, $invert, $parts[self::MINUTE] ?? null);
continue;
}
@ -393,19 +548,19 @@ class CronExpression
/**
* Workout what timeZone should be used.
*
* @param string|\DateTimeInterface $currentTime Relative calculation date
* @param string|\DateTimeInterface|null $currentTime Relative calculation date
* @param string|null $timeZone TimeZone to use instead of the system default
*
* @return string
*/
protected function determineTimeZone($currentTime, $timeZone)
protected function determineTimeZone($currentTime, ?string $timeZone): string
{
if (! is_null($timeZone)) {
if (null !== $timeZone) {
return $timeZone;
}
if ($currentTime instanceOf DateTimeInterface) {
return $currentTime->getTimeZone()->getName();
if ($currentTime instanceof DateTimeInterface) {
return $currentTime->getTimezone()->getName();
}
return date_default_timezone_get();

View file

@ -1,12 +1,14 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTime;
use DateTimeInterface;
/**
* Day of month field. Allows: * , / - ? L W
* Day of month field. Allows: * , / - ? L W.
*
* 'L' stands for "last" and specifies the last day of the month.
*
@ -26,28 +28,33 @@ use DateTimeInterface;
class DayOfMonthField extends AbstractField
{
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeStart = 1;
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeEnd = 31;
/**
* Get the nearest day of the week for a given day in a month
* Get the nearest day of the week for a given day in a month.
*
* @param int $currentYear Current year
* @param int $currentMonth Current month
* @param int $targetDay Target day of the month
*
* @return \DateTime Returns the nearest date
* @return \DateTime|null Returns the nearest date
*/
private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
private static function getNearestWeekday(int $currentYear, int $currentMonth, int $targetDay): ?DateTime
{
$tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday");
$tday = str_pad((string) $targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "{$currentYear}-{$currentMonth}-{$tday}");
if ($target === false) {
return null;
}
$currentWeekday = (int) $target->format('N');
if ($currentWeekday < 6) {
@ -55,81 +62,93 @@ class DayOfMonthField extends AbstractField
}
$lastDayOfMonth = $target->format('t');
foreach (array(-1, 1, -2, 2) as $i) {
foreach ([-1, 1, -2, 2] as $i) {
$adjusted = $targetDay + $i;
if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
$target->setDate($currentYear, $currentMonth, $adjusted);
if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
if ((int) $target->format('N') < 6 && (int) $target->format('m') === $currentMonth) {
return $target;
}
}
}
return null;
}
/**
* @inheritDoc
* {@inheritdoc}
*/
public function isSatisfiedBy(DateTimeInterface $date, $value)
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
// ? states that the field value is to be skipped
if ($value == '?') {
if ('?' === $value) {
return true;
}
$fieldValue = $date->format('d');
// Check to see if this is the last day of the month
if ($value == 'L') {
return $fieldValue == $date->format('t');
if ('L' === $value) {
return $fieldValue === $date->format('t');
}
// Check to see if this is the nearest weekday to a particular value
if (strpos($value, 'W')) {
if ($wPosition = strpos($value, 'W')) {
// Parse the target day
$targetDay = substr($value, 0, strpos($value, 'W'));
$targetDay = (int) substr($value, 0, $wPosition);
// Find out if the current day is the nearest day of the week
return $date->format('j') == self::getNearestWeekday(
$date->format('Y'),
$date->format('m'),
$nearest = self::getNearestWeekday(
(int) $date->format('Y'),
(int) $date->format('m'),
$targetDay
)->format('j');
);
if ($nearest) {
return $date->format('j') === $nearest->format('j');
}
return $this->isSatisfied($date->format('d'), $value);
throw new \RuntimeException('Unable to return nearest weekday');
}
return $this->isSatisfied((int) $date->format('d'), $value);
}
/**
* @inheritDoc
*
* @param \DateTime|\DateTimeImmutable &$date
* @param \DateTime|\DateTimeImmutable $date
*/
public function increment(DateTimeInterface &$date, $invert = false)
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if ($invert) {
$date = $date->modify('previous day')->setTime(23, 59);
if (! $invert) {
$date = $date->add(new \DateInterval('P1D'));
$date = $date->setTime(0, 0);
} else {
$date = $date->modify('next day')->setTime(0, 0);
$date = $date->sub(new \DateInterval('P1D'));
$date = $date->setTime(23, 59);
}
return $this;
}
/**
* @inheritDoc
* {@inheritdoc}
*/
public function validate($value)
public function validate(string $value): bool
{
$basicChecks = parent::validate($value);
// Validate that a list don't have W or L
if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) {
if (false !== strpos($value, ',') && (false !== strpos($value, 'W') || false !== strpos($value, 'L'))) {
return false;
}
if (!$basicChecks) {
if ('?' === $value) {
return true;
}
if ($value === 'L') {
if ('L' === $value) {
return true;
}

View file

@ -1,13 +1,14 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTime;
use DateTimeInterface;
use InvalidArgumentException;
/**
* Day of week field. Allows: * / , - ? L #
* Day of week field. Allows: * / , - ? L #.
*
* Days of the week can be represented as a number 0-7 (0|7 = Sunday)
* or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT.
@ -22,12 +23,12 @@ use InvalidArgumentException;
class DayOfWeekField extends AbstractField
{
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeStart = 0;
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeEnd = 7;
@ -37,7 +38,7 @@ class DayOfWeekField extends AbstractField
protected $nthRange;
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN'];
@ -52,42 +53,33 @@ class DayOfWeekField extends AbstractField
/**
* @inheritDoc
*
* @param \DateTime|\DateTimeImmutable $date
*/
public function isSatisfiedBy(DateTimeInterface $date, $value)
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
if ($value == '?') {
if ('?' === $value) {
return true;
}
// Convert text day of the week values to integers
$value = $this->convertLiterals($value);
$currentYear = $date->format('Y');
$currentMonth = $date->format('m');
$lastDayOfMonth = $date->format('t');
$currentYear = (int) $date->format('Y');
$currentMonth = (int) $date->format('m');
$lastDayOfMonth = (int) $date->format('t');
// Find out if this is the last specific weekday of the month
if (strpos($value, 'L')) {
$weekday = (int) $this->convertLiterals(substr($value, 0, strpos($value, 'L')));
if ($lPosition = strpos($value, 'L')) {
$weekday = $this->convertLiterals(substr($value, 0, $lPosition));
$weekday %= 7;
$tdate = clone $date;
$tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
while ($tdate->format('w') != $weekday) {
$tdateClone = new DateTime();
$tdate = $tdateClone
->setTimezone($tdate->getTimezone())
->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
}
return $date->format('j') == $lastDayOfMonth;
$daysInMonth = (int) $date->format('t');
$remainingDaysInMonth = $daysInMonth - (int) $date->format('d');
return (($weekday === (int) $date->format('w')) && ($remainingDaysInMonth < 7));
}
// Handle # hash tokens
if (strpos($value, '#')) {
list($weekday, $nth) = explode('#', $value);
[$weekday, $nth] = explode('#', $value);
if (!is_numeric($nth)) {
throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given");
@ -96,23 +88,23 @@ class DayOfWeekField extends AbstractField
}
// 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601
if ($weekday === '0') {
if ('0' === $weekday) {
$weekday = 7;
}
$weekday = $this->convertLiterals($weekday);
$weekday = (int) $this->convertLiterals((string) $weekday);
// Validate the hash fields
if ($weekday < 0 || $weekday > 7) {
throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given");
}
if (!in_array($nth, $this->nthRange)) {
if (!\in_array($nth, $this->nthRange, true)) {
throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given");
}
// The current weekday must match the targeted weekday to proceed
if ($date->format('N') != $weekday) {
if ((int) $date->format('N') !== $weekday) {
return false;
}
@ -121,7 +113,7 @@ class DayOfWeekField extends AbstractField
$dayCount = 0;
$currentDay = 1;
while ($currentDay < $lastDayOfMonth + 1) {
if ($tdate->format('N') == $weekday) {
if ((int) $tdate->format('N') === $weekday) {
if (++$dayCount >= $nth) {
break;
}
@ -129,57 +121,63 @@ class DayOfWeekField extends AbstractField
$tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay);
}
return $date->format('j') == $currentDay;
return (int) $date->format('j') === $currentDay;
}
// Handle day of the week values
if (strpos($value, '-')) {
if (false !== strpos($value, '-')) {
$parts = explode('-', $value);
if ($parts[0] == '7') {
$parts[0] = '0';
} elseif ($parts[1] == '0') {
$parts[1] = '7';
if ('7' === $parts[0]) {
$parts[0] = 0;
} elseif ('0' === $parts[1]) {
$parts[1] = 7;
}
$value = implode('-', $parts);
}
// Test to see which Sunday to use -- 0 == 7 == Sunday
$format = in_array(7, str_split($value)) ? 'N' : 'w';
$fieldValue = $date->format($format);
$format = \in_array(7, array_map(function ($value) {
return (int) $value;
}, str_split($value)), true) ? 'N' : 'w';
$fieldValue = (int) $date->format($format);
return $this->isSatisfied($fieldValue, $value);
}
/**
* @inheritDoc
*
* @param \DateTime|\DateTimeImmutable &$date
*/
public function increment(DateTimeInterface &$date, $invert = false)
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if ($invert) {
$date = $date->modify('-1 day')->setTime(23, 59, 0);
if (! $invert) {
$date = $date->add(new \DateInterval('P1D'));
$date = $date->setTime(0, 0);
} else {
$date = $date->modify('+1 day')->setTime(0, 0, 0);
$date = $date->sub(new \DateInterval('P1D'));
$date = $date->setTime(23, 59);
}
return $this;
}
/**
* @inheritDoc
* {@inheritdoc}
*/
public function validate($value)
public function validate(string $value): bool
{
$basicChecks = parent::validate($value);
if (!$basicChecks) {
if ('?' === $value) {
return true;
}
// Handle the # value
if (strpos($value, '#') !== false) {
if (false !== strpos($value, '#')) {
$chunks = explode('#', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) {
if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && \in_array((int) $chunks[1], $this->nthRange, true)) {
return true;
}
}

View file

@ -1,54 +1,52 @@
<?php
declare(strict_types=1);
namespace Cron;
use InvalidArgumentException;
/**
* CRON field factory implementing a flyweight factory
* @link http://en.wikipedia.org/wiki/Cron
* CRON field factory implementing a flyweight factory.
*
* @see http://en.wikipedia.org/wiki/Cron
*/
class FieldFactory
class FieldFactory implements FieldFactoryInterface
{
/**
* @var array Cache of instantiated fields
*/
private $fields = array();
private $fields = [];
/**
* Get an instance of a field object for a cron expression position
* Get an instance of a field object for a cron expression position.
*
* @param int $position CRON expression position value to retrieve
*
* @return FieldInterface
* @throws InvalidArgumentException if a position is not valid
*/
public function getField($position)
public function getField(int $position): FieldInterface
{
return $this->fields[$position] ?? $this->fields[$position] = $this->instantiateField($position);
}
private function instantiateField(int $position): FieldInterface
{
if (!isset($this->fields[$position])) {
switch ($position) {
case 0:
$this->fields[$position] = new MinutesField();
break;
case 1:
$this->fields[$position] = new HoursField();
break;
case 2:
$this->fields[$position] = new DayOfMonthField();
break;
case 3:
$this->fields[$position] = new MonthField();
break;
case 4:
$this->fields[$position] = new DayOfWeekField();
break;
default:
case CronExpression::MINUTE:
return new MinutesField();
case CronExpression::HOUR:
return new HoursField();
case CronExpression::DAY:
return new DayOfMonthField();
case CronExpression::MONTH:
return new MonthField();
case CronExpression::WEEKDAY:
return new DayOfWeekField();
}
throw new InvalidArgumentException(
($position + 1) . ' is not a valid position'
);
}
}
return $this->fields[$position];
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Cron;
interface FieldFactoryInterface
{
public function getField(int $position): FieldInterface;
}

View file

@ -1,41 +1,46 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
/**
* CRON field interface
* CRON field interface.
*/
interface FieldInterface
{
/**
* Check if the respective value of a DateTime field satisfies a CRON exp
* Check if the respective value of a DateTime field satisfies a CRON exp.
*
* @internal
* @param DateTimeInterface $date DateTime object to check
* @param string $value CRON expression to test against
*
* @return bool Returns TRUE if satisfied, FALSE otherwise
*/
public function isSatisfiedBy(DateTimeInterface $date, $value);
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool;
/**
* When a CRON expression is not satisfied, this method is used to increment
* or decrement a DateTime object by the unit of the cron field
* or decrement a DateTime object by the unit of the cron field.
*
* @param DateTimeInterface &$date DateTime object to change
* @internal
* @param DateTimeInterface $date DateTime object to change
* @param bool $invert (optional) Set to TRUE to decrement
* @param string|null $parts (optional) Set parts to use
*
* @return FieldInterface
*/
public function increment(DateTimeInterface &$date, $invert = false);
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface;
/**
* Validates a CRON expression for a given field
* Validates a CRON expression for a given field.
*
* @param string $value CRON expression value to validate
*
* @return bool Returns TRUE if valid, FALSE otherwise
*/
public function validate($value);
public function validate(string $value): bool;
}

View file

@ -1,83 +1,210 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
use DateTimeZone;
/**
* Hours field. Allows: * , / -
* Hours field. Allows: * , / -.
*/
class HoursField extends AbstractField
{
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeStart = 0;
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeEnd = 23;
/**
* @inheritDoc
* @var array|null Transitions returned by DateTimeZone::getTransitions()
*/
public function isSatisfiedBy(DateTimeInterface $date, $value)
protected $transitions = [];
/**
* @var int|null Timestamp of the start of the transitions range
*/
protected $transitionsStart = null;
/**
* @var int|null Timestamp of the end of the transitions range
*/
protected $transitionsEnd = null;
/**
* {@inheritdoc}
*/
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
if ($value == '?') {
return true;
$checkValue = (int) $date->format('H');
$retval = $this->isSatisfied($checkValue, $value);
if ($retval) {
return $retval;
}
return $this->isSatisfied($date->format('H'), $value);
// Are we on the edge of a transition
$lastTransition = $this->getPastTransition($date);
if (($lastTransition !== null) && ($lastTransition["ts"] > ((int) $date->format('U') - 3600))) {
$dtLastOffset = clone $date;
$this->timezoneSafeModify($dtLastOffset, "-1 hour");
$lastOffset = $dtLastOffset->getOffset();
$dtNextOffset = clone $date;
$this->timezoneSafeModify($dtNextOffset, "+1 hour");
$nextOffset = $dtNextOffset->getOffset();
$offsetChange = $nextOffset - $lastOffset;
if ($offsetChange >= 3600) {
$checkValue -= 1;
return $this->isSatisfied($checkValue, $value);
}
if ((! $invert) && ($offsetChange <= -3600)) {
$checkValue += 1;
return $this->isSatisfied($checkValue, $value);
}
}
return $retval;
}
public function getPastTransition(DateTimeInterface $date): ?array
{
$currentTimestamp = (int) $date->format('U');
if (
($this->transitions === null)
|| ($this->transitionsStart < ($currentTimestamp + 86400))
|| ($this->transitionsEnd > ($currentTimestamp - 86400))
) {
// We start a day before current time so we can differentiate between the first transition entry
// and a change that happens now
$dtLimitStart = clone $date;
$dtLimitStart = $dtLimitStart->modify("-12 months");
$dtLimitEnd = clone $date;
$dtLimitEnd = $dtLimitEnd->modify('+12 months');
$this->transitions = $date->getTimezone()->getTransitions(
$dtLimitStart->getTimestamp(),
$dtLimitEnd->getTimestamp()
);
if (empty($this->transitions)) {
return null;
}
$this->transitionsStart = $dtLimitStart->getTimestamp();
$this->transitionsEnd = $dtLimitEnd->getTimestamp();
}
$nextTransition = null;
foreach ($this->transitions as $transition) {
if ($transition["ts"] > $currentTimestamp) {
continue;
}
if (($nextTransition !== null) && ($transition["ts"] < $nextTransition["ts"])) {
continue;
}
$nextTransition = $transition;
}
return ($nextTransition ?? null);
}
/**
* {@inheritDoc}
* {@inheritdoc}
*
* @param \DateTime|\DateTimeImmutable &$date
* @param string|null $parts
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null)
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
$originalTimestamp = (int) $date->format('U');
// Change timezone to UTC temporarily. This will
// allow us to go back or forwards and hour even
// if DST will be changed between the hours.
if (is_null($parts) || $parts == '*') {
$timezone = $date->getTimezone();
$date = $date->setTimezone(new DateTimeZone('UTC'));
$date = $date->modify(($invert ? '-' : '+') . '1 hour');
$date = $date->setTimezone($timezone);
if (null === $parts || '*' === $parts) {
if ($invert) {
$date = $date->sub(new \DateInterval('PT1H'));
} else {
$date = $date->add(new \DateInterval('PT1H'));
}
$date = $date->setTime($date->format('H'), $invert ? 59 : 0);
$date = $this->setTimeHour($date, $invert, $originalTimestamp);
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$hours = array();
$parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];
$hours = [];
foreach ($parts as $part) {
$hours = array_merge($hours, $this->getRangeForExpression($part, 23));
}
$current_hour = $date->format('H');
$position = $invert ? count($hours) - 1 : 0;
if (count($hours) > 1) {
for ($i = 0; $i < count($hours) - 1; $i++) {
$current_hour = (int) $date->format('H');
$position = $invert ? \count($hours) - 1 : 0;
$countHours = \count($hours);
if ($countHours > 1) {
for ($i = 0; $i < $countHours - 1; ++$i) {
if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) ||
($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
$hour = $hours[$position];
if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) {
$date = $date->modify(($invert ? '-' : '+') . '1 day');
$date = $date->setTime($invert ? 23 : 0, $invert ? 59 : 0);
$target = (int) $hours[$position];
$originalHour = (int)$date->format('H');
$originalDay = (int)$date->format('d');
$previousOffset = $date->getOffset();
if (! $invert) {
if ($originalHour >= $target) {
$distance = 24 - $originalHour;
$date = $this->timezoneSafeModify($date, "+{$distance} hours");
$actualDay = (int)$date->format('d');
$actualHour = (int)$date->format('H');
if (($actualDay !== ($originalDay + 1)) && ($actualHour !== 0)) {
$offsetChange = ($previousOffset - $date->getOffset());
$date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds");
}
else {
$date = $date->setTime($hour, $invert ? 59 : 0);
$originalHour = (int)$date->format('H');
}
$distance = $target - $originalHour;
$date = $this->timezoneSafeModify($date, "+{$distance} hours");
} else {
if ($originalHour <= $target) {
$distance = ($originalHour + 1);
$date = $this->timezoneSafeModify($date, "-" . $distance . " hours");
$actualDay = (int)$date->format('d');
$actualHour = (int)$date->format('H');
if (($actualDay !== ($originalDay - 1)) && ($actualHour !== 23)) {
$offsetChange = ($previousOffset - $date->getOffset());
$date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds");
}
$originalHour = (int)$date->format('H');
}
$distance = $originalHour - $target;
$date = $this->timezoneSafeModify($date, "-{$distance} hours");
}
$date = $this->setTimeHour($date, $invert, $originalTimestamp);
$actualHour = (int)$date->format('H');
if ($invert && ($actualHour === ($target - 1) || (($actualHour === 23) && ($target === 0)))) {
$date = $this->timezoneSafeModify($date, "+1 hour");
}
return $this;

View file

@ -1,73 +1,94 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
/**
* Minutes field. Allows: * , / -
* Minutes field. Allows: * , / -.
*/
class MinutesField extends AbstractField
{
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeStart = 0;
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeEnd = 59;
/**
* @inheritDoc
* {@inheritdoc}
*/
public function isSatisfiedBy(DateTimeInterface $date, $value)
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):bool
{
if ($value == '?') {
if ($value === '?') {
return true;
}
return $this->isSatisfied($date->format('i'), $value);
return $this->isSatisfied((int)$date->format('i'), $value);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*
* @param \DateTime|\DateTimeImmutable &$date
* @param string|null $parts
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null)
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (is_null($parts)) {
$date = $date->modify(($invert ? '-' : '+') . '1 minute');
$date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 minute");
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$minutes = array();
$current_minute = (int) $date->format('i');
$parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];
$minutes = [];
foreach ($parts as $part) {
$minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));
}
$current_minute = $date->format('i');
$position = $invert ? count($minutes) - 1 : 0;
if (count($minutes) > 1) {
for ($i = 0; $i < count($minutes) - 1; $i++) {
$position = $invert ? \count($minutes) - 1 : 0;
if (\count($minutes) > 1) {
for ($i = 0; $i < \count($minutes) - 1; ++$i) {
if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) ||
($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) {
$date = $date->modify(($invert ? '-' : '+') . '1 hour');
$date = $date->setTime($date->format('H'), $invert ? 59 : 0);
$target = (int) $minutes[$position];
$originalMinute = (int) $date->format("i");
if (! $invert) {
if ($originalMinute >= $target) {
$distance = 60 - $originalMinute;
$date = $this->timezoneSafeModify($date, "+{$distance} minutes");
$originalMinute = (int) $date->format("i");
}
else {
$date = $date->setTime($date->format('H'), $minutes[$position]);
$distance = $target - $originalMinute;
$date = $this->timezoneSafeModify($date, "+{$distance} minutes");
} else {
if ($originalMinute <= $target) {
$distance = ($originalMinute + 1);
$date = $this->timezoneSafeModify($date, "-{$distance} minutes");
$originalMinute = (int) $date->format("i");
}
$distance = $originalMinute - $target;
$date = $this->timezoneSafeModify($date, "-{$distance} minutes");
}
return $this;

View file

@ -1,59 +1,61 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
/**
* Month field. Allows: * , / -
* Month field. Allows: * , / -.
*/
class MonthField extends AbstractField
{
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeStart = 1;
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $rangeEnd = 12;
/**
* @inheritDoc
* {@inheritdoc}
*/
protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL',
8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC'];
8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC', ];
/**
* @inheritDoc
* {@inheritdoc}
*/
public function isSatisfiedBy(DateTimeInterface $date, $value)
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
if ($value == '?') {
if ($value === '?') {
return true;
}
$value = $this->convertLiterals($value);
return $this->isSatisfied($date->format('m'), $value);
return $this->isSatisfied((int) $date->format('m'), $value);
}
/**
* @inheritDoc
*
* @param \DateTime|\DateTimeImmutable &$date
* @param \DateTime|\DateTimeImmutable $date
*/
public function increment(DateTimeInterface &$date, $invert = false)
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if ($invert) {
$date = $date->modify('last day of previous month')->setTime(23, 59);
if (! $invert) {
$date = $date->modify('first day of next month');
$date = $date->setTime(0, 0);
} else {
$date = $date->modify('first day of next month')->setTime(0, 0);
$date = $date->modify('last day of previous month');
$date = $date->setTime(23, 59);
}
return $this;
}
}

View file

@ -1,139 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\DayOfWeekField;
use Cron\HoursField;
use Cron\MinutesField;
use Cron\MonthField;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class AbstractFieldTest extends TestCase
{
/**
* @covers \Cron\AbstractField::isRange
*/
public function testTestsIfRange()
{
$f = new DayOfWeekField();
$this->assertTrue($f->isRange('1-2'));
$this->assertFalse($f->isRange('2'));
}
/**
* @covers \Cron\AbstractField::isIncrementsOfRanges
*/
public function testTestsIfIncrementsOfRanges()
{
$f = new DayOfWeekField();
$this->assertFalse($f->isIncrementsOfRanges('1-2'));
$this->assertTrue($f->isIncrementsOfRanges('1/2'));
$this->assertTrue($f->isIncrementsOfRanges('*/2'));
$this->assertTrue($f->isIncrementsOfRanges('3-12/2'));
}
/**
* @covers \Cron\AbstractField::isInRange
*/
public function testTestsIfInRange()
{
$f = new DayOfWeekField();
$this->assertTrue($f->isInRange('1', '1-2'));
$this->assertTrue($f->isInRange('2', '1-2'));
$this->assertTrue($f->isInRange('5', '4-12'));
$this->assertFalse($f->isInRange('3', '4-12'));
$this->assertFalse($f->isInRange('13', '4-12'));
}
/**
* @covers \Cron\AbstractField::isInIncrementsOfRanges
*/
public function testTestsIfInIncrementsOfRangesOnZeroStartRange()
{
$f = new MinutesField();
$this->assertTrue($f->isInIncrementsOfRanges('3', '3-59/2'));
$this->assertTrue($f->isInIncrementsOfRanges('13', '3-59/2'));
$this->assertTrue($f->isInIncrementsOfRanges('15', '3-59/2'));
$this->assertTrue($f->isInIncrementsOfRanges('14', '*/2'));
$this->assertFalse($f->isInIncrementsOfRanges('2', '3-59/13'));
$this->assertFalse($f->isInIncrementsOfRanges('14', '*/13'));
$this->assertFalse($f->isInIncrementsOfRanges('14', '3-59/2'));
$this->assertFalse($f->isInIncrementsOfRanges('3', '2-59'));
$this->assertFalse($f->isInIncrementsOfRanges('3', '2'));
$this->assertFalse($f->isInIncrementsOfRanges('3', '*'));
$this->assertFalse($f->isInIncrementsOfRanges('0', '*/0'));
$this->assertFalse($f->isInIncrementsOfRanges('1', '*/0'));
$this->assertTrue($f->isInIncrementsOfRanges('4', '4/1'));
$this->assertFalse($f->isInIncrementsOfRanges('14', '4/1'));
$this->assertFalse($f->isInIncrementsOfRanges('34', '4/1'));
}
/**
* @covers \Cron\AbstractField::isInIncrementsOfRanges
*/
public function testTestsIfInIncrementsOfRangesOnOneStartRange()
{
$f = new MonthField();
$this->assertTrue($f->isInIncrementsOfRanges('3', '3-12/2'));
$this->assertFalse($f->isInIncrementsOfRanges('13', '3-12/2'));
$this->assertFalse($f->isInIncrementsOfRanges('15', '3-12/2'));
$this->assertTrue($f->isInIncrementsOfRanges('3', '*/2'));
$this->assertFalse($f->isInIncrementsOfRanges('3', '*/3'));
$this->assertTrue($f->isInIncrementsOfRanges('7', '*/3'));
$this->assertFalse($f->isInIncrementsOfRanges('14', '3-12/2'));
$this->assertFalse($f->isInIncrementsOfRanges('3', '2-12'));
$this->assertFalse($f->isInIncrementsOfRanges('3', '2'));
$this->assertFalse($f->isInIncrementsOfRanges('3', '*'));
$this->assertFalse($f->isInIncrementsOfRanges('0', '*/0'));
$this->assertFalse($f->isInIncrementsOfRanges('1', '*/0'));
$this->assertTrue($f->isInIncrementsOfRanges('4', '4/1'));
$this->assertFalse($f->isInIncrementsOfRanges('14', '4/1'));
$this->assertFalse($f->isInIncrementsOfRanges('34', '4/1'));
}
/**
* @covers \Cron\AbstractField::isSatisfied
*/
public function testTestsIfSatisfied()
{
$f = new DayOfWeekField();
$this->assertTrue($f->isSatisfied('12', '3-13'));
$this->assertFalse($f->isSatisfied('15', '3-7/2'));
$this->assertTrue($f->isSatisfied('12', '*'));
$this->assertTrue($f->isSatisfied('12', '12'));
$this->assertFalse($f->isSatisfied('12', '3-11'));
$this->assertFalse($f->isSatisfied('12', '3-7/2'));
$this->assertFalse($f->isSatisfied('12', '11'));
}
/**
* Allows ranges and lists to coexist in the same expression
*
* @see https://github.com/dragonmantank/cron-expression/issues/5
*/
public function testAllowRangesAndLists()
{
$expression = '5-7,11-13';
$f = new HoursField();
$this->assertTrue($f->validate($expression));
}
/**
* Makes sure that various types of ranges expand out properly
*
* @see https://github.com/dragonmantank/cron-expression/issues/5
*/
public function testGetRangeForExpressionExpandsCorrectly()
{
$f = new HoursField();
$this->assertSame([5, 6, 7, 11, 12, 13], $f->getRangeForExpression('5-7,11-13', 23));
$this->assertSame(['5', '6', '7', '11', '12', '13'], $f->getRangeForExpression('5,6,7,11,12,13', 23));
$this->assertSame([0, 6, 12, 18], $f->getRangeForExpression('*/6', 23));
$this->assertSame([5, 11], $f->getRangeForExpression('5-13/6', 23));
}
}

View file

@ -1,589 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\CronExpression;
use Cron\MonthField;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class CronExpressionTest extends TestCase
{
/**
* @covers \Cron\CronExpression::factory
*/
public function testFactoryRecognizesTemplates()
{
$this->assertSame('0 0 1 1 *', CronExpression::factory('@annually')->getExpression());
$this->assertSame('0 0 1 1 *', CronExpression::factory('@yearly')->getExpression());
$this->assertSame('0 0 * * 0', CronExpression::factory('@weekly')->getExpression());
}
/**
* @covers \Cron\CronExpression::__construct
* @covers \Cron\CronExpression::getExpression
* @covers \Cron\CronExpression::__toString
*/
public function testParsesCronSchedule()
{
// '2010-09-10 12:00:00'
$cron = CronExpression::factory('1 2-4 * 4,5,6 */3');
$this->assertSame('1', $cron->getExpression(CronExpression::MINUTE));
$this->assertSame('2-4', $cron->getExpression(CronExpression::HOUR));
$this->assertSame('*', $cron->getExpression(CronExpression::DAY));
$this->assertSame('4,5,6', $cron->getExpression(CronExpression::MONTH));
$this->assertSame('*/3', $cron->getExpression(CronExpression::WEEKDAY));
$this->assertSame('1 2-4 * 4,5,6 */3', $cron->getExpression());
$this->assertSame('1 2-4 * 4,5,6 */3', (string) $cron);
$this->assertNull($cron->getExpression('foo'));
}
/**
* @covers \Cron\CronExpression::__construct
* @covers \Cron\CronExpression::getExpression
* @covers \Cron\CronExpression::__toString
*/
public function testParsesCronScheduleThrowsAnException()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid CRON field value A at position 0');
CronExpression::factory('A 1 2 3 4');
}
/**
* @covers \Cron\CronExpression::__construct
* @covers \Cron\CronExpression::getExpression
* @dataProvider scheduleWithDifferentSeparatorsProvider
*/
public function testParsesCronScheduleWithAnySpaceCharsAsSeparators($schedule, array $expected)
{
$cron = CronExpression::factory($schedule);
$this->assertSame($expected[0], $cron->getExpression(CronExpression::MINUTE));
$this->assertSame($expected[1], $cron->getExpression(CronExpression::HOUR));
$this->assertSame($expected[2], $cron->getExpression(CronExpression::DAY));
$this->assertSame($expected[3], $cron->getExpression(CronExpression::MONTH));
$this->assertSame($expected[4], $cron->getExpression(CronExpression::WEEKDAY));
}
/**
* Data provider for testParsesCronScheduleWithAnySpaceCharsAsSeparators
*
* @return array
*/
public static function scheduleWithDifferentSeparatorsProvider()
{
return array(
array("*\t*\t*\t*\t*\t", array('*', '*', '*', '*', '*', '*')),
array("* * * * * ", array('*', '*', '*', '*', '*', '*')),
array("* \t * \t * \t * \t * \t", array('*', '*', '*', '*', '*', '*')),
array("*\t \t*\t \t*\t \t*\t \t*\t \t", array('*', '*', '*', '*', '*', '*')),
);
}
/**
* @covers \Cron\CronExpression::__construct
* @covers \Cron\CronExpression::setExpression
* @covers \Cron\CronExpression::setPart
*/
public function testInvalidCronsWillFail()
{
$this->expectException(\InvalidArgumentException::class);
// Only four values
$cron = CronExpression::factory('* * * 1');
}
/**
* @covers \Cron\CronExpression::setPart
*/
public function testInvalidPartsWillFail()
{
$this->expectException(\InvalidArgumentException::class);
// Only four values
$cron = CronExpression::factory('* * * * *');
$cron->setPart(1, 'abc');
}
/**
* Data provider for cron schedule
*
* @return array
*/
public function scheduleProvider()
{
return array(
array('*/2 */2 * * *', '2015-08-10 21:47:27', '2015-08-10 22:00:00', false),
array('* * * * *', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true),
array('* 20,21,22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true),
// Handles CSV values
array('* 20,22 * * *', '2015-08-10 21:50:00', '2015-08-10 22:00:00', false),
// CSV values can be complex
array('7-9 * */9 * *', '2015-08-10 22:02:33', '2015-08-10 22:07:00', false),
// 15th minute, of the second hour, every 15 days, in January, every Friday
array('1 * * * 7', '2015-08-10 21:47:27', '2015-08-16 00:01:00', false),
// Test with exact times
array('47 21 * * *', strtotime('2015-08-10 21:47:30'), '2015-08-10 21:47:00', true),
// Test Day of the week (issue #1)
// According cron implementation, 0|7 = sunday, 1 => monday, etc
array('* * * * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
array('* * * * 7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
array('* * * * 1', strtotime('2011-06-15 23:09:00'), '2011-06-20 00:00:00', false),
// Should return the sunday date as 7 equals 0
array('0 0 * * MON,SUN', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
array('0 0 * * 1,7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
array('0 0 * * 0-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
array('0 0 * * 7-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
array('0 0 * * 4-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
array('0 0 * * 7-3', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
array('0 0 * * 3-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
array('0 0 * * 3-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false),
// Test lists of values and ranges (Abhoryo)
array('0 0 * * 2-7', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false),
array('0 0 * * 2-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false),
array('0 0 * * 4-7', strtotime('2011-07-19 00:00:00'), '2011-07-21 00:00:00', false),
// Test increments of ranges
array('0-12/4 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true),
array('4-59/2 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true),
array('4-59/2 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:06:00', true),
array('4-59/3 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:07:00', false),
// Test Day of the Week and the Day of the Month (issue #1)
array('0 0 1 1 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
array('0 0 1 JAN 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
array('0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
// Test the W day of the week modifier for day of the month field
array('0 0 2W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
array('0 0 1W * *', strtotime('2011-05-01 00:00:00'), '2011-05-02 00:00:00', false),
array('0 0 1W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
array('0 0 3W * *', strtotime('2011-07-01 00:00:00'), '2011-07-04 00:00:00', false),
array('0 0 16W * *', strtotime('2011-07-01 00:00:00'), '2011-07-15 00:00:00', false),
array('0 0 28W * *', strtotime('2011-07-01 00:00:00'), '2011-07-28 00:00:00', false),
array('0 0 30W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
array('0 0 31W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
// Test the last weekday of a month
array('* * * * 5L', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
array('* * * * 6L', strtotime('2011-07-01 00:00:00'), '2011-07-30 00:00:00', false),
array('* * * * 7L', strtotime('2011-07-01 00:00:00'), '2011-07-31 00:00:00', false),
array('* * * * 1L', strtotime('2011-07-24 00:00:00'), '2011-07-25 00:00:00', false),
array('* * * 1 5L', strtotime('2011-12-25 00:00:00'), '2012-01-27 00:00:00', false),
// Test the hash symbol for the nth weekday of a given month
array('* * * * 5#2', strtotime('2011-07-01 00:00:00'), '2011-07-08 00:00:00', false),
array('* * * * 5#1', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
array('* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false),
// Issue #7, documented example failed
['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-31 06:03:00', false],
// https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403
['* * * * MON-FRI', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-09 00:00:00'), false],
['* * * * TUE', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-10 00:00:00'), false],
);
}
/**
* @covers \Cron\CronExpression::isDue
* @covers \Cron\CronExpression::getNextRunDate
* @covers \Cron\DayOfMonthField
* @covers \Cron\DayOfWeekField
* @covers \Cron\MinutesField
* @covers \Cron\HoursField
* @covers \Cron\MonthField
* @covers \Cron\CronExpression::getRunDate
* @dataProvider scheduleProvider
*/
public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $isDue)
{
$relativeTimeString = is_int($relativeTime) ? date('Y-m-d H:i:s', $relativeTime) : $relativeTime;
// Test next run date
$cron = CronExpression::factory($schedule);
if (is_string($relativeTime)) {
$relativeTime = new DateTime($relativeTime);
} elseif (is_int($relativeTime)) {
$relativeTime = date('Y-m-d H:i:s', $relativeTime);
}
if (is_string($nextRun)) {
$nextRunDate = new DateTime($nextRun);
} elseif (is_int($nextRun)) {
$nextRunDate = new DateTime();
$nextRunDate->setTimestamp($nextRun);
}
$this->assertSame($isDue, $cron->isDue($relativeTime));
$next = $cron->getNextRunDate($relativeTime, 0, true);
$this->assertEquals($nextRunDate, $next);
}
/**
* @covers \Cron\CronExpression::isDue
*/
public function testIsDueHandlesDifferentDates()
{
$cron = CronExpression::factory('* * * * *');
$this->assertTrue($cron->isDue());
$this->assertTrue($cron->isDue('now'));
$this->assertTrue($cron->isDue(new DateTime('now')));
$this->assertTrue($cron->isDue(date('Y-m-d H:i')));
$this->assertTrue($cron->isDue(new DateTimeImmutable('now')));
}
/**
* @covers \Cron\CronExpression::isDue
*/
public function testIsDueHandlesDifferentDefaultTimezones()
{
$originalTimezone = date_default_timezone_get();
$cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00
$date = '2014-01-01 15:00'; //Wednesday
date_default_timezone_set('UTC');
$this->assertTrue($cron->isDue(new DateTime($date), 'UTC'));
$this->assertFalse($cron->isDue(new DateTime($date), 'Europe/Amsterdam'));
$this->assertFalse($cron->isDue(new DateTime($date), 'Asia/Tokyo'));
date_default_timezone_set('Europe/Amsterdam');
$this->assertFalse($cron->isDue(new DateTime($date), 'UTC'));
$this->assertTrue($cron->isDue(new DateTime($date), 'Europe/Amsterdam'));
$this->assertFalse($cron->isDue(new DateTime($date), 'Asia/Tokyo'));
date_default_timezone_set('Asia/Tokyo');
$this->assertFalse($cron->isDue(new DateTime($date), 'UTC'));
$this->assertFalse($cron->isDue(new DateTime($date), 'Europe/Amsterdam'));
$this->assertTrue($cron->isDue(new DateTime($date), 'Asia/Tokyo'));
date_default_timezone_set($originalTimezone);
}
/**
* @covers \Cron\CronExpression::isDue
*/
public function testIsDueHandlesDifferentSuppliedTimezones()
{
$cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00
$date = '2014-01-01 15:00'; //Wednesday
$this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'UTC'));
$this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'Europe/Amsterdam'));
$this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'Asia/Tokyo'));
$this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'UTC'));
$this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'Europe/Amsterdam'));
$this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'Asia/Tokyo'));
$this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'UTC'));
$this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'Europe/Amsterdam'));
$this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'Asia/Tokyo'));
}
/**
* @covers Cron\CronExpression::isDue
*/
public function testIsDueHandlesDifferentTimezonesAsArgument()
{
$cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00
$date = '2014-01-01 15:00'; //Wednesday
$utc = new \DateTimeZone('UTC');
$amsterdam = new \DateTimeZone('Europe/Amsterdam');
$tokyo = new \DateTimeZone('Asia/Tokyo');
$this->assertTrue($cron->isDue(new DateTime($date, $utc), 'UTC'));
$this->assertFalse($cron->isDue(new DateTime($date, $amsterdam), 'UTC'));
$this->assertFalse($cron->isDue(new DateTime($date, $tokyo), 'UTC'));
$this->assertFalse($cron->isDue(new DateTime($date, $utc), 'Europe/Amsterdam'));
$this->assertTrue($cron->isDue(new DateTime($date, $amsterdam), 'Europe/Amsterdam'));
$this->assertFalse($cron->isDue(new DateTime($date, $tokyo), 'Europe/Amsterdam'));
$this->assertFalse($cron->isDue(new DateTime($date, $utc), 'Asia/Tokyo'));
$this->assertFalse($cron->isDue(new DateTime($date, $amsterdam), 'Asia/Tokyo'));
$this->assertTrue($cron->isDue(new DateTime($date, $tokyo), 'Asia/Tokyo'));
}
/**
* @covers Cron\CronExpression::isDue
*/
public function testRecognisesTimezonesAsPartOfDateTime()
{
$cron = CronExpression::factory("0 7 * * *");
$tzCron = "America/New_York";
$tzServer = new \DateTimeZone("Europe/London");
$dtCurrent = \DateTime::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
$dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron);
$this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
$dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
$dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron);
$this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
$dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
$dtPrev = $cron->getPreviousRunDate($dtCurrent->format("c"), 0, true, $tzCron);
$this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
$dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
$dtPrev = $cron->getPreviousRunDate($dtCurrent->format("\@U"), 0, true, $tzCron);
$this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
}
/**
* @covers \Cron\CronExpression::getPreviousRunDate
*/
public function testCanGetPreviousRunDates()
{
$cron = CronExpression::factory('* * * * *');
$next = $cron->getNextRunDate('now');
$two = $cron->getNextRunDate('now', 1);
$this->assertEquals($next, $cron->getPreviousRunDate($two));
$cron = CronExpression::factory('* */2 * * *');
$next = $cron->getNextRunDate('now');
$two = $cron->getNextRunDate('now', 1);
$this->assertEquals($next, $cron->getPreviousRunDate($two));
$cron = CronExpression::factory('* * * */2 *');
$next = $cron->getNextRunDate('now');
$two = $cron->getNextRunDate('now', 1);
$this->assertEquals($next, $cron->getPreviousRunDate($two));
}
/**
* @covers \Cron\CronExpression::getMultipleRunDates
*/
public function testProvidesMultipleRunDates()
{
$cron = CronExpression::factory('*/2 * * * *');
$this->assertEquals(array(
new DateTime('2008-11-09 00:00:00'),
new DateTime('2008-11-09 00:02:00'),
new DateTime('2008-11-09 00:04:00'),
new DateTime('2008-11-09 00:06:00')
), $cron->getMultipleRunDates(4, '2008-11-09 00:00:00', false, true));
}
/**
* @covers \Cron\CronExpression::getMultipleRunDates
* @covers \Cron\CronExpression::setMaxIterationCount
*/
public function testProvidesMultipleRunDatesForTheFarFuture() {
// Fails with the default 1000 iteration limit
$cron = CronExpression::factory('0 0 12 1 *');
$cron->setMaxIterationCount(2000);
$this->assertEquals(array(
new DateTime('2016-01-12 00:00:00'),
new DateTime('2017-01-12 00:00:00'),
new DateTime('2018-01-12 00:00:00'),
new DateTime('2019-01-12 00:00:00'),
new DateTime('2020-01-12 00:00:00'),
new DateTime('2021-01-12 00:00:00'),
new DateTime('2022-01-12 00:00:00'),
new DateTime('2023-01-12 00:00:00'),
new DateTime('2024-01-12 00:00:00'),
), $cron->getMultipleRunDates(9, '2015-04-28 00:00:00', false, true));
}
/**
* @covers \Cron\CronExpression
*/
public function testCanIterateOverNextRuns()
{
$cron = CronExpression::factory('@weekly');
$nextRun = $cron->getNextRunDate("2008-11-09 08:00:00");
$this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00"));
// true is cast to 1
$nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", true, true);
$this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00"));
// You can iterate over them
$nextRun = $cron->getNextRunDate($cron->getNextRunDate("2008-11-09 00:00:00", 1, true), 1, true);
$this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00"));
// You can skip more than one
$nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 2, true);
$this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00"));
$nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 3, true);
$this->assertEquals($nextRun, new DateTime("2008-11-30 00:00:00"));
}
/**
* @covers \Cron\CronExpression::getRunDate
*/
public function testGetRunDateHandlesDifferentDates()
{
$cron = CronExpression::factory('@weekly');
$date = new DateTime("2019-03-10 00:00:00");
$this->assertEquals($date, $cron->getNextRunDate("2019-03-03 08:00:00"));
$this->assertEquals($date, $cron->getNextRunDate(new DateTime("2019-03-03 08:00:00")));
$this->assertEquals($date, $cron->getNextRunDate(new DateTimeImmutable("2019-03-03 08:00:00")));
}
/**
* @covers \Cron\CronExpression::getRunDate
*/
public function testSkipsCurrentDateByDefault()
{
$cron = CronExpression::factory('* * * * *');
$current = new DateTime('now');
$next = $cron->getNextRunDate($current);
$nextPrev = $cron->getPreviousRunDate($next);
$this->assertSame($current->format('Y-m-d H:i:00'), $nextPrev->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\CronExpression::getRunDate
* @ticket 7
*/
public function testStripsForSeconds()
{
$cron = CronExpression::factory('* * * * *');
$current = new DateTime('2011-09-27 10:10:54');
$this->assertSame('2011-09-27 10:11:00', $cron->getNextRunDate($current)->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\CronExpression::getRunDate
*/
public function testFixesPhpBugInDateIntervalMonth()
{
$cron = CronExpression::factory('0 0 27 JAN *');
$this->assertSame('2011-01-27 00:00:00', $cron->getPreviousRunDate('2011-08-22 00:00:00')->format('Y-m-d H:i:s'));
}
public function testIssue29()
{
$cron = CronExpression::factory('@weekly');
$this->assertSame(
'2013-03-10 00:00:00',
$cron->getPreviousRunDate('2013-03-17 00:00:00')->format('Y-m-d H:i:s')
);
}
/**
* @see https://github.com/mtdowling/cron-expression/issues/20
*/
public function testIssue20() {
$e = CronExpression::factory('* * * * MON#1');
$this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
$this->assertFalse($e->isDue(new DateTime('2014-04-14 00:00:00')));
$this->assertFalse($e->isDue(new DateTime('2014-04-21 00:00:00')));
$e = CronExpression::factory('* * * * SAT#2');
$this->assertFalse($e->isDue(new DateTime('2014-04-05 00:00:00')));
$this->assertTrue($e->isDue(new DateTime('2014-04-12 00:00:00')));
$this->assertFalse($e->isDue(new DateTime('2014-04-19 00:00:00')));
$e = CronExpression::factory('* * * * SUN#3');
$this->assertFalse($e->isDue(new DateTime('2014-04-13 00:00:00')));
$this->assertTrue($e->isDue(new DateTime('2014-04-20 00:00:00')));
$this->assertFalse($e->isDue(new DateTime('2014-04-27 00:00:00')));
}
/**
* @covers \Cron\CronExpression::getRunDate
*/
public function testKeepOriginalTime()
{
$now = new \DateTime;
$strNow = $now->format(DateTime::ISO8601);
$cron = CronExpression::factory('0 0 * * *');
$cron->getPreviousRunDate($now);
$this->assertSame($strNow, $now->format(DateTime::ISO8601));
}
/**
* @covers \Cron\CronExpression::__construct
* @covers \Cron\CronExpression::factory
* @covers \Cron\CronExpression::isValidExpression
* @covers \Cron\CronExpression::setExpression
* @covers \Cron\CronExpression::setPart
*/
public function testValidationWorks()
{
// Invalid. Only four values
$this->assertFalse(CronExpression::isValidExpression('* * * 1'));
// Valid
$this->assertTrue(CronExpression::isValidExpression('* * * * 1'));
// Issue #156, 13 is an invalid month
$this->assertFalse(CronExpression::isValidExpression("* * * 13 * "));
// Issue #155, 90 is an invalid second
$this->assertFalse(CronExpression::isValidExpression('90 * * * *'));
// Issue #154, 24 is an invalid hour
$this->assertFalse(CronExpression::isValidExpression("0 24 1 12 0"));
// Issue #125, this is just all sorts of wrong
$this->assertFalse(CronExpression::isValidExpression('990 14 * * mon-fri0345345'));
// see https://github.com/dragonmantank/cron-expression/issues/5
$this->assertTrue(CronExpression::isValidExpression('2,17,35,47 5-7,11-13 * * *'));
}
/**
* Makes sure that 00 is considered a valid value for 0-based fields
* cronie allows numbers with a leading 0, so adding support for this as well
*
* @see https://github.com/dragonmantank/cron-expression/issues/12
*/
public function testDoubleZeroIsValid()
{
$this->assertTrue(CronExpression::isValidExpression('00 * * * *'));
$this->assertTrue(CronExpression::isValidExpression('01 * * * *'));
$this->assertTrue(CronExpression::isValidExpression('* 00 * * *'));
$this->assertTrue(CronExpression::isValidExpression('* 01 * * *'));
$e = CronExpression::factory('00 * * * *');
$this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
$e = CronExpression::factory('01 * * * *');
$this->assertTrue($e->isDue(new DateTime('2014-04-07 00:01:00')));
$e = CronExpression::factory('* 00 * * *');
$this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
$e = CronExpression::factory('* 01 * * *');
$this->assertTrue($e->isDue(new DateTime('2014-04-07 01:00:00')));
}
/**
* Ranges with large steps should "wrap around" to the appropriate value
* cronie allows for steps that are larger than the range of a field, with it wrapping around like a ring buffer. We
* should do the same.
*
* @see https://github.com/dragonmantank/cron-expression/issues/6
*/
public function testRangesWrapAroundWithLargeSteps()
{
$f = new MonthField();
$this->assertTrue($f->validate('*/123'));
$this->assertSame([4], $f->getRangeForExpression('*/123', 12));
$e = CronExpression::factory('* * * */123 *');
$this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
$nextRunDate = $e->getNextRunDate(new DateTime('2014-04-07 00:00:00'));
$this->assertSame('2014-04-07 00:01:00', $nextRunDate->format('Y-m-d H:i:s'));
$nextRunDate = $e->getNextRunDate(new DateTime('2014-05-07 00:00:00'));
$this->assertSame('2015-04-01 00:00:00', $nextRunDate->format('Y-m-d H:i:s'));
}
/**
* When there is an issue with a field, we should report the human readable position
*
* @see https://github.com/dragonmantank/cron-expression/issues/29
*/
public function testFieldPositionIsHumanAdjusted()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("6 is not a valid position");
$e = CronExpression::factory('0 * * * * ? *');
}
}

View file

@ -1,77 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\DayOfMonthField;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class DayOfMonthFieldTest extends TestCase
{
/**
* @covers \Cron\DayOfMonthField::validate
*/
public function testValidatesField()
{
$f = new DayOfMonthField();
$this->assertTrue($f->validate('1'));
$this->assertTrue($f->validate('*'));
$this->assertTrue($f->validate('L'));
$this->assertTrue($f->validate('5W'));
$this->assertTrue($f->validate('01'));
$this->assertFalse($f->validate('5W,L'));
$this->assertFalse($f->validate('1.'));
}
/**
* @covers \Cron\DayOfMonthField::isSatisfiedBy
*/
public function testChecksIfSatisfied()
{
$f = new DayOfMonthField();
$this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
$this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
}
/**
* @covers \Cron\DayOfMonthField::increment
*/
public function testIncrementsDate()
{
$d = new DateTime('2011-03-15 11:15:00');
$f = new DayOfMonthField();
$f->increment($d);
$this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
$d = new DateTime('2011-03-15 11:15:00');
$f->increment($d, true);
$this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\DayOfMonthField::increment
*/
public function testIncrementsDateTimeImmutable()
{
$d = new DateTimeImmutable('2011-03-15 11:15:00');
$f = new DayOfMonthField();
$f->increment($d);
$this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
}
/**
* Day of the month cannot accept a 0 value, it must be between 1 and 31
* See Github issue #120
*
* @since 2017-01-22
*/
public function testDoesNotAccept0Date()
{
$f = new DayOfMonthField();
$this->assertFalse($f->validate(0));
}
}

View file

@ -1,156 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\DayOfWeekField;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class DayOfWeekFieldTest extends TestCase
{
/**
* @covers \Cron\DayOfWeekField::validate
*/
public function testValidatesField()
{
$f = new DayOfWeekField();
$this->assertTrue($f->validate('1'));
$this->assertTrue($f->validate('01'));
$this->assertTrue($f->validate('00'));
$this->assertTrue($f->validate('*'));
$this->assertFalse($f->validate('*/3,1,1-12'));
$this->assertTrue($f->validate('SUN-2'));
$this->assertFalse($f->validate('1.'));
}
/**
* @covers \Cron\DayOfWeekField::isSatisfiedBy
*/
public function testChecksIfSatisfied()
{
$f = new DayOfWeekField();
$this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
$this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
}
/**
* @covers \Cron\DayOfWeekField::increment
*/
public function testIncrementsDate()
{
$d = new DateTime('2011-03-15 11:15:00');
$f = new DayOfWeekField();
$f->increment($d);
$this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
$d = new DateTime('2011-03-15 11:15:00');
$f->increment($d, true);
$this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\DayOfWeekField::increment
*/
public function testIncrementsDateTimeImmutable()
{
$d = new DateTimeImmutable('2011-03-15 11:15:00');
$f = new DayOfWeekField();
$f->increment($d);
$this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\DayOfWeekField::isSatisfiedBy
*/
public function testValidatesHashValueWeekday()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Weekday must be a value between 0 and 7. 12 given');
$f = new DayOfWeekField();
$this->assertTrue($f->isSatisfiedBy(new DateTime(), '12#1'));
}
/**
* @covers \Cron\DayOfWeekField::isSatisfiedBy
*/
public function testValidatesHashValueNth()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('There are never more than 5 or less than 1 of a given weekday in a month');
$f = new DayOfWeekField();
$this->assertTrue($f->isSatisfiedBy(new DateTime(), '3#6'));
}
/**
* @covers \Cron\DayOfWeekField::validate
*/
public function testValidateWeekendHash()
{
$f = new DayOfWeekField();
$this->assertTrue($f->validate('MON#1'));
$this->assertTrue($f->validate('TUE#2'));
$this->assertTrue($f->validate('WED#3'));
$this->assertTrue($f->validate('THU#4'));
$this->assertTrue($f->validate('FRI#5'));
$this->assertTrue($f->validate('SAT#1'));
$this->assertTrue($f->validate('SUN#3'));
$this->assertTrue($f->validate('MON#1,MON#3'));
}
/**
* @covers \Cron\DayOfWeekField::isSatisfiedBy
*/
public function testHandlesZeroAndSevenDayOfTheWeekValues()
{
$f = new DayOfWeekField();
$this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '0-2'));
$this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '6-0'));
$this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN'));
$this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN#3'));
$this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '0#3'));
$this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '7#3'));
}
/**
* @covers \Cron\DayOfWeekField::isSatisfiedBy
*/
public function testHandlesLastWeekdayOfTheMonth()
{
$f = new DayOfWeekField();
$this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), 'FRIL'));
$this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), '5L'));
$this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), 'FRIL'));
$this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), '5L'));
}
/**
* @see https://github.com/mtdowling/cron-expression/issues/47
*/
public function testIssue47() {
$f = new DayOfWeekField();
$this->assertFalse($f->validate('mon,'));
$this->assertFalse($f->validate('mon-'));
$this->assertFalse($f->validate('*/2,'));
$this->assertFalse($f->validate('-mon'));
$this->assertFalse($f->validate(',1'));
$this->assertFalse($f->validate('*-'));
$this->assertFalse($f->validate(',-'));
}
/**
* @see https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403
*/
public function testLiteralsExpandProperly()
{
$f = new DayOfWeekField();
$this->assertTrue($f->validate('MON-FRI'));
$this->assertSame([1,2,3,4,5], $f->getRangeForExpression('MON-FRI', 7));
}
}

View file

@ -1,43 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\FieldFactory;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class FieldFactoryTest extends TestCase
{
/**
* @covers \Cron\FieldFactory::getField
*/
public function testRetrievesFieldInstances()
{
$mappings = array(
0 => 'Cron\MinutesField',
1 => 'Cron\HoursField',
2 => 'Cron\DayOfMonthField',
3 => 'Cron\MonthField',
4 => 'Cron\DayOfWeekField',
);
$f = new FieldFactory();
foreach ($mappings as $position => $class) {
$this->assertSame($class, get_class($f->getField($position)));
}
}
/**
* @covers \Cron\FieldFactory::getField
*/
public function testValidatesFieldPosition()
{
$this->expectException(\InvalidArgumentException::class);
$f = new FieldFactory();
$f->getField(-1);
}
}

View file

@ -1,99 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\HoursField;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class HoursFieldTest extends TestCase
{
/**
* @covers \Cron\HoursField::validate
*/
public function testValidatesField()
{
$f = new HoursField();
$this->assertTrue($f->validate('1'));
$this->assertTrue($f->validate('00'));
$this->assertTrue($f->validate('01'));
$this->assertTrue($f->validate('*'));
$this->assertFalse($f->validate('*/3,1,1-12'));
}
/**
* @covers \Cron\HoursField::isSatisfiedBy
*/
public function testChecksIfSatisfied()
{
$f = new HoursField();
$this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
$this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
}
/**
* @covers \Cron\HoursField::increment
*/
public function testIncrementsDate()
{
$d = new DateTime('2011-03-15 11:15:00');
$f = new HoursField();
$f->increment($d);
$this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
$d->setTime(11, 15, 0);
$f->increment($d, true);
$this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\HoursField::increment
*/
public function testIncrementsDateTimeImmutable()
{
$d = new DateTimeImmutable('2011-03-15 11:15:00');
$f = new HoursField();
$f->increment($d);
$this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\HoursField::increment
*/
public function testIncrementsDateWithThirtyMinuteOffsetTimezone()
{
$tz = date_default_timezone_get();
date_default_timezone_set('America/St_Johns');
$d = new DateTime('2011-03-15 11:15:00');
$f = new HoursField();
$f->increment($d);
$this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
$d->setTime(11, 15, 0);
$f->increment($d, true);
$this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s'));
date_default_timezone_set($tz);
}
/**
* @covers \Cron\HoursField::increment
*/
public function testIncrementDateWithFifteenMinuteOffsetTimezone()
{
$tz = date_default_timezone_get();
date_default_timezone_set('Asia/Kathmandu');
$d = new DateTime('2011-03-15 11:15:00');
$f = new HoursField();
$f->increment($d);
$this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
$d->setTime(11, 15, 0);
$f->increment($d, true);
$this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s'));
date_default_timezone_set($tz);
}
}

View file

@ -1,73 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\MinutesField;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class MinutesFieldTest extends TestCase
{
/**
* @covers \Cron\MinutesField::validate
*/
public function testValidatesField()
{
$f = new MinutesField();
$this->assertTrue($f->validate('1'));
$this->assertTrue($f->validate('*'));
$this->assertFalse($f->validate('*/3,1,1-12'));
}
/**
* @covers \Cron\MinutesField::isSatisfiedBy
*/
public function testChecksIfSatisfied()
{
$f = new MinutesField();
$this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
$this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
}
/**
* @covers \Cron\MinutesField::increment
*/
public function testIncrementsDate()
{
$d = new DateTime('2011-03-15 11:15:00');
$f = new MinutesField();
$f->increment($d);
$this->assertSame('2011-03-15 11:16:00', $d->format('Y-m-d H:i:s'));
$f->increment($d, true);
$this->assertSame('2011-03-15 11:15:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\MinutesField::increment
*/
public function testIncrementsDateTimeImmutable()
{
$d = new DateTimeImmutable('2011-03-15 11:15:00');
$f = new MinutesField();
$f->increment($d);
$this->assertSame('2011-03-15 11:16:00', $d->format('Y-m-d H:i:s'));
}
/**
* Various bad syntaxes that are reported to work, but shouldn't.
*
* @author Chris Tankersley
* @since 2017-08-18
*/
public function testBadSyntaxesShouldNotValidate()
{
$f = new MinutesField();
$this->assertFalse($f->validate('*-1'));
$this->assertFalse($f->validate('1-2-3'));
$this->assertFalse($f->validate('-1'));
}
}

View file

@ -1,103 +0,0 @@
<?php
namespace Cron\Tests;
use Cron\MonthField;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @author Michael Dowling <mtdowling@gmail.com>
*/
class MonthFieldTest extends TestCase
{
/**
* @covers \Cron\MonthField::validate
*/
public function testValidatesField()
{
$f = new MonthField();
$this->assertTrue($f->validate('12'));
$this->assertTrue($f->validate('*'));
$this->assertFalse($f->validate('*/10,2,1-12'));
$this->assertFalse($f->validate('1.fix-regexp'));
}
/**
* @covers \Cron\MonthField::isSatisfiedBy
*/
public function testChecksIfSatisfied()
{
$f = new MonthField();
$this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
$this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
}
/**
* @covers \Cron\MonthField::increment
*/
public function testIncrementsDate()
{
$d = new DateTime('2011-03-15 11:15:00');
$f = new MonthField();
$f->increment($d);
$this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s'));
$d = new DateTime('2011-03-15 11:15:00');
$f->increment($d, true);
$this->assertSame('2011-02-28 23:59:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\MonthField::increment
*/
public function testIncrementsDateTimeImmutable()
{
$d = new DateTimeImmutable('2011-03-15 11:15:00');
$f = new MonthField();
$f->increment($d);
$this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\MonthField::increment
*/
public function testIncrementsDateWithThirtyMinuteTimezone()
{
$tz = date_default_timezone_get();
date_default_timezone_set('America/St_Johns');
$d = new DateTime('2011-03-31 11:59:59');
$f = new MonthField();
$f->increment($d);
$this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s'));
$d = new DateTime('2011-03-15 11:15:00');
$f->increment($d, true);
$this->assertSame('2011-02-28 23:59:00', $d->format('Y-m-d H:i:s'));
date_default_timezone_set($tz);
}
/**
* @covers \Cron\MonthField::increment
*/
public function testIncrementsYearAsNeeded()
{
$f = new MonthField();
$d = new DateTime('2011-12-15 00:00:00');
$f->increment($d);
$this->assertSame('2012-01-01 00:00:00', $d->format('Y-m-d H:i:s'));
}
/**
* @covers \Cron\MonthField::increment
*/
public function testDecrementsYearAsNeeded()
{
$f = new MonthField();
$d = new DateTime('2011-01-15 00:00:00');
$f->increment($d, true);
$this->assertSame('2010-12-31 23:59:00', $d->format('Y-m-d H:i:s'));
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,44 @@
<?php
$finder = Symfony\Component\Finder\Finder::create()
->notPath('bootstrap/*')
->notPath('storage/*')
->notPath('resources/view/mail/*')
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->notName('GitConflictController.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);

View file

@ -0,0 +1,114 @@
# Changelog
All notable changes to `flare-client-php` will be documented in this file
## 1.9.1 - 2021-09-13
- let `report` return the created report
## 1.9.0 - 2021-09-13
- add report tracking uuid
## 1.8.1 - 2021-05-31
- improve compatibility with Symfony 5.3
## 1.8.0 - 2021-04-30
- add ability to ignore errors and exceptions (#23)
- fix curl parameters
## 1.7.0 - 2021-04-12
- use new Flare endpoint and allow 1 redirect to it
## 1.6.1 - 2021-04-08
- make `censorRequestBodyFields` chainable
## 1.6.0 - 2021-04-08
- add ability to censor request body fields (#18)
## 1.5.0 - 2021-03-31
- add `determineVersionUsing`
## 1.4.0 - 2021-02-16
- remove custom grouping
## 1.3.7 - 2020-10-21
- allow PHP 8
## 1.3.6 - 2020-09-18
- remove `larapack/dd` (#15)
## 1.3.5 - 2020-08-26
- allow Laravel 8 (#13)
## 1.3.4 - 2020-07-14
- use directory separator constant
## 1.3.3 - 2020-07-14
- fix tests by requiring symfony/mime
- display real exception class for view errors (see https://github.com/facade/ignition/discussions/237)
## 1.3.2 - 2020-03-02
- allow L7
## 1.3.1 - 2019-12-15
- allow var-dumper v5.0
## 1.3.0 - 2019-11-27
- Allow custom grouping types
## 1.2.1 - 2019-11-19
- Let `registerFlareHandlers` return $this
## 1.2.0 - 2019-11-19
- Add `registerFlareHandlers` method to register error and exception handlers in non-Laravel applications
- Fix get requests with query parameters (#4)
## 1.1.2 - 2019-11-08
- Ignore invalid mime type detection issues
## 1.1.1 - 2019-10-07
- Wrap filesize detection in try-catch block
## 1.1.0 - 2019-09-27
- Add ability to log messages
## 1.0.4 - 2019-09-11
- Fixes an issue when sending exceptions inside a queue worker
## 1.0.3 - 2019-09-05
- Ensure valid session data
## 1.0.2 - 2019-09-05
- Fix error when uploading multiple files using an array name
## 1.0.1 - 2019-09-02
- Fix issue with uploaded files in request context
## 1.0.0 - 2019-08-30
- initial release

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Facade <info@facade.company>
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,36 @@
# Send PHP errors to Flare
[![Latest Version on Packagist](https://img.shields.io/packagist/v/facade/flare-client-php.svg?style=flat-square)](https://packagist.org/packages/facade/flare-client-php)
![Tests](https://github.com/facade/flare-client-php/workflows/Run%20tests/badge.svg)
[![Total Downloads](https://img.shields.io/packagist/dt/facade/flare-client-php.svg?style=flat-square)](https://packagist.org/packages/facade/flare-client-php)
This repository contains a PHP client to send PHP errors to [Flare](https://flareapp.io).
![Screenshot of error in Flare](https://facade.github.io/flare-client-php/screenshot.png)
## Documentation
You can find the documentation of this package at [the docs of Flare](https://flareapp.io/docs/general/projects).
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Testing
``` bash
composer test
```
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## Security
If you discover any security related issues, please email support@flareapp.io instead of using the issue tracker.
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View file

@ -0,0 +1,52 @@
{
"name": "facade/flare-client-php",
"description": "Send PHP errors to Flare",
"keywords": [
"facade",
"flare",
"exception",
"reporting"
],
"homepage": "https://github.com/facade/flare-client-php",
"license": "MIT",
"require": {
"php": "^7.1|^8.0",
"facade/ignition-contracts": "~1.0",
"illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0",
"symfony/http-foundation": "^3.3|^4.1|^5.0",
"symfony/mime": "^3.4|^4.0|^5.1",
"symfony/var-dumper": "^3.4|^4.0|^5.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"spatie/phpunit-snapshot-assertions": "^2.0",
"phpunit/phpunit": "^7.5"
},
"autoload": {
"psr-4": {
"Facade\\FlareClient\\": "src"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Facade\\FlareClient\\Tests\\": "tests"
}
},
"scripts": {
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes",
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Facade\FlareClient;
use Exception;
use Facade\FlareClient\Http\Client;
use Facade\FlareClient\Truncation\ReportTrimmer;
class Api
{
/** @var \Facade\FlareClient\Http\Client */
protected $client;
/** @var bool */
public static $sendInBatches = true;
/** @var array */
protected $queue = [];
public function __construct(Client $client)
{
$this->client = $client;
register_shutdown_function([$this, 'sendQueuedReports']);
}
public static function sendReportsInBatches(bool $batchSending = true)
{
static::$sendInBatches = $batchSending;
}
public function report(Report $report)
{
try {
if (static::$sendInBatches) {
$this->addReportToQueue($report);
} else {
$this->sendReportToApi($report);
}
} catch (Exception $e) {
//
}
}
public function sendTestReport(Report $report)
{
$this->sendReportToApi($report);
}
protected function addReportToQueue(Report $report)
{
$this->queue[] = $report;
}
public function sendQueuedReports()
{
try {
foreach ($this->queue as $report) {
$this->sendReportToApi($report);
}
} catch (Exception $e) {
//
} finally {
$this->queue = [];
}
}
protected function sendReportToApi(Report $report)
{
$this->client->post('reports', $this->truncateReport($report->toArray()));
}
protected function truncateReport(array $payload): array
{
return (new ReportTrimmer())->trim($payload);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Facade\FlareClient\Concerns;
trait HasContext
{
/** @var string|null */
private $messageLevel;
/** @var string|null */
private $stage;
/** @var array */
private $userProvidedContext = [];
public function stage(?string $stage)
{
$this->stage = $stage;
return $this;
}
public function messageLevel(?string $messageLevel)
{
$this->messageLevel = $messageLevel;
return $this;
}
public function getGroup(string $groupName = 'context', $default = []): array
{
return $this->userProvidedContext[$groupName] ?? $default;
}
public function context($key, $value)
{
return $this->group('context', [$key => $value]);
}
public function group(string $groupName, array $properties)
{
$group = $this->userProvidedContext[$groupName] ?? [];
$this->userProvidedContext[$groupName] = array_merge_recursive_distinct(
$group,
$properties
);
return $this;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Facade\FlareClient\Concerns;
use Facade\FlareClient\Time\SystemTime;
use Facade\FlareClient\Time\Time;
trait UsesTime
{
/** @var \Facade\FlareClient\Time\Time */
public static $time;
public static function useTime(Time $time)
{
self::$time = $time;
}
public function getCurrentTime(): int
{
$time = self::$time ?? new SystemTime();
return $time->getCurrentTime();
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Facade\FlareClient\Context;
class ConsoleContext implements ContextInterface
{
/** @var array */
private $arguments = [];
public function __construct(array $arguments = [])
{
$this->arguments = $arguments;
}
public function toArray(): array
{
return [
'arguments' => $this->arguments,
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Facade\FlareClient\Context;
class ContextContextDetector implements ContextDetectorInterface
{
public function detectCurrentContext(): ContextInterface
{
if ($this->runningInConsole()) {
return new ConsoleContext($_SERVER['argv'] ?? []);
}
return new RequestContext();
}
private function runningInConsole(): bool
{
if (isset($_ENV['APP_RUNNING_IN_CONSOLE'])) {
return $_ENV['APP_RUNNING_IN_CONSOLE'] === 'true';
}
if (isset($_ENV['FLARE_FAKE_WEB_REQUEST'])) {
return false;
}
return in_array(php_sapi_name(), ['cli', 'phpdb']);
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Context;
interface ContextDetectorInterface
{
public function detectCurrentContext(): ContextInterface;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Facade\FlareClient\Context;
interface ContextInterface
{
public function toArray(): array;
}

View file

@ -0,0 +1,126 @@
<?php
namespace Facade\FlareClient\Context;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Throwable;
class RequestContext implements ContextInterface
{
/** @var \Symfony\Component\HttpFoundation\Request|null */
protected $request;
public function __construct(Request $request = null)
{
$this->request = $request ?? Request::createFromGlobals();
}
public function getRequest(): array
{
return [
'url' => $this->request->getUri(),
'ip' => $this->request->getClientIp(),
'method' => $this->request->getMethod(),
'useragent' => $this->request->headers->get('User-Agent'),
];
}
private function getFiles(): array
{
if (is_null($this->request->files)) {
return [];
}
return $this->mapFiles($this->request->files->all());
}
protected function mapFiles(array $files)
{
return array_map(function ($file) {
if (is_array($file)) {
return $this->mapFiles($file);
}
if (! $file instanceof UploadedFile) {
return;
}
try {
$fileSize = $file->getSize();
} catch (\RuntimeException $e) {
$fileSize = 0;
}
try {
$mimeType = $file->getMimeType();
} catch (InvalidArgumentException $e) {
$mimeType = 'undefined';
}
return [
'pathname' => $file->getPathname(),
'size' => $fileSize,
'mimeType' => $mimeType,
];
}, $files);
}
public function getSession(): array
{
try {
$session = $this->request->getSession();
} catch (\Exception $exception) {
$session = [];
}
return $session ? $this->getValidSessionData($session) : [];
}
/**
* @param SessionInterface $session
* @return array
*/
protected function getValidSessionData($session): array
{
try {
json_encode($session->all());
} catch (Throwable $e) {
return [];
}
return $session->all();
}
public function getCookies(): array
{
return $this->request->cookies->all();
}
public function getHeaders(): array
{
return $this->request->headers->all();
}
public function getRequestData(): array
{
return [
'queryString' => $this->request->query->all(),
'body' => $this->request->request->all(),
'files' => $this->getFiles(),
];
}
public function toArray(): array
{
return [
'request' => $this->getRequest(),
'request_data' => $this->getRequestData(),
'headers' => $this->getHeaders(),
'cookies' => $this->getCookies(),
'session' => $this->getSession(),
];
}
}

Some files were not shown because too many files have changed in this diff Show more