New contact form with friendlycaptcha

This commit is contained in:
Jorit Tijsen
2026-02-24 14:04:04 +01:00
parent 6786ac7ce1
commit 3b4030113b
80 changed files with 2860 additions and 1918 deletions

View File

@@ -18,7 +18,7 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_DRIVER=smtp MAIL_DRIVER=smtp
MAIL_HOST=in-v3.mailjet.com MAIL_HOST=in-v3.mailjet.com
MAIL_PORT=25 MAIL_PORT=25
MAIL_USERNAME=mailjet_username MAIL_USERNAME=mailjet_username
MAIL_PASSWORD=mailjet_password MAIL_PASSWORD=mailjet_password
@@ -37,3 +37,7 @@ DB_PASSWORD=dbpass
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
FRIENDLY_CAPTCHA_SITEKEY=
FRIENDLY_CAPTCHA_SECRET=
FRIENDLY_CAPTCHA_DEBUG=true

View File

@@ -1,4 +1,4 @@
FROM php:8.1-apache FROM php:8.2-apache
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install \ && apt-get -y install \

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use App\Mail\ContactFormSubmitted;
class ContactController extends Controller
{
public function show()
{
return view('contact');
}
public function submit(Request $request)
{
$rules = [
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'message' => 'required|string',
'image' => 'nullable|image|max:10240',
'video' => 'nullable|mimetypes:video/avi,video/mpeg,video/quicktime,video/mp4|max:512000',
];
$debugCaptcha = env('FRIENDLY_CAPTCHA_DEBUG', false)
|| (app()->environment('local')
&& (!env('FRIENDLY_CAPTCHA_SITEKEY') || !env('FRIENDLY_CAPTCHA_SECRET')));
if ($debugCaptcha) {
$rules['frc-captcha-solution'] = 'nullable';
} else {
$rules['frc-captcha-solution'] = ['required', Rule::friendlycaptcha()];
}
$request->validate($rules);
$data = $request->only(['name', 'email', 'message']);
$attachments = [];
// Handle Image
if ($request->hasFile('image')) {
// Store temporarily to attach
$imagePath = $request->file('image')->store('contact_images');
$attachments['image'] = storage_path('app/' . $imagePath);
}
// Handle Video
if ($request->hasFile('video')) {
$videoPath = $request->file('video')->store('contact_videos', 'local');
$data['video_link'] = route('contact.video', ['filename' => basename($videoPath)]);
}
// Send Email
Mail::to('info@nhgooi.nl')->send(new ContactFormSubmitted($data, $attachments));
return redirect()->route('contact')->with('success', 'Bedankt voor uw bericht. We nemen zo snel mogelijk contact met u op.');
}
public function video(string $filename)
{
$path = 'contact_videos/' . basename($filename);
if (!Storage::disk('local')->exists($path)) {
abort(404);
}
$fullPath = Storage::disk('local')->path($path);
return response()->file($fullPath);
}
}

View File

@@ -91,7 +91,7 @@ class Controller extends BaseController
$this->getDataFromFileAndConvert('laatste_podcasts.json', ['podcasts'], '\Model\Podcast') $this->getDataFromFileAndConvert('laatste_podcasts.json', ['podcasts'], '\Model\Podcast')
); );
}); });
View::composer('widgets.menu', function($view) { View::composer('widgets.menu', function($view) {
$view->with('items', json_decode(Storage::get('static/menu.json'))); $view->with('items', json_decode(Storage::get('static/menu.json')));
}); });
@@ -198,7 +198,7 @@ class Controller extends BaseController
if ($page->edited) { if ($page->edited) {
$page->edited = Controller::JsonToDateTime($page->edited); $page->edited = Controller::JsonToDateTime($page->edited);
} }
return view('static', compact('page')); return view('static', compact('page'));
} }
} }

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class ContactFormSubmitted extends Mailable
{
use Queueable, SerializesModels;
public $data;
public $attachments_files;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data, $attachments_files = [])
{
$this->data = $data;
$this->attachments_files = $attachments_files;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$email = $this->view('emails.contact_submitted')
->subject('Nieuw contactformulier bericht van NH Gooi')
->replyTo($this->data['email'], $this->data['name']);
if (isset($this->attachments_files['image'])) {
$email->attach($this->attachments_files['image']);
}
return $email;
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;
use Ossycodes\FriendlyCaptcha\Rules\FriendlyCaptcha as FriendlyCaptchaRule;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@@ -14,6 +16,10 @@ class AppServiceProvider extends ServiceProvider
public function boot() public function boot()
{ {
\Illuminate\Support\Facades\URL::forceScheme('https'); \Illuminate\Support\Facades\URL::forceScheme('https');
Rule::macro('friendlycaptcha', function () {
return app(FriendlyCaptchaRule::class);
});
} }
/** /**

View File

@@ -53,6 +53,17 @@
0 => 'Termwind\\Laravel\\TermwindServiceProvider', 0 => 'Termwind\\Laravel\\TermwindServiceProvider',
), ),
), ),
'ossycodes/friendlycaptcha' =>
array (
'aliases' =>
array (
'FriendlyCaptcha' => 'Ossycodes\\FriendlyCaptcha\\Facades\\FriendlyCaptcha',
),
'providers' =>
array (
0 => 'Ossycodes\\FriendlyCaptcha\\FriendlyCaptchaServiceProvider',
),
),
'spatie/laravel-ignition' => 'spatie/laravel-ignition' =>
array ( array (
'providers' => 'providers' =>

View File

@@ -30,13 +30,14 @@
26 => 'Carbon\\Laravel\\ServiceProvider', 26 => 'Carbon\\Laravel\\ServiceProvider',
27 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', 27 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
28 => 'Termwind\\Laravel\\TermwindServiceProvider', 28 => 'Termwind\\Laravel\\TermwindServiceProvider',
29 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider', 29 => 'Ossycodes\\FriendlyCaptcha\\FriendlyCaptchaServiceProvider',
30 => 'Collective\\Html\\HtmlServiceProvider', 30 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
31 => 'Laravel\\Tinker\\TinkerServiceProvider', 31 => 'Collective\\Html\\HtmlServiceProvider',
32 => 'App\\Providers\\AppServiceProvider', 32 => 'Laravel\\Tinker\\TinkerServiceProvider',
33 => 'App\\Providers\\AuthServiceProvider', 33 => 'App\\Providers\\AppServiceProvider',
34 => 'App\\Providers\\EventServiceProvider', 34 => 'App\\Providers\\AuthServiceProvider',
35 => 'App\\Providers\\RouteServiceProvider', 35 => 'App\\Providers\\EventServiceProvider',
36 => 'App\\Providers\\RouteServiceProvider',
), ),
'eager' => 'eager' =>
array ( array (
@@ -54,11 +55,12 @@
11 => 'Carbon\\Laravel\\ServiceProvider', 11 => 'Carbon\\Laravel\\ServiceProvider',
12 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', 12 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
13 => 'Termwind\\Laravel\\TermwindServiceProvider', 13 => 'Termwind\\Laravel\\TermwindServiceProvider',
14 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider', 14 => 'Ossycodes\\FriendlyCaptcha\\FriendlyCaptchaServiceProvider',
15 => 'App\\Providers\\AppServiceProvider', 15 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
16 => 'App\\Providers\\AuthServiceProvider', 16 => 'App\\Providers\\AppServiceProvider',
17 => 'App\\Providers\\EventServiceProvider', 17 => 'App\\Providers\\AuthServiceProvider',
18 => 'App\\Providers\\RouteServiceProvider', 18 => 'App\\Providers\\EventServiceProvider',
19 => 'App\\Providers\\RouteServiceProvider',
), ),
'deferred' => 'deferred' =>
array ( array (

View File

@@ -10,7 +10,8 @@
"laravel/framework": "^9.19", "laravel/framework": "^9.19",
"laravel/sanctum": "^3.0", "laravel/sanctum": "^3.0",
"laravel/tinker": "^2.7", "laravel/tinker": "^2.7",
"laravelcollective/html": "^6.3" "laravelcollective/html": "^6.3",
"ossycodes/friendlycaptcha": "^3.0"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",

132
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f70c81adf2990f185a8632535bd08631", "content-hash": "f9a3eebc09010b2b187541a3e59a0eff",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -1934,34 +1934,37 @@
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
"version": "v1.2.3", "version": "v1.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/schema.git", "url": "https://github.com/nette/schema.git",
"reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f" "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004",
"reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", "nette/utils": "^4.0",
"php": ">=7.1 <8.3" "php": "8.1 - 8.5"
}, },
"require-dev": { "require-dev": {
"nette/tester": "^2.3 || ^2.4", "nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^1.0", "phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.7" "tracy/tracy": "^2.8"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.2-dev" "dev-master": "1.3-dev"
} }
}, },
"autoload": { "autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [ "classmap": [
"src/" "src/"
] ]
@@ -1990,34 +1993,36 @@
], ],
"support": { "support": {
"issues": "https://github.com/nette/schema/issues", "issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.3" "source": "https://github.com/nette/schema/tree/v1.3.3"
}, },
"time": "2022-10-13T01:24:26+00:00" "time": "2025-10-30T22:57:59+00:00"
}, },
{ {
"name": "nette/utils", "name": "nette/utils",
"version": "v3.2.8", "version": "v4.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/utils.git", "url": "https://github.com/nette/utils.git",
"reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368" "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/02a54c4c872b99e4ec05c4aec54b5a06eb0f6368", "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72",
"reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368", "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.2 <8.3" "php": "8.2 - 8.5"
}, },
"conflict": { "conflict": {
"nette/di": "<3.0.6" "nette/finder": "<3",
"nette/schema": "<1.2.2"
}, },
"require-dev": { "require-dev": {
"nette/tester": "~2.0", "jetbrains/phpstorm-attributes": "^1.2",
"phpstan/phpstan": "^1.0", "nette/tester": "^2.5",
"tracy/tracy": "^2.3" "phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
}, },
"suggest": { "suggest": {
"ext-gd": "to use Image", "ext-gd": "to use Image",
@@ -2025,16 +2030,18 @@
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json", "ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...", "ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
"ext-xml": "to use Strings::length() etc. when mbstring is not available"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.2-dev" "dev-master": "4.1-dev"
} }
}, },
"autoload": { "autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [ "classmap": [
"src/" "src/"
] ]
@@ -2075,9 +2082,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nette/utils/issues", "issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v3.2.8" "source": "https://github.com/nette/utils/tree/v4.1.1"
}, },
"time": "2022-09-12T23:36:20+00:00" "time": "2025-12-22T12:14:32+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
@@ -2221,6 +2228,69 @@
], ],
"time": "2022-12-20T19:00:15+00:00" "time": "2022-12-20T19:00:15+00:00"
}, },
{
"name": "ossycodes/friendlycaptcha",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/ossycodes/friendlycaptcha.git",
"reference": "b18dfab44ee5fff7d75412232eb6f834864efde6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ossycodes/friendlycaptcha/zipball/b18dfab44ee5fff7d75412232eb6f834864efde6",
"reference": "b18dfab44ee5fff7d75412232eb6f834864efde6",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^7.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.4|^8.0|^8.1|^8.2|^8.3"
},
"require-dev": {
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0",
"phpunit/phpunit": "^8.0 || ^9.5 || ^10.5 || ^11.0 || ^12.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"FriendlyCaptcha": "Ossycodes\\FriendlyCaptcha\\Facades\\FriendlyCaptcha"
},
"providers": [
"Ossycodes\\FriendlyCaptcha\\FriendlyCaptchaServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Ossycodes\\FriendlyCaptcha\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "ossycodes",
"email": "osaigbovoemmanuel1@gmail.com",
"role": "Developer"
}
],
"description": "A simple package to help integrate FriendlyCaptcha in your Laravel applications.",
"homepage": "https://github.com/ossycodes/friendlycaptcha",
"keywords": [
"captcha",
"friendlycaptcha",
"laravel"
],
"support": {
"issues": "https://github.com/ossycodes/friendlycaptcha/issues",
"source": "https://github.com/ossycodes/friendlycaptcha/tree/v3.0.0"
},
"time": "2025-05-08T13:30:49+00:00"
},
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.0", "version": "1.9.0",
@@ -7931,12 +8001,12 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "dev", "minimum-stability": "dev",
"stability-flags": [], "stability-flags": {},
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.0.2" "php": "^8.0"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

View File

@@ -22,3 +22,8 @@ services:
MYSQL_DATABASE: forge MYSQL_DATABASE: forge
MYSQL_USER: forge MYSQL_USER: forge
MYSQL_PASSWORD: secret MYSQL_PASSWORD: secret
mailpit:
image: axllent/mailpit:latest
ports:
- "8025:8025"
- "1025:1025"

View File

@@ -23,7 +23,7 @@ ServerTokens Prod
<VirtualHost *:443> <VirtualHost *:443>
ServerName localhost ServerName localhost
ServerAdmin support@websight.nl ServerAdmin support@websight.nl
DocumentRoot /var/www/html DocumentRoot /var/www/html/public
SSLEngine on SSLEngine on
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
@@ -32,15 +32,15 @@ ServerTokens Prod
SSLProtocol All -SSLv2 -SSLv3 SSLProtocol All -SSLv2 -SSLv3
SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
<Directory /var/www/html/> <Directory /var/www/html/public>
Options -Indexes +FollowSymLinks +MultiViews Options -Indexes +FollowSymLinks +MultiViews
AllowOverride All AllowOverride All
Order deny,allow Order deny,allow
Allow from all Allow from all
</Directory> </Directory>
ErrorLog /var/log/apache2/ssl-vhost-error.log ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/ssl-vhost-access.log combined CustomLog /var/log/apache2/access.log combined
</VirtualHost> </VirtualHost>
</IfModule> </IfModule>

View File

@@ -18,7 +18,7 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_DRIVER=smtp MAIL_DRIVER=smtp
MAIL_HOST=in-v3.mailjet.com MAIL_HOST=in-v3.mailjet.com
MAIL_PORT=25 MAIL_PORT=25
MAIL_USERNAME=mailjet_username MAIL_USERNAME=mailjet_username
MAIL_PASSWORD=mailjet_password MAIL_PASSWORD=mailjet_password
@@ -37,3 +37,7 @@ DB_PASSWORD=dbpass
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
FRIENDLY_CAPTCHA_SITEKEY=
FRIENDLY_CAPTCHA_SECRET=
FRIENDLY_CAPTCHA_DEBUG=true

72
public/css/style.css vendored
View File

@@ -58,7 +58,7 @@ body {
.btn { .btn {
display: block; display: block;
width: calc(100% - 78px); width: calc(100% - 78px);
padding: 10px 39px 10px 39px; padding: 10px 39px;
border-radius: 3px; border-radius: 3px;
background-image: linear-gradient(to right, #0f259d, #5ba8f4); background-image: linear-gradient(to right, #0f259d, #5ba8f4);
font-family: Nunito, serif; font-family: Nunito, serif;
@@ -74,21 +74,6 @@ body {
.btn:hover, .btn:active { .btn:hover, .btn:active {
color: #fff; color: #fff;
} }
.btn.auto_width {
width: fit-content;
margin: 0 auto;
}
.btn.fit_content {
width: fit-content;
}
.btn.btn_facebook_share {
background-image: none;
background-color: #0f259d;
}
.btn.btn_twitter_x_share {
background-image: none;
background-color: #5ba8f4;
}
div.pp_default .pp_content_container .pp_left, div.pp_default .pp_content_container .pp_left,
div.pp_default .pp_content_container .pp_right, div.pp_default .pp_content_container .pp_right,
@@ -172,6 +157,43 @@ div.pp_default .pp_close:hover {
color: #666; color: #666;
} }
.contant-form .form-group {
display: flex;
margin-bottom: 10px;
}
.contant-form .form-group label {
width: 160px;
}
.contant-form .form-group input, .contant-form .form-group textarea {
flex-grow: 1;
}
.contant-form .action_button {
margin-top: 10px;
cursor: pointer;
}
.contant-form .form-control-file::file-selector-button {
display: block;
width: calc(100% - 78px);
padding: 10px 39px;
border-radius: 3px;
background-image: linear-gradient(to right, #0f259d, #5ba8f4);
font-family: Nunito, serif;
font-size: 14px;
font-weight: bold;
line-height: 0.93;
text-align: center;
color: #fff;
margin-bottom: 10px;
text-decoration: none;
text-transform: uppercase;
width: fit-content;
margin-bottom: 0;
cursor: pointer;
}
.contant-form .form-control-file::file-selector-button:hover, .contant-form .form-control-file::file-selector-button:active {
color: #fff;
}
.header { .header {
height: 111px; height: 111px;
} }
@@ -303,18 +325,6 @@ div.pp_default .pp_close:hover {
margin-left: -10px; margin-left: -10px;
} }
.submenu li.selected a {
color: #0f259d !important;
}
.mobile-menu .submenu li.selected a,
.mobile-menu .submenu li:hover a,
.mobile-menu .submenu li.hover a {
color: white !important;
background: linear-gradient(to right, #0f259d, #5ba8f4);
}
.top_menu_container, .menu_mobile_container { .top_menu_container, .menu_mobile_container {
height: 50px; height: 50px;
max-width: 1170px; max-width: 1170px;
@@ -1282,6 +1292,9 @@ div.pp_default .pp_close:hover {
.post_container .post_body h3 { .post_container .post_body h3 {
font-size: 15px; font-size: 15px;
} }
.post_container .post_body div.text {
margin-bottom: 20px;
}
.post_container .post_body blockquote { .post_container .post_body blockquote {
border-left: 3px solid #5ba8f4; border-left: 3px solid #5ba8f4;
margin-left: 0; margin-left: 0;
@@ -1299,9 +1312,6 @@ div.pp_default .pp_close:hover {
line-height: 3.17; line-height: 3.17;
color: #585858; color: #585858;
} }
.post_container .post_body div.text {
margin-bottom: 20px;
}
.post_container .post_body .post_details { .post_container .post_body .post_details {
margin: 0; margin: 0;
padding: 0; padding: 0;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -63,3 +63,25 @@
@include post_date; @include post_date;
} }
} }
@mixin btn-base {
display: block;
width: calc(100% - 78px);
padding: 10px 39px;
border-radius: 3px;
background-image: linear-gradient(to right, #0f259d, #5ba8f4);
font-family: Nunito, serif;
font-size: 14px;
font-weight: bold;
line-height: 0.93;
text-align: center;
color: $text-inverted-color;
margin-bottom: 10px;
text-decoration: none;
text-transform: uppercase;
&:hover,
&:active {
color: $text-inverted-color;
}
}

View File

@@ -6,6 +6,7 @@
@use "../components/cookie"; @use "../components/cookie";
@use "../components/list"; @use "../components/list";
@use "../components/banners"; @use "../components/banners";
@use "../components/contact_form";
@use "../layout"; @use "../layout";

View File

@@ -1,40 +1,5 @@
@use "../abstracts/variables" as *; @use "../abstracts/mixin" as *;
.btn { .btn {
display: block; @include btn-base;
width: CALC(100% - 78px);
padding: 10px 39px 10px 39px;
border-radius: 3px;
background-image: linear-gradient(to right, #0f259d, #5ba8f4);
font-family: Nunito, serif;
font-size: 14px;
font-weight: bold;
line-height: 0.93;
text-align: center;
color: $text-inverted-color;
margin-bottom: 10px;
text-decoration: none;
text-transform: uppercase;
&:hover, &:active {
color: $text-inverted-color;
}
&.auto_width {
width: fit-content;
margin: 0 auto;
}
&.fit_content {
width: fit-content;
}
&.btn_facebook_share {
background-image: none;
background-color: #0f259d;
}
&.btn_twitter_x_share {
background-image: none;
background-color: #5ba8f4;
}
} }

View File

@@ -0,0 +1,24 @@
@use "../abstracts/mixin" as *;
.contant-form {
.form-group {
display: flex;
margin-bottom: 10px;
label {
width: 160px;
}
input, textarea {
flex-grow: 1;
}
}
.action_button {
margin-top: 10px;
cursor: pointer;
}
.form-control-file::file-selector-button {
@include btn-base;
width: fit-content;
margin-bottom: 0;
cursor: pointer;
}
}

View File

@@ -0,0 +1,116 @@
@extends('layouts/full')
@section('title')
Neem contact op
@endsection
@section('breadcrumb')
<ul class="bread_crumb">
<li><a title="Home" href="/">Home</a></li>
<li class="separator"><i class="fa-solid fa-chevron-right"></i></li>
<li><a title="NH Gooi" href="{{route('contact')}}">NH Gooi</a></li>
<li class="separator"><i class="fa-solid fa-chevron-right"></i></li>
<li>Neem contact op</li>
</ul>
@endsection
@section('content')
<div class="page_body">
<div class="row ">
<div class="col-12 col-md-6">
<p>NH Gooi is de publieke streekomroep van Gooi en Vechtstreek. We houden je 24 uur per dag
op de hoogte van al het nieuws, betrouwbaar en snel. Dat doen we via een eigen nieuws-
app, onze website en social media, maar ook op radio en televisie. Daarnaast brengen we
een gevarieerd aanbod van podcasts, radio- en televisieprogramma's.</p>
<p>Ons team van journalisten en programmamakers bestaat uit betaalde krachten, vrijwilligers
en stagiaires. We vinden het belangrijk mensen op te leiden en een goede plek te bieden
voor talent.</p>
<p>De redactie van NH Gooi is journalistiek onafhankelijk en wordt geleid door de chef redactie.</p>
<h3>Contactinformatie</h3>
<p>Neem contact op met NH Gooi, de streekomroep voor Gooi &amp; Vechtstreek.</p>
<p>Wij zijn te ontvangen in heel Gooi en Eemland. <br>
<a href="{{url('frequenties')}}" class="action_button">
<span class="fa fa-list"></span>
<span>Frequenties</span>
</a>
</p>
<p class="page_margin_top">
<b>NHGooi</b><br>
Postbus 83<br>
1270 AB Huizen<br>
Tel: <a href="tel:+31356424776">035-6424774</a><br>
KvK: 41194132<br>
<br>
<b>Bezoekadres / Studio:</b><br>
IJsselmeerstraat 3B<br>
1271 AA Huizen<br><br>
<p>
<b>NHGooi Radio</b><br>
IJsselmeerstraat 3B<br/>
1271 AA, Huizen<br/>
studio: <a href="tel:+31356424776">035-6424776</a><br/>
KvK: 41194132<br>
</p>
</div>
<div class="col-12 col-md-6">
<h3>Contactformulier</h3>
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form class="contant-form" action="{{ route('contact.submit') }}" method="POST" enctype="multipart/form-data">
{{ csrf_field() }}
<div class="form-group">
<label for="name">Naam</label>
<input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" class="form-control" id="email" name="email" value="{{ old('email') }}" required>
</div>
<div class="form-group">
<label for="message">Bericht</label>
<textarea class="form-control" id="message" name="message" rows="5" required>{{ old('message') }}</textarea>
</div>
<div class="form-group">
<label for="image">Afbeelding (optioneel)</label>
<input type="file" class="form-control-file" id="image" name="image" accept="image/*">
</div>
<div class="form-group">
<label for="video">Video (optioneel)</label>
<input type="file" class="form-control-file" id="video" name="video" accept="video/*">
</div>
<div class="form-group">
{!! FriendlyCaptcha::renderWidget() !!}
</div>
<button type="submit" class="btn action_button">Verstuur</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
{!! FriendlyCaptcha::renderWidgetScripts() !!}
@endpush

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Contactformulier Bericht</title>
</head>
<body>
<h1>Nieuw bericht van {{ $data['name'] }}</h1>
<p><strong>Email:</strong> {{ $data['email'] }}</p>
<p><strong>Bericht:</strong></p>
<p>{!! nl2br(e($data['message'])) !!}</p>
@if(isset($data['video_link']))
<p><strong>Video Link:</strong> <a href="{{ $data['video_link'] }}">{{ $data['video_link'] }}</a></p>
@endif
</body>
</html>

View File

@@ -112,5 +112,12 @@ Route::get('/cookie-statement', 'Controller@view_cookie_statement')->name('cooki
Route::get('/special/stmaarten', function() { return file_get_contents('http://api-dev.6fm.nl/special/stmaarten'); }); Route::get('/special/stmaarten', function() { return file_get_contents('http://api-dev.6fm.nl/special/stmaarten'); });
Route::get('/kabelkrant', function() { return view('kabelkrant'); }); Route::get('/kabelkrant', function() { return view('kabelkrant'); });
// New contact form
Route::get('/contact', 'ContactController@show')->name('contact');
Route::post('/contact', 'ContactController@submit')->name('contact.submit');
Route::get('/contact/video/{filename}', 'ContactController@video')
->where(['filename' => '[A-Za-z0-9_\-\.]+'])
->name('contact.video');
// Catch all route for API-based static routes // Catch all route for API-based static routes
Route::get('{slug}', 'Controller@static_page')->where('slug', '^.*')->name('static_page'); Route::get('{slug}', 'Controller@static_page')->where('slug', '^.*')->name('static_page');

View File

@@ -0,0 +1,61 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule as ValidationRule;
use Illuminate\Contracts\Validation\Rule;
use App\Mail\ContactFormSubmitted;
class ContactPageTest extends TestCase
{
public function test_contact_page_is_accessible()
{
$response = $this->get(route('contact'));
$response->assertStatus(200);
$response->assertSee('Contactformulier');
}
public function test_contact_form_can_be_submitted()
{
ValidationRule::macro('friendlycaptcha', function () {
return new class implements Rule {
public function passes($attribute, $value)
{
return true;
}
public function message()
{
return 'The :attribute is invalid.';
}
};
});
Mail::fake();
Storage::fake('public');
$image = UploadedFile::fake()->image('example.jpg');
$video = UploadedFile::fake()->create('example.mp4', 1000, 'video/mp4');
$response = $this->post(route('contact.submit'), [
'name' => 'Test User',
'email' => 'test@example.com',
'message' => 'Dit is een testbericht.',
'image' => $image,
'video' => $video,
'frc-captcha-solution' => 'dummy-solution',
]);
$response->assertRedirect(route('contact'));
$response->assertSessionHas('success');
Mail::assertSent(function (ContactFormSubmitted $mail) {
return $mail->hasTo('info@nhgooi.nl');
});
}
}

5
vendor/autoload.php vendored
View File

@@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
echo $err; echo $err;
} }
} }
trigger_error( throw new RuntimeException($err);
$err,
E_USER_ERROR
);
} }
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';

View File

@@ -26,12 +26,23 @@ use Composer\Semver\VersionParser;
*/ */
class InstalledVersions class InstalledVersions
{ {
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/** /**
* @var mixed[]|null * @var mixed[]|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 * @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; private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/** /**
* @var bool|null * @var bool|null
*/ */
@@ -98,7 +109,7 @@ class InstalledVersions
{ {
foreach (self::getInstalled() as $installed) { foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) { if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
} }
} }
@@ -119,7 +130,7 @@ class InstalledVersions
*/ */
public static function satisfies(VersionParser $parser, $packageName, $constraint) public static function satisfies(VersionParser $parser, $packageName, $constraint)
{ {
$constraint = $parser->parseConstraints($constraint); $constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint); return $provided->matches($constraint);
@@ -309,6 +320,24 @@ class InstalledVersions
{ {
self::$installed = $data; self::$installed = $data;
self::$installedByVendor = array(); self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
} }
/** /**
@@ -322,17 +351,27 @@ class InstalledVersions
} }
$installed = array(); $installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir]; $installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) { } elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; /** @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[]}>} $required */
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { $required = require $vendorDir.'/composer/installed.php';
self::$installed = $installed[count($installed) - 1]; self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
} }
} }
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
} }
} }
@@ -340,12 +379,17 @@ class InstalledVersions
// only require the installed.php file if this file is loaded from its dumped location, // only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') { if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php'; /** @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[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else { } else {
self::$installed = array(); self::$installed = array();
} }
} }
$installed[] = self::$installed;
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed; return $installed;
} }

View File

@@ -14,6 +14,7 @@ return array(
'App\\Http\\Controllers\\Auth\\RegisterController' => $baseDir . '/app/Http/Controllers/Auth/RegisterController.php', 'App\\Http\\Controllers\\Auth\\RegisterController' => $baseDir . '/app/Http/Controllers/Auth/RegisterController.php',
'App\\Http\\Controllers\\Auth\\ResetPasswordController' => $baseDir . '/app/Http/Controllers/Auth/ResetPasswordController.php', 'App\\Http\\Controllers\\Auth\\ResetPasswordController' => $baseDir . '/app/Http/Controllers/Auth/ResetPasswordController.php',
'App\\Http\\Controllers\\CalendarController' => $baseDir . '/app/Http/Controllers/CalendarController.php', 'App\\Http\\Controllers\\CalendarController' => $baseDir . '/app/Http/Controllers/CalendarController.php',
'App\\Http\\Controllers\\ContactController' => $baseDir . '/app/Http/Controllers/ContactController.php',
'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php',
'App\\Http\\Controllers\\HomeController' => $baseDir . '/app/Http/Controllers/HomeController.php', 'App\\Http\\Controllers\\HomeController' => $baseDir . '/app/Http/Controllers/HomeController.php',
'App\\Http\\Controllers\\ImagesController' => $baseDir . '/app/Http/Controllers/ImagesController.php', 'App\\Http\\Controllers\\ImagesController' => $baseDir . '/app/Http/Controllers/ImagesController.php',
@@ -29,6 +30,7 @@ return array(
'App\\Http\\Middleware\\RedirectIfAuthenticated' => $baseDir . '/app/Http/Middleware/RedirectIfAuthenticated.php', 'App\\Http\\Middleware\\RedirectIfAuthenticated' => $baseDir . '/app/Http/Middleware/RedirectIfAuthenticated.php',
'App\\Http\\Middleware\\TrimStrings' => $baseDir . '/app/Http/Middleware/TrimStrings.php', 'App\\Http\\Middleware\\TrimStrings' => $baseDir . '/app/Http/Middleware/TrimStrings.php',
'App\\Http\\Middleware\\VerifyCsrfToken' => $baseDir . '/app/Http/Middleware/VerifyCsrfToken.php', 'App\\Http\\Middleware\\VerifyCsrfToken' => $baseDir . '/app/Http/Middleware/VerifyCsrfToken.php',
'App\\Mail\\ContactFormSubmitted' => $baseDir . '/app/Mail/ContactFormSubmitted.php',
'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php',
'App\\Providers\\AuthServiceProvider' => $baseDir . '/app/Providers/AuthServiceProvider.php', 'App\\Providers\\AuthServiceProvider' => $baseDir . '/app/Providers/AuthServiceProvider.php',
'App\\Providers\\BroadcastServiceProvider' => $baseDir . '/app/Providers/BroadcastServiceProvider.php', 'App\\Providers\\BroadcastServiceProvider' => $baseDir . '/app/Providers/BroadcastServiceProvider.php',
@@ -2802,6 +2804,7 @@ return array(
'Nette\\Schema\\Processor' => $vendorDir . '/nette/schema/src/Schema/Processor.php', 'Nette\\Schema\\Processor' => $vendorDir . '/nette/schema/src/Schema/Processor.php',
'Nette\\Schema\\Schema' => $vendorDir . '/nette/schema/src/Schema/Schema.php', 'Nette\\Schema\\Schema' => $vendorDir . '/nette/schema/src/Schema/Schema.php',
'Nette\\Schema\\ValidationException' => $vendorDir . '/nette/schema/src/Schema/ValidationException.php', 'Nette\\Schema\\ValidationException' => $vendorDir . '/nette/schema/src/Schema/ValidationException.php',
'Nette\\ShouldNotHappenException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\SmartObject' => $vendorDir . '/nette/utils/src/SmartObject.php', 'Nette\\SmartObject' => $vendorDir . '/nette/utils/src/SmartObject.php',
'Nette\\StaticClass' => $vendorDir . '/nette/utils/src/StaticClass.php', 'Nette\\StaticClass' => $vendorDir . '/nette/utils/src/StaticClass.php',
'Nette\\UnexpectedValueException' => $vendorDir . '/nette/utils/src/exceptions.php', 'Nette\\UnexpectedValueException' => $vendorDir . '/nette/utils/src/exceptions.php',
@@ -2811,20 +2814,25 @@ return array(
'Nette\\Utils\\AssertionException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\AssertionException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Callback' => $vendorDir . '/nette/utils/src/Utils/Callback.php', 'Nette\\Utils\\Callback' => $vendorDir . '/nette/utils/src/Utils/Callback.php',
'Nette\\Utils\\DateTime' => $vendorDir . '/nette/utils/src/Utils/DateTime.php', 'Nette\\Utils\\DateTime' => $vendorDir . '/nette/utils/src/Utils/DateTime.php',
'Nette\\Utils\\FileInfo' => $vendorDir . '/nette/utils/src/Utils/FileInfo.php',
'Nette\\Utils\\FileSystem' => $vendorDir . '/nette/utils/src/Utils/FileSystem.php', 'Nette\\Utils\\FileSystem' => $vendorDir . '/nette/utils/src/Utils/FileSystem.php',
'Nette\\Utils\\Finder' => $vendorDir . '/nette/utils/src/Utils/Finder.php',
'Nette\\Utils\\Floats' => $vendorDir . '/nette/utils/src/Utils/Floats.php', 'Nette\\Utils\\Floats' => $vendorDir . '/nette/utils/src/Utils/Floats.php',
'Nette\\Utils\\Helpers' => $vendorDir . '/nette/utils/src/Utils/Helpers.php', 'Nette\\Utils\\Helpers' => $vendorDir . '/nette/utils/src/Utils/Helpers.php',
'Nette\\Utils\\Html' => $vendorDir . '/nette/utils/src/Utils/Html.php', 'Nette\\Utils\\Html' => $vendorDir . '/nette/utils/src/Utils/Html.php',
'Nette\\Utils\\IHtmlString' => $vendorDir . '/nette/utils/src/compatibility.php', 'Nette\\Utils\\IHtmlString' => $vendorDir . '/nette/utils/src/compatibility.php',
'Nette\\Utils\\Image' => $vendorDir . '/nette/utils/src/Utils/Image.php', 'Nette\\Utils\\Image' => $vendorDir . '/nette/utils/src/Utils/Image.php',
'Nette\\Utils\\ImageColor' => $vendorDir . '/nette/utils/src/Utils/ImageColor.php',
'Nette\\Utils\\ImageException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\ImageException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ImageType' => $vendorDir . '/nette/utils/src/Utils/ImageType.php',
'Nette\\Utils\\Iterables' => $vendorDir . '/nette/utils/src/Utils/Iterables.php',
'Nette\\Utils\\Json' => $vendorDir . '/nette/utils/src/Utils/Json.php', 'Nette\\Utils\\Json' => $vendorDir . '/nette/utils/src/Utils/Json.php',
'Nette\\Utils\\JsonException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\JsonException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => $vendorDir . '/nette/utils/src/Utils/ObjectHelpers.php', 'Nette\\Utils\\ObjectHelpers' => $vendorDir . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\ObjectMixin' => $vendorDir . '/nette/utils/src/Utils/ObjectMixin.php',
'Nette\\Utils\\Paginator' => $vendorDir . '/nette/utils/src/Utils/Paginator.php', 'Nette\\Utils\\Paginator' => $vendorDir . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Random' => $vendorDir . '/nette/utils/src/Utils/Random.php', 'Nette\\Utils\\Random' => $vendorDir . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => $vendorDir . '/nette/utils/src/Utils/Reflection.php', 'Nette\\Utils\\Reflection' => $vendorDir . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => $vendorDir . '/nette/utils/src/Utils/ReflectionMethod.php',
'Nette\\Utils\\RegexpException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\RegexpException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Strings' => $vendorDir . '/nette/utils/src/Utils/Strings.php', 'Nette\\Utils\\Strings' => $vendorDir . '/nette/utils/src/Utils/Strings.php',
'Nette\\Utils\\Type' => $vendorDir . '/nette/utils/src/Utils/Type.php', 'Nette\\Utils\\Type' => $vendorDir . '/nette/utils/src/Utils/Type.php',
@@ -2863,6 +2871,10 @@ return array(
'NunoMaduro\\Collision\\Provider' => $vendorDir . '/nunomaduro/collision/src/Provider.php', 'NunoMaduro\\Collision\\Provider' => $vendorDir . '/nunomaduro/collision/src/Provider.php',
'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => $vendorDir . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php', 'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => $vendorDir . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php',
'NunoMaduro\\Collision\\Writer' => $vendorDir . '/nunomaduro/collision/src/Writer.php', 'NunoMaduro\\Collision\\Writer' => $vendorDir . '/nunomaduro/collision/src/Writer.php',
'Ossycodes\\FriendlyCaptcha\\Facades\\FriendlyCaptcha' => $vendorDir . '/ossycodes/friendlycaptcha/src/Facades/FriendlyCaptcha.php',
'Ossycodes\\FriendlyCaptcha\\FriendlyCaptcha' => $vendorDir . '/ossycodes/friendlycaptcha/src/FriendlyCaptcha.php',
'Ossycodes\\FriendlyCaptcha\\FriendlyCaptchaServiceProvider' => $vendorDir . '/ossycodes/friendlycaptcha/src/FriendlyCaptchaServiceProvider.php',
'Ossycodes\\FriendlyCaptcha\\Rules\\FriendlyCaptcha' => $vendorDir . '/ossycodes/friendlycaptcha/src/Rules/FriendlyCaptcha.php',
'PHPUnit\\Exception' => $vendorDir . '/phpunit/phpunit/src/Exception.php', 'PHPUnit\\Exception' => $vendorDir . '/phpunit/phpunit/src/Exception.php',
'PHPUnit\\Framework\\ActualValueIsNotAnObjectException' => $vendorDir . '/phpunit/phpunit/src/Framework/Exception/ActualValueIsNotAnObjectException.php', 'PHPUnit\\Framework\\ActualValueIsNotAnObjectException' => $vendorDir . '/phpunit/phpunit/src/Framework/Exception/ActualValueIsNotAnObjectException.php',
'PHPUnit\\Framework\\Assert' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert.php', 'PHPUnit\\Framework\\Assert' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert.php',
@@ -5149,6 +5161,7 @@ return array(
'Termwind\\ValueObjects\\Style' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Style.php', 'Termwind\\ValueObjects\\Style' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Style.php',
'Termwind\\ValueObjects\\Styles' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Styles.php', 'Termwind\\ValueObjects\\Styles' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Styles.php',
'Tests\\CreatesApplication' => $baseDir . '/tests/CreatesApplication.php', 'Tests\\CreatesApplication' => $baseDir . '/tests/CreatesApplication.php',
'Tests\\Feature\\ContactPageTest' => $baseDir . '/tests/Feature/ContactPageTest.php',
'Tests\\Feature\\ExampleTest' => $baseDir . '/tests/Feature/ExampleTest.php', 'Tests\\Feature\\ExampleTest' => $baseDir . '/tests/Feature/ExampleTest.php',
'Tests\\TestCase' => $baseDir . '/tests/TestCase.php', 'Tests\\TestCase' => $baseDir . '/tests/TestCase.php',
'Tests\\Unit\\ExampleTest' => $baseDir . '/tests/Unit/ExampleTest.php', 'Tests\\Unit\\ExampleTest' => $baseDir . '/tests/Unit/ExampleTest.php',

View File

@@ -8,25 +8,25 @@ $baseDir = dirname($vendorDir);
return array( return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'3bd81c9b8fcc150b69d8b63b4d2ccf23' => $vendorDir . '/spatie/flare-client-php/src/helpers.php', '3bd81c9b8fcc150b69d8b63b4d2ccf23' => $vendorDir . '/spatie/flare-client-php/src/helpers.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php', '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php', '09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php', '35a6ad97d21e794e7e22a17d806652e4' => $vendorDir . '/nunomaduro/termwind/src/Functions.php',
'801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php', '801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php', 'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php', '265b4faa2b3a9766332744949e83bf97' => $vendorDir . '/laravel/framework/src/Illuminate/Collections/helpers.php',
'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php', 'c7a3c339e7e14b60e06a2d7fcce9476b' => $vendorDir . '/laravel/framework/src/Illuminate/Events/functions.php',
'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php', 'f0906e6318348a765ffb6eb24e0d0938' => $vendorDir . '/laravel/framework/src/Illuminate/Foundation/helpers.php',

View File

@@ -54,7 +54,9 @@ return array(
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'), 'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'Ossycodes\\FriendlyCaptcha\\' => array($vendorDir . '/ossycodes/friendlycaptcha/src'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'), 'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), 'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'Model\\' => array($baseDir . '/app/Models'), 'Model\\' => array($baseDir . '/app/Models'),
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'), 'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),

View File

@@ -9,25 +9,25 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
public static $files = array ( public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'3bd81c9b8fcc150b69d8b63b4d2ccf23' => __DIR__ . '/..' . '/spatie/flare-client-php/src/helpers.php', '3bd81c9b8fcc150b69d8b63b4d2ccf23' => __DIR__ . '/..' . '/spatie/flare-client-php/src/helpers.php',
'23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php', '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', 'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'09f6b20656683369174dd6fa83b7e5fb' => __DIR__ . '/..' . '/symfony/polyfill-uuid/bootstrap.php', '09f6b20656683369174dd6fa83b7e5fb' => __DIR__ . '/..' . '/symfony/polyfill-uuid/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'35a6ad97d21e794e7e22a17d806652e4' => __DIR__ . '/..' . '/nunomaduro/termwind/src/Functions.php', '35a6ad97d21e794e7e22a17d806652e4' => __DIR__ . '/..' . '/nunomaduro/termwind/src/Functions.php',
'801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php', '801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php',
'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php', 'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'265b4faa2b3a9766332744949e83bf97' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Collections/helpers.php', '265b4faa2b3a9766332744949e83bf97' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Collections/helpers.php',
'c7a3c339e7e14b60e06a2d7fcce9476b' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Events/functions.php', 'c7a3c339e7e14b60e06a2d7fcce9476b' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Events/functions.php',
'f0906e6318348a765ffb6eb24e0d0938' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Foundation/helpers.php', 'f0906e6318348a765ffb6eb24e0d0938' => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Foundation/helpers.php',
@@ -38,22 +38,22 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
); );
public static $prefixLengthsPsr4 = array ( public static $prefixLengthsPsr4 = array (
'v' => 'v' =>
array ( array (
'voku\\' => 5, 'voku\\' => 5,
), ),
'W' => 'W' =>
array ( array (
'Whoops\\' => 7, 'Whoops\\' => 7,
'Webmozart\\Assert\\' => 17, 'Webmozart\\Assert\\' => 17,
), ),
'T' => 'T' =>
array ( array (
'TijsVerkoyen\\CssToInlineStyles\\' => 31, 'TijsVerkoyen\\CssToInlineStyles\\' => 31,
'Tests\\' => 6, 'Tests\\' => 6,
'Termwind\\' => 9, 'Termwind\\' => 9,
), ),
'S' => 'S' =>
array ( array (
'Symfony\\Polyfill\\Uuid\\' => 22, 'Symfony\\Polyfill\\Uuid\\' => 22,
'Symfony\\Polyfill\\Php81\\' => 23, 'Symfony\\Polyfill\\Php81\\' => 23,
@@ -87,12 +87,12 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'Spatie\\FlareClient\\' => 19, 'Spatie\\FlareClient\\' => 19,
'Spatie\\Backtrace\\' => 17, 'Spatie\\Backtrace\\' => 17,
), ),
'R' => 'R' =>
array ( array (
'Ramsey\\Uuid\\' => 12, 'Ramsey\\Uuid\\' => 12,
'Ramsey\\Collection\\' => 18, 'Ramsey\\Collection\\' => 18,
), ),
'P' => 'P' =>
array ( array (
'Psy\\' => 4, 'Psy\\' => 4,
'Psr\\SimpleCache\\' => 16, 'Psr\\SimpleCache\\' => 16,
@@ -104,16 +104,21 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'PhpParser\\' => 10, 'PhpParser\\' => 10,
'PhpOption\\' => 10, 'PhpOption\\' => 10,
), ),
'N' => 'O' =>
array (
'Ossycodes\\FriendlyCaptcha\\' => 26,
),
'N' =>
array ( array (
'NunoMaduro\\Collision\\' => 21, 'NunoMaduro\\Collision\\' => 21,
'Nette\\' => 6,
), ),
'M' => 'M' =>
array ( array (
'Monolog\\' => 8, 'Monolog\\' => 8,
'Model\\' => 6, 'Model\\' => 6,
), ),
'L' => 'L' =>
array ( array (
'League\\MimeTypeDetection\\' => 25, 'League\\MimeTypeDetection\\' => 25,
'League\\Flysystem\\' => 17, 'League\\Flysystem\\' => 17,
@@ -124,32 +129,32 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'Laravel\\Sanctum\\' => 16, 'Laravel\\Sanctum\\' => 16,
'Laravel\\Sail\\' => 13, 'Laravel\\Sail\\' => 13,
), ),
'I' => 'I' =>
array ( array (
'Illuminate\\Support\\' => 19, 'Illuminate\\Support\\' => 19,
'Illuminate\\' => 11, 'Illuminate\\' => 11,
), ),
'H' => 'H' =>
array ( array (
'Helpers\\' => 8, 'Helpers\\' => 8,
), ),
'G' => 'G' =>
array ( array (
'GuzzleHttp\\Psr7\\' => 16, 'GuzzleHttp\\Psr7\\' => 16,
'GuzzleHttp\\Promise\\' => 19, 'GuzzleHttp\\Promise\\' => 19,
'GuzzleHttp\\' => 11, 'GuzzleHttp\\' => 11,
'GrahamCampbell\\ResultType\\' => 26, 'GrahamCampbell\\ResultType\\' => 26,
), ),
'F' => 'F' =>
array ( array (
'Fruitcake\\Cors\\' => 15, 'Fruitcake\\Cors\\' => 15,
'Faker\\' => 6, 'Faker\\' => 6,
), ),
'E' => 'E' =>
array ( array (
'Egulias\\EmailValidator\\' => 23, 'Egulias\\EmailValidator\\' => 23,
), ),
'D' => 'D' =>
array ( array (
'Dotenv\\' => 7, 'Dotenv\\' => 7,
'Doctrine\\Instantiator\\' => 22, 'Doctrine\\Instantiator\\' => 22,
@@ -161,357 +166,366 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'Database\\Seeders\\' => 17, 'Database\\Seeders\\' => 17,
'Database\\Factories\\' => 19, 'Database\\Factories\\' => 19,
), ),
'C' => 'C' =>
array ( array (
'Cron\\' => 5, 'Cron\\' => 5,
'Collective\\Html\\' => 16, 'Collective\\Html\\' => 16,
'Carbon\\' => 7, 'Carbon\\' => 7,
), ),
'B' => 'B' =>
array ( array (
'Brick\\Math\\' => 11, 'Brick\\Math\\' => 11,
), ),
'A' => 'A' =>
array ( array (
'App\\' => 4, 'App\\' => 4,
), ),
); );
public static $prefixDirsPsr4 = array ( public static $prefixDirsPsr4 = array (
'voku\\' => 'voku\\' =>
array ( array (
0 => __DIR__ . '/..' . '/voku/portable-ascii/src/voku', 0 => __DIR__ . '/..' . '/voku/portable-ascii/src/voku',
), ),
'Whoops\\' => 'Whoops\\' =>
array ( array (
0 => __DIR__ . '/..' . '/filp/whoops/src/Whoops', 0 => __DIR__ . '/..' . '/filp/whoops/src/Whoops',
), ),
'Webmozart\\Assert\\' => 'Webmozart\\Assert\\' =>
array ( array (
0 => __DIR__ . '/..' . '/webmozart/assert/src', 0 => __DIR__ . '/..' . '/webmozart/assert/src',
), ),
'TijsVerkoyen\\CssToInlineStyles\\' => 'TijsVerkoyen\\CssToInlineStyles\\' =>
array ( array (
0 => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src', 0 => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src',
), ),
'Tests\\' => 'Tests\\' =>
array ( array (
0 => __DIR__ . '/../..' . '/tests', 0 => __DIR__ . '/../..' . '/tests',
), ),
'Termwind\\' => 'Termwind\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nunomaduro/termwind/src', 0 => __DIR__ . '/..' . '/nunomaduro/termwind/src',
), ),
'Symfony\\Polyfill\\Uuid\\' => 'Symfony\\Polyfill\\Uuid\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-uuid', 0 => __DIR__ . '/..' . '/symfony/polyfill-uuid',
), ),
'Symfony\\Polyfill\\Php81\\' => 'Symfony\\Polyfill\\Php81\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php81', 0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
), ),
'Symfony\\Polyfill\\Php80\\' => 'Symfony\\Polyfill\\Php80\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80', 0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
), ),
'Symfony\\Polyfill\\Php72\\' => 'Symfony\\Polyfill\\Php72\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php72', 0 => __DIR__ . '/..' . '/symfony/polyfill-php72',
), ),
'Symfony\\Polyfill\\Mbstring\\' => 'Symfony\\Polyfill\\Mbstring\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
), ),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 'Symfony\\Polyfill\\Intl\\Normalizer\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer', 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
), ),
'Symfony\\Polyfill\\Intl\\Idn\\' => 'Symfony\\Polyfill\\Intl\\Idn\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
), ),
'Symfony\\Polyfill\\Intl\\Grapheme\\' => 'Symfony\\Polyfill\\Intl\\Grapheme\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme', 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme',
), ),
'Symfony\\Polyfill\\Ctype\\' => 'Symfony\\Polyfill\\Ctype\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
), ),
'Symfony\\Contracts\\Translation\\' => 'Symfony\\Contracts\\Translation\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/translation-contracts', 0 => __DIR__ . '/..' . '/symfony/translation-contracts',
), ),
'Symfony\\Contracts\\Service\\' => 'Symfony\\Contracts\\Service\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/service-contracts', 0 => __DIR__ . '/..' . '/symfony/service-contracts',
), ),
'Symfony\\Contracts\\EventDispatcher\\' => 'Symfony\\Contracts\\EventDispatcher\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts', 0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts',
), ),
'Symfony\\Component\\VarDumper\\' => 'Symfony\\Component\\VarDumper\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/var-dumper', 0 => __DIR__ . '/..' . '/symfony/var-dumper',
), ),
'Symfony\\Component\\Uid\\' => 'Symfony\\Component\\Uid\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/uid', 0 => __DIR__ . '/..' . '/symfony/uid',
), ),
'Symfony\\Component\\Translation\\' => 'Symfony\\Component\\Translation\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/translation', 0 => __DIR__ . '/..' . '/symfony/translation',
), ),
'Symfony\\Component\\String\\' => 'Symfony\\Component\\String\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/string', 0 => __DIR__ . '/..' . '/symfony/string',
), ),
'Symfony\\Component\\Routing\\' => 'Symfony\\Component\\Routing\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/routing', 0 => __DIR__ . '/..' . '/symfony/routing',
), ),
'Symfony\\Component\\Process\\' => 'Symfony\\Component\\Process\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/process', 0 => __DIR__ . '/..' . '/symfony/process',
), ),
'Symfony\\Component\\Mime\\' => 'Symfony\\Component\\Mime\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/mime', 0 => __DIR__ . '/..' . '/symfony/mime',
), ),
'Symfony\\Component\\Mailer\\' => 'Symfony\\Component\\Mailer\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/mailer', 0 => __DIR__ . '/..' . '/symfony/mailer',
), ),
'Symfony\\Component\\HttpKernel\\' => 'Symfony\\Component\\HttpKernel\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/http-kernel', 0 => __DIR__ . '/..' . '/symfony/http-kernel',
), ),
'Symfony\\Component\\HttpFoundation\\' => 'Symfony\\Component\\HttpFoundation\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/http-foundation', 0 => __DIR__ . '/..' . '/symfony/http-foundation',
), ),
'Symfony\\Component\\Finder\\' => 'Symfony\\Component\\Finder\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/finder', 0 => __DIR__ . '/..' . '/symfony/finder',
), ),
'Symfony\\Component\\EventDispatcher\\' => 'Symfony\\Component\\EventDispatcher\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/event-dispatcher', 0 => __DIR__ . '/..' . '/symfony/event-dispatcher',
), ),
'Symfony\\Component\\ErrorHandler\\' => 'Symfony\\Component\\ErrorHandler\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/error-handler', 0 => __DIR__ . '/..' . '/symfony/error-handler',
), ),
'Symfony\\Component\\CssSelector\\' => 'Symfony\\Component\\CssSelector\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/css-selector', 0 => __DIR__ . '/..' . '/symfony/css-selector',
), ),
'Symfony\\Component\\Console\\' => 'Symfony\\Component\\Console\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/console', 0 => __DIR__ . '/..' . '/symfony/console',
), ),
'Spatie\\LaravelIgnition\\' => 'Spatie\\LaravelIgnition\\' =>
array ( array (
0 => __DIR__ . '/..' . '/spatie/laravel-ignition/src', 0 => __DIR__ . '/..' . '/spatie/laravel-ignition/src',
), ),
'Spatie\\Ignition\\' => 'Spatie\\Ignition\\' =>
array ( array (
0 => __DIR__ . '/..' . '/spatie/ignition/src', 0 => __DIR__ . '/..' . '/spatie/ignition/src',
), ),
'Spatie\\FlareClient\\' => 'Spatie\\FlareClient\\' =>
array ( array (
0 => __DIR__ . '/..' . '/spatie/flare-client-php/src', 0 => __DIR__ . '/..' . '/spatie/flare-client-php/src',
), ),
'Spatie\\Backtrace\\' => 'Spatie\\Backtrace\\' =>
array ( array (
0 => __DIR__ . '/..' . '/spatie/backtrace/src', 0 => __DIR__ . '/..' . '/spatie/backtrace/src',
), ),
'Ramsey\\Uuid\\' => 'Ramsey\\Uuid\\' =>
array ( array (
0 => __DIR__ . '/..' . '/ramsey/uuid/src', 0 => __DIR__ . '/..' . '/ramsey/uuid/src',
), ),
'Ramsey\\Collection\\' => 'Ramsey\\Collection\\' =>
array ( array (
0 => __DIR__ . '/..' . '/ramsey/collection/src', 0 => __DIR__ . '/..' . '/ramsey/collection/src',
), ),
'Psy\\' => 'Psy\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psy/psysh/src', 0 => __DIR__ . '/..' . '/psy/psysh/src',
), ),
'Psr\\SimpleCache\\' => 'Psr\\SimpleCache\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/simple-cache/src', 0 => __DIR__ . '/..' . '/psr/simple-cache/src',
), ),
'Psr\\Log\\' => 'Psr\\Log\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/log/src', 0 => __DIR__ . '/..' . '/psr/log/src',
), ),
'Psr\\Http\\Message\\' => 'Psr\\Http\\Message\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/http-factory/src', 0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src', 1 => __DIR__ . '/..' . '/psr/http-message/src',
), ),
'Psr\\Http\\Client\\' => 'Psr\\Http\\Client\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/http-client/src', 0 => __DIR__ . '/..' . '/psr/http-client/src',
), ),
'Psr\\EventDispatcher\\' => 'Psr\\EventDispatcher\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src', 0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
), ),
'Psr\\Container\\' => 'Psr\\Container\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/container/src', 0 => __DIR__ . '/..' . '/psr/container/src',
), ),
'PhpParser\\' => 'PhpParser\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser', 0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser',
), ),
'PhpOption\\' => 'PhpOption\\' =>
array ( array (
0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption', 0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption',
), ),
'NunoMaduro\\Collision\\' => 'Ossycodes\\FriendlyCaptcha\\' =>
array (
0 => __DIR__ . '/..' . '/ossycodes/friendlycaptcha/src',
),
'NunoMaduro\\Collision\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nunomaduro/collision/src', 0 => __DIR__ . '/..' . '/nunomaduro/collision/src',
), ),
'Monolog\\' => 'Nette\\' =>
array (
0 => __DIR__ . '/..' . '/nette/schema/src',
1 => __DIR__ . '/..' . '/nette/utils/src',
),
'Monolog\\' =>
array ( array (
0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog', 0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
), ),
'Model\\' => 'Model\\' =>
array ( array (
0 => __DIR__ . '/../..' . '/app/Models', 0 => __DIR__ . '/../..' . '/app/Models',
), ),
'League\\MimeTypeDetection\\' => 'League\\MimeTypeDetection\\' =>
array ( array (
0 => __DIR__ . '/..' . '/league/mime-type-detection/src', 0 => __DIR__ . '/..' . '/league/mime-type-detection/src',
), ),
'League\\Flysystem\\' => 'League\\Flysystem\\' =>
array ( array (
0 => __DIR__ . '/..' . '/league/flysystem/src', 0 => __DIR__ . '/..' . '/league/flysystem/src',
), ),
'League\\Config\\' => 'League\\Config\\' =>
array ( array (
0 => __DIR__ . '/..' . '/league/config/src', 0 => __DIR__ . '/..' . '/league/config/src',
), ),
'League\\CommonMark\\' => 'League\\CommonMark\\' =>
array ( array (
0 => __DIR__ . '/..' . '/league/commonmark/src', 0 => __DIR__ . '/..' . '/league/commonmark/src',
), ),
'Laravel\\Tinker\\' => 'Laravel\\Tinker\\' =>
array ( array (
0 => __DIR__ . '/..' . '/laravel/tinker/src', 0 => __DIR__ . '/..' . '/laravel/tinker/src',
), ),
'Laravel\\SerializableClosure\\' => 'Laravel\\SerializableClosure\\' =>
array ( array (
0 => __DIR__ . '/..' . '/laravel/serializable-closure/src', 0 => __DIR__ . '/..' . '/laravel/serializable-closure/src',
), ),
'Laravel\\Sanctum\\' => 'Laravel\\Sanctum\\' =>
array ( array (
0 => __DIR__ . '/..' . '/laravel/sanctum/src', 0 => __DIR__ . '/..' . '/laravel/sanctum/src',
), ),
'Laravel\\Sail\\' => 'Laravel\\Sail\\' =>
array ( array (
0 => __DIR__ . '/..' . '/laravel/sail/src', 0 => __DIR__ . '/..' . '/laravel/sail/src',
), ),
'Illuminate\\Support\\' => 'Illuminate\\Support\\' =>
array ( array (
0 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Macroable', 0 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Macroable',
1 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Collections', 1 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Collections',
2 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Conditionable', 2 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate/Conditionable',
), ),
'Illuminate\\' => 'Illuminate\\' =>
array ( array (
0 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate', 0 => __DIR__ . '/..' . '/laravel/framework/src/Illuminate',
), ),
'Helpers\\' => 'Helpers\\' =>
array ( array (
0 => __DIR__ . '/../..' . '/app/Helpers', 0 => __DIR__ . '/../..' . '/app/Helpers',
), ),
'GuzzleHttp\\Psr7\\' => 'GuzzleHttp\\Psr7\\' =>
array ( array (
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
), ),
'GuzzleHttp\\Promise\\' => 'GuzzleHttp\\Promise\\' =>
array ( array (
0 => __DIR__ . '/..' . '/guzzlehttp/promises/src', 0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
), ),
'GuzzleHttp\\' => 'GuzzleHttp\\' =>
array ( array (
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src', 0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
), ),
'GrahamCampbell\\ResultType\\' => 'GrahamCampbell\\ResultType\\' =>
array ( array (
0 => __DIR__ . '/..' . '/graham-campbell/result-type/src', 0 => __DIR__ . '/..' . '/graham-campbell/result-type/src',
), ),
'Fruitcake\\Cors\\' => 'Fruitcake\\Cors\\' =>
array ( array (
0 => __DIR__ . '/..' . '/fruitcake/php-cors/src', 0 => __DIR__ . '/..' . '/fruitcake/php-cors/src',
), ),
'Faker\\' => 'Faker\\' =>
array ( array (
0 => __DIR__ . '/..' . '/fakerphp/faker/src/Faker', 0 => __DIR__ . '/..' . '/fakerphp/faker/src/Faker',
), ),
'Egulias\\EmailValidator\\' => 'Egulias\\EmailValidator\\' =>
array ( array (
0 => __DIR__ . '/..' . '/egulias/email-validator/src', 0 => __DIR__ . '/..' . '/egulias/email-validator/src',
), ),
'Dotenv\\' => 'Dotenv\\' =>
array ( array (
0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src', 0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src',
), ),
'Doctrine\\Instantiator\\' => 'Doctrine\\Instantiator\\' =>
array ( array (
0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator', 0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator',
), ),
'Doctrine\\Inflector\\' => 'Doctrine\\Inflector\\' =>
array ( array (
0 => __DIR__ . '/..' . '/doctrine/inflector/lib/Doctrine/Inflector', 0 => __DIR__ . '/..' . '/doctrine/inflector/lib/Doctrine/Inflector',
), ),
'Doctrine\\Deprecations\\' => 'Doctrine\\Deprecations\\' =>
array ( array (
0 => __DIR__ . '/..' . '/doctrine/deprecations/lib/Doctrine/Deprecations', 0 => __DIR__ . '/..' . '/doctrine/deprecations/lib/Doctrine/Deprecations',
), ),
'Doctrine\\Common\\Lexer\\' => 'Doctrine\\Common\\Lexer\\' =>
array ( array (
0 => __DIR__ . '/..' . '/doctrine/lexer/src', 0 => __DIR__ . '/..' . '/doctrine/lexer/src',
), ),
'Dflydev\\DotAccessData\\' => 'Dflydev\\DotAccessData\\' =>
array ( array (
0 => __DIR__ . '/..' . '/dflydev/dot-access-data/src', 0 => __DIR__ . '/..' . '/dflydev/dot-access-data/src',
), ),
'DeepCopy\\' => 'DeepCopy\\' =>
array ( array (
0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy', 0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy',
), ),
'Database\\Seeders\\' => 'Database\\Seeders\\' =>
array ( array (
0 => __DIR__ . '/../..' . '/database/seeders', 0 => __DIR__ . '/../..' . '/database/seeders',
1 => __DIR__ . '/..' . '/laravel/pint/database/seeders', 1 => __DIR__ . '/..' . '/laravel/pint/database/seeders',
), ),
'Database\\Factories\\' => 'Database\\Factories\\' =>
array ( array (
0 => __DIR__ . '/../..' . '/database/factories', 0 => __DIR__ . '/../..' . '/database/factories',
1 => __DIR__ . '/..' . '/laravel/pint/database/factories', 1 => __DIR__ . '/..' . '/laravel/pint/database/factories',
), ),
'Cron\\' => 'Cron\\' =>
array ( array (
0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron', 0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron',
), ),
'Collective\\Html\\' => 'Collective\\Html\\' =>
array ( array (
0 => __DIR__ . '/..' . '/laravelcollective/html/src', 0 => __DIR__ . '/..' . '/laravelcollective/html/src',
), ),
'Carbon\\' => 'Carbon\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon', 0 => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon',
), ),
'Brick\\Math\\' => 'Brick\\Math\\' =>
array ( array (
0 => __DIR__ . '/..' . '/brick/math/src', 0 => __DIR__ . '/..' . '/brick/math/src',
), ),
'App\\' => 'App\\' =>
array ( array (
0 => __DIR__ . '/../..' . '/app', 0 => __DIR__ . '/../..' . '/app',
1 => __DIR__ . '/..' . '/laravel/pint/app', 1 => __DIR__ . '/..' . '/laravel/pint/app',
@@ -519,9 +533,9 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
); );
public static $prefixesPsr0 = array ( public static $prefixesPsr0 = array (
'M' => 'M' =>
array ( array (
'Mockery' => 'Mockery' =>
array ( array (
0 => __DIR__ . '/..' . '/mockery/mockery/library', 0 => __DIR__ . '/..' . '/mockery/mockery/library',
), ),
@@ -537,6 +551,7 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'App\\Http\\Controllers\\Auth\\RegisterController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php', 'App\\Http\\Controllers\\Auth\\RegisterController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php',
'App\\Http\\Controllers\\Auth\\ResetPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ResetPasswordController.php', 'App\\Http\\Controllers\\Auth\\ResetPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ResetPasswordController.php',
'App\\Http\\Controllers\\CalendarController' => __DIR__ . '/../..' . '/app/Http/Controllers/CalendarController.php', 'App\\Http\\Controllers\\CalendarController' => __DIR__ . '/../..' . '/app/Http/Controllers/CalendarController.php',
'App\\Http\\Controllers\\ContactController' => __DIR__ . '/../..' . '/app/Http/Controllers/ContactController.php',
'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php',
'App\\Http\\Controllers\\HomeController' => __DIR__ . '/../..' . '/app/Http/Controllers/HomeController.php', 'App\\Http\\Controllers\\HomeController' => __DIR__ . '/../..' . '/app/Http/Controllers/HomeController.php',
'App\\Http\\Controllers\\ImagesController' => __DIR__ . '/../..' . '/app/Http/Controllers/ImagesController.php', 'App\\Http\\Controllers\\ImagesController' => __DIR__ . '/../..' . '/app/Http/Controllers/ImagesController.php',
@@ -552,6 +567,7 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'App\\Http\\Middleware\\RedirectIfAuthenticated' => __DIR__ . '/../..' . '/app/Http/Middleware/RedirectIfAuthenticated.php', 'App\\Http\\Middleware\\RedirectIfAuthenticated' => __DIR__ . '/../..' . '/app/Http/Middleware/RedirectIfAuthenticated.php',
'App\\Http\\Middleware\\TrimStrings' => __DIR__ . '/../..' . '/app/Http/Middleware/TrimStrings.php', 'App\\Http\\Middleware\\TrimStrings' => __DIR__ . '/../..' . '/app/Http/Middleware/TrimStrings.php',
'App\\Http\\Middleware\\VerifyCsrfToken' => __DIR__ . '/../..' . '/app/Http/Middleware/VerifyCsrfToken.php', 'App\\Http\\Middleware\\VerifyCsrfToken' => __DIR__ . '/../..' . '/app/Http/Middleware/VerifyCsrfToken.php',
'App\\Mail\\ContactFormSubmitted' => __DIR__ . '/../..' . '/app/Mail/ContactFormSubmitted.php',
'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php',
'App\\Providers\\AuthServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AuthServiceProvider.php', 'App\\Providers\\AuthServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AuthServiceProvider.php',
'App\\Providers\\BroadcastServiceProvider' => __DIR__ . '/../..' . '/app/Providers/BroadcastServiceProvider.php', 'App\\Providers\\BroadcastServiceProvider' => __DIR__ . '/../..' . '/app/Providers/BroadcastServiceProvider.php',
@@ -3325,6 +3341,7 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'Nette\\Schema\\Processor' => __DIR__ . '/..' . '/nette/schema/src/Schema/Processor.php', 'Nette\\Schema\\Processor' => __DIR__ . '/..' . '/nette/schema/src/Schema/Processor.php',
'Nette\\Schema\\Schema' => __DIR__ . '/..' . '/nette/schema/src/Schema/Schema.php', 'Nette\\Schema\\Schema' => __DIR__ . '/..' . '/nette/schema/src/Schema/Schema.php',
'Nette\\Schema\\ValidationException' => __DIR__ . '/..' . '/nette/schema/src/Schema/ValidationException.php', 'Nette\\Schema\\ValidationException' => __DIR__ . '/..' . '/nette/schema/src/Schema/ValidationException.php',
'Nette\\ShouldNotHappenException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\SmartObject' => __DIR__ . '/..' . '/nette/utils/src/SmartObject.php', 'Nette\\SmartObject' => __DIR__ . '/..' . '/nette/utils/src/SmartObject.php',
'Nette\\StaticClass' => __DIR__ . '/..' . '/nette/utils/src/StaticClass.php', 'Nette\\StaticClass' => __DIR__ . '/..' . '/nette/utils/src/StaticClass.php',
'Nette\\UnexpectedValueException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php', 'Nette\\UnexpectedValueException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
@@ -3334,20 +3351,25 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'Nette\\Utils\\AssertionException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\AssertionException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Callback' => __DIR__ . '/..' . '/nette/utils/src/Utils/Callback.php', 'Nette\\Utils\\Callback' => __DIR__ . '/..' . '/nette/utils/src/Utils/Callback.php',
'Nette\\Utils\\DateTime' => __DIR__ . '/..' . '/nette/utils/src/Utils/DateTime.php', 'Nette\\Utils\\DateTime' => __DIR__ . '/..' . '/nette/utils/src/Utils/DateTime.php',
'Nette\\Utils\\FileInfo' => __DIR__ . '/..' . '/nette/utils/src/Utils/FileInfo.php',
'Nette\\Utils\\FileSystem' => __DIR__ . '/..' . '/nette/utils/src/Utils/FileSystem.php', 'Nette\\Utils\\FileSystem' => __DIR__ . '/..' . '/nette/utils/src/Utils/FileSystem.php',
'Nette\\Utils\\Finder' => __DIR__ . '/..' . '/nette/utils/src/Utils/Finder.php',
'Nette\\Utils\\Floats' => __DIR__ . '/..' . '/nette/utils/src/Utils/Floats.php', 'Nette\\Utils\\Floats' => __DIR__ . '/..' . '/nette/utils/src/Utils/Floats.php',
'Nette\\Utils\\Helpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/Helpers.php', 'Nette\\Utils\\Helpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/Helpers.php',
'Nette\\Utils\\Html' => __DIR__ . '/..' . '/nette/utils/src/Utils/Html.php', 'Nette\\Utils\\Html' => __DIR__ . '/..' . '/nette/utils/src/Utils/Html.php',
'Nette\\Utils\\IHtmlString' => __DIR__ . '/..' . '/nette/utils/src/compatibility.php', 'Nette\\Utils\\IHtmlString' => __DIR__ . '/..' . '/nette/utils/src/compatibility.php',
'Nette\\Utils\\Image' => __DIR__ . '/..' . '/nette/utils/src/Utils/Image.php', 'Nette\\Utils\\Image' => __DIR__ . '/..' . '/nette/utils/src/Utils/Image.php',
'Nette\\Utils\\ImageColor' => __DIR__ . '/..' . '/nette/utils/src/Utils/ImageColor.php',
'Nette\\Utils\\ImageException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\ImageException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ImageType' => __DIR__ . '/..' . '/nette/utils/src/Utils/ImageType.php',
'Nette\\Utils\\Iterables' => __DIR__ . '/..' . '/nette/utils/src/Utils/Iterables.php',
'Nette\\Utils\\Json' => __DIR__ . '/..' . '/nette/utils/src/Utils/Json.php', 'Nette\\Utils\\Json' => __DIR__ . '/..' . '/nette/utils/src/Utils/Json.php',
'Nette\\Utils\\JsonException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\JsonException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/ObjectHelpers.php', 'Nette\\Utils\\ObjectHelpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\ObjectMixin' => __DIR__ . '/..' . '/nette/utils/src/Utils/ObjectMixin.php',
'Nette\\Utils\\Paginator' => __DIR__ . '/..' . '/nette/utils/src/Utils/Paginator.php', 'Nette\\Utils\\Paginator' => __DIR__ . '/..' . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Random' => __DIR__ . '/..' . '/nette/utils/src/Utils/Random.php', 'Nette\\Utils\\Random' => __DIR__ . '/..' . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => __DIR__ . '/..' . '/nette/utils/src/Utils/Reflection.php', 'Nette\\Utils\\Reflection' => __DIR__ . '/..' . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => __DIR__ . '/..' . '/nette/utils/src/Utils/ReflectionMethod.php',
'Nette\\Utils\\RegexpException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php', 'Nette\\Utils\\RegexpException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Strings' => __DIR__ . '/..' . '/nette/utils/src/Utils/Strings.php', 'Nette\\Utils\\Strings' => __DIR__ . '/..' . '/nette/utils/src/Utils/Strings.php',
'Nette\\Utils\\Type' => __DIR__ . '/..' . '/nette/utils/src/Utils/Type.php', 'Nette\\Utils\\Type' => __DIR__ . '/..' . '/nette/utils/src/Utils/Type.php',
@@ -3386,6 +3408,10 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'NunoMaduro\\Collision\\Provider' => __DIR__ . '/..' . '/nunomaduro/collision/src/Provider.php', 'NunoMaduro\\Collision\\Provider' => __DIR__ . '/..' . '/nunomaduro/collision/src/Provider.php',
'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => __DIR__ . '/..' . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php', 'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => __DIR__ . '/..' . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php',
'NunoMaduro\\Collision\\Writer' => __DIR__ . '/..' . '/nunomaduro/collision/src/Writer.php', 'NunoMaduro\\Collision\\Writer' => __DIR__ . '/..' . '/nunomaduro/collision/src/Writer.php',
'Ossycodes\\FriendlyCaptcha\\Facades\\FriendlyCaptcha' => __DIR__ . '/..' . '/ossycodes/friendlycaptcha/src/Facades/FriendlyCaptcha.php',
'Ossycodes\\FriendlyCaptcha\\FriendlyCaptcha' => __DIR__ . '/..' . '/ossycodes/friendlycaptcha/src/FriendlyCaptcha.php',
'Ossycodes\\FriendlyCaptcha\\FriendlyCaptchaServiceProvider' => __DIR__ . '/..' . '/ossycodes/friendlycaptcha/src/FriendlyCaptchaServiceProvider.php',
'Ossycodes\\FriendlyCaptcha\\Rules\\FriendlyCaptcha' => __DIR__ . '/..' . '/ossycodes/friendlycaptcha/src/Rules/FriendlyCaptcha.php',
'PHPUnit\\Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Exception.php', 'PHPUnit\\Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Exception.php',
'PHPUnit\\Framework\\ActualValueIsNotAnObjectException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Exception/ActualValueIsNotAnObjectException.php', 'PHPUnit\\Framework\\ActualValueIsNotAnObjectException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Exception/ActualValueIsNotAnObjectException.php',
'PHPUnit\\Framework\\Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert.php', 'PHPUnit\\Framework\\Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert.php',
@@ -5672,6 +5698,7 @@ class ComposerStaticInit5216a35d72a5119d2f4646cd700f802d
'Termwind\\ValueObjects\\Style' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Style.php', 'Termwind\\ValueObjects\\Style' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Style.php',
'Termwind\\ValueObjects\\Styles' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Styles.php', 'Termwind\\ValueObjects\\Styles' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Styles.php',
'Tests\\CreatesApplication' => __DIR__ . '/../..' . '/tests/CreatesApplication.php', 'Tests\\CreatesApplication' => __DIR__ . '/../..' . '/tests/CreatesApplication.php',
'Tests\\Feature\\ContactPageTest' => __DIR__ . '/../..' . '/tests/Feature/ContactPageTest.php',
'Tests\\Feature\\ExampleTest' => __DIR__ . '/../..' . '/tests/Feature/ExampleTest.php', 'Tests\\Feature\\ExampleTest' => __DIR__ . '/../..' . '/tests/Feature/ExampleTest.php',
'Tests\\TestCase' => __DIR__ . '/../..' . '/tests/TestCase.php', 'Tests\\TestCase' => __DIR__ . '/../..' . '/tests/TestCase.php',
'Tests\\Unit\\ExampleTest' => __DIR__ . '/../..' . '/tests/Unit/ExampleTest.php', 'Tests\\Unit\\ExampleTest' => __DIR__ . '/../..' . '/tests/Unit/ExampleTest.php',

View File

@@ -2538,37 +2538,40 @@
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
"version": "v1.2.3", "version": "v1.3.3",
"version_normalized": "1.2.3.0", "version_normalized": "1.3.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/schema.git", "url": "https://github.com/nette/schema.git",
"reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f" "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004",
"reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", "nette/utils": "^4.0",
"php": ">=7.1 <8.3" "php": "8.1 - 8.5"
}, },
"require-dev": { "require-dev": {
"nette/tester": "^2.3 || ^2.4", "nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^1.0", "phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.7" "tracy/tracy": "^2.8"
}, },
"time": "2022-10-13T01:24:26+00:00", "time": "2025-10-30T22:57:59+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.2-dev" "dev-master": "1.3-dev"
} }
}, },
"installation-source": "dist", "installation-source": "dist",
"autoload": { "autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [ "classmap": [
"src/" "src/"
] ]
@@ -2597,35 +2600,37 @@
], ],
"support": { "support": {
"issues": "https://github.com/nette/schema/issues", "issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.3" "source": "https://github.com/nette/schema/tree/v1.3.3"
}, },
"install-path": "../nette/schema" "install-path": "../nette/schema"
}, },
{ {
"name": "nette/utils", "name": "nette/utils",
"version": "v3.2.8", "version": "v4.1.1",
"version_normalized": "3.2.8.0", "version_normalized": "4.1.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/utils.git", "url": "https://github.com/nette/utils.git",
"reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368" "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/02a54c4c872b99e4ec05c4aec54b5a06eb0f6368", "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72",
"reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368", "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.2 <8.3" "php": "8.2 - 8.5"
}, },
"conflict": { "conflict": {
"nette/di": "<3.0.6" "nette/finder": "<3",
"nette/schema": "<1.2.2"
}, },
"require-dev": { "require-dev": {
"nette/tester": "~2.0", "jetbrains/phpstorm-attributes": "^1.2",
"phpstan/phpstan": "^1.0", "nette/tester": "^2.5",
"tracy/tracy": "^2.3" "phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
}, },
"suggest": { "suggest": {
"ext-gd": "to use Image", "ext-gd": "to use Image",
@@ -2633,18 +2638,20 @@
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json", "ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...", "ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
"ext-xml": "to use Strings::length() etc. when mbstring is not available"
}, },
"time": "2022-09-12T23:36:20+00:00", "time": "2025-12-22T12:14:32+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.2-dev" "dev-master": "4.1-dev"
} }
}, },
"installation-source": "dist", "installation-source": "dist",
"autoload": { "autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [ "classmap": [
"src/" "src/"
] ]
@@ -2685,7 +2692,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/nette/utils/issues", "issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v3.2.8" "source": "https://github.com/nette/utils/tree/v4.1.1"
}, },
"install-path": "../nette/utils" "install-path": "../nette/utils"
}, },
@@ -2928,6 +2935,72 @@
], ],
"install-path": "../nunomaduro/termwind" "install-path": "../nunomaduro/termwind"
}, },
{
"name": "ossycodes/friendlycaptcha",
"version": "v3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/ossycodes/friendlycaptcha.git",
"reference": "b18dfab44ee5fff7d75412232eb6f834864efde6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ossycodes/friendlycaptcha/zipball/b18dfab44ee5fff7d75412232eb6f834864efde6",
"reference": "b18dfab44ee5fff7d75412232eb6f834864efde6",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^7.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.4|^8.0|^8.1|^8.2|^8.3"
},
"require-dev": {
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0",
"phpunit/phpunit": "^8.0 || ^9.5 || ^10.5 || ^11.0 || ^12.0"
},
"time": "2025-05-08T13:30:49+00:00",
"type": "library",
"extra": {
"laravel": {
"aliases": {
"FriendlyCaptcha": "Ossycodes\\FriendlyCaptcha\\Facades\\FriendlyCaptcha"
},
"providers": [
"Ossycodes\\FriendlyCaptcha\\FriendlyCaptchaServiceProvider"
]
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Ossycodes\\FriendlyCaptcha\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "ossycodes",
"email": "osaigbovoemmanuel1@gmail.com",
"role": "Developer"
}
],
"description": "A simple package to help integrate FriendlyCaptcha in your Laravel applications.",
"homepage": "https://github.com/ossycodes/friendlycaptcha",
"keywords": [
"captcha",
"friendlycaptcha",
"laravel"
],
"support": {
"issues": "https://github.com/ossycodes/friendlycaptcha/issues",
"source": "https://github.com/ossycodes/friendlycaptcha/tree/v3.0.0"
},
"install-path": "../ossycodes/friendlycaptcha"
},
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",
"version": "2.0.3", "version": "2.0.3",

View File

@@ -1,9 +1,9 @@
<?php return array( <?php return array(
'root' => array( 'root' => array(
'name' => 'laravel/laravel', 'name' => 'laravel/laravel',
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-main',
'version' => '1.0.0.0', 'version' => 'dev-main',
'reference' => NULL, 'reference' => '6786ac7ce14c02cbed9252a569ceecc3d9f4b38d',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -374,9 +374,9 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'laravel/laravel' => array( 'laravel/laravel' => array(
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-main',
'version' => '1.0.0.0', 'version' => 'dev-main',
'reference' => NULL, 'reference' => '6786ac7ce14c02cbed9252a569ceecc3d9f4b38d',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -515,18 +515,18 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'nette/schema' => array( 'nette/schema' => array(
'pretty_version' => 'v1.2.3', 'pretty_version' => 'v1.3.3',
'version' => '1.2.3.0', 'version' => '1.3.3.0',
'reference' => 'abbdbb70e0245d5f3bf77874cea1dfb0c930d06f', 'reference' => '2befc2f42d7c715fd9d95efc31b1081e5d765004',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../nette/schema', 'install_path' => __DIR__ . '/../nette/schema',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'nette/utils' => array( 'nette/utils' => array(
'pretty_version' => 'v3.2.8', 'pretty_version' => 'v4.1.1',
'version' => '3.2.8.0', 'version' => '4.1.1.0',
'reference' => '02a54c4c872b99e4ec05c4aec54b5a06eb0f6368', 'reference' => 'c99059c0315591f1a0db7ad6002000288ab8dc72',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../nette/utils', 'install_path' => __DIR__ . '/../nette/utils',
'aliases' => array(), 'aliases' => array(),
@@ -559,6 +559,15 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ossycodes/friendlycaptcha' => array(
'pretty_version' => 'v3.0.0',
'version' => '3.0.0.0',
'reference' => 'b18dfab44ee5fff7d75412232eb6f834864efde6',
'type' => 'library',
'install_path' => __DIR__ . '/../ossycodes/friendlycaptcha',
'aliases' => array(),
'dev_requirement' => false,
),
'phar-io/manifest' => array( 'phar-io/manifest' => array(
'pretty_version' => '2.0.3', 'pretty_version' => '2.0.3',
'version' => '2.0.3.0', 'version' => '2.0.3.0',

View File

@@ -4,8 +4,8 @@
$issues = array(); $issues = array();
if (!(PHP_VERSION_ID >= 80002)) { if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.2". You are running ' . PHP_VERSION . '.'; $issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
} }
if ($issues) { if ($issues) {
@@ -19,8 +19,7 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
} }
} }
trigger_error( throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues), 'Composer detected issues in your platform: ' . implode(' ', $issues)
E_USER_ERROR
); );
} }

View File

@@ -15,16 +15,19 @@
} }
], ],
"require": { "require": {
"php": ">=7.1 <8.3", "php": "8.1 - 8.5",
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0" "nette/utils": "^4.0"
}, },
"require-dev": { "require-dev": {
"nette/tester": "^2.3 || ^2.4", "nette/tester": "^2.5.2",
"tracy/tracy": "^2.7", "tracy/tracy": "^2.8",
"phpstan/phpstan-nette": "^1.0" "phpstan/phpstan-nette": "^2.0@stable"
}, },
"autoload": { "autoload": {
"classmap": ["src/"] "classmap": ["src/"],
"psr-4": {
"Nette\\": "src"
}
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"scripts": { "scripts": {
@@ -33,7 +36,7 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.2-dev" "dev-master": "1.3-dev"
} }
} }
} }

View File

@@ -1,33 +0,0 @@
How to contribute & use the issue tracker
=========================================
Nette welcomes your contributions. There are several ways to help out:
* Create an issue on GitHub, if you have found a bug
* Write test cases for open bug issues
* Write fixes for open bug/feature issues, preferably with test cases included
* Contribute to the [documentation](https://nette.org/en/writing)
Issues
------
Please **do not use the issue tracker to ask questions**. We will be happy to help you
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report.
**Feature requests** are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature.
Contributing
------------
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
Please do not fix whitespace, format code, or make a purely cosmetic patch.
Thanks! :heart:

View File

@@ -1,5 +1,4 @@
Nette Schema # Nette Schema
************
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/schema.svg)](https://packagist.org/packages/nette/schema) [![Downloads this Month](https://img.shields.io/packagist/dm/nette/schema.svg)](https://packagist.org/packages/nette/schema)
[![Tests](https://github.com/nette/schema/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/schema/actions) [![Tests](https://github.com/nette/schema/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/schema/actions)
@@ -21,7 +20,7 @@ Installation:
composer require nette/schema composer require nette/schema
``` ```
It requires PHP version 7.1 and supports PHP up to 8.2. It requires PHP version 8.1 and supports PHP up to 8.5.
[Support Me](https://github.com/sponsors/dg) [Support Me](https://github.com/sponsors/dg)
@@ -39,7 +38,7 @@ Basic Usage
In variable `$schema` we have a validation schema (what exactly this means and how to create it we will say later) and in variable `$data` we have a data structure that we want to validate and normalize. This can be, for example, data sent by the user through an API, configuration file, etc. In variable `$schema` we have a validation schema (what exactly this means and how to create it we will say later) and in variable `$data` we have a data structure that we want to validate and normalize. This can be, for example, data sent by the user through an API, configuration file, etc.
The task is handled by the [Nette\Schema\Processor](https://api.nette.org/3.0/Nette/Schema/Processor.html) class, which processes the input and either returns normalized data or throws an [Nette\Schema\ValidationException](https://api.nette.org/3.0/Nette/Schema/ValidationException.html) exception on error. The task is handled by the [Nette\Schema\Processor](https://api.nette.org/schema/master/Nette/Schema/Processor.html) class, which processes the input and either returns normalized data or throws an [Nette\Schema\ValidationException](https://api.nette.org/schema/master/Nette/Schema/ValidationException.html) exception on error.
```php ```php
$processor = new Nette\Schema\Processor; $processor = new Nette\Schema\Processor;
@@ -51,20 +50,20 @@ try {
} }
``` ```
Method `$e->getMessages()` returns array of all message strings and `$e->getMessageObjects()` return all messages as [Nette\Schema\Message](https://api.nette.org/3.1/Nette/Schema/Message.html) objects. Method `$e->getMessages()` returns array of all message strings and `$e->getMessageObjects()` return all messages as [Nette\Schema\Message](https://api.nette.org/schema/master/Nette/Schema/Message.html) objects.
Defining Schema Defining Schema
--------------- ---------------
And now let's create a schema. The class [Nette\Schema\Expect](https://api.nette.org/3.0/Nette/Schema/Expect.html) is used to define it, we actually define expectations of what the data should look like. Let's say that the input data must be a structure (e.g. an array) containing elements `processRefund` of type bool and `refundAmount` of type int. And now let's create a schema. The class [Nette\Schema\Expect](https://api.nette.org/schema/master/Nette/Schema/Expect.html) is used to define it, we actually define expectations of what the data should look like. Let's say that the input data must be a structure (e.g. an array) containing elements `processRefund` of type bool and `refundAmount` of type int.
```php ```php
use Nette\Schema\Expect; use Nette\Schema\Expect;
$schema = Expect::structure([ $schema = Expect::structure([
'processRefund' => Expect::bool(), 'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(), 'refundAmount' => Expect::int(),
]); ]);
``` ```
@@ -74,8 +73,8 @@ Lets send the following data for validation:
```php ```php
$data = [ $data = [
'processRefund' => true, 'processRefund' => true,
'refundAmount' => 17, 'refundAmount' => 17,
]; ];
$normalized = $processor->process($schema, $data); // OK, it passes $normalized = $processor->process($schema, $data); // OK, it passes
@@ -87,7 +86,7 @@ All elements of the structure are optional and have a default value `null`. Exam
```php ```php
$data = [ $data = [
'refundAmount' => 17, 'refundAmount' => 17,
]; ];
$normalized = $processor->process($schema, $data); // OK, it passes $normalized = $processor->process($schema, $data); // OK, it passes
@@ -102,8 +101,8 @@ And what if we wanted to accept `1` and `0` besides booleans? Then we list the a
```php ```php
$schema = Expect::structure([ $schema = Expect::structure([
'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'), 'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'),
'refundAmount' => Expect::int(), 'refundAmount' => Expect::int(),
]); ]);
$normalized = $processor->process($schema, $data); $normalized = $processor->process($schema, $data);
@@ -113,7 +112,6 @@ is_bool($normalized->processRefund); // true
Now you know the basics of how the schema is defined and how the individual elements of the structure behave. We will now show what all the other elements can be used in defining a schema. Now you know the basics of how the schema is defined and how the individual elements of the structure behave. We will now show what all the other elements can be used in defining a schema.
Data Types: type() Data Types: type()
------------------ ------------------
@@ -152,6 +150,15 @@ $processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERROR: 123 is not a string $processor->process($schema, ['key' => 123]); // ERROR: 123 is not a string
``` ```
The second parameter can be used to specify keys (since version 1.2):
```php
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERROR: 'a' is not int
```
The list is an indexed array: The list is an indexed array:
```php ```php
@@ -169,7 +176,7 @@ The parameter can also be a schema, so we can write:
Expect::arrayOf(Expect::bool()) Expect::arrayOf(Expect::bool())
``` ```
The default value is an empty array. If you specify default value, it will be merged with the passed data. This can be disabled using `mergeDefaults(false)`. The default value is an empty array. If you specify a default value, it will be merged with the passed data. This can be disabled using `mergeDefaults(false)`.
Enumeration: anyOf() Enumeration: anyOf()
@@ -179,7 +186,7 @@ Enumeration: anyOf()
```php ```php
$schema = Expect::listOf( $schema = Expect::listOf(
Expect::anyOf('a', true, null) Expect::anyOf('a', true, null),
); );
$processor->process($schema, ['a', true, null, 'a']); // OK $processor->process($schema, ['a', true, null, 'a']); // OK
@@ -190,14 +197,21 @@ The enumeration elements can also be schemas:
```php ```php
$schema = Expect::listOf( $schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null) Expect::anyOf(Expect::string(), true, null),
); );
$processor->process($schema, ['foo', true, null, 'bar']); // OK $processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // ERROR $processor->process($schema, [123]); // ERROR
``` ```
The default value is `null`. The `anyOf()` method accepts variants as individual parameters, not as array. To pass it an array of values, use the unpacking operator `anyOf(...$variants)`.
The default value is `null`. Use the `firstIsDefault()` method to make the first element the default:
```php
// default is 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
```
Structures Structures
@@ -216,12 +230,24 @@ $schema = Expect::structure([
]); ]);
$processor->process($schema, ['optional' => '']); $processor->process($schema, ['optional' => '']);
// ERROR: item 'required' is missing // ERROR: option 'required' is missing
$processor->process($schema, ['required' => 'foo']); $processor->process($schema, ['required' => 'foo']);
// OK, returns {'required' => 'foo', 'optional' => null} // OK, returns {'required' => 'foo', 'optional' => null}
``` ```
If you do not want to output properties with only a default value, use `skipDefaults()`:
```php
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, returns {'required' => 'foo'}
```
Although `null` is the default value of the `optional` property, it is not allowed in the input data (the value must be a string). Properties accepting `null` are defined using `nullable()`: Although `null` is the default value of the `optional` property, it is not allowed in the input data (the value must be a string). Properties accepting `null` are defined using `nullable()`:
```php ```php
@@ -259,10 +285,11 @@ $processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ERROR $processor->process($schema, ['additional' => true]); // ERROR
``` ```
Deprecations Deprecations
------------ ------------
You can deprecate property using the `deprecated([string $message])` method. Deprecation notices are returned by `$processor->getWarnings()` (since v1.1): You can deprecate property using the `deprecated([string $message])` method. Deprecation notices are returned by `$processor->getWarnings()`:
```php ```php
$schema = Expect::structure([ $schema = Expect::structure([
@@ -273,6 +300,7 @@ $processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["The item 'old' is deprecated"] $processor->getWarnings(); // ["The item 'old' is deprecated"]
``` ```
Ranges: min() max() Ranges: min() max()
------------------- -------------------
@@ -322,7 +350,7 @@ Custom Assertions: assert()
You can add any other restrictions using `assert(callable $fn)`. You can add any other restrictions using `assert(callable $fn)`.
```php ```php
$countIsEven = function ($v) { return count($v) % 2 === 0; }; $countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string') $schema = Expect::arrayOf('string')
->assert($countIsEven); // the count must be even ->assert($countIsEven); // the count must be even
@@ -337,7 +365,7 @@ Or
Expect::string()->assert('is_file'); // the file must exist Expect::string()->assert('is_file'); // the file must exist
``` ```
You can add your own description for each assertions. It will be part of the error message. You can add your own description for each assertion. It will be part of the error message.
```php ```php
$schema = Expect::arrayOf('string') $schema = Expect::arrayOf('string')
@@ -347,7 +375,106 @@ $processor->process($schema, ['a', 'b', 'c']);
// Failed assertion "Even items in array" for item with value array. // Failed assertion "Even items in array" for item with value array.
``` ```
The method can be called repeatedly to add more assertions. The method can be called repeatedly to add multiple constraints. It can be intermixed with calls to `transform()` and `castTo()`.
Transformation: transform()
---------------------------
Successfully validated data can be modified using a custom function:
```php
// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));
```
The method can be called repeatedly to add multiple transformations. It can be intermixed with calls to `assert()` and `castTo()`. The operations will be executed in the order in which they are declared:
```php
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'All characters must be lowercased')
->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase
```
The `transform()` method can both transform and validate the value simultaneously. This is often simpler and less redundant than chaining `transform()` and `assert()`. For this purpose, the function receives a [Nette\Schema\Context](https://api.nette.org/schema/master/Nette/Schema/Context.html) object with an `addError()` method, which can be used to add information about validation issues:
```php
Expect::string()
->transform(function (string $s, Nette\Schema\Context $context) {
if (!ctype_lower($s)) {
$context->addError('All characters must be lowercased', 'my.case.error');
return null;
}
return strtoupper($s);
});
```
Casting: castTo()
-----------------
Successfully validated data can be cast:
```php
Expect::scalar()->castTo('string');
```
In addition to native PHP types, you can also cast to classes. It distinguishes whether it is a simple class without a constructor or a class with a constructor. If the class has no constructor, an instance of it is created and all elements of the structure are written to its properties:
```php
class Info
{
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
])->castTo(Info::class);
// creates '$obj = new Info' and writes to $obj->processRefund and $obj->refundAmount
```
If the class has a constructor, the elements of the structure are passed as named parameters to the constructor:
```php
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
```
Casting combined with a scalar parameter creates an object and passes the value as the sole parameter to the constructor:
```php
Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)
```
Normalization: before()
-----------------------
Prior to the validation itself, the data can be normalized using the method `before()`. As an example, let's have an element that must be an array of strings (eg `['a', 'b', 'c']`), but receives input in the form of a string `a b c`:
```php
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK, returns ['a', 'b', 'c']
```
Mapping to Objects: from() Mapping to Objects: from()
@@ -407,35 +534,3 @@ $schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'), 'name' => Expect::string()->pattern('\w:.*'),
]); ]);
``` ```
Casting: castTo()
-----------------
Successfully validated data can be cast:
```php
Expect::scalar()->castTo('string');
```
In addition to native PHP types, you can also cast to classes:
```php
Expect::scalar()->castTo('AddressEntity');
```
Normalization: before()
-----------------------
Prior to the validation itself, the data can be normalized using the method `before()`. As an example, let's have an element that must be an array of strings (eg `['a', 'b', 'c']`), but receives input in the form of a string `a b c`:
```php
$explode = function ($v) { return explode(' ', $v); };
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK, returns ['a', 'b', 'c']
```

View File

@@ -9,30 +9,26 @@ declare(strict_types=1);
namespace Nette\Schema; namespace Nette\Schema;
use Nette; use function count;
final class Context final class Context
{ {
use Nette\SmartObject; public bool $skipDefaults = false;
/** @var bool */
public $skipDefaults = false;
/** @var string[] */ /** @var string[] */
public $path = []; public array $path = [];
/** @var bool */ public bool $isKey = false;
public $isKey = false;
/** @var Message[] */ /** @var Message[] */
public $errors = []; public array $errors = [];
/** @var Message[] */ /** @var Message[] */
public $warnings = []; public array $warnings = [];
/** @var array[] */ /** @var array[] */
public $dynamics = []; public array $dynamics = [];
public function addError(string $message, string $code, array $variables = []): Message public function addError(string $message, string $code, array $variables = []): Message
@@ -46,4 +42,12 @@ final class Context
{ {
return $this->warnings[] = new Message($message, $code, $this->path, $variables); return $this->warnings[] = new Message($message, $code, $this->path, $variables);
} }
/** @return \Closure(): bool */
public function createChecker(): \Closure
{
$count = count($this->errors);
return fn(): bool => $count === count($this->errors);
}
} }

View File

@@ -13,21 +13,17 @@ use Nette;
use Nette\Schema\Context; use Nette\Schema\Context;
use Nette\Schema\Helpers; use Nette\Schema\Helpers;
use Nette\Schema\Schema; use Nette\Schema\Schema;
use function array_merge, array_unique, implode, is_array;
final class AnyOf implements Schema final class AnyOf implements Schema
{ {
use Base; use Base;
use Nette\SmartObject;
/** @var array */ private array $set;
private $set;
/** public function __construct(mixed ...$set)
* @param mixed|Schema ...$set
*/
public function __construct(...$set)
{ {
if (!$set) { if (!$set) {
throw new Nette\InvalidStateException('The enumeration must not be empty.'); throw new Nette\InvalidStateException('The enumeration must not be empty.');
@@ -61,16 +57,16 @@ final class AnyOf implements Schema
/********************* processing ****************d*g**/ /********************* processing ****************d*g**/
public function normalize($value, Context $context) public function normalize(mixed $value, Context $context): mixed
{ {
return $this->doNormalize($value, $context); return $this->doNormalize($value, $context);
} }
public function merge($value, $base) public function merge(mixed $value, mixed $base): mixed
{ {
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PREVENT_MERGING]); unset($value[Helpers::PreventMerging]);
return $value; return $value;
} }
@@ -78,7 +74,16 @@ final class AnyOf implements Schema
} }
public function complete($value, Context $context) public function complete(mixed $value, Context $context): mixed
{
$isOk = $context->createChecker();
$value = $this->findAlternative($value, $context);
$isOk() && $value = $this->doTransform($value, $context);
return $isOk() ? $value : null;
}
private function findAlternative(mixed $value, Context $context): mixed
{ {
$expecteds = $innerErrors = []; $expecteds = $innerErrors = [];
foreach ($this->set as $item) { foreach ($this->set as $item) {
@@ -88,7 +93,7 @@ final class AnyOf implements Schema
$res = $item->complete($item->normalize($value, $dolly), $dolly); $res = $item->complete($item->normalize($value, $dolly), $dolly);
if (!$dolly->errors) { if (!$dolly->errors) {
$context->warnings = array_merge($context->warnings, $dolly->warnings); $context->warnings = array_merge($context->warnings, $dolly->warnings);
return $this->doFinalize($res, $context); return $res;
} }
foreach ($dolly->errors as $error) { foreach ($dolly->errors as $error) {
@@ -100,7 +105,7 @@ final class AnyOf implements Schema
} }
} else { } else {
if ($item === $value) { if ($item === $value) {
return $this->doFinalize($value, $context); return $value;
} }
$expecteds[] = Nette\Schema\Helpers::formatValue($item); $expecteds[] = Nette\Schema\Helpers::formatValue($item);
@@ -112,22 +117,24 @@ final class AnyOf implements Schema
} else { } else {
$context->addError( $context->addError(
'The %label% %path% expects to be %expected%, %value% given.', 'The %label% %path% expects to be %expected%, %value% given.',
Nette\Schema\Message::TYPE_MISMATCH, Nette\Schema\Message::TypeMismatch,
[ [
'value' => $value, 'value' => $value,
'expected' => implode('|', array_unique($expecteds)), 'expected' => implode('|', array_unique($expecteds)),
] ],
); );
} }
return null;
} }
public function completeDefault(Context $context) public function completeDefault(Context $context): mixed
{ {
if ($this->required) { if ($this->required) {
$context->addError( $context->addError(
'The mandatory item %path% is missing.', 'The mandatory item %path% is missing.',
Nette\Schema\Message::MISSING_ITEM Nette\Schema\Message::MissingItem,
); );
return null; return null;
} }

View File

@@ -11,6 +11,8 @@ namespace Nette\Schema\Elements;
use Nette; use Nette;
use Nette\Schema\Context; use Nette\Schema\Context;
use Nette\Schema\Helpers;
use function count, is_string;
/** /**
@@ -18,26 +20,18 @@ use Nette\Schema\Context;
*/ */
trait Base trait Base
{ {
/** @var bool */ private bool $required = false;
private $required = false; private mixed $default = null;
/** @var mixed */ /** @var ?callable */
private $default;
/** @var callable|null */
private $before; private $before;
/** @var array[] */ /** @var callable[] */
private $asserts = []; private array $transforms = [];
private ?string $deprecated = null;
/** @var string|null */
private $castTo;
/** @var string|null */
private $deprecated;
public function default($value): self public function default(mixed $value): self
{ {
$this->default = $value; $this->default = $value;
return $this; return $this;
@@ -60,15 +54,30 @@ trait Base
public function castTo(string $type): self public function castTo(string $type): self
{ {
$this->castTo = $type; return $this->transform(Helpers::getCastStrategy($type));
}
public function transform(callable $handler): self
{
$this->transforms[] = $handler;
return $this; return $this;
} }
public function assert(callable $handler, ?string $description = null): self public function assert(callable $handler, ?string $description = null): self
{ {
$this->asserts[] = [$handler, $description]; $expected = $description ?: (is_string($handler) ? "$handler()" : '#' . count($this->transforms));
return $this; return $this->transform(function ($value, Context $context) use ($handler, $description, $expected) {
if ($handler($value)) {
return $value;
}
$context->addError(
'Failed assertion ' . ($description ? "'%assertion%'" : '%assertion%') . ' for %label% %path% with value %value%.',
Nette\Schema\Message::FailedAssertion,
['value' => $value, 'assertion' => $expected],
);
});
} }
@@ -80,12 +89,12 @@ trait Base
} }
public function completeDefault(Context $context) public function completeDefault(Context $context): mixed
{ {
if ($this->required) { if ($this->required) {
$context->addError( $context->addError(
'The mandatory item %path% is missing.', 'The mandatory item %path% is missing.',
Nette\Schema\Message::MISSING_ITEM Nette\Schema\Message::MissingItem,
); );
return null; return null;
} }
@@ -94,7 +103,7 @@ trait Base
} }
public function doNormalize($value, Context $context) public function doNormalize(mixed $value, Context $context): mixed
{ {
if ($this->before) { if ($this->before) {
$value = ($this->before)($value); $value = ($this->before)($value);
@@ -109,92 +118,46 @@ trait Base
if ($this->deprecated !== null) { if ($this->deprecated !== null) {
$context->addWarning( $context->addWarning(
$this->deprecated, $this->deprecated,
Nette\Schema\Message::DEPRECATED Nette\Schema\Message::Deprecated,
); );
} }
} }
private function doValidate($value, string $expected, Context $context): bool private function doTransform(mixed $value, Context $context): mixed
{ {
if (!Nette\Utils\Validators::is($value, $expected)) { $isOk = $context->createChecker();
$expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); foreach ($this->transforms as $handler) {
$context->addError( $value = $handler($value, $context);
'The %label% %path% expects to be %expected%, %value% given.', if (!$isOk()) {
Nette\Schema\Message::TYPE_MISMATCH, return null;
['value' => $value, 'expected' => $expected]
);
return false;
}
return true;
}
private function doValidateRange($value, array $range, Context $context, string $types = ''): bool
{
if (is_array($value) || is_string($value)) {
[$length, $label] = is_array($value)
? [count($value), 'items']
: (in_array('unicode', explode('|', $types), true)
? [Nette\Utils\Strings::length($value), 'characters']
: [strlen($value), 'bytes']);
if (!self::isInRange($length, $range)) {
$context->addError(
"The length of %label% %path% expects to be in range %expected%, %length% $label given.",
Nette\Schema\Message::LENGTH_OUT_OF_RANGE,
['value' => $value, 'length' => $length, 'expected' => implode('..', $range)]
);
return false;
}
} elseif ((is_int($value) || is_float($value)) && !self::isInRange($value, $range)) {
$context->addError(
'The %label% %path% expects to be in range %expected%, %value% given.',
Nette\Schema\Message::VALUE_OUT_OF_RANGE,
['value' => $value, 'expected' => implode('..', $range)]
);
return false;
}
return true;
}
private function isInRange($value, array $range): bool
{
return ($range[0] === null || $value >= $range[0])
&& ($range[1] === null || $value <= $range[1]);
}
private function doFinalize($value, Context $context)
{
if ($this->castTo) {
if (Nette\Utils\Reflection::isBuiltinType($this->castTo)) {
settype($value, $this->castTo);
} else {
$object = new $this->castTo;
foreach ($value as $k => $v) {
$object->$k = $v;
}
$value = $object;
} }
} }
foreach ($this->asserts as $i => [$handler, $description]) {
if (!$handler($value)) {
$expected = $description ?: (is_string($handler) ? "$handler()" : "#$i");
$context->addError(
'Failed assertion ' . ($description ? "'%assertion%'" : '%assertion%') . ' for %label% %path% with value %value%.',
Nette\Schema\Message::FAILED_ASSERTION,
['value' => $value, 'assertion' => $expected]
);
return;
}
}
return $value; return $value;
} }
/** @deprecated use Nette\Schema\Validators::validateType() */
private function doValidate(mixed $value, string $expected, Context $context): bool
{
$isOk = $context->createChecker();
Helpers::validateType($value, $expected, $context);
return $isOk();
}
/** @deprecated use Nette\Schema\Validators::validateRange() */
private static function doValidateRange(mixed $value, array $range, Context $context, string $types = ''): bool
{
$isOk = $context->createChecker();
Helpers::validateRange($value, $range, $context, $types);
return $isOk();
}
/** @deprecated use doTransform() */
private function doFinalize(mixed $value, Context $context): mixed
{
return $this->doTransform($value, $context);
}
} }

View File

@@ -13,39 +13,37 @@ use Nette;
use Nette\Schema\Context; use Nette\Schema\Context;
use Nette\Schema\Helpers; use Nette\Schema\Helpers;
use Nette\Schema\Schema; use Nette\Schema\Schema;
use function array_diff_key, array_fill_keys, array_key_exists, array_keys, array_map, array_merge, array_pop, array_values, is_array, is_object;
final class Structure implements Schema final class Structure implements Schema
{ {
use Base; use Base;
use Nette\SmartObject;
/** @var Schema[] */ /** @var Schema[] */
private $items; private array $items;
/** @var Schema|null for array|list */ /** for array|list */
private $otherItems; private ?Schema $otherItems = null;
/** @var array{?int, ?int} */ /** @var array{?int, ?int} */
private $range = [null, null]; private array $range = [null, null];
private bool $skipDefaults = false;
/** @var bool */
private $skipDefaults = false;
/** /**
* @param Schema[] $items * @param Schema[] $shape
*/ */
public function __construct(array $items) public function __construct(array $shape)
{ {
(function (Schema ...$items) {})(...array_values($items)); (function (Schema ...$items) {})(...array_values($shape));
$this->items = $items; $this->items = $shape;
$this->castTo = 'object'; $this->castTo('object');
$this->required = true; $this->required = true;
} }
public function default($value): self public function default(mixed $value): self
{ {
throw new Nette\InvalidStateException('Structure cannot have default value.'); throw new Nette\InvalidStateException('Structure cannot have default value.');
} }
@@ -65,10 +63,7 @@ final class Structure implements Schema
} }
/** public function otherItems(string|Schema $type = 'mixed'): self
* @param string|Schema $type
*/
public function otherItems($type = 'mixed'): self
{ {
$this->otherItems = $type instanceof Schema ? $type : new Type($type); $this->otherItems = $type instanceof Schema ? $type : new Type($type);
return $this; return $this;
@@ -82,13 +77,26 @@ final class Structure implements Schema
} }
public function extend(array|self $shape): self
{
$shape = $shape instanceof self ? $shape->items : $shape;
return new self(array_merge($this->items, $shape));
}
public function getShape(): array
{
return $this->items;
}
/********************* processing ****************d*g**/ /********************* processing ****************d*g**/
public function normalize($value, Context $context) public function normalize(mixed $value, Context $context): mixed
{ {
if ($prevent = (is_array($value) && isset($value[Helpers::PREVENT_MERGING]))) { if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) {
unset($value[Helpers::PREVENT_MERGING]); unset($value[Helpers::PreventMerging]);
} }
$value = $this->doNormalize($value, $context); $value = $this->doNormalize($value, $context);
@@ -107,7 +115,7 @@ final class Structure implements Schema
} }
if ($prevent) { if ($prevent) {
$value[Helpers::PREVENT_MERGING] = true; $value[Helpers::PreventMerging] = true;
} }
} }
@@ -115,37 +123,34 @@ final class Structure implements Schema
} }
public function merge($value, $base) public function merge(mixed $value, mixed $base): mixed
{ {
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PREVENT_MERGING]); unset($value[Helpers::PreventMerging]);
$base = null; $base = null;
} }
if (is_array($value) && is_array($base)) { if (is_array($value) && is_array($base)) {
$index = 0; $index = $this->otherItems === null ? null : 0;
foreach ($value as $key => $val) { foreach ($value as $key => $val) {
if ($key === $index) { if ($key === $index) {
$base[] = $val; $base[] = $val;
$index++; $index++;
} elseif (array_key_exists($key, $base)) {
$itemSchema = $this->items[$key] ?? $this->otherItems;
$base[$key] = $itemSchema
? $itemSchema->merge($val, $base[$key])
: Helpers::merge($val, $base[$key]);
} else { } else {
$base[$key] = $val; $base[$key] = array_key_exists($key, $base) && ($itemSchema = $this->items[$key] ?? $this->otherItems)
? $itemSchema->merge($val, $base[$key])
: $val;
} }
} }
return $base; return $base;
} }
return Helpers::merge($value, $base); return $value ?? $base;
} }
public function complete($value, Context $context) public function complete(mixed $value, Context $context): mixed
{ {
if ($value === null) { if ($value === null) {
$value = []; // is unable to distinguish null from array in NEON $value = []; // is unable to distinguish null from array in NEON
@@ -153,13 +158,17 @@ final class Structure implements Schema
$this->doDeprecation($context); $this->doDeprecation($context);
if (!$this->doValidate($value, 'array', $context) $isOk = $context->createChecker();
|| !$this->doValidateRange($value, $this->range, $context) Helpers::validateType($value, 'array', $context);
) { $isOk() && Helpers::validateRange($value, $this->range, $context);
return; $isOk() && $this->validateItems($value, $context);
} $isOk() && $value = $this->doTransform($value, $context);
return $isOk() ? $value : null;
}
$errCount = count($context->errors);
private function validateItems(array &$value, Context $context): void
{
$items = $this->items; $items = $this->items;
if ($extraKeys = array_keys(array_diff_key($value, $items))) { if ($extraKeys = array_keys(array_diff_key($value, $items))) {
if ($this->otherItems) { if ($this->otherItems) {
@@ -167,11 +176,11 @@ final class Structure implements Schema
} else { } else {
$keys = array_map('strval', array_keys($items)); $keys = array_map('strval', array_keys($items));
foreach ($extraKeys as $key) { foreach ($extraKeys as $key) {
$hint = Nette\Utils\ObjectHelpers::getSuggestion($keys, (string) $key); $hint = Nette\Utils\Helpers::getSuggestion($keys, (string) $key);
$context->addError( $context->addError(
'Unexpected item %path%' . ($hint ? ", did you mean '%hint%'?" : '.'), 'Unexpected item %path%' . ($hint ? ", did you mean '%hint%'?" : '.'),
Nette\Schema\Message::UNEXPECTED_ITEM, Nette\Schema\Message::UnexpectedItem,
['hint' => $hint] ['hint' => $hint],
)->path[] = $key; )->path[] = $key;
} }
} }
@@ -190,16 +199,10 @@ final class Structure implements Schema
array_pop($context->path); array_pop($context->path);
} }
if (count($context->errors) > $errCount) {
return;
}
return $this->doFinalize($value, $context);
} }
public function completeDefault(Context $context) public function completeDefault(Context $context): mixed
{ {
return $this->required return $this->required
? $this->complete([], $context) ? $this->complete([], $context)

View File

@@ -9,35 +9,25 @@ declare(strict_types=1);
namespace Nette\Schema\Elements; namespace Nette\Schema\Elements;
use Nette;
use Nette\Schema\Context; use Nette\Schema\Context;
use Nette\Schema\DynamicParameter; use Nette\Schema\DynamicParameter;
use Nette\Schema\Helpers; use Nette\Schema\Helpers;
use Nette\Schema\Schema; use Nette\Schema\Schema;
use function array_key_exists, array_pop, implode, is_array, str_replace, strpos;
final class Type implements Schema final class Type implements Schema
{ {
use Base; use Base;
use Nette\SmartObject;
/** @var string */ private string $type;
private $type; private ?Schema $itemsValue = null;
private ?Schema $itemsKey = null;
/** @var Schema|null for arrays */
private $itemsValue;
/** @var Schema|null for arrays */
private $itemsKey;
/** @var array{?float, ?float} */ /** @var array{?float, ?float} */
private $range = [null, null]; private array $range = [null, null];
private ?string $pattern = null;
/** @var string|null */ private bool $merge = true;
private $pattern;
/** @var bool */
private $merge = true;
public function __construct(string $type) public function __construct(string $type)
@@ -84,11 +74,9 @@ final class Type implements Schema
/** /**
* @param string|Schema $valueType
* @param string|Schema|null $keyType
* @internal use arrayOf() or listOf() * @internal use arrayOf() or listOf()
*/ */
public function items($valueType = 'mixed', $keyType = null): self public function items(string|Schema $valueType = 'mixed', string|Schema|null $keyType = null): self
{ {
$this->itemsValue = $valueType instanceof Schema $this->itemsValue = $valueType instanceof Schema
? $valueType ? $valueType
@@ -110,10 +98,10 @@ final class Type implements Schema
/********************* processing ****************d*g**/ /********************* processing ****************d*g**/
public function normalize($value, Context $context) public function normalize(mixed $value, Context $context): mixed
{ {
if ($prevent = (is_array($value) && isset($value[Helpers::PREVENT_MERGING]))) { if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) {
unset($value[Helpers::PREVENT_MERGING]); unset($value[Helpers::PreventMerging]);
} }
$value = $this->doNormalize($value, $context); $value = $this->doNormalize($value, $context);
@@ -134,17 +122,17 @@ final class Type implements Schema
} }
if ($prevent && is_array($value)) { if ($prevent && is_array($value)) {
$value[Helpers::PREVENT_MERGING] = true; $value[Helpers::PreventMerging] = true;
} }
return $value; return $value;
} }
public function merge($value, $base) public function merge(mixed $value, mixed $base): mixed
{ {
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PREVENT_MERGING]); unset($value[Helpers::PreventMerging]);
return $value; return $value;
} }
@@ -168,11 +156,11 @@ final class Type implements Schema
} }
public function complete($value, Context $context) public function complete(mixed $value, Context $context): mixed
{ {
$merge = $this->merge; $merge = $this->merge;
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PREVENT_MERGING]); unset($value[Helpers::PreventMerging]);
$merge = false; $merge = false;
} }
@@ -182,49 +170,40 @@ final class Type implements Schema
$this->doDeprecation($context); $this->doDeprecation($context);
if (!$this->doValidate($value, $this->type, $context) $isOk = $context->createChecker();
|| !$this->doValidateRange($value, $this->range, $context, $this->type) Helpers::validateType($value, $this->type, $context);
) { $isOk() && Helpers::validateRange($value, $this->range, $context, $this->type);
return; $isOk() && $value !== null && $this->pattern !== null && Helpers::validatePattern($value, $this->pattern, $context);
} $isOk() && is_array($value) && $this->validateItems($value, $context);
$isOk() && $merge && $value = Helpers::merge($value, $this->default);
if ($value !== null && $this->pattern !== null && !preg_match("\x01^(?:$this->pattern)$\x01Du", $value)) { $isOk() && $value = $this->doTransform($value, $context);
$context->addError( if (!$isOk()) {
"The %label% %path% expects to match pattern '%pattern%', %value% given.", return null;
Nette\Schema\Message::PATTERN_MISMATCH,
['value' => $value, 'pattern' => $this->pattern]
);
return;
} }
if ($value instanceof DynamicParameter) { if ($value instanceof DynamicParameter) {
$expected = $this->type . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range)); $expected = $this->type . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range));
$context->dynamics[] = [$value, str_replace(DynamicParameter::class . '|', '', $expected)]; $context->dynamics[] = [$value, str_replace(DynamicParameter::class . '|', '', $expected), $context->path];
}
return $value;
}
private function validateItems(array &$value, Context $context): void
{
if (!$this->itemsValue) {
return;
} }
if ($this->itemsValue) { $res = [];
$errCount = count($context->errors); foreach ($value as $key => $val) {
$res = []; $context->path[] = $key;
foreach ($value as $key => $val) { $context->isKey = true;
$context->path[] = $key; $key = $this->itemsKey ? $this->itemsKey->complete($key, $context) : $key;
$context->isKey = true; $context->isKey = false;
$key = $this->itemsKey ? $this->itemsKey->complete($key, $context) : $key; $res[$key ?? ''] = $this->itemsValue->complete($val, $context);
$context->isKey = false; array_pop($context->path);
$res[$key] = $this->itemsValue->complete($val, $context);
array_pop($context->path);
}
if (count($context->errors) > $errCount) {
return null;
}
$value = $res;
} }
$value = $res;
if ($merge) {
$value = Helpers::merge($value, $this->default);
}
return $this->doFinalize($value, $context);
} }
} }

View File

@@ -13,6 +13,7 @@ use Nette;
use Nette\Schema\Elements\AnyOf; use Nette\Schema\Elements\AnyOf;
use Nette\Schema\Elements\Structure; use Nette\Schema\Elements\Structure;
use Nette\Schema\Elements\Type; use Nette\Schema\Elements\Type;
use function is_object;
/** /**
@@ -24,7 +25,6 @@ use Nette\Schema\Elements\Type;
* @method static Type float($default = null) * @method static Type float($default = null)
* @method static Type bool($default = null) * @method static Type bool($default = null)
* @method static Type null() * @method static Type null()
* @method static Type array($default = [])
* @method static Type list($default = []) * @method static Type list($default = [])
* @method static Type mixed($default = null) * @method static Type mixed($default = null)
* @method static Type email($default = null) * @method static Type email($default = null)
@@ -32,8 +32,6 @@ use Nette\Schema\Elements\Type;
*/ */
final class Expect final class Expect
{ {
use Nette\SmartObject;
public static function __callStatic(string $name, array $args): Type public static function __callStatic(string $name, array $args): Type
{ {
$type = new Type($name); $type = new Type($name);
@@ -51,39 +49,35 @@ final class Expect
} }
/** public static function anyOf(mixed ...$set): AnyOf
* @param mixed|Schema ...$set
*/
public static function anyOf(...$set): AnyOf
{ {
return new AnyOf(...$set); return new AnyOf(...$set);
} }
/** /**
* @param Schema[] $items * @param Schema[] $shape
*/ */
public static function structure(array $items): Structure public static function structure(array $shape): Structure
{ {
return new Structure($items); return new Structure($shape);
} }
/** public static function from(object $object, array $items = []): Structure
* @param object $object
*/
public static function from($object, array $items = []): Structure
{ {
$ro = new \ReflectionObject($object); $ro = new \ReflectionObject($object);
foreach ($ro->getProperties() as $prop) { $props = $ro->hasMethod('__construct')
$type = Helpers::getPropertyType($prop) ?? 'mixed'; ? $ro->getMethod('__construct')->getParameters()
: $ro->getProperties();
foreach ($props as $prop) {
$item = &$items[$prop->getName()]; $item = &$items[$prop->getName()];
if (!$item) { if (!$item) {
$type = Helpers::getPropertyType($prop) ?? 'mixed';
$item = new Type($type); $item = new Type($type);
if (PHP_VERSION_ID >= 70400 && !$prop->isInitialized($object)) { if ($prop instanceof \ReflectionProperty ? $prop->isInitialized($object) : $prop->isOptional()) {
$item->required(); $def = ($prop instanceof \ReflectionProperty ? $prop->getValue($object) : $prop->getDefaultValue());
} else {
$def = $prop->getValue($object);
if (is_object($def)) { if (is_object($def)) {
$item = static::from($def); $item = static::from($def);
} elseif ($def === null && !Nette\Utils\Validators::is(null, $type)) { } elseif ($def === null && !Nette\Utils\Validators::is(null, $type)) {
@@ -91,6 +85,8 @@ final class Expect
} else { } else {
$item->default($def); $item->default($def);
} }
} else {
$item->required();
} }
} }
} }
@@ -100,19 +96,23 @@ final class Expect
/** /**
* @param string|Schema $valueType * @param mixed[] $shape
* @param string|Schema|null $keyType
*/ */
public static function arrayOf($valueType, $keyType = null): Type public static function array(?array $shape = []): Structure|Type
{
return Nette\Utils\Arrays::first($shape ?? []) instanceof Schema
? (new Structure($shape))->castTo('array')
: (new Type('array'))->default($shape);
}
public static function arrayOf(string|Schema $valueType, string|Schema|null $keyType = null): Type
{ {
return (new Type('array'))->items($valueType, $keyType); return (new Type('array'))->items($valueType, $keyType);
} }
/** public static function listOf(string|Schema $type): Type
* @param string|Schema $type
*/
public static function listOf($type): Type
{ {
return (new Type('list'))->items($type); return (new Type('list'))->items($type);
} }

View File

@@ -11,6 +11,7 @@ namespace Nette\Schema;
use Nette; use Nette;
use Nette\Utils\Reflection; use Nette\Utils\Reflection;
use function count, explode, get_debug_type, implode, in_array, is_array, is_float, is_int, is_object, is_scalar, is_string, method_exists, preg_match, preg_quote, preg_replace, preg_replace_callback, settype, str_replace, strlen, trim, var_export;
/** /**
@@ -20,17 +21,16 @@ final class Helpers
{ {
use Nette\StaticClass; use Nette\StaticClass;
public const PREVENT_MERGING = '_prevent_merging'; public const PreventMerging = '_prevent_merging';
/** /**
* Merges dataset. Left has higher priority than right one. * Merges dataset. Left has higher priority than right one.
* @return array|string
*/ */
public static function merge($value, $base) public static function merge(mixed $value, mixed $base): mixed
{ {
if (is_array($value) && isset($value[self::PREVENT_MERGING])) { if (is_array($value) && isset($value[self::PreventMerging])) {
unset($value[self::PREVENT_MERGING]); unset($value[self::PreventMerging]);
return $value; return $value;
} }
@@ -56,17 +56,16 @@ final class Helpers
} }
public static function getPropertyType(\ReflectionProperty $prop): ?string public static function getPropertyType(\ReflectionProperty|\ReflectionParameter $prop): ?string
{ {
if (!class_exists(Nette\Utils\Type::class)) { if ($type = Nette\Utils\Type::fromReflection($prop)) {
throw new Nette\NotSupportedException('Expect::from() requires nette/utils 3.x');
} elseif ($type = Nette\Utils\Type::fromReflection($prop)) {
return (string) $type; return (string) $type;
} elseif ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var'))) { } elseif (
($prop instanceof \ReflectionProperty)
&& ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var')))
) {
$class = Reflection::getPropertyDeclaringClass($prop); $class = Reflection::getPropertyDeclaringClass($prop);
return preg_replace_callback('#[\w\\\\]+#', function ($m) use ($class) { return preg_replace_callback('#[\w\\\]+#', fn($m) => Reflection::expandClassName($m[0], $class), $type);
return Reflection::expandClassName($m[0], $class);
}, $type);
} }
return null; return null;
@@ -92,19 +91,94 @@ final class Helpers
} }
/** public static function formatValue(mixed $value): string
* @param mixed $value
*/
public static function formatValue($value): string
{ {
if (is_object($value)) { if ($value instanceof DynamicParameter) {
return 'object ' . get_class($value); return 'dynamic';
} elseif (is_object($value)) {
return 'object ' . $value::class;
} elseif (is_string($value)) { } elseif (is_string($value)) {
return "'" . Nette\Utils\Strings::truncate($value, 15, '...') . "'"; return "'" . Nette\Utils\Strings::truncate($value, 15, '...') . "'";
} elseif (is_scalar($value)) { } elseif (is_scalar($value)) {
return var_export($value, true); return var_export($value, return: true);
} else { } else {
return strtolower(gettype($value)); return get_debug_type($value);
}
}
public static function validateType(mixed $value, string $expected, Context $context): void
{
if (!Nette\Utils\Validators::is($value, $expected)) {
$expected = str_replace(DynamicParameter::class . '|', '', $expected);
$expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
$context->addError(
'The %label% %path% expects to be %expected%, %value% given.',
Message::TypeMismatch,
['value' => $value, 'expected' => $expected],
);
}
}
public static function validateRange(mixed $value, array $range, Context $context, string $types = ''): void
{
if (is_array($value) || is_string($value)) {
[$length, $label] = is_array($value)
? [count($value), 'items']
: (in_array('unicode', explode('|', $types), true)
? [Nette\Utils\Strings::length($value), 'characters']
: [strlen($value), 'bytes']);
if (!self::isInRange($length, $range)) {
$context->addError(
"The length of %label% %path% expects to be in range %expected%, %length% $label given.",
Message::LengthOutOfRange,
['value' => $value, 'length' => $length, 'expected' => implode('..', $range)],
);
}
} elseif ((is_int($value) || is_float($value)) && !self::isInRange($value, $range)) {
$context->addError(
'The %label% %path% expects to be in range %expected%, %value% given.',
Message::ValueOutOfRange,
['value' => $value, 'expected' => implode('..', $range)],
);
}
}
public static function isInRange(mixed $value, array $range): bool
{
return ($range[0] === null || $value >= $range[0])
&& ($range[1] === null || $value <= $range[1]);
}
public static function validatePattern(string $value, string $pattern, Context $context): void
{
if (!preg_match("\x01^(?:$pattern)$\x01Du", $value)) {
$context->addError(
"The %label% %path% expects to match pattern '%pattern%', %value% given.",
Message::PatternMismatch,
['value' => $value, 'pattern' => $pattern],
);
}
}
public static function getCastStrategy(string $type): \Closure
{
if (Nette\Utils\Validators::isBuiltinType($type)) {
return static function ($value) use ($type) {
settype($value, $type);
return $value;
};
} elseif (method_exists($type, '__construct')) {
return static fn($value) => is_array($value) || $value instanceof \stdClass
? new $type(...(array) $value)
: new $type($value);
} else {
return static fn($value) => Nette\Utils\Arrays::toObject((array) $value, new $type);
} }
} }
} }

View File

@@ -10,47 +10,67 @@ declare(strict_types=1);
namespace Nette\Schema; namespace Nette\Schema;
use Nette; use Nette;
use function implode, preg_last_error_msg, preg_replace_callback;
final class Message final class Message
{ {
use Nette\SmartObject; /** variables: {value: mixed, expected: string} */
public const TypeMismatch = 'schema.typeMismatch';
/** variables: {value: mixed, expected: string} */ /** variables: {value: mixed, expected: string} */
public const TYPE_MISMATCH = 'schema.typeMismatch'; public const ValueOutOfRange = 'schema.valueOutOfRange';
/** variables: {value: mixed, expected: string} */
public const VALUE_OUT_OF_RANGE = 'schema.valueOutOfRange';
/** variables: {value: mixed, length: int, expected: string} */ /** variables: {value: mixed, length: int, expected: string} */
public const LENGTH_OUT_OF_RANGE = 'schema.lengthOutOfRange'; public const LengthOutOfRange = 'schema.lengthOutOfRange';
/** variables: {value: string, pattern: string} */ /** variables: {value: string, pattern: string} */
public const PATTERN_MISMATCH = 'schema.patternMismatch'; public const PatternMismatch = 'schema.patternMismatch';
/** variables: {value: mixed, assertion: string} */ /** variables: {value: mixed, assertion: string} */
public const FAILED_ASSERTION = 'schema.failedAssertion'; public const FailedAssertion = 'schema.failedAssertion';
/** no variables */ /** no variables */
public const MISSING_ITEM = 'schema.missingItem'; public const MissingItem = 'schema.missingItem';
/** variables: {hint: string} */ /** variables: {hint: string} */
public const UNEXPECTED_ITEM = 'schema.unexpectedItem'; public const UnexpectedItem = 'schema.unexpectedItem';
/** no variables */ /** no variables */
public const DEPRECATED = 'schema.deprecated'; public const Deprecated = 'schema.deprecated';
/** @var string */ /** @deprecated use Message::TypeMismatch */
public $message; public const TYPE_MISMATCH = self::TypeMismatch;
/** @var string */ /** @deprecated use Message::ValueOutOfRange */
public $code; public const VALUE_OUT_OF_RANGE = self::ValueOutOfRange;
/** @deprecated use Message::LengthOutOfRange */
public const LENGTH_OUT_OF_RANGE = self::LengthOutOfRange;
/** @deprecated use Message::PatternMismatch */
public const PATTERN_MISMATCH = self::PatternMismatch;
/** @deprecated use Message::FailedAssertion */
public const FAILED_ASSERTION = self::FailedAssertion;
/** @deprecated use Message::MissingItem */
public const MISSING_ITEM = self::MissingItem;
/** @deprecated use Message::UnexpectedItem */
public const UNEXPECTED_ITEM = self::UnexpectedItem;
/** @deprecated use Message::Deprecated */
public const DEPRECATED = self::Deprecated;
public string $message;
public string $code;
/** @var string[] */ /** @var string[] */
public $path; public array $path;
/** @var string[] */ /** @var string[] */
public $variables; public array $variables;
public function __construct(string $message, string $code, array $path, array $variables = []) public function __construct(string $message, string $code, array $path, array $variables = [])
@@ -74,6 +94,6 @@ final class Message
return preg_replace_callback('~( ?)%(\w+)%~', function ($m) use ($vars) { return preg_replace_callback('~( ?)%(\w+)%~', function ($m) use ($vars) {
[, $space, $key] = $m; [, $space, $key] = $m;
return $vars[$key] === null ? '' : $space . $vars[$key]; return $vars[$key] === null ? '' : $space . $vars[$key];
}, $this->message); }, $this->message) ?? throw new Nette\InvalidStateException(preg_last_error_msg());
} }
} }

View File

@@ -17,19 +17,12 @@ use Nette;
*/ */
final class Processor final class Processor
{ {
use Nette\SmartObject; public array $onNewContext = [];
private Context $context;
/** @var array */ private bool $skipDefaults = false;
public $onNewContext = [];
/** @var Context|null */
private $context;
/** @var bool */
private $skipDefaults;
public function skipDefaults(bool $value = true) public function skipDefaults(bool $value = true): void
{ {
$this->skipDefaults = $value; $this->skipDefaults = $value;
} }
@@ -37,10 +30,9 @@ final class Processor
/** /**
* Normalizes and validates data. Result is a clean completed data. * Normalizes and validates data. Result is a clean completed data.
* @return mixed
* @throws ValidationException * @throws ValidationException
*/ */
public function process(Schema $schema, $data) public function process(Schema $schema, mixed $data): mixed
{ {
$this->createContext(); $this->createContext();
$data = $schema->normalize($data, $this->context); $data = $schema->normalize($data, $this->context);
@@ -53,10 +45,9 @@ final class Processor
/** /**
* Normalizes and validates and merges multiple data. Result is a clean completed data. * Normalizes and validates and merges multiple data. Result is a clean completed data.
* @return mixed
* @throws ValidationException * @throws ValidationException
*/ */
public function processMultiple(Schema $schema, array $dataset) public function processMultiple(Schema $schema, array $dataset): mixed
{ {
$this->createContext(); $this->createContext();
$flatten = null; $flatten = null;
@@ -96,10 +87,10 @@ final class Processor
} }
private function createContext() private function createContext(): void
{ {
$this->context = new Context; $this->context = new Context;
$this->context->skipDefaults = $this->skipDefaults; $this->context->skipDefaults = $this->skipDefaults;
$this->onNewContext($this->context); Nette\Utils\Arrays::invoke($this->onNewContext, $this->context);
} }
} }

View File

@@ -16,19 +16,19 @@ interface Schema
* Normalization. * Normalization.
* @return mixed * @return mixed
*/ */
function normalize($value, Context $context); function normalize(mixed $value, Context $context);
/** /**
* Merging. * Merging.
* @return mixed * @return mixed
*/ */
function merge($value, $base); function merge(mixed $value, mixed $base);
/** /**
* Validation and finalization. * Validation and finalization.
* @return mixed * @return mixed
*/ */
function complete($value, Context $context); function complete(mixed $value, Context $context);
/** /**
* @return mixed * @return mixed

View File

@@ -18,7 +18,7 @@ use Nette;
class ValidationException extends Nette\InvalidStateException class ValidationException extends Nette\InvalidStateException
{ {
/** @var Message[] */ /** @var Message[] */
private $messages; private array $messages;
/** /**

View File

@@ -9,11 +9,5 @@ override(\Nette\Utils\Arrays::getRef(0), elementType(0));
override(\Nette\Utils\Arrays::grep(0), type(0)); override(\Nette\Utils\Arrays::grep(0), type(0));
override(\Nette\Utils\Arrays::toObject(0), type(1)); override(\Nette\Utils\Arrays::toObject(0), type(1));
expectedArguments(\Nette\Utils\Arrays::grep(), 2, PREG_GREP_INVERT); expectedArguments(\Nette\Utils\Image::resize(), 2, \Nette\Utils\Image::ShrinkOnly, \Nette\Utils\Image::Stretch, \Nette\Utils\Image::OrSmaller, \Nette\Utils\Image::OrBigger, \Nette\Utils\Image::Cover);
expectedArguments(\Nette\Utils\Image::resize(), 2, \Nette\Utils\Image::SHRINK_ONLY, \Nette\Utils\Image::STRETCH, \Nette\Utils\Image::FIT, \Nette\Utils\Image::FILL, \Nette\Utils\Image::EXACT); expectedArguments(\Nette\Utils\Image::calculateSize(), 4, \Nette\Utils\Image::ShrinkOnly, \Nette\Utils\Image::Stretch, \Nette\Utils\Image::OrSmaller, \Nette\Utils\Image::OrBigger, \Nette\Utils\Image::Cover);
expectedArguments(\Nette\Utils\Image::calculateSize(), 4, \Nette\Utils\Image::SHRINK_ONLY, \Nette\Utils\Image::STRETCH, \Nette\Utils\Image::FIT, \Nette\Utils\Image::FILL, \Nette\Utils\Image::EXACT);
expectedArguments(\Nette\Utils\Json::encode(), 1, \Nette\Utils\Json::PRETTY);
expectedArguments(\Nette\Utils\Json::decode(), 1, \Nette\Utils\Json::FORCE_ARRAY);
expectedArguments(\Nette\Utils\Strings::split(), 2, \PREG_SPLIT_NO_EMPTY | \PREG_OFFSET_CAPTURE);
expectedArguments(\Nette\Utils\Strings::match(), 2, \PREG_OFFSET_CAPTURE | \PREG_UNMATCHED_AS_NULL);
expectedArguments(\Nette\Utils\Strings::matchAll(), 2, \PREG_OFFSET_CAPTURE | \PREG_UNMATCHED_AS_NULL | \PREG_PATTERN_ORDER);

View File

@@ -15,27 +15,31 @@
} }
], ],
"require": { "require": {
"php": ">=7.2 <8.3" "php": "8.2 - 8.5"
}, },
"require-dev": { "require-dev": {
"nette/tester": "~2.0", "nette/tester": "^2.5",
"tracy/tracy": "^2.3", "tracy/tracy": "^2.9",
"phpstan/phpstan": "^1.0" "phpstan/phpstan-nette": "^2.0@stable",
"jetbrains/phpstorm-attributes": "^1.2"
}, },
"conflict": { "conflict": {
"nette/di": "<3.0.6" "nette/finder": "<3",
"nette/schema": "<1.2.2"
}, },
"suggest": { "suggest": {
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-json": "to use Nette\\Utils\\Json", "ext-json": "to use Nette\\Utils\\Json",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-mbstring": "to use Strings::lower() etc...", "ext-mbstring": "to use Strings::lower() etc...",
"ext-xml": "to use Strings::length() etc. when mbstring is not available",
"ext-gd": "to use Image", "ext-gd": "to use Image",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
}, },
"autoload": { "autoload": {
"classmap": ["src/"] "classmap": ["src/"],
"psr-4": {
"Nette\\": "src"
}
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"scripts": { "scripts": {
@@ -44,7 +48,7 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.2-dev" "dev-master": "4.1-dev"
} }
} }
} }

View File

@@ -1,33 +0,0 @@
How to contribute & use the issue tracker
=========================================
Nette welcomes your contributions. There are several ways to help out:
* Create an issue on GitHub, if you have found a bug
* Write test cases for open bug issues
* Write fixes for open bug/feature issues, preferably with test cases included
* Contribute to the [documentation](https://nette.org/en/writing)
Issues
------
Please **do not use the issue tracker to ask questions**. We will be happy to help you
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report.
**Feature requests** are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature.
Contributing
------------
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
Please do not fix whitespace, format code, or make a purely cosmetic patch.
Thanks! :heart:

View File

@@ -1,14 +0,0 @@
<?php
/**
* Rules for Nette Coding Standard
* https://github.com/nette/coding-standard
*/
declare(strict_types=1);
return [
// use function in Arrays.php, Callback.php, Html.php, Strings.php
'single_import_per_statement' => false,
'ordered_imports' => false,
];

View File

@@ -1,18 +0,0 @@
<?xml version="1.0"?>
<ruleset name="Custom" namespace="Nette">
<rule ref="$presets/php72.xml"/>
<!-- bug in SlevomatCodingStandard -->
<rule ref="SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator">
<severity>0</severity>
</rule>
<!-- bug in FunctionSpacingSniff -->
<exclude-pattern>./tests/Utils/Reflection.getDeclaringMethod.alias.phpt</exclude-pattern>
<exclude-pattern>./tests/Utils/Reflection.getDeclaringMethod.insteadof.phpt</exclude-pattern>
<!-- use function in Arrays.php, Callback.php, Html.php, Strings.php -->
<rule ref="SlevomatCodingStandard.Namespaces.MultipleUsesPerLine.MultipleUsesPerLine">
<severity>0</severity>
</rule>
</ruleset>

View File

@@ -1,5 +1,4 @@
Nette Utility Classes [![Nette Utils](https://github.com/nette/utils/assets/194960/c33fdb74-0652-4cad-ac6e-c1ce0d29e32a)](https://doc.nette.org/en/utils)
=====================
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/utils.svg)](https://packagist.org/packages/nette/utils) [![Downloads this Month](https://img.shields.io/packagist/dm/nette/utils.svg)](https://packagist.org/packages/nette/utils)
[![Tests](https://github.com/nette/utils/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/utils/actions) [![Tests](https://github.com/nette/utils/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/utils/actions)
@@ -11,24 +10,27 @@ Nette Utility Classes
Introduction Introduction
------------ ------------
In package nette/utils you will find a set of [useful classes](https://doc.nette.org/utils) for everyday use: In package nette/utils you will find a set of useful classes for everyday use:
- [Arrays](https://doc.nette.org/arrays) - manipulate arrays [Arrays](https://doc.nette.org/utils/arrays)<br>
- [Callback](https://doc.nette.org/callback) - PHP callbacks [Callback](https://doc.nette.org/utils/callback) - PHP callbacks<br>
- [Date and Time](https://doc.nette.org/datetime) - modify times and dates ✅ [Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, …<br>
- [Filesystem](https://doc.nette.org/filesystem) - copying, renaming, … [Finder](https://doc.nette.org/utils/finder) - finds files and directories<br>
- [Helper Functions](https://doc.nette.org/helpers) ✅ [Floats](https://doc.nette.org/utils/floats) - floating point numbers<br>
- [HTML elements](https://doc.nette.org/html-elements) - generate HTML [Helper Functions](https://doc.nette.org/utils/helpers)<br>
- [Images](https://doc.nette.org/images) - crop, resize, rotate images ✅ [HTML elements](https://doc.nette.org/utils/html-elements) - generate HTML<br>
- [JSON](https://doc.nette.org/json) - encoding and decoding ✅ [Images](https://doc.nette.org/utils/images) - crop, resize, rotate images<br>
- [Generating Random Strings](https://doc.nette.org/random) ✅ [Iterables](https://doc.nette.org/utils/iterables) <br>
- [Paginator](https://doc.nette.org/paginator) - pagination math ✅ [JSON](https://doc.nette.org/utils/json) - encoding and decoding<br>
- [PHP Reflection](https://doc.nette.org/reflection) ✅ [Generating Random Strings](https://doc.nette.org/utils/random)<br>
- [Strings](https://doc.nette.org/strings) - useful text functions ✅ [Paginator](https://doc.nette.org/utils/paginator) - pagination math<br>
- [SmartObject](https://doc.nette.org/smartobject) - PHP object enhancements ✅ [PHP Reflection](https://doc.nette.org/utils/reflection)<br>
- [Validation](https://doc.nette.org/validators) - validate inputs ✅ [Strings](https://doc.nette.org/utils/strings) - useful text functions<br>
- [Type](https://doc.nette.org/type) - PHP data type ✅ [SmartObject](https://doc.nette.org/utils/smartobject) - PHP object enhancements<br>
✅ [Type](https://doc.nette.org/utils/type) - PHP data type<br>
✅ [Validation](https://doc.nette.org/utils/validators) - validate inputs<br>
 <!---->
Installation Installation
------------ ------------
@@ -39,10 +41,9 @@ The recommended way to install is via Composer:
composer require nette/utils composer require nette/utils
``` ```
- Nette Utils 3.2 is compatible with PHP 7.2 to 8.2 Nette Utils 4.1 is compatible with PHP 8.2 to 8.5.
- Nette Utils 3.1 is compatible with PHP 7.1 to 8.0
- Nette Utils 3.0 is compatible with PHP 7.1 to 8.0  <!---->
- Nette Utils 2.5 is compatible with PHP 5.6 to 8.0
[Support Me](https://github.com/sponsors/dg) [Support Me](https://github.com/sponsors/dg)
-------------------------------------------- --------------------------------------------

View File

@@ -28,30 +28,15 @@ class CachingIterator extends \CachingIterator implements \Countable
{ {
use Nette\SmartObject; use Nette\SmartObject;
/** @var int */ private int $counter = 0;
private $counter = 0;
public function __construct($iterator) public function __construct(iterable|\stdClass $iterable)
{ {
if (is_array($iterator) || $iterator instanceof \stdClass) { $iterable = $iterable instanceof \stdClass
$iterator = new \ArrayIterator($iterator); ? new \ArrayIterator((array) $iterable)
: Nette\Utils\Iterables::toIterator($iterable);
} elseif ($iterator instanceof \IteratorAggregate) { parent::__construct($iterable, 0);
do {
$iterator = $iterator->getIterator();
} while ($iterator instanceof \IteratorAggregate);
assert($iterator instanceof \Iterator);
} elseif ($iterator instanceof \Iterator) {
} elseif ($iterator instanceof \Traversable) {
$iterator = new \IteratorIterator($iterator);
} else {
throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', self::class, is_object($iterator) ? get_class($iterator) : gettype($iterator)));
}
parent::__construct($iterator, 0);
} }
@@ -148,9 +133,8 @@ class CachingIterator extends \CachingIterator implements \Countable
/** /**
* Returns the next key. * Returns the next key.
* @return mixed
*/ */
public function getNextKey() public function getNextKey(): mixed
{ {
return $this->getInnerIterator()->key(); return $this->getInnerIterator()->key();
} }
@@ -158,9 +142,8 @@ class CachingIterator extends \CachingIterator implements \Countable
/** /**
* Returns the next element. * Returns the next element.
* @return mixed
*/ */
public function getNextValue() public function getNextValue(): mixed
{ {
return $this->getInnerIterator()->current(); return $this->getInnerIterator()->current();
} }

View File

@@ -10,9 +10,8 @@ declare(strict_types=1);
namespace Nette\Iterators; namespace Nette\Iterators;
/** /**
* Applies the callback to the elements of the inner iterator. * @deprecated use Nette\Utils\Iterables::map()
*/ */
class Mapper extends \IteratorIterator class Mapper extends \IteratorIterator
{ {
@@ -27,8 +26,7 @@ class Mapper extends \IteratorIterator
} }
#[\ReturnTypeWillChange] public function current(): mixed
public function current()
{ {
return ($this->callback)(parent::current(), parent::key()); return ($this->callback)(parent::current(), parent::key());
} }

View File

@@ -22,6 +22,7 @@ use Nette\Utils\ObjectHelpers;
trait SmartObject trait SmartObject
{ {
/** /**
* @return mixed
* @throws MemberAccessException * @throws MemberAccessException
*/ */
public function __call(string $name, array $args) public function __call(string $name, array $args)
@@ -35,11 +36,13 @@ trait SmartObject
$handler(...$args); $handler(...$args);
} }
} elseif ($handlers !== null) { } elseif ($handlers !== null) {
throw new UnexpectedValueException("Property $class::$$name must be iterable or null, " . gettype($handlers) . ' given.'); throw new UnexpectedValueException("Property $class::$$name must be iterable or null, " . get_debug_type($handlers) . ' given.');
} }
} else {
ObjectHelpers::strictCall($class, $name); return null;
} }
ObjectHelpers::strictCall($class, $name);
} }
@@ -87,11 +90,9 @@ trait SmartObject
/** /**
* @param mixed $value
* @return void
* @throws MemberAccessException if the property is not defined or is read-only * @throws MemberAccessException if the property is not defined or is read-only
*/ */
public function __set(string $name, $value) public function __set(string $name, mixed $value): void
{ {
$class = static::class; $class = static::class;
@@ -121,10 +122,9 @@ trait SmartObject
/** /**
* @return void
* @throws MemberAccessException * @throws MemberAccessException
*/ */
public function __unset(string $name) public function __unset(string $name): void
{ {
$class = static::class; $class = static::class;
if (!ObjectHelpers::hasProperty($class, $name)) { if (!ObjectHelpers::hasProperty($class, $name)) {

View File

@@ -16,22 +16,9 @@ namespace Nette;
trait StaticClass trait StaticClass
{ {
/** /**
* @return never * Class is static and cannot be instantiated.
* @throws \Error
*/ */
final public function __construct() private function __construct()
{ {
throw new \Error('Class ' . static::class . ' is static and cannot be instantiated.');
}
/**
* Call to undefined static method.
* @return void
* @throws MemberAccessException
*/
public static function __callStatic(string $name, array $args)
{
Utils\ObjectHelpers::strictStaticCall(static::class, $name);
} }
} }

View File

@@ -17,10 +17,8 @@ interface Translator
{ {
/** /**
* Translates the given string. * Translates the given string.
* @param mixed $message
* @param mixed ...$parameters
*/ */
function translate($message, ...$parameters): string; function translate(string|\Stringable $message, mixed ...$parameters): string|\Stringable;
} }

View File

@@ -10,25 +10,27 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function count, is_array, is_scalar, sprintf;
/** /**
* Provides objects to work as array. * Provides objects to work as array.
* @template T * @template T
* @implements \IteratorAggregate<array-key, T>
* @implements \ArrayAccess<array-key, T>
*/ */
class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate
{ {
/** /**
* Transforms array to ArrayHash. * Transforms array to ArrayHash.
* @param array<T> $array * @param array<T> $array
* @return static
*/ */
public static function from(array $array, bool $recursive = true) public static function from(array $array, bool $recursive = true): static
{ {
$obj = new static; $obj = new static;
foreach ($array as $key => $value) { foreach ($array as $key => $value) {
$obj->$key = $recursive && is_array($value) $obj->$key = $recursive && is_array($value)
? static::from($value, true) ? static::from($value)
: $value; : $value;
} }
@@ -38,11 +40,13 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \Iterator
/** /**
* Returns an iterator over all items. * Returns an iterator over all items.
* @return \RecursiveArrayIterator<array-key, T> * @return \Iterator<array-key, T>
*/ */
public function getIterator(): \RecursiveArrayIterator public function &getIterator(): \Iterator
{ {
return new \RecursiveArrayIterator((array) $this); foreach ((array) $this as $key => $foo) {
yield $key => $this->$key;
}
} }
@@ -56,14 +60,14 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \Iterator
/** /**
* Replaces or appends a item. * Replaces or appends an item.
* @param string|int $key * @param array-key $key
* @param T $value * @param T $value
*/ */
public function offsetSet($key, $value): void public function offsetSet($key, $value): void
{ {
if (!is_scalar($key)) { // prevents null if (!is_scalar($key)) { // prevents null
throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', gettype($key))); throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', get_debug_type($key)));
} }
$this->$key = $value; $this->$key = $value;
@@ -71,20 +75,19 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \Iterator
/** /**
* Returns a item. * Returns an item.
* @param string|int $key * @param array-key $key
* @return T * @return T
*/ */
#[\ReturnTypeWillChange] public function offsetGet($key): mixed
public function offsetGet($key)
{ {
return $this->$key; return $this->$key;
} }
/** /**
* Determines whether a item exists. * Determines whether an item exists.
* @param string|int $key * @param array-key $key
*/ */
public function offsetExists($key): bool public function offsetExists($key): bool
{ {
@@ -94,7 +97,7 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \Iterator
/** /**
* Removes the element from this list. * Removes the element from this list.
* @param string|int $key * @param array-key $key
*/ */
public function offsetUnset($key): void public function offsetUnset($key): void
{ {

View File

@@ -10,26 +10,25 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function array_slice, array_splice, count, is_int;
/** /**
* Provides the base class for a generic list (items can be accessed by index). * Provides the base class for a generic list (items can be accessed by index).
* @template T * @template T
* @implements \IteratorAggregate<int, T>
* @implements \ArrayAccess<int, T>
*/ */
class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
{ {
use Nette\SmartObject; private array $list = [];
/** @var mixed[] */
private $list = [];
/** /**
* Transforms array to ArrayList. * Transforms array to ArrayList.
* @param array<T> $array * @param list<T> $array
* @return static
*/ */
public static function from(array $array) public static function from(array $array): static
{ {
if (!Arrays::isList($array)) { if (!Arrays::isList($array)) {
throw new Nette\InvalidArgumentException('Array is not valid list.'); throw new Nette\InvalidArgumentException('Array is not valid list.');
@@ -43,11 +42,13 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
/** /**
* Returns an iterator over all items. * Returns an iterator over all items.
* @return \ArrayIterator<int, T> * @return \Iterator<int, T>
*/ */
public function getIterator(): \ArrayIterator public function &getIterator(): \Iterator
{ {
return new \ArrayIterator($this->list); foreach ($this->list as &$item) {
yield $item;
}
} }
@@ -61,7 +62,7 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
/** /**
* Replaces or appends a item. * Replaces or appends an item.
* @param int|null $index * @param int|null $index
* @param T $value * @param T $value
* @throws Nette\OutOfRangeException * @throws Nette\OutOfRangeException
@@ -81,13 +82,12 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
/** /**
* Returns a item. * Returns an item.
* @param int $index * @param int $index
* @return T * @return T
* @throws Nette\OutOfRangeException * @throws Nette\OutOfRangeException
*/ */
#[\ReturnTypeWillChange] public function offsetGet($index): mixed
public function offsetGet($index)
{ {
if (!is_int($index) || $index < 0 || $index >= count($this->list)) { if (!is_int($index) || $index < 0 || $index >= count($this->list)) {
throw new Nette\OutOfRangeException('Offset invalid or out of range'); throw new Nette\OutOfRangeException('Offset invalid or out of range');
@@ -98,7 +98,7 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
/** /**
* Determines whether a item exists. * Determines whether an item exists.
* @param int $index * @param int $index
*/ */
public function offsetExists($index): bool public function offsetExists($index): bool
@@ -123,10 +123,10 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
/** /**
* Prepends a item. * Prepends an item.
* @param T $value * @param T $value
*/ */
public function prepend($value): void public function prepend(mixed $value): void
{ {
$first = array_slice($this->list, 0, 1); $first = array_slice($this->list, 0, 1);
$this->offsetSet(0, $value); $this->offsetSet(0, $value);

View File

@@ -9,8 +9,10 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use JetBrains\PhpStorm\Language;
use Nette; use Nette;
use function is_array, is_int, is_object, count; use function array_combine, array_intersect_key, array_is_list, array_key_exists, array_key_first, array_key_last, array_keys, array_reverse, array_search, array_slice, array_walk_recursive, count, func_num_args, in_array, is_array, is_int, is_object, key, preg_split;
use const PREG_GREP_INVERT, PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
/** /**
@@ -29,7 +31,7 @@ class Arrays
* @return ?T * @return ?T
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
*/ */
public static function get(array $array, $key, $default = null) public static function get(array $array, string|int|array $key, mixed $default = null): mixed
{ {
foreach (is_array($key) ? $key : [$key] as $k) { foreach (is_array($key) ? $key : [$key] as $k) {
if (is_array($array) && array_key_exists($k, $array)) { if (is_array($array) && array_key_exists($k, $array)) {
@@ -55,7 +57,7 @@ class Arrays
* @return ?T * @return ?T
* @throws Nette\InvalidArgumentException if traversed item is not an array * @throws Nette\InvalidArgumentException if traversed item is not an array
*/ */
public static function &getRef(array &$array, $key) public static function &getRef(array &$array, string|int|array $key): mixed
{ {
foreach (is_array($key) ? $key : [$key] as $k) { foreach (is_array($key) ? $key : [$key] as $k) {
if (is_array($array) || $array === null) { if (is_array($array) || $array === null) {
@@ -94,12 +96,10 @@ class Arrays
/** /**
* Returns zero-indexed position of given array key. Returns null if key is not found. * Returns zero-indexed position of given array key. Returns null if key is not found.
* @param array-key $key
* @return int|null offset if it is found, null otherwise
*/ */
public static function getKeyOffset(array $array, $key): ?int public static function getKeyOffset(array $array, string|int $key): ?int
{ {
return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), true)); return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), strict: true));
} }
@@ -114,75 +114,118 @@ class Arrays
/** /**
* Tests an array for the presence of value. * Tests an array for the presence of value.
* @param mixed $value
*/ */
public static function contains(array $array, $value): bool public static function contains(array $array, mixed $value): bool
{ {
return in_array($value, $array, true); return in_array($value, $array, true);
} }
/** /**
* Returns the first item from the array or null if array is empty. * Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template T * @template K of int|string
* @param array<T> $array * @template V
* @return ?T * @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
*/ */
public static function first(array $array) public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{ {
return count($array) ? reset($array) : null; $key = self::firstKey($array, $predicate);
return $key === null
? ($else ? $else() : null)
: $array[$key];
} }
/** /**
* Returns the last item from the array or null if array is empty. * Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template T * @template K of int|string
* @param array<T> $array * @template V
* @return ?T * @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
*/ */
public static function last(array $array) public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{ {
return count($array) ? end($array) : null; $key = self::lastKey($array, $predicate);
return $key === null
? ($else ? $else() : null)
: $array[$key];
}
/**
* Returns the key of first item (matching the specified predicate if given) or null if there is no such item.
* @template K of int|string
* @template V
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?K
*/
public static function firstKey(array $array, ?callable $predicate = null): int|string|null
{
if (!$predicate) {
return array_key_first($array);
}
foreach ($array as $k => $v) {
if ($predicate($v, $k, $array)) {
return $k;
}
}
return null;
}
/**
* Returns the key of last item (matching the specified predicate if given) or null if there is no such item.
* @template K of int|string
* @template V
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?K
*/
public static function lastKey(array $array, ?callable $predicate = null): int|string|null
{
return $predicate
? self::firstKey(array_reverse($array, preserve_keys: true), $predicate)
: array_key_last($array);
} }
/** /**
* Inserts the contents of the $inserted array into the $array immediately after the $key. * Inserts the contents of the $inserted array into the $array immediately after the $key.
* If $key is null (or does not exist), it is inserted at the beginning. * If $key is null (or does not exist), it is inserted at the beginning.
* @param array-key|null $key
*/ */
public static function insertBefore(array &$array, $key, array $inserted): void public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
{ {
$offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key); $offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key);
$array = array_slice($array, 0, $offset, true) $array = array_slice($array, 0, $offset, preserve_keys: true)
+ $inserted + $inserted
+ array_slice($array, $offset, count($array), true); + array_slice($array, $offset, count($array), preserve_keys: true);
} }
/** /**
* Inserts the contents of the $inserted array into the $array before the $key. * Inserts the contents of the $inserted array into the $array before the $key.
* If $key is null (or does not exist), it is inserted at the end. * If $key is null (or does not exist), it is inserted at the end.
* @param array-key|null $key
*/ */
public static function insertAfter(array &$array, $key, array $inserted): void public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
{ {
if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) { if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) {
$offset = count($array) - 1; $offset = count($array) - 1;
} }
$array = array_slice($array, 0, $offset + 1, true) $array = array_slice($array, 0, $offset + 1, preserve_keys: true)
+ $inserted + $inserted
+ array_slice($array, $offset + 1, count($array), true); + array_slice($array, $offset + 1, count($array), preserve_keys: true);
} }
/** /**
* Renames key in array. * Renames key in array.
* @param array-key $oldKey
* @param array-key $newKey
*/ */
public static function renameKey(array &$array, $oldKey, $newKey): bool public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool
{ {
$offset = self::getKeyOffset($array, $oldKey); $offset = self::getKeyOffset($array, $oldKey);
if ($offset === null) { if ($offset === null) {
@@ -203,8 +246,14 @@ class Arrays
* @param string[] $array * @param string[] $array
* @return string[] * @return string[]
*/ */
public static function grep(array $array, string $pattern, int $flags = 0): array public static function grep(
array $array,
#[Language('RegExp')]
string $pattern,
bool|int $invert = false,
): array
{ {
$flags = $invert ? PREG_GREP_INVERT : 0;
return Strings::pcre('preg_grep', [$pattern, $array, $flags]); return Strings::pcre('preg_grep', [$pattern, $array, $flags]);
} }
@@ -225,23 +274,19 @@ class Arrays
/** /**
* Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list. * Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list.
* @param mixed $value * @return ($value is list ? true : false)
*/ */
public static function isList($value): bool public static function isList(mixed $value): bool
{ {
return is_array($value) && (PHP_VERSION_ID < 80100 return is_array($value) && array_is_list($value);
? !$value || array_keys($value) === range(0, count($value) - 1)
: array_is_list($value)
);
} }
/** /**
* Reformats table to associative tree. Path looks like 'field|field[]field->field=field'. * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'.
* @param string|string[] $path * @param string|string[] $path
* @return array|\stdClass
*/ */
public static function associate(array $array, $path) public static function associate(array $array, $path): array|\stdClass
{ {
$parts = is_array($path) $parts = is_array($path)
? $path ? $path
@@ -293,9 +338,8 @@ class Arrays
/** /**
* Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling. * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling.
* @param mixed $filling
*/ */
public static function normalize(array $array, $filling = null): array public static function normalize(array $array, mixed $filling = null): array
{ {
$res = []; $res = [];
foreach ($array as $k => $v) { foreach ($array as $k => $v) {
@@ -311,12 +355,11 @@ class Arrays
* or returns $default, if provided. * or returns $default, if provided.
* @template T * @template T
* @param array<T> $array * @param array<T> $array
* @param array-key $key
* @param ?T $default * @param ?T $default
* @return ?T * @return ?T
* @throws Nette\InvalidArgumentException if item does not exist and default value is not provided * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
*/ */
public static function pick(array &$array, $key, $default = null) public static function pick(array &$array, string|int $key, mixed $default = null): mixed
{ {
if (array_key_exists($key, $array)) { if (array_key_exists($key, $array)) {
$value = $array[$key]; $value = $array[$key];
@@ -333,13 +376,16 @@ class Arrays
/** /**
* Tests whether at least one element in the array passes the test implemented by the * Tests whether at least one element in the array passes the test implemented by the provided function.
* provided callback with signature `function ($value, $key, array $array): bool`. * @template K of int|string
* @template V
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): bool $predicate
*/ */
public static function some(iterable $array, callable $callback): bool public static function some(iterable $array, callable $predicate): bool
{ {
foreach ($array as $k => $v) { foreach ($array as $k => $v) {
if ($callback($v, $k, $array)) { if ($predicate($v, $k, $array)) {
return true; return true;
} }
} }
@@ -349,13 +395,16 @@ class Arrays
/** /**
* Tests whether all elements in the array pass the test implemented by the provided function, * Tests whether all elements in the array pass the test implemented by the provided function.
* which has the signature `function ($value, $key, array $array): bool`. * @template K of int|string
* @template V
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): bool $predicate
*/ */
public static function every(iterable $array, callable $callback): bool public static function every(iterable $array, callable $predicate): bool
{ {
foreach ($array as $k => $v) { foreach ($array as $k => $v) {
if (!$callback($v, $k, $array)) { if (!$predicate($v, $k, $array)) {
return false; return false;
} }
} }
@@ -365,14 +414,64 @@ class Arrays
/** /**
* Calls $callback on all elements in the array and returns the array of return values. * Returns a new array containing all key-value pairs matching the given $predicate.
* The callback has the signature `function ($value, $key, array $array): bool`. * @template K of int|string
* @template V
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): bool $predicate
* @return array<K, V>
*/ */
public static function map(iterable $array, callable $callback): array public static function filter(array $array, callable $predicate): array
{ {
$res = []; $res = [];
foreach ($array as $k => $v) { foreach ($array as $k => $v) {
$res[$k] = $callback($v, $k, $array); if ($predicate($v, $k, $array)) {
$res[$k] = $v;
}
}
return $res;
}
/**
* Returns an array containing the original keys and results of applying the given transform function to each element.
* @template K of int|string
* @template V
* @template R
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): R $transformer
* @return array<K, R>
*/
public static function map(iterable $array, callable $transformer): array
{
$res = [];
foreach ($array as $k => $v) {
$res[$k] = $transformer($v, $k, $array);
}
return $res;
}
/**
* Returns an array containing new keys and values generated by applying the given transform function to each element.
* If the function returns null, the element is skipped.
* @template K of int|string
* @template V
* @template ResK of int|string
* @template ResV
* @param array<K, V> $array
* @param callable(V, K, array<K, V>): ?array{ResK, ResV} $transformer
* @return array<ResK, ResV>
*/
public static function mapWithKeys(array $array, callable $transformer): array
{
$res = [];
foreach ($array as $k => $v) {
$pair = $transformer($v, $k, $array);
if ($pair) {
$res[$pair[0]] = $pair[1];
}
} }
return $res; return $res;
@@ -415,7 +514,7 @@ class Arrays
* @param T $object * @param T $object
* @return T * @return T
*/ */
public static function toObject(iterable $array, $object) public static function toObject(iterable $array, object $object): object
{ {
foreach ($array as $k => $v) { foreach ($array as $k => $v) {
$object->$k = $v; $object->$k = $v;
@@ -427,12 +526,10 @@ class Arrays
/** /**
* Converts value to array key. * Converts value to array key.
* @param mixed $value
* @return array-key
*/ */
public static function toKey($value) public static function toKey(mixed $value): int|string
{ {
return key([$value => null]); return key(@[$value => null]);
} }

View File

@@ -10,7 +10,7 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function is_array, is_object, is_string; use function explode, func_get_args, ini_get, is_array, is_callable, is_object, is_string, preg_replace, restore_error_handler, set_error_handler, sprintf, str_contains, str_ends_with;
/** /**
@@ -20,52 +20,10 @@ final class Callback
{ {
use Nette\StaticClass; use Nette\StaticClass;
/**
* @param string|object|callable $callable class, object, callable
* @deprecated use Closure::fromCallable()
*/
public static function closure($callable, ?string $method = null): \Closure
{
trigger_error(__METHOD__ . '() is deprecated, use Closure::fromCallable().', E_USER_DEPRECATED);
try {
return \Closure::fromCallable($method === null ? $callable : [$callable, $method]);
} catch (\TypeError $e) {
throw new Nette\InvalidArgumentException($e->getMessage());
}
}
/**
* Invokes callback.
* @return mixed
* @deprecated
*/
public static function invoke($callable, ...$args)
{
trigger_error(__METHOD__ . '() is deprecated, use native invoking.', E_USER_DEPRECATED);
self::check($callable);
return $callable(...$args);
}
/**
* Invokes callback with an array of parameters.
* @return mixed
* @deprecated
*/
public static function invokeArgs($callable, array $args = [])
{
trigger_error(__METHOD__ . '() is deprecated, use native invoking.', E_USER_DEPRECATED);
self::check($callable);
return $callable(...$args);
}
/** /**
* Invokes internal PHP function with own error handler. * Invokes internal PHP function with own error handler.
* @return mixed
*/ */
public static function invokeSafe(string $function, array $args, callable $onError) public static function invokeSafe(string $function, array $args, callable $onError): mixed
{ {
$prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool { $prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool {
if ($file === __FILE__) { if ($file === __FILE__) {
@@ -92,17 +50,16 @@ final class Callback
/** /**
* Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies * Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies
* that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists. * that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists.
* @param mixed $callable
* @return callable * @return callable
* @throws Nette\InvalidArgumentException * @throws Nette\InvalidArgumentException
*/ */
public static function check($callable, bool $syntax = false) public static function check(mixed $callable, bool $syntax = false)
{ {
if (!is_callable($callable, $syntax)) { if (!is_callable($callable, $syntax)) {
throw new Nette\InvalidArgumentException( throw new Nette\InvalidArgumentException(
$syntax $syntax
? 'Given value is not a callable type.' ? 'Given value is not a callable type.'
: sprintf("Callback '%s' is not callable.", self::toString($callable)) : sprintf("Callback '%s' is not callable.", self::toString($callable)),
); );
} }
@@ -112,15 +69,12 @@ final class Callback
/** /**
* Converts PHP callback to textual form. Class or method may not exists. * Converts PHP callback to textual form. Class or method may not exists.
* @param mixed $callable
*/ */
public static function toString($callable): string public static function toString(mixed $callable): string
{ {
if ($callable instanceof \Closure) { if ($callable instanceof \Closure) {
$inner = self::unwrap($callable); $inner = self::unwrap($callable);
return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}'); return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}');
} elseif (is_string($callable) && $callable[0] === "\0") {
return '{lambda}';
} else { } else {
is_callable(is_object($callable) ? [$callable, '__invoke'] : $callable, true, $textual); is_callable(is_object($callable) ? [$callable, '__invoke'] : $callable, true, $textual);
return $textual; return $textual;
@@ -131,21 +85,20 @@ final class Callback
/** /**
* Returns reflection for method or function used in PHP callback. * Returns reflection for method or function used in PHP callback.
* @param callable $callable type check is escalated to ReflectionException * @param callable $callable type check is escalated to ReflectionException
* @return \ReflectionMethod|\ReflectionFunction
* @throws \ReflectionException if callback is not valid * @throws \ReflectionException if callback is not valid
*/ */
public static function toReflection($callable): \ReflectionFunctionAbstract public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction
{ {
if ($callable instanceof \Closure) { if ($callable instanceof \Closure) {
$callable = self::unwrap($callable); $callable = self::unwrap($callable);
} }
if (is_string($callable) && strpos($callable, '::')) { if (is_string($callable) && str_contains($callable, '::')) {
return new \ReflectionMethod($callable); return new ReflectionMethod(...explode('::', $callable, 2));
} elseif (is_array($callable)) { } elseif (is_array($callable)) {
return new \ReflectionMethod($callable[0], $callable[1]); return new ReflectionMethod($callable[0], $callable[1]);
} elseif (is_object($callable) && !$callable instanceof \Closure) { } elseif (is_object($callable) && !$callable instanceof \Closure) {
return new \ReflectionMethod($callable, '__invoke'); return new ReflectionMethod($callable, '__invoke');
} else { } else {
return new \ReflectionFunction($callable); return new \ReflectionFunction($callable);
} }
@@ -157,25 +110,25 @@ final class Callback
*/ */
public static function isStatic(callable $callable): bool public static function isStatic(callable $callable): bool
{ {
return is_array($callable) ? is_string($callable[0]) : is_string($callable); return is_string(is_array($callable) ? $callable[0] : $callable);
} }
/** /**
* Unwraps closure created by Closure::fromCallable(). * Unwraps closure created by Closure::fromCallable().
* @return callable|array
*/ */
public static function unwrap(\Closure $closure) public static function unwrap(\Closure $closure): callable|array
{ {
$r = new \ReflectionFunction($closure); $r = new \ReflectionFunction($closure);
if (substr($r->name, -1) === '}') { $class = $r->getClosureScopeClass()?->name;
if (str_ends_with($r->name, '}')) {
return $closure; return $closure;
} elseif ($obj = $r->getClosureThis()) { } elseif (($obj = $r->getClosureThis()) && $obj::class === $class) {
return [$obj, $r->name]; return [$obj, $r->name];
} elseif ($class = $r->getClosureScopeClass()) { } elseif ($class) {
return [$class->name, $r->name]; return [$class, $r->name];
} else { } else {
return $r->name; return $r->name;

View File

@@ -9,7 +9,7 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use function array_merge, checkdate, implode, is_numeric, is_string, preg_replace_callback, sprintf, time, trim;
/** /**
@@ -17,8 +17,6 @@ use Nette;
*/ */
class DateTime extends \DateTime implements \JsonSerializable class DateTime extends \DateTime implements \JsonSerializable
{ {
use Nette\SmartObject;
/** minute in seconds */ /** minute in seconds */
public const MINUTE = 60; public const MINUTE = 60;
@@ -32,29 +30,27 @@ class DateTime extends \DateTime implements \JsonSerializable
public const WEEK = 7 * self::DAY; public const WEEK = 7 * self::DAY;
/** average month in seconds */ /** average month in seconds */
public const MONTH = 2629800; public const MONTH = 2_629_800;
/** average year in seconds */ /** average year in seconds */
public const YEAR = 31557600; public const YEAR = 31_557_600;
/** /**
* Creates a DateTime object from a string, UNIX timestamp, or other DateTimeInterface object. * Creates a DateTime object from a string, UNIX timestamp, or other DateTimeInterface object.
* @param string|int|\DateTimeInterface $time
* @return static
* @throws \Exception if the date and time are not valid. * @throws \Exception if the date and time are not valid.
*/ */
public static function from($time) public static function from(string|int|\DateTimeInterface|null $time): static
{ {
if ($time instanceof \DateTimeInterface) { if ($time instanceof \DateTimeInterface) {
return new static($time->format('Y-m-d H:i:s.u'), $time->getTimezone()); return static::createFromInterface($time);
} elseif (is_numeric($time)) { } elseif (is_numeric($time)) {
if ($time <= self::YEAR) { if ($time <= self::YEAR) {
$time += time(); $time += time();
} }
return (new static('@' . $time))->setTimezone(new \DateTimeZone(date_default_timezone_get())); return (new static)->setTimestamp((int) $time);
} else { // textual or null } else { // textual or null
return new static((string) $time); return new static((string) $time);
@@ -64,8 +60,7 @@ class DateTime extends \DateTime implements \JsonSerializable
/** /**
* Creates DateTime object. * Creates DateTime object.
* @return static * @throws \Exception if the date and time are not valid.
* @throws Nette\InvalidArgumentException if the date and time are not valid.
*/ */
public static function fromParts( public static function fromParts(
int $year, int $year,
@@ -73,50 +68,108 @@ class DateTime extends \DateTime implements \JsonSerializable
int $day, int $day,
int $hour = 0, int $hour = 0,
int $minute = 0, int $minute = 0,
float $second = 0.0 float $second = 0.0,
) { ): static
$s = sprintf('%04d-%02d-%02d %02d:%02d:%02.5F', $year, $month, $day, $hour, $minute, $second); {
if ( $sec = (int) floor($second);
!checkdate($month, $day, $year) return (new static(''))
|| $hour < 0 ->setDate($year, $month, $day)
|| $hour > 23 ->setTime($hour, $minute, $sec, (int) round(($second - $sec) * 1e6));
|| $minute < 0
|| $minute > 59
|| $second < 0
|| $second >= 60
) {
throw new Nette\InvalidArgumentException("Invalid date '$s'");
}
return new static($s);
} }
/** /**
* Returns new DateTime object formatted according to the specified format. * Returns a new DateTime object formatted according to the specified format.
* @param string $format The format the $time parameter should be in
* @param string $time
* @param string|\DateTimeZone $timezone (default timezone is used if null is passed)
* @return static|false
*/ */
#[\ReturnTypeWillChange] public static function createFromFormat(
public static function createFromFormat($format, $time, $timezone = null) string $format,
string $datetime,
string|\DateTimeZone|null $timezone = null,
): static|false
{ {
if ($timezone === null) { if (is_string($timezone)) {
$timezone = new \DateTimeZone(date_default_timezone_get());
} elseif (is_string($timezone)) {
$timezone = new \DateTimeZone($timezone); $timezone = new \DateTimeZone($timezone);
} elseif (!$timezone instanceof \DateTimeZone) {
throw new Nette\InvalidArgumentException('Invalid timezone given');
} }
$date = parent::createFromFormat($format, $time, $timezone); $date = parent::createFromFormat($format, $datetime, $timezone);
return $date ? static::from($date) : false; return $date ? static::from($date) : false;
} }
public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null)
{
$this->apply($datetime, $timezone, true);
}
public function modify(string $modifier): static
{
$this->apply($modifier);
return $this;
}
public function setDate(int $year, int $month, int $day): static
{
if (!checkdate($month, $day, $year)) {
throw new \Exception(sprintf('The date %04d-%02d-%02d is not valid.', $year, $month, $day));
}
return parent::setDate($year, $month, $day);
}
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
{
if (
$hour < 0 || $hour > 23
|| $minute < 0 || $minute > 59
|| $second < 0 || $second >= 60
|| $microsecond < 0 || $microsecond >= 1_000_000
) {
throw new \Exception(sprintf('The time %02d:%02d:%08.5F is not valid.', $hour, $minute, $second + $microsecond / 1_000_000));
}
return parent::setTime($hour, $minute, $second, $microsecond);
}
/**
* Converts a relative time string (e.g. '10 minut') to seconds.
*/
public static function relativeToSeconds(string $relativeTime): int
{
return (new self('@0 ' . $relativeTime))
->getTimestamp();
}
private function apply(string $datetime, $timezone = null, bool $ctr = false): void
{
$relPart = '';
$absPart = preg_replace_callback(
'/[+-]?\s*\d+\s+((microsecond|millisecond|[mµu]sec)s?|[mµ]s|sec(ond)?s?|min(ute)?s?|hours?)(\s+ago)?\b/iu',
function ($m) use (&$relPart) {
$relPart .= $m[0] . ' ';
return '';
},
$datetime,
);
if ($ctr) {
parent::__construct($absPart, $timezone);
$this->handleErrors($datetime);
} elseif (trim($absPart)) {
parent::modify($absPart) && $this->handleErrors($datetime);
}
if ($relPart) {
$timezone ??= $this->getTimezone();
$this->setTimezone(new \DateTimeZone('UTC'));
parent::modify($relPart) && $this->handleErrors($datetime);
$this->setTimezone($timezone);
}
}
/** /**
* Returns JSON representation in ISO 8601 (used by JavaScript). * Returns JSON representation in ISO 8601 (used by JavaScript).
*/ */
@@ -136,12 +189,21 @@ class DateTime extends \DateTime implements \JsonSerializable
/** /**
* Creates a copy with a modified time. * You'd better use: (clone $dt)->modify(...)
* @return static
*/ */
public function modifyClone(string $modify = '') public function modifyClone(string $modify = ''): static
{ {
$dolly = clone $this; $dolly = clone $this;
return $modify ? $dolly->modify($modify) : $dolly; return $modify ? $dolly->modify($modify) : $dolly;
} }
private function handleErrors(string $value): void
{
$errors = self::getLastErrors();
$errors = array_merge($errors['errors'] ?? [], $errors['warnings'] ?? []);
if ($errors) {
throw new \Exception(implode(', ', $errors) . " '$value'");
}
}
} }

View File

@@ -10,6 +10,8 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function array_pop, chmod, decoct, dirname, end, fclose, file_exists, file_get_contents, file_put_contents, fopen, implode, is_dir, is_file, is_link, mkdir, preg_match, preg_split, realpath, rename, rmdir, rtrim, sprintf, str_replace, stream_copy_to_stream, stream_is_local, strtr;
use const DIRECTORY_SEPARATOR;
/** /**
@@ -17,27 +19,25 @@ use Nette;
*/ */
final class FileSystem final class FileSystem
{ {
use Nette\StaticClass;
/** /**
* Creates a directory if it doesn't exist. * Creates a directory if it does not exist, including parent directories.
* @throws Nette\IOException on error occurred * @throws Nette\IOException on error occurred
*/ */
public static function createDir(string $dir, int $mode = 0777): void public static function createDir(string $dir, int $mode = 0o777): void
{ {
if (!is_dir($dir) && !@mkdir($dir, $mode, true) && !is_dir($dir)) { // @ - dir may already exist if (!is_dir($dir) && !@mkdir($dir, $mode, recursive: true) && !is_dir($dir)) { // @ - dir may already exist
throw new Nette\IOException(sprintf( throw new Nette\IOException(sprintf(
"Unable to create directory '%s' with mode %s. %s", "Unable to create directory '%s' with mode %s. %s",
self::normalizePath($dir), self::normalizePath($dir),
decoct($mode), decoct($mode),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} }
/** /**
* Copies a file or a directory. Overwrites existing files and directories by default. * Copies a file or an entire directory. Overwrites existing files and directories by default.
* @throws Nette\IOException on error occurred * @throws Nette\IOException on error occurred
* @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists * @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists
*/ */
@@ -64,16 +64,12 @@ final class FileSystem
} }
} else { } else {
static::createDir(dirname($target)); static::createDir(dirname($target));
if ( if (@stream_copy_to_stream(static::open($origin, 'rb'), static::open($target, 'wb')) === false) { // @ is escalated to exception
($s = @fopen($origin, 'rb'))
&& ($d = @fopen($target, 'wb'))
&& @stream_copy_to_stream($s, $d) === false
) { // @ is escalated to exception
throw new Nette\IOException(sprintf( throw new Nette\IOException(sprintf(
"Unable to copy file '%s' to '%s'. %s", "Unable to copy file '%s' to '%s'. %s",
self::normalizePath($origin), self::normalizePath($origin),
self::normalizePath($target), self::normalizePath($target),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} }
@@ -81,7 +77,26 @@ final class FileSystem
/** /**
* Deletes a file or directory if exists. * Opens file and returns resource.
* @return resource
* @throws Nette\IOException on error occurred
*/
public static function open(string $path, string $mode)
{
$f = @fopen($path, $mode); // @ is escalated to exception
if (!$f) {
throw new Nette\IOException(sprintf(
"Unable to open file '%s'. %s",
self::normalizePath($path),
Helpers::getLastError(),
));
}
return $f;
}
/**
* Deletes a file or an entire directory if exists. If the directory is not empty, it deletes its contents first.
* @throws Nette\IOException on error occurred * @throws Nette\IOException on error occurred
*/ */
public static function delete(string $path): void public static function delete(string $path): void
@@ -92,7 +107,7 @@ final class FileSystem
throw new Nette\IOException(sprintf( throw new Nette\IOException(sprintf(
"Unable to delete '%s'. %s", "Unable to delete '%s'. %s",
self::normalizePath($path), self::normalizePath($path),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} elseif (is_dir($path)) { } elseif (is_dir($path)) {
@@ -104,7 +119,7 @@ final class FileSystem
throw new Nette\IOException(sprintf( throw new Nette\IOException(sprintf(
"Unable to delete directory '%s'. %s", "Unable to delete directory '%s'. %s",
self::normalizePath($path), self::normalizePath($path),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} }
@@ -135,7 +150,7 @@ final class FileSystem
"Unable to rename file or directory '%s' to '%s'. %s", "Unable to rename file or directory '%s' to '%s'. %s",
self::normalizePath($origin), self::normalizePath($origin),
self::normalizePath($target), self::normalizePath($target),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} }
@@ -153,7 +168,7 @@ final class FileSystem
throw new Nette\IOException(sprintf( throw new Nette\IOException(sprintf(
"Unable to read file '%s'. %s", "Unable to read file '%s'. %s",
self::normalizePath($file), self::normalizePath($file),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
@@ -161,18 +176,49 @@ final class FileSystem
} }
/**
* Reads the file content line by line. Because it reads continuously as we iterate over the lines,
* it is possible to read files larger than the available memory.
* @return \Generator<int, string>
* @throws Nette\IOException on error occurred
*/
public static function readLines(string $file, bool $stripNewLines = true): \Generator
{
return (function ($f) use ($file, $stripNewLines) {
$counter = 0;
do {
$line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf(
"Unable to read file '%s'. %s",
self::normalizePath($file),
$error,
)));
if ($line === false) {
fclose($f);
break;
}
if ($stripNewLines) {
$line = rtrim($line, "\r\n");
}
yield $counter++ => $line;
} while (true);
})(static::open($file, 'r'));
}
/** /**
* Writes the string to a file. * Writes the string to a file.
* @throws Nette\IOException on error occurred * @throws Nette\IOException on error occurred
*/ */
public static function write(string $file, string $content, ?int $mode = 0666): void public static function write(string $file, string $content, ?int $mode = 0o666): void
{ {
static::createDir(dirname($file)); static::createDir(dirname($file));
if (@file_put_contents($file, $content) === false) { // @ is escalated to exception if (@file_put_contents($file, $content) === false) { // @ is escalated to exception
throw new Nette\IOException(sprintf( throw new Nette\IOException(sprintf(
"Unable to write file '%s'. %s", "Unable to write file '%s'. %s",
self::normalizePath($file), self::normalizePath($file),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
@@ -181,17 +227,18 @@ final class FileSystem
"Unable to chmod file '%s' to mode %s. %s", "Unable to chmod file '%s' to mode %s. %s",
self::normalizePath($file), self::normalizePath($file),
decoct($mode), decoct($mode),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} }
/** /**
* Fixes permissions to a specific file or directory. Directories can be fixed recursively. * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`.
* Recursively traverses and sets permissions on the entire contents of the directory as well.
* @throws Nette\IOException on error occurred * @throws Nette\IOException on error occurred
*/ */
public static function makeWritable(string $path, int $dirMode = 0777, int $fileMode = 0666): void public static function makeWritable(string $path, int $dirMode = 0o777, int $fileMode = 0o666): void
{ {
if (is_file($path)) { if (is_file($path)) {
if (!@chmod($path, $fileMode)) { // @ is escalated to exception if (!@chmod($path, $fileMode)) { // @ is escalated to exception
@@ -199,7 +246,7 @@ final class FileSystem
"Unable to chmod file '%s' to mode %s. %s", "Unable to chmod file '%s' to mode %s. %s",
self::normalizePath($path), self::normalizePath($path),
decoct($fileMode), decoct($fileMode),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} elseif (is_dir($path)) { } elseif (is_dir($path)) {
@@ -212,7 +259,7 @@ final class FileSystem
"Unable to chmod directory '%s' to mode %s. %s", "Unable to chmod directory '%s' to mode %s. %s",
self::normalizePath($path), self::normalizePath($path),
decoct($dirMode), decoct($dirMode),
Helpers::getLastError() Helpers::getLastError(),
)); ));
} }
} else { } else {
@@ -226,7 +273,7 @@ final class FileSystem
*/ */
public static function isAbsolute(string $path): bool public static function isAbsolute(string $path): bool
{ {
return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path); return (bool) preg_match('#([a-z]:)?[/\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
} }
@@ -235,7 +282,7 @@ final class FileSystem
*/ */
public static function normalizePath(string $path): string public static function normalizePath(string $path): string
{ {
$parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path); $parts = $path === '' ? [] : preg_split('~[/\\\]+~', $path);
$res = []; $res = [];
foreach ($parts as $part) { foreach ($parts as $part) {
if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') { if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
@@ -258,4 +305,37 @@ final class FileSystem
{ {
return self::normalizePath(implode('/', $paths)); return self::normalizePath(implode('/', $paths));
} }
/**
* Resolves a path against a base path. If the path is absolute, returns it directly, if it's relative, joins it with the base path.
*/
public static function resolvePath(string $basePath, string $path): string
{
return match (true) {
self::isAbsolute($path) => self::platformSlashes($path),
$path === '' => self::platformSlashes($basePath),
default => self::joinPaths($basePath, $path),
};
}
/**
* Converts backslashes to slashes.
*/
public static function unixSlashes(string $path): string
{
return strtr($path, '\\', '/');
}
/**
* Converts slashes to platform-specific directory separators.
*/
public static function platformSlashes(string $path): string
{
return DIRECTORY_SEPARATOR === '/'
? strtr($path, '\\', '/')
: str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
}
} }

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function abs, is_finite, is_nan, max, round;
/** /**

View File

@@ -10,16 +10,21 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function array_unique, ini_get, levenshtein, max, min, ob_end_clean, ob_get_clean, ob_start, preg_replace, strlen;
use const PHP_OS_FAMILY;
class Helpers class Helpers
{ {
public const IsWindows = PHP_OS_FAMILY === 'Windows';
/** /**
* Executes a callback and returns the captured output as a string. * Executes a callback and returns the captured output as a string.
*/ */
public static function capture(callable $func): string public static function capture(callable $func): string
{ {
ob_start(function () {}); ob_start(fn() => '');
try { try {
$func(); $func();
return ob_get_clean(); return ob_get_clean();
@@ -45,10 +50,8 @@ class Helpers
/** /**
* Converts false to null, does not change other values. * Converts false to null, does not change other values.
* @param mixed $value
* @return mixed
*/ */
public static function falseToNull($value) public static function falseToNull(mixed $value): mixed
{ {
return $value === false ? null : $value; return $value === false ? null : $value;
} }
@@ -56,12 +59,8 @@ class Helpers
/** /**
* Returns value clamped to the inclusive range of min and max. * Returns value clamped to the inclusive range of min and max.
* @param int|float $value
* @param int|float $min
* @param int|float $max
* @return int|float
*/ */
public static function clamp($value, $min, $max) public static function clamp(int|float $value, int|float $min, int|float $max): int|float
{ {
if ($min > $max) { if ($min > $max) {
throw new Nette\InvalidArgumentException("Minimum ($min) is not less than maximum ($max)."); throw new Nette\InvalidArgumentException("Minimum ($min) is not less than maximum ($max).");
@@ -88,4 +87,35 @@ class Helpers
return $best; return $best;
} }
/**
* Compares two values in the same way that PHP does. Recognizes operators: >, >=, <, <=, =, ==, ===, !=, !==, <>
*/
public static function compare(mixed $left, string $operator, mixed $right): bool
{
return match ($operator) {
'>' => $left > $right,
'>=' => $left >= $right,
'<' => $left < $right,
'<=' => $left <= $right,
'=', '==' => $left == $right,
'===' => $left === $right,
'!=', '<>' => $left != $right,
'!==' => $left !== $right,
default => throw new Nette\InvalidArgumentException("Unknown operator '$operator'"),
};
}
/**
* Splits a class name into namespace and short class name.
* @return array{string, string}
*/
public static function splitClassName(string $name): array
{
return ($pos = strrpos($name, '\\')) === false
? ['', $name]
: [substr($name, 0, $pos), substr($name, $pos + 1)];
}
} }

View File

@@ -9,9 +9,9 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette;
use Nette\HtmlStringable; use Nette\HtmlStringable;
use function is_array, is_float, is_object, is_string; use function array_merge, array_splice, count, explode, func_num_args, html_entity_decode, htmlspecialchars, http_build_query, implode, is_array, is_bool, is_float, is_object, is_string, json_encode, max, number_format, rtrim, str_contains, str_repeat, str_replace, strip_tags, strncmp, strpbrk, substr;
use const ENT_HTML5, ENT_NOQUOTES, ENT_QUOTES;
/** /**
@@ -233,37 +233,30 @@ use function is_array, is_float, is_object, is_string;
*/ */
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
{ {
use Nette\SmartObject;
/** @var array<string, mixed> element's attributes */ /** @var array<string, mixed> element's attributes */
public $attrs = []; public array $attrs = [];
/** @var bool use XHTML syntax? */ /** void elements */
public static $xhtml = false; public static array $emptyElements = [
/** @var array<string, int> void elements */
public static $emptyElements = [
'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1, 'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1, 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1, 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
]; ];
/** @var array<int, HtmlStringable|string> nodes */ /** @var array<int, HtmlStringable|string> nodes */
protected $children = []; protected array $children = [];
/** @var string element's name */ /** element's name */
private $name; private string $name = '';
/** @var bool is element empty? */ private bool $isEmpty = false;
private $isEmpty;
/** /**
* Constructs new HTML element. * Constructs new HTML element.
* @param array|string $attrs element's attributes or plain text content * @param array|string $attrs element's attributes or plain text content
* @return static
*/ */
public static function el(?string $name = null, $attrs = null) public static function el(?string $name = null, array|string|null $attrs = null): static
{ {
$el = new static; $el = new static;
$parts = explode(' ', (string) $name, 2); $parts = explode(' ', (string) $name, 2);
@@ -289,7 +282,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Returns an object representing HTML text. * Returns an object representing HTML text.
*/ */
public static function fromHtml(string $html): self public static function fromHtml(string $html): static
{ {
return (new static)->setHtml($html); return (new static)->setHtml($html);
} }
@@ -298,7 +291,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Returns an object representing plain text. * Returns an object representing plain text.
*/ */
public static function fromText(string $text): self public static function fromText(string $text): static
{ {
return (new static)->setText($text); return (new static)->setText($text);
} }
@@ -333,9 +326,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Changes element's name. * Changes element's name.
* @return static
*/ */
final public function setName(string $name, ?bool $isEmpty = null) final public function setName(string $name, ?bool $isEmpty = null): static
{ {
$this->name = $name; $this->name = $name;
$this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]); $this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);
@@ -363,9 +355,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Sets multiple attributes. * Sets multiple attributes.
* @return static
*/ */
public function addAttributes(array $attrs) public function addAttributes(array $attrs): static
{ {
$this->attrs = array_merge($this->attrs, $attrs); $this->attrs = array_merge($this->attrs, $attrs);
return $this; return $this;
@@ -374,11 +365,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Appends value to element's attribute. * Appends value to element's attribute.
* @param mixed $value
* @param mixed $option
* @return static
*/ */
public function appendAttribute(string $name, $value, $option = true) public function appendAttribute(string $name, mixed $value, mixed $option = true): static
{ {
if (is_array($value)) { if (is_array($value)) {
$prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : []; $prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];
@@ -400,10 +388,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Sets element's attribute. * Sets element's attribute.
* @param mixed $value
* @return static
*/ */
public function setAttribute(string $name, $value) public function setAttribute(string $name, mixed $value): static
{ {
$this->attrs[$name] = $value; $this->attrs[$name] = $value;
return $this; return $this;
@@ -412,9 +398,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Returns element's attribute. * Returns element's attribute.
* @return mixed
*/ */
public function getAttribute(string $name) public function getAttribute(string $name): mixed
{ {
return $this->attrs[$name] ?? null; return $this->attrs[$name] ?? null;
} }
@@ -422,9 +407,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Unsets element's attribute. * Unsets element's attribute.
* @return static
*/ */
public function removeAttribute(string $name) public function removeAttribute(string $name): static
{ {
unset($this->attrs[$name]); unset($this->attrs[$name]);
return $this; return $this;
@@ -433,9 +417,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Unsets element's attributes. * Unsets element's attributes.
* @return static
*/ */
public function removeAttributes(array $attributes) public function removeAttributes(array $attributes): static
{ {
foreach ($attributes as $name) { foreach ($attributes as $name) {
unset($this->attrs[$name]); unset($this->attrs[$name]);
@@ -447,9 +430,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Overloaded setter for element's attribute. * Overloaded setter for element's attribute.
* @param mixed $value
*/ */
final public function __set(string $name, $value): void final public function __set(string $name, mixed $value): void
{ {
$this->attrs[$name] = $value; $this->attrs[$name] = $value;
} }
@@ -457,9 +439,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Overloaded getter for element's attribute. * Overloaded getter for element's attribute.
* @return mixed
*/ */
final public function &__get(string $name) final public function &__get(string $name): mixed
{ {
return $this->attrs[$name]; return $this->attrs[$name];
} }
@@ -485,9 +466,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Overloaded setter for element's attribute. * Overloaded setter for element's attribute.
* @return mixed
*/ */
final public function __call(string $m, array $args) final public function __call(string $m, array $args): mixed
{ {
$p = substr($m, 0, 3); $p = substr($m, 0, 3);
if ($p === 'get' || $p === 'set' || $p === 'add') { if ($p === 'get' || $p === 'set' || $p === 'add') {
@@ -516,9 +496,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Special setter for element's attribute. * Special setter for element's attribute.
* @return static
*/ */
final public function href(string $path, ?array $query = null) final public function href(string $path, array $query = []): static
{ {
if ($query) { if ($query) {
$query = http_build_query($query, '', '&'); $query = http_build_query($query, '', '&');
@@ -534,10 +513,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'. * Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'.
* @param mixed $value
* @return static
*/ */
public function data(string $name, $value = null) public function data(string $name, mixed $value = null): static
{ {
if (func_num_args() === 1) { if (func_num_args() === 1) {
$this->attrs['data'] = $name; $this->attrs['data'] = $name;
@@ -553,10 +530,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Sets element's HTML content. * Sets element's HTML content.
* @param HtmlStringable|string $html
* @return static
*/ */
final public function setHtml($html) final public function setHtml(mixed $html): static
{ {
$this->children = [(string) $html]; $this->children = [(string) $html];
return $this; return $this;
@@ -574,10 +549,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Sets element's textual content. * Sets element's textual content.
* @param HtmlStringable|string|int|float $text
* @return static
*/ */
final public function setText($text) final public function setText(mixed $text): static
{ {
if (!$text instanceof HtmlStringable) { if (!$text instanceof HtmlStringable) {
$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
@@ -599,10 +572,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Adds new element's child. * Adds new element's child.
* @param HtmlStringable|string $child Html node or raw HTML string
* @return static
*/ */
final public function addHtml($child) final public function addHtml(HtmlStringable|string $child): static
{ {
return $this->insert(null, $child); return $this->insert(null, $child);
} }
@@ -610,10 +581,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Appends plain-text string to element content. * Appends plain-text string to element content.
* @param HtmlStringable|string|int|float $text
* @return static
*/ */
public function addText($text) public function addText(\Stringable|string|int|null $text): static
{ {
if (!$text instanceof HtmlStringable) { if (!$text instanceof HtmlStringable) {
$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
@@ -625,10 +594,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Creates and adds a new Html child. * Creates and adds a new Html child.
* @param array|string $attrs element's attributes or raw HTML string
* @return static created element
*/ */
final public function create(string $name, $attrs = null) final public function create(string $name, array|string|null $attrs = null): static
{ {
$this->insert(null, $child = static::el($name, $attrs)); $this->insert(null, $child = static::el($name, $attrs));
return $child; return $child;
@@ -637,10 +604,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/** /**
* Inserts child node. * Inserts child node.
* @param HtmlStringable|string $child Html node or raw HTML string
* @return static
*/ */
public function insert(?int $index, $child, bool $replace = false) public function insert(?int $index, HtmlStringable|string $child, bool $replace = false): static
{ {
$child = $child instanceof self ? $child : (string) $child; $child = $child instanceof self ? $child : (string) $child;
if ($index === null) { // append if ($index === null) { // append
@@ -661,17 +626,15 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
*/ */
final public function offsetSet($index, $child): void final public function offsetSet($index, $child): void
{ {
$this->insert($index, $child, true); $this->insert($index, $child, replace: true);
} }
/** /**
* Returns child node (\ArrayAccess implementation). * Returns child node (\ArrayAccess implementation).
* @param int $index * @param int $index
* @return HtmlStringable|string
*/ */
#[\ReturnTypeWillChange] final public function offsetGet($index): HtmlStringable|string
final public function offsetGet($index)
{ {
return $this->children[$index]; return $this->children[$index];
} }
@@ -771,16 +734,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
final public function __toString(): string final public function __toString(): string
{ {
try { return $this->render();
return $this->render();
} catch (\Throwable $e) {
if (PHP_VERSION_ID >= 70400) {
throw $e;
}
trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
return '';
}
} }
@@ -790,7 +744,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
final public function startTag(): string final public function startTag(): string
{ {
return $this->name return $this->name
? '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>') ? '<' . $this->name . $this->attributes() . '>'
: ''; : '';
} }
@@ -821,11 +775,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
continue; continue;
} elseif ($value === true) { } elseif ($value === true) {
if (static::$xhtml) { $s .= ' ' . $key;
$s .= ' ' . $key . '="' . $key . '"';
} else {
$s .= ' ' . $key;
}
continue; continue;
@@ -857,14 +807,14 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
$value = (string) $value; $value = (string) $value;
} }
$q = strpos($value, '"') === false ? '"' : "'"; $q = str_contains($value, '"') ? "'" : '"';
$s .= ' ' . $key . '=' . $q $s .= ' ' . $key . '=' . $q
. str_replace( . str_replace(
['&', $q, '<'], ['&', $q, '<'],
['&amp;', $q === '"' ? '&quot;' : '&#39;', self::$xhtml ? '&lt;' : '<'], ['&amp;', $q === '"' ? '&quot;' : '&#39;', '<'],
$value $value,
) )
. (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '') . (str_contains($value, '`') && strpbrk($value, ' <>"\'') === false ? ' ' : '')
. $q; . $q;
} }

View File

@@ -10,6 +10,8 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function is_array, is_int, is_string;
use const IMG_BMP, IMG_FLIP_BOTH, IMG_FLIP_HORIZONTAL, IMG_FLIP_VERTICAL, IMG_GIF, IMG_JPG, IMG_PNG, IMG_WEBP, PATHINFO_EXTENSION;
/** /**
@@ -22,116 +24,128 @@ use Nette;
* $image->send(); * $image->send();
* </code> * </code>
* *
* @method Image affine(array $affine, array $clip = null) * @method Image affine(array $affine, ?array $clip = null)
* @method array affineMatrixConcat(array $m1, array $m2) * @method void alphaBlending(bool $enable)
* @method array affineMatrixGet(int $type, mixed $options = null) * @method void antialias(bool $enable)
* @method void alphaBlending(bool $on) * @method void arc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color)
* @method void antialias(bool $on) * @method int colorAllocate(int $red, int $green, int $blue)
* @method void arc($x, $y, $w, $h, $start, $end, $color) * @method int colorAllocateAlpha(int $red, int $green, int $blue, int $alpha)
* @method void char(int $font, $x, $y, string $char, $color) * @method int colorAt(int $x, int $y)
* @method void charUp(int $font, $x, $y, string $char, $color) * @method int colorClosest(int $red, int $green, int $blue)
* @method int colorAllocate($red, $green, $blue) * @method int colorClosestAlpha(int $red, int $green, int $blue, int $alpha)
* @method int colorAllocateAlpha($red, $green, $blue, $alpha) * @method int colorClosestHWB(int $red, int $green, int $blue)
* @method int colorAt($x, $y) * @method void colorDeallocate(int $color)
* @method int colorClosest($red, $green, $blue) * @method int colorExact(int $red, int $green, int $blue)
* @method int colorClosestAlpha($red, $green, $blue, $alpha) * @method int colorExactAlpha(int $red, int $green, int $blue, int $alpha)
* @method int colorClosestHWB($red, $green, $blue)
* @method void colorDeallocate($color)
* @method int colorExact($red, $green, $blue)
* @method int colorExactAlpha($red, $green, $blue, $alpha)
* @method void colorMatch(Image $image2) * @method void colorMatch(Image $image2)
* @method int colorResolve($red, $green, $blue) * @method int colorResolve(int $red, int $green, int $blue)
* @method int colorResolveAlpha($red, $green, $blue, $alpha) * @method int colorResolveAlpha(int $red, int $green, int $blue, int $alpha)
* @method void colorSet($index, $red, $green, $blue) * @method void colorSet(int $index, int $red, int $green, int $blue, int $alpha = 0)
* @method array colorsForIndex($index) * @method array colorsForIndex(int $color)
* @method int colorsTotal() * @method int colorsTotal()
* @method int colorTransparent($color = null) * @method int colorTransparent(?int $color = null)
* @method void convolution(array $matrix, float $div, float $offset) * @method void convolution(array $matrix, float $div, float $offset)
* @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH) * @method void copy(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH)
* @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyMerge(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
* @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyMergeGray(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
* @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method void copyResampled(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
* @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method void copyResized(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
* @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1) * @method Image cropAuto(int $mode = IMG_CROP_DEFAULT, float $threshold = .5, ?ImageColor $color = null)
* @method void ellipse($cx, $cy, $w, $h, $color) * @method void ellipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
* @method void fill($x, $y, $color) * @method void fill(int $x, int $y, ImageColor $color)
* @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style) * @method void filledArc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color, int $style)
* @method void filledEllipse($cx, $cy, $w, $h, $color) * @method void filledEllipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
* @method void filledPolygon(array $points, $numPoints, $color) * @method void filledPolygon(array $points, ImageColor $color)
* @method void filledRectangle($x1, $y1, $x2, $y2, $color) * @method void filledRectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method void fillToBorder($x, $y, $border, $color) * @method void fillToBorder(int $x, int $y, ImageColor $borderColor, ImageColor $color)
* @method void filter($filtertype) * @method void filter(int $filter, ...$args)
* @method void flip(int $mode) * @method void flip(int $mode)
* @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null) * @method array ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array $options = [])
* @method void gammaCorrect(float $inputgamma, float $outputgamma) * @method void gammaCorrect(float $inputgamma, float $outputgamma)
* @method array getClip() * @method array getClip()
* @method int interlace($interlace = null) * @method int getInterpolation()
* @method int interlace(?bool $enable = null)
* @method bool isTrueColor() * @method bool isTrueColor()
* @method void layerEffect($effect) * @method void layerEffect(int $effect)
* @method void line($x1, $y1, $x2, $y2, $color) * @method void line(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method void openPolygon(array $points, int $num_points, int $color) * @method void openPolygon(array $points, ImageColor $color)
* @method void paletteCopy(Image $source) * @method void paletteCopy(Image $source)
* @method void paletteToTrueColor() * @method void paletteToTrueColor()
* @method void polygon(array $points, $numPoints, $color) * @method void polygon(array $points, ImageColor $color)
* @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null) * @method void rectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method void rectangle($x1, $y1, $x2, $y2, $col) * @method mixed resolution(?int $resolutionX = null, ?int $resolutionY = null)
* @method mixed resolution(int $res_x = null, int $res_y = null) * @method Image rotate(float $angle, ImageColor $backgroundColor)
* @method Image rotate(float $angle, $backgroundColor) * @method void saveAlpha(bool $enable)
* @method void saveAlpha(bool $saveflag)
* @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED) * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED)
* @method void setBrush(Image $brush) * @method void setBrush(Image $brush)
* @method void setClip(int $x1, int $y1, int $x2, int $y2) * @method void setClip(int $x1, int $y1, int $x2, int $y2)
* @method void setInterpolation(int $method = IMG_BILINEAR_FIXED) * @method void setInterpolation(int $method = IMG_BILINEAR_FIXED)
* @method void setPixel($x, $y, $color) * @method void setPixel(int $x, int $y, ImageColor $color)
* @method void setStyle(array $style) * @method void setStyle(array $style)
* @method void setThickness($thickness) * @method void setThickness(int $thickness)
* @method void setTile(Image $tile) * @method void setTile(Image $tile)
* @method void string($font, $x, $y, string $s, $col) * @method void trueColorToPalette(bool $dither, int $ncolors)
* @method void stringUp($font, $x, $y, string $s, $col) * @method array ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array $options = [])
* @method void trueColorToPalette(bool $dither, $ncolors) * @property-read positive-int $width
* @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text) * @property-read positive-int $height
* @property-read int $width * @property-read \GdImage $imageResource
* @property-read int $height
* @property-read resource|\GdImage $imageResource
*/ */
class Image class Image
{ {
use Nette\SmartObject; use Nette\SmartObject;
/** {@link resize()} only shrinks images */ /** Prevent from getting resized to a bigger size than the original */
public const SHRINK_ONLY = 0b0001; public const ShrinkOnly = 0b0001;
/** {@link resize()} will ignore aspect ratio */ /** Resizes to a specified width and height without keeping aspect ratio */
public const STRETCH = 0b0010; public const Stretch = 0b0010;
/** {@link resize()} fits in given area so its dimensions are less than or equal to the required dimensions */ /** Resizes to fit into a specified width and height and preserves aspect ratio */
public const FIT = 0b0000; public const OrSmaller = 0b0000;
/** {@link resize()} fills given area so its dimensions are greater than or equal to the required dimensions */ /** Resizes while bounding the smaller dimension to the specified width or height and preserves aspect ratio */
public const FILL = 0b0100; public const OrBigger = 0b0100;
/** {@link resize()} fills given area exactly */ /** Resizes to the smallest possible size to completely cover specified width and height and reserves aspect ratio */
public const EXACT = 0b1000; public const Cover = 0b1000;
/** @deprecated use Image::ShrinkOnly */
public const SHRINK_ONLY = self::ShrinkOnly;
/** @deprecated use Image::Stretch */
public const STRETCH = self::Stretch;
/** @deprecated use Image::OrSmaller */
public const FIT = self::OrSmaller;
/** @deprecated use Image::OrBigger */
public const FILL = self::OrBigger;
/** @deprecated use Image::Cover */
public const EXACT = self::Cover;
/** @deprecated use Image::EmptyGIF */
public const EMPTY_GIF = self::EmptyGIF;
/** image types */ /** image types */
public const public const
JPEG = IMAGETYPE_JPEG, JPEG = ImageType::JPEG,
PNG = IMAGETYPE_PNG, PNG = ImageType::PNG,
GIF = IMAGETYPE_GIF, GIF = ImageType::GIF,
WEBP = IMAGETYPE_WEBP, WEBP = ImageType::WEBP,
AVIF = 19, // IMAGETYPE_AVIF, AVIF = ImageType::AVIF,
BMP = IMAGETYPE_BMP; BMP = ImageType::BMP;
public const EMPTY_GIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"; public const EmptyGIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;";
private const Formats = [self::JPEG => 'jpeg', self::PNG => 'png', self::GIF => 'gif', self::WEBP => 'webp', self::AVIF => 'avif', self::BMP => 'bmp']; private const Formats = [ImageType::JPEG => 'jpeg', ImageType::PNG => 'png', ImageType::GIF => 'gif', ImageType::WEBP => 'webp', ImageType::AVIF => 'avif', ImageType::BMP => 'bmp'];
/** @var resource|\GdImage */ private \GdImage $image;
private $image;
/** /**
* Returns RGB color (0..255) and transparency (0..127). * Returns RGB color (0..255) and transparency (0..127).
* @deprecated use ImageColor::rgb()
*/ */
public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array
{ {
@@ -148,14 +162,10 @@ class Image
* Reads an image from a file and returns its type in $type. * Reads an image from a file and returns its type in $type.
* @throws Nette\NotSupportedException if gd extension is not loaded * @throws Nette\NotSupportedException if gd extension is not loaded
* @throws UnknownImageFileException if file not found or file type is not known * @throws UnknownImageFileException if file not found or file type is not known
* @return static
*/ */
public static function fromFile(string $file, ?int &$type = null) public static function fromFile(string $file, ?int &$type = null): static
{ {
if (!extension_loaded('gd')) { self::ensureExtension();
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
$type = self::detectTypeFromFile($file); $type = self::detectTypeFromFile($file);
if (!$type) { if (!$type) {
throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found."); throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
@@ -167,16 +177,12 @@ class Image
/** /**
* Reads an image from a string and returns its type in $type. * Reads an image from a string and returns its type in $type.
* @return static
* @throws Nette\NotSupportedException if gd extension is not loaded * @throws Nette\NotSupportedException if gd extension is not loaded
* @throws ImageException * @throws ImageException
*/ */
public static function fromString(string $s, ?int &$type = null) public static function fromString(string $s, ?int &$type = null): static
{ {
if (!extension_loaded('gd')) { self::ensureExtension();
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
$type = self::detectTypeFromString($s); $type = self::detectTypeFromString($s);
if (!$type) { if (!$type) {
throw new UnknownImageFileException('Unknown type of image.'); throw new UnknownImageFileException('Unknown type of image.');
@@ -186,7 +192,7 @@ class Image
} }
private static function invokeSafe(string $func, string $arg, string $message, string $callee): self private static function invokeSafe(string $func, string $arg, string $message, string $callee): static
{ {
$errors = []; $errors = [];
$res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void { $res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void {
@@ -205,54 +211,54 @@ class Image
/** /**
* Creates a new true color image of the given dimensions. The default color is black. * Creates a new true color image of the given dimensions. The default color is black.
* @return static * @param positive-int $width
* @param positive-int $height
* @throws Nette\NotSupportedException if gd extension is not loaded * @throws Nette\NotSupportedException if gd extension is not loaded
*/ */
public static function fromBlank(int $width, int $height, ?array $color = null) public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
{ {
if (!extension_loaded('gd')) { self::ensureExtension();
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
if ($width < 1 || $height < 1) { if ($width < 1 || $height < 1) {
throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.'); throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.');
} }
$image = imagecreatetruecolor($width, $height); $image = new static(imagecreatetruecolor($width, $height));
if ($color) { if ($color) {
$color += ['alpha' => 0]; $image->alphablending(false);
$color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']); $image->filledrectangle(0, 0, $width - 1, $height - 1, $color);
imagealphablending($image, false); $image->alphablending(true);
imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color);
imagealphablending($image, true);
} }
return new static($image); return $image;
} }
/** /**
* Returns the type of image from file. * Returns the type of image from file.
* @return ImageType::*|null
*/ */
public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int
{ {
[$width, $height, $type] = @getimagesize($file); // @ - files smaller than 12 bytes causes read error [$width, $height, $type] = Helpers::falseToNull(@getimagesize($file)); // @ - files smaller than 12 bytes causes read error
return isset(self::Formats[$type]) ? $type : null; return $type && isset(self::Formats[$type]) ? $type : null;
} }
/** /**
* Returns the type of image from string. * Returns the type of image from string.
* @return ImageType::*|null
*/ */
public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int
{ {
[$width, $height, $type] = @getimagesizefromstring($s); // @ - strings smaller than 12 bytes causes read error [$width, $height, $type] = Helpers::falseToNull(@getimagesizefromstring($s)); // @ - strings smaller than 12 bytes causes read error
return isset(self::Formats[$type]) ? $type : null; return $type && isset(self::Formats[$type]) ? $type : null;
} }
/** /**
* Returns the file extension for the given `Image::XXX` constant. * Returns the file extension for the given image type.
* @param ImageType::* $type
* @return value-of<self::Formats>
*/ */
public static function typeToExtension(int $type): string public static function typeToExtension(int $type): string
{ {
@@ -265,11 +271,12 @@ class Image
/** /**
* Returns the `Image::XXX` constant for given file extension. * Returns the image type for given file extension.
* @return ImageType::*
*/ */
public static function extensionToType(string $extension): int public static function extensionToType(string $extension): int
{ {
$extensions = array_flip(self::Formats) + ['jpg' => self::JPEG]; $extensions = array_flip(self::Formats) + ['jpg' => ImageType::JPEG];
$extension = strtolower($extension); $extension = strtolower($extension);
if (!isset($extensions[$extension])) { if (!isset($extensions[$extension])) {
throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'."); throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'.");
@@ -280,7 +287,8 @@ class Image
/** /**
* Returns the mime type for the given `Image::XXX` constant. * Returns the mime type for the given image type.
* @param ImageType::* $type
*/ */
public static function typeToMimeType(int $type): string public static function typeToMimeType(int $type): string
{ {
@@ -289,10 +297,43 @@ class Image
/** /**
* Wraps GD image. * @param ImageType::* $type
* @param resource|\GdImage $image
*/ */
public function __construct($image) public static function isTypeSupported(int $type): bool
{
self::ensureExtension();
return (bool) (imagetypes() & match ($type) {
ImageType::JPEG => IMG_JPG,
ImageType::PNG => IMG_PNG,
ImageType::GIF => IMG_GIF,
ImageType::WEBP => IMG_WEBP,
ImageType::AVIF => 256, // IMG_AVIF,
ImageType::BMP => IMG_BMP,
default => 0,
});
}
/** @return ImageType[] */
public static function getSupportedTypes(): array
{
self::ensureExtension();
$flag = imagetypes();
return array_filter([
$flag & IMG_GIF ? ImageType::GIF : null,
$flag & IMG_JPG ? ImageType::JPEG : null,
$flag & IMG_PNG ? ImageType::PNG : null,
$flag & IMG_WEBP ? ImageType::WEBP : null,
$flag & 256 ? ImageType::AVIF : null, // IMG_AVIF
$flag & IMG_BMP ? ImageType::BMP : null,
]);
}
/**
* Wraps GD image.
*/
public function __construct(\GdImage $image)
{ {
$this->setImageResource($image); $this->setImageResource($image);
imagesavealpha($image, true); imagesavealpha($image, true);
@@ -301,6 +342,7 @@ class Image
/** /**
* Returns image width. * Returns image width.
* @return positive-int
*/ */
public function getWidth(): int public function getWidth(): int
{ {
@@ -310,6 +352,7 @@ class Image
/** /**
* Returns image height. * Returns image height.
* @return positive-int
*/ */
public function getHeight(): int public function getHeight(): int
{ {
@@ -319,15 +362,9 @@ class Image
/** /**
* Sets image resource. * Sets image resource.
* @param resource|\GdImage $image
* @return static
*/ */
protected function setImageResource($image) protected function setImageResource(\GdImage $image): static
{ {
if (!$image instanceof \GdImage && !(is_resource($image) && get_resource_type($image) === 'gd')) {
throw new Nette\InvalidArgumentException('Image is not valid.');
}
$this->image = $image; $this->image = $image;
return $this; return $this;
} }
@@ -335,30 +372,27 @@ class Image
/** /**
* Returns image GD resource. * Returns image GD resource.
* @return resource|\GdImage
*/ */
public function getImageResource() public function getImageResource(): \GdImage
{ {
return $this->image; return $this->image;
} }
/** /**
* Scales an image. * Scales an image. Width and height accept pixels or percent.
* @param int|string|null $width in pixels or percent * @param int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly> $mode
* @param int|string|null $height in pixels or percent
* @return static
*/ */
public function resize($width, $height, int $flags = self::FIT) public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static
{ {
if ($flags & self::EXACT) { if ($mode & self::Cover) {
return $this->resize($width, $height, self::FILL)->crop('50%', '50%', $width, $height); return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height);
} }
[$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $flags); [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode);
if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize
$newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource(); $newImage = static::fromBlank($newWidth, $newHeight, ImageColor::rgb(0, 0, 0, 0))->getImageResource();
imagecopyresampled( imagecopyresampled(
$newImage, $newImage,
$this->image, $this->image,
@@ -369,7 +403,7 @@ class Image
$newWidth, $newWidth,
$newHeight, $newHeight,
$this->getWidth(), $this->getWidth(),
$this->getHeight() $this->getHeight(),
); );
$this->image = $newImage; $this->image = $newImage;
} }
@@ -383,17 +417,17 @@ class Image
/** /**
* Calculates dimensions of resized image. * Calculates dimensions of resized image. Width and height accept pixels or percent.
* @param int|string|null $newWidth in pixels or percent * @param int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly> $mode
* @param int|string|null $newHeight in pixels or percent
*/ */
public static function calculateSize( public static function calculateSize(
int $srcWidth, int $srcWidth,
int $srcHeight, int $srcHeight,
$newWidth, $newWidth,
$newHeight, $newHeight,
int $flags = self::FIT int $mode = self::OrSmaller,
): array { ): array
{
if ($newWidth === null) { if ($newWidth === null) {
} elseif (self::isPercent($newWidth)) { } elseif (self::isPercent($newWidth)) {
$newWidth = (int) round($srcWidth / 100 * abs($newWidth)); $newWidth = (int) round($srcWidth / 100 * abs($newWidth));
@@ -405,19 +439,19 @@ class Image
if ($newHeight === null) { if ($newHeight === null) {
} elseif (self::isPercent($newHeight)) { } elseif (self::isPercent($newHeight)) {
$newHeight = (int) round($srcHeight / 100 * abs($newHeight)); $newHeight = (int) round($srcHeight / 100 * abs($newHeight));
$flags |= empty($percents) ? 0 : self::STRETCH; $mode |= empty($percents) ? 0 : self::Stretch;
} else { } else {
$newHeight = abs($newHeight); $newHeight = abs($newHeight);
} }
if ($flags & self::STRETCH) { // non-proportional if ($mode & self::Stretch) { // non-proportional
if (!$newWidth || !$newHeight) { if (!$newWidth || !$newHeight) {
throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.'); throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.');
} }
if ($flags & self::SHRINK_ONLY) { if ($mode & self::ShrinkOnly) {
$newWidth = (int) round($srcWidth * min(1, $newWidth / $srcWidth)); $newWidth = min($srcWidth, $newWidth);
$newHeight = (int) round($srcHeight * min(1, $newHeight / $srcHeight)); $newHeight = min($srcHeight, $newHeight);
} }
} else { // proportional } else { // proportional
if (!$newWidth && !$newHeight) { if (!$newWidth && !$newHeight) {
@@ -433,11 +467,11 @@ class Image
$scale[] = $newHeight / $srcHeight; $scale[] = $newHeight / $srcHeight;
} }
if ($flags & self::FILL) { if ($mode & self::OrBigger) {
$scale = [max($scale)]; $scale = [max($scale)];
} }
if ($flags & self::SHRINK_ONLY) { if ($mode & self::ShrinkOnly) {
$scale[] = 1; $scale[] = 1;
} }
@@ -451,14 +485,9 @@ class Image
/** /**
* Crops image. * Crops image. Arguments accepts pixels or percent.
* @param int|string $left in pixels or percent
* @param int|string $top in pixels or percent
* @param int|string $width in pixels or percent
* @param int|string $height in pixels or percent
* @return static
*/ */
public function crop($left, $top, $width, $height) public function crop(int|string $left, int|string $top, int|string $width, int|string $height): static
{ {
[$r['x'], $r['y'], $r['width'], $r['height']] [$r['x'], $r['y'], $r['width'], $r['height']]
= static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height); = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height);
@@ -466,7 +495,7 @@ class Image
$this->image = imagecrop($this->image, $r); $this->image = imagecrop($this->image, $r);
imagesavealpha($this->image, true); imagesavealpha($this->image, true);
} else { } else {
$newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource(); $newImage = static::fromBlank($r['width'], $r['height'], ImageColor::rgb(0, 0, 0, 0))->getImageResource();
imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']); imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
$this->image = $newImage; $this->image = $newImage;
} }
@@ -476,13 +505,16 @@ class Image
/** /**
* Calculates dimensions of cutout in image. * Calculates dimensions of cutout in image. Arguments accepts pixels or percent.
* @param int|string $left in pixels or percent
* @param int|string $top in pixels or percent
* @param int|string $newWidth in pixels or percent
* @param int|string $newHeight in pixels or percent
*/ */
public static function calculateCutout(int $srcWidth, int $srcHeight, $left, $top, $newWidth, $newHeight): array public static function calculateCutout(
int $srcWidth,
int $srcHeight,
int|string $left,
int|string $top,
int|string $newWidth,
int|string $newHeight,
): array
{ {
if (self::isPercent($newWidth)) { if (self::isPercent($newWidth)) {
$newWidth = (int) round($srcWidth / 100 * $newWidth); $newWidth = (int) round($srcWidth / 100 * $newWidth);
@@ -518,9 +550,8 @@ class Image
/** /**
* Sharpens image a little bit. * Sharpens image a little bit.
* @return static
*/ */
public function sharpen() public function sharpen(): static
{ {
imageconvolution($this->image, [ // my magic numbers ;) imageconvolution($this->image, [ // my magic numbers ;)
[-1, -1, -1], [-1, -1, -1],
@@ -532,13 +563,10 @@ class Image
/** /**
* Puts another image into this image. * Puts another image into this image. Left and top accepts pixels or percent.
* @param int|string $left in pixels or percent * @param int<0, 100> $opacity 0..100
* @param int|string $top in pixels or percent
* @param int $opacity 0..100
* @return static
*/ */
public function place(self $image, $left = 0, $top = 0, int $opacity = 100) public function place(self $image, int|string $left = 0, int|string $top = 0, int $opacity = 100): static
{ {
$opacity = max(0, min(100, $opacity)); $opacity = max(0, min(100, $opacity));
if ($opacity === 0) { if ($opacity === 0) {
@@ -590,29 +618,75 @@ class Image
0, 0,
0, 0,
$width, $width,
$height $height,
); );
return $this; return $this;
} }
/**
* Calculates the bounding box for a TrueType text. Returns keys left, top, width and height.
*/
public static function calculateTextBox(
string $text,
string $fontFile,
float $size,
float $angle = 0,
array $options = [],
): array
{
self::ensureExtension();
$box = imagettfbbox($size, $angle, $fontFile, $text, $options);
return [
'left' => $minX = min([$box[0], $box[2], $box[4], $box[6]]),
'top' => $minY = min([$box[1], $box[3], $box[5], $box[7]]),
'width' => max([$box[0], $box[2], $box[4], $box[6]]) - $minX + 1,
'height' => max([$box[1], $box[3], $box[5], $box[7]]) - $minY + 1,
];
}
/**
* Draw a rectangle.
*/
public function rectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
{
if ($width !== 0 && $height !== 0) {
$this->rectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
}
}
/**
* Draw a filled rectangle.
*/
public function filledRectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
{
if ($width !== 0 && $height !== 0) {
$this->filledRectangle($x, $y, $x + $width + ($width > 0 ? -1 : 1), $y + $height + ($height > 0 ? -1 : 1), $color);
}
}
/** /**
* Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9).
* @param ImageType::*|null $type
* @throws ImageException * @throws ImageException
*/ */
public function save(string $file, ?int $quality = null, ?int $type = null): void public function save(string $file, ?int $quality = null, ?int $type = null): void
{ {
$type = $type ?? self::extensionToType(pathinfo($file, PATHINFO_EXTENSION)); $type ??= self::extensionToType(pathinfo($file, PATHINFO_EXTENSION));
$this->output($type, $quality, $file); $this->output($type, $quality, $file);
} }
/** /**
* Outputs image to string. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * Outputs image to string. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9).
* @param ImageType::* $type
*/ */
public function toString(int $type = self::JPEG, ?int $quality = null): string public function toString(int $type = ImageType::JPEG, ?int $quality = null): string
{ {
return Helpers::capture(function () use ($type, $quality) { return Helpers::capture(function () use ($type, $quality): void {
$this->output($type, $quality); $this->output($type, $quality);
}); });
} }
@@ -623,24 +697,16 @@ class Image
*/ */
public function __toString(): string public function __toString(): string
{ {
try { return $this->toString();
return $this->toString();
} catch (\Throwable $e) {
if (func_num_args() || PHP_VERSION_ID >= 70400) {
throw $e;
}
trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
return '';
}
} }
/** /**
* Outputs image to browser. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * Outputs image to browser. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9).
* @param ImageType::* $type
* @throws ImageException * @throws ImageException
*/ */
public function send(int $type = self::JPEG, ?int $quality = null): void public function send(int $type = ImageType::JPEG, ?int $quality = null): void
{ {
header('Content-Type: ' . self::typeToMimeType($type)); header('Content-Type: ' . self::typeToMimeType($type));
$this->output($type, $quality); $this->output($type, $quality);
@@ -649,55 +715,40 @@ class Image
/** /**
* Outputs image to browser or file. * Outputs image to browser or file.
* @param ImageType::* $type
* @throws ImageException * @throws ImageException
*/ */
private function output(int $type, ?int $quality, ?string $file = null): void private function output(int $type, ?int $quality, ?string $file = null): void
{ {
switch ($type) { [$defQuality, $min, $max] = match ($type) {
case self::JPEG: ImageType::JPEG => [85, 0, 100],
$quality = $quality === null ? 85 : max(0, min(100, $quality)); ImageType::PNG => [9, 0, 9],
$success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception ImageType::GIF => [null, null, null],
break; ImageType::WEBP => [80, 0, 100],
ImageType::AVIF => [30, 0, 100],
ImageType::BMP => [null, null, null],
default => throw new Nette\InvalidArgumentException("Unsupported image type '$type'."),
};
case self::PNG: $args = [$this->image, $file];
$quality = $quality === null ? 9 : max(0, min(9, $quality)); if ($defQuality !== null) {
$success = @imagepng($this->image, $file, $quality); // @ is escalated to exception $args[] = $quality === null ? $defQuality : max($min, min($max, $quality));
break;
case self::GIF:
$success = @imagegif($this->image, $file); // @ is escalated to exception
break;
case self::WEBP:
$quality = $quality === null ? 80 : max(0, min(100, $quality));
$success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception
break;
case self::AVIF:
$quality = $quality === null ? 30 : max(0, min(100, $quality));
$success = @imageavif($this->image, $file, $quality); // @ is escalated to exception
break;
case self::BMP:
$success = @imagebmp($this->image, $file); // @ is escalated to exception
break;
default:
throw new Nette\InvalidArgumentException("Unsupported image type '$type'.");
} }
if (!$success) { Callback::invokeSafe('image' . self::Formats[$type], $args, function (string $message) use ($file): void {
throw new ImageException(Helpers::getLastError() ?: 'Unknown error'); if ($file !== null) {
} @unlink($file);
}
throw new ImageException($message);
});
} }
/** /**
* Call to undefined method. * Call to undefined method.
* @return mixed
* @throws Nette\MemberAccessException * @throws Nette\MemberAccessException
*/ */
public function __call(string $name, array $args) public function __call(string $name, array $args): mixed
{ {
$function = 'image' . $name; $function = 'image' . $name;
if (!function_exists($function)) { if (!function_exists($function)) {
@@ -708,25 +759,13 @@ class Image
if ($value instanceof self) { if ($value instanceof self) {
$args[$key] = $value->getImageResource(); $args[$key] = $value->getImageResource();
} elseif (is_array($value) && isset($value['red'])) { // rgb } elseif ($value instanceof ImageColor || (is_array($value) && isset($value['red']))) {
$args[$key] = imagecolorallocatealpha( $args[$key] = $this->resolveColor($value);
$this->image,
$value['red'],
$value['green'],
$value['blue'],
$value['alpha']
) ?: imagecolorresolvealpha(
$this->image,
$value['red'],
$value['green'],
$value['blue'],
$value['alpha']
);
} }
} }
$res = $function($this->image, ...$args); $res = $function($this->image, ...$args);
return $res instanceof \GdImage || (is_resource($res) && get_resource_type($res) === 'gd') return $res instanceof \GdImage
? $this->setImageResource($res) ? $this->setImageResource($res)
: $res; : $res;
} }
@@ -734,18 +773,15 @@ class Image
public function __clone() public function __clone()
{ {
ob_start(function () {}); ob_start(fn() => '');
imagepng($this->image, null, 0); imagepng($this->image, null, 0);
$this->setImageResource(imagecreatefromstring(ob_get_clean())); $this->setImageResource(imagecreatefromstring(ob_get_clean()));
} }
/** private static function isPercent(int|string &$num): bool
* @param int|string $num in pixels or percent
*/
private static function isPercent(&$num): bool
{ {
if (is_string($num) && substr($num, -1) === '%') { if (is_string($num) && str_ends_with($num, '%')) {
$num = (float) substr($num, 0, -1); $num = (float) substr($num, 0, -1);
return true; return true;
} elseif (is_int($num) || $num === (string) (int) $num) { } elseif (is_int($num) || $num === (string) (int) $num) {
@@ -760,8 +796,23 @@ class Image
/** /**
* Prevents serialization. * Prevents serialization.
*/ */
public function __sleep(): array public function __serialize(): array
{ {
throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.'); throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.');
} }
public function resolveColor(ImageColor|array $color): int
{
$color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color);
return imagecolorallocatealpha($this->image, ...$color) ?: imagecolorresolvealpha($this->image, ...$color);
}
private static function ensureExtension(): void
{
if (!extension_loaded('gd')) {
throw new Nette\NotSupportedException('PHP extension GD is not loaded.');
}
}
} }

View File

@@ -10,6 +10,8 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function defined, is_int, json_decode, json_encode, json_last_error, json_last_error_msg;
use const JSON_BIGINT_AS_STRING, JSON_FORCE_OBJECT, JSON_HEX_AMP, JSON_HEX_APOS, JSON_HEX_QUOT, JSON_HEX_TAG, JSON_OBJECT_AS_ARRAY, JSON_PRESERVE_ZERO_FRACTION, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE;
/** /**
@@ -19,22 +21,39 @@ final class Json
{ {
use Nette\StaticClass; use Nette\StaticClass;
/** @deprecated use Json::decode(..., forceArrays: true) */
public const FORCE_ARRAY = JSON_OBJECT_AS_ARRAY; public const FORCE_ARRAY = JSON_OBJECT_AS_ARRAY;
/** @deprecated use Json::encode(..., pretty: true) */
public const PRETTY = JSON_PRETTY_PRINT; public const PRETTY = JSON_PRETTY_PRINT;
/** @deprecated use Json::encode(..., asciiSafe: true) */
public const ESCAPE_UNICODE = 1 << 19; public const ESCAPE_UNICODE = 1 << 19;
/** /**
* Converts value to JSON format. The flag can be Json::PRETTY, which formats JSON for easier reading and clarity, * Converts value to JSON format. Use $pretty for easier reading and clarity, $asciiSafe for ASCII output
* and Json::ESCAPE_UNICODE for ASCII output. * and $htmlSafe for HTML escaping, $forceObjects enforces the encoding of non-associateve arrays as objects.
* @param mixed $value
* @throws JsonException * @throws JsonException
*/ */
public static function encode($value, int $flags = 0): string public static function encode(
mixed $value,
bool|int $pretty = false,
bool $asciiSafe = false,
bool $htmlSafe = false,
bool $forceObjects = false,
): string
{ {
$flags = ($flags & self::ESCAPE_UNICODE ? 0 : JSON_UNESCAPED_UNICODE) if (is_int($pretty)) { // back compatibility
| JSON_UNESCAPED_SLASHES $flags = ($pretty & self::ESCAPE_UNICODE ? 0 : JSON_UNESCAPED_UNICODE) | ($pretty & ~self::ESCAPE_UNICODE);
| ($flags & ~self::ESCAPE_UNICODE) } else {
$flags = ($asciiSafe ? 0 : JSON_UNESCAPED_UNICODE)
| ($pretty ? JSON_PRETTY_PRINT : 0)
| ($forceObjects ? JSON_FORCE_OBJECT : 0)
| ($htmlSafe ? JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG : 0);
}
$flags |= JSON_UNESCAPED_SLASHES
| (defined('JSON_PRESERVE_ZERO_FRACTION') ? JSON_PRESERVE_ZERO_FRACTION : 0); // since PHP 5.6.6 & PECL JSON-C 1.3.7 | (defined('JSON_PRESERVE_ZERO_FRACTION') ? JSON_PRESERVE_ZERO_FRACTION : 0); // since PHP 5.6.6 & PECL JSON-C 1.3.7
$json = json_encode($value, $flags); $json = json_encode($value, $flags);
@@ -47,13 +66,17 @@ final class Json
/** /**
* Parses JSON to PHP value. The flag can be Json::FORCE_ARRAY, which forces an array instead of an object as the return value. * Parses JSON to PHP value. The $forceArrays enforces the decoding of objects as arrays.
* @return mixed
* @throws JsonException * @throws JsonException
*/ */
public static function decode(string $json, int $flags = 0) public static function decode(string $json, bool|int $forceArrays = false): mixed
{ {
$value = json_decode($json, null, 512, $flags | JSON_BIGINT_AS_STRING); $flags = is_int($forceArrays) // back compatibility
? $forceArrays
: ($forceArrays ? JSON_OBJECT_AS_ARRAY : 0);
$flags |= JSON_BIGINT_AS_STRING;
$value = json_decode($json, flags: $flags);
if ($error = json_last_error()) { if ($error = json_last_error()) {
throw new JsonException(json_last_error_msg(), $error); throw new JsonException(json_last_error_msg(), $error);
} }

View File

@@ -11,10 +11,13 @@ namespace Nette\Utils;
use Nette; use Nette;
use Nette\MemberAccessException; use Nette\MemberAccessException;
use function array_filter, array_merge, array_pop, array_unique, get_class_methods, get_parent_class, implode, is_a, levenshtein, method_exists, preg_match_all, preg_replace, strlen, ucfirst;
use const PREG_SET_ORDER, SORT_REGULAR;
/** /**
* Nette\SmartObject helpers. * Nette\SmartObject helpers.
* @internal
*/ */
final class ObjectHelpers final class ObjectHelpers
{ {
@@ -28,8 +31,8 @@ final class ObjectHelpers
{ {
$rc = new \ReflectionClass($class); $rc = new \ReflectionClass($class);
$hint = self::getSuggestion(array_merge( $hint = self::getSuggestion(array_merge(
array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }), array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()),
self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m') self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'),
), $name); ), $name);
throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.'));
} }
@@ -43,8 +46,8 @@ final class ObjectHelpers
{ {
$rc = new \ReflectionClass($class); $rc = new \ReflectionClass($class);
$hint = self::getSuggestion(array_merge( $hint = self::getSuggestion(array_merge(
array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); }), array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()),
self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m') self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'),
), $name); ), $name);
throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.'));
} }
@@ -76,7 +79,7 @@ final class ObjectHelpers
$hint = self::getSuggestion(array_merge( $hint = self::getSuggestion(array_merge(
get_class_methods($class), get_class_methods($class),
self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:static[ \t]+)?(?:\S+[ \t]+)??(\w+)\(~m'), self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:static[ \t]+)?(?:\S+[ \t]+)??(\w+)\(~m'),
$additionalMethods $additionalMethods,
), $method); ), $method);
throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
} }
@@ -107,8 +110,8 @@ final class ObjectHelpers
} else { } else {
$hint = self::getSuggestion( $hint = self::getSuggestion(
array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), function ($m) { return $m->isStatic(); }), array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), fn($m) => $m->isStatic()),
$method $method,
); );
throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.'));
} }
@@ -133,7 +136,7 @@ final class ObjectHelpers
'~^ [ \t*]* @property(|-read|-write|-deprecated) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx', '~^ [ \t*]* @property(|-read|-write|-deprecated) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx',
(string) $rc->getDocComment(), (string) $rc->getDocComment(),
$matches, $matches,
PREG_SET_ORDER PREG_SET_ORDER,
); );
$props = []; $props = [];
@@ -199,16 +202,16 @@ final class ObjectHelpers
} }
} while ($rc = $rc->getParentClass()); } while ($rc = $rc->getParentClass());
return preg_match_all($pattern, implode($doc), $m) ? $m[1] : []; return preg_match_all($pattern, implode('', $doc), $m) ? $m[1] : [];
} }
/** /**
* Checks if the public non-static property exists. * Checks if the public non-static property exists.
* @return bool|string returns 'event' if the property exists and has event like name * Returns 'event' if the property exists and has event like name
* @internal * @internal
*/ */
public static function hasProperty(string $class, string $name) public static function hasProperty(string $class, string $name): bool|string
{ {
static $cache; static $cache;
$prop = &$cache[$class][$name]; $prop = &$cache[$class][$name];

View File

@@ -1,41 +0,0 @@
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
/**
* Nette\Object behaviour mixin.
* @deprecated
*/
final class ObjectMixin
{
use Nette\StaticClass;
/** @deprecated use ObjectHelpers::getSuggestion() */
public static function getSuggestion(array $possibilities, string $value): ?string
{
trigger_error(__METHOD__ . '() has been renamed to Nette\Utils\ObjectHelpers::getSuggestion()', E_USER_DEPRECATED);
return ObjectHelpers::getSuggestion($possibilities, $value);
}
public static function setExtensionMethod(): void
{
trigger_error('Class Nette\Utils\ObjectMixin is deprecated', E_USER_DEPRECATED);
}
public static function getExtensionMethod(): void
{
trigger_error('Class Nette\Utils\ObjectMixin is deprecated', E_USER_DEPRECATED);
}
}

View File

@@ -18,40 +18,37 @@ use Nette;
* @property int $page * @property int $page
* @property-read int $firstPage * @property-read int $firstPage
* @property-read int|null $lastPage * @property-read int|null $lastPage
* @property-read int $firstItemOnPage * @property-read int<0,max> $firstItemOnPage
* @property-read int $lastItemOnPage * @property-read int<0,max> $lastItemOnPage
* @property int $base * @property int $base
* @property-read bool $first * @property-read bool $first
* @property-read bool $last * @property-read bool $last
* @property-read int|null $pageCount * @property-read int<0,max>|null $pageCount
* @property int $itemsPerPage * @property positive-int $itemsPerPage
* @property int|null $itemCount * @property int<0,max>|null $itemCount
* @property-read int $offset * @property-read int<0,max> $offset
* @property-read int|null $countdownOffset * @property-read int<0,max>|null $countdownOffset
* @property-read int $length * @property-read int<0,max> $length
*/ */
class Paginator class Paginator
{ {
use Nette\SmartObject; use Nette\SmartObject;
/** @var int */ private int $base = 1;
private $base = 1;
/** @var int */ /** @var positive-int */
private $itemsPerPage = 1; private int $itemsPerPage = 1;
/** @var int */ private int $page = 1;
private $page = 1;
/** @var int|null */ /** @var int<0, max>|null */
private $itemCount; private ?int $itemCount = null;
/** /**
* Sets current page number. * Sets current page number.
* @return static
*/ */
public function setPage(int $page) public function setPage(int $page): static
{ {
$this->page = $page; $this->page = $page;
return $this; return $this;
@@ -89,6 +86,7 @@ class Paginator
/** /**
* Returns the sequence number of the first element on the page * Returns the sequence number of the first element on the page
* @return int<0, max>
*/ */
public function getFirstItemOnPage(): int public function getFirstItemOnPage(): int
{ {
@@ -100,6 +98,7 @@ class Paginator
/** /**
* Returns the sequence number of the last element on the page * Returns the sequence number of the last element on the page
* @return int<0, max>
*/ */
public function getLastItemOnPage(): int public function getLastItemOnPage(): int
{ {
@@ -109,9 +108,8 @@ class Paginator
/** /**
* Sets first page (base) number. * Sets first page (base) number.
* @return static
*/ */
public function setBase(int $base) public function setBase(int $base): static
{ {
$this->base = $base; $this->base = $base;
return $this; return $this;
@@ -129,6 +127,7 @@ class Paginator
/** /**
* Returns zero-based page number. * Returns zero-based page number.
* @return int<0, max>
*/ */
protected function getPageIndex(): int protected function getPageIndex(): int
{ {
@@ -161,6 +160,7 @@ class Paginator
/** /**
* Returns the total number of pages. * Returns the total number of pages.
* @return int<0, max>|null
*/ */
public function getPageCount(): ?int public function getPageCount(): ?int
{ {
@@ -172,9 +172,8 @@ class Paginator
/** /**
* Sets the number of items to display on a single page. * Sets the number of items to display on a single page.
* @return static
*/ */
public function setItemsPerPage(int $itemsPerPage) public function setItemsPerPage(int $itemsPerPage): static
{ {
$this->itemsPerPage = max(1, $itemsPerPage); $this->itemsPerPage = max(1, $itemsPerPage);
return $this; return $this;
@@ -183,6 +182,7 @@ class Paginator
/** /**
* Returns the number of items to display on a single page. * Returns the number of items to display on a single page.
* @return positive-int
*/ */
public function getItemsPerPage(): int public function getItemsPerPage(): int
{ {
@@ -192,9 +192,8 @@ class Paginator
/** /**
* Sets the total number of items. * Sets the total number of items.
* @return static
*/ */
public function setItemCount(?int $itemCount = null) public function setItemCount(?int $itemCount = null): static
{ {
$this->itemCount = $itemCount === null ? null : max(0, $itemCount); $this->itemCount = $itemCount === null ? null : max(0, $itemCount);
return $this; return $this;
@@ -203,6 +202,7 @@ class Paginator
/** /**
* Returns the total number of items. * Returns the total number of items.
* @return int<0, max>|null
*/ */
public function getItemCount(): ?int public function getItemCount(): ?int
{ {
@@ -212,6 +212,7 @@ class Paginator
/** /**
* Returns the absolute index of the first item on current page. * Returns the absolute index of the first item on current page.
* @return int<0, max>
*/ */
public function getOffset(): int public function getOffset(): int
{ {
@@ -221,6 +222,7 @@ class Paginator
/** /**
* Returns the absolute index of the first item on current page in countdown paging. * Returns the absolute index of the first item on current page in countdown paging.
* @return int<0, max>|null
*/ */
public function getCountdownOffset(): ?int public function getCountdownOffset(): ?int
{ {
@@ -232,6 +234,7 @@ class Paginator
/** /**
* Returns the number of items on current page. * Returns the number of items on current page.
* @return int<0, max>
*/ */
public function getLength(): int public function getLength(): int
{ {

View File

@@ -10,6 +10,9 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use Random\Randomizer;
use function strlen;
use const PHP_VERSION_ID;
/** /**
@@ -25,15 +28,20 @@ final class Random
*/ */
public static function generate(int $length = 10, string $charlist = '0-9a-z'): string public static function generate(int $length = 10, string $charlist = '0-9a-z'): string
{ {
$charlist = count_chars(preg_replace_callback('#.-.#', function (array $m): string { $charlist = preg_replace_callback(
return implode('', range($m[0][0], $m[0][2])); '#.-.#',
}, $charlist), 3); fn(array $m): string => implode('', range($m[0][0], $m[0][2])),
$charlist,
);
$charlist = count_chars($charlist, mode: 3);
$chLen = strlen($charlist); $chLen = strlen($charlist);
if ($length < 1) { if ($length < 1) {
throw new Nette\InvalidArgumentException('Length must be greater than zero.'); throw new Nette\InvalidArgumentException('Length must be greater than zero.');
} elseif ($chLen < 2) { } elseif ($chLen < 2) {
throw new Nette\InvalidArgumentException('Character list must contain at least two chars.'); throw new Nette\InvalidArgumentException('Character list must contain at least two chars.');
} elseif (PHP_VERSION_ID >= 80300) {
return (new Randomizer)->getBytesFromString($charlist, $length);
} }
$res = ''; $res = '';

View File

@@ -10,6 +10,8 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function constant, current, defined, end, explode, file_get_contents, implode, ltrim, next, ord, strrchr, strtolower, substr;
use const T_AS, T_CLASS, T_COMMENT, T_CURLY_OPEN, T_DOC_COMMENT, T_DOLLAR_OPEN_CURLY_BRACES, T_ENUM, T_INTERFACE, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, T_NAMESPACE, T_NS_SEPARATOR, T_STRING, T_TRAIT, T_USE, T_WHITESPACE, TOKEN_PARSE;
/** /**
@@ -19,125 +21,21 @@ final class Reflection
{ {
use Nette\StaticClass; use Nette\StaticClass;
private const BuiltinTypes = [ /** @deprecated use Nette\Utils\Validators::isBuiltinType() */
'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1,
'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1,
'never' => 1,
];
private const ClassKeywords = [
'self' => 1, 'parent' => 1, 'static' => 1,
];
/**
* Determines if type is PHP built-in type. Otherwise, it is the class name.
*/
public static function isBuiltinType(string $type): bool public static function isBuiltinType(string $type): bool
{ {
return isset(self::BuiltinTypes[strtolower($type)]); return Validators::isBuiltinType($type);
} }
/** #[\Deprecated('use Nette\Utils\Validators::isClassKeyword()')]
* Determines if type is special class name self/parent/static.
*/
public static function isClassKeyword(string $name): bool public static function isClassKeyword(string $name): bool
{ {
return isset(self::ClassKeywords[strtolower($name)]); return Validators::isClassKeyword($name);
} }
/** public static function getParameterDefaultValue(\ReflectionParameter $param): mixed
* Returns the type of return value of given function or method and normalizes `self`, `static`, and `parent` to actual class names.
* If the function does not have a return type, it returns null.
* If the function has union or intersection type, it throws Nette\InvalidStateException.
*/
public static function getReturnType(\ReflectionFunctionAbstract $func): ?string
{
$type = $func->getReturnType() ?? (PHP_VERSION_ID >= 80100 && $func instanceof \ReflectionMethod ? $func->getTentativeReturnType() : null);
return self::getType($func, $type);
}
/**
* @deprecated
*/
public static function getReturnTypes(\ReflectionFunctionAbstract $func): array
{
$type = Type::fromReflection($func);
return $type ? $type->getNames() : [];
}
/**
* Returns the type of given parameter and normalizes `self` and `parent` to the actual class names.
* If the parameter does not have a type, it returns null.
* If the parameter has union or intersection type, it throws Nette\InvalidStateException.
*/
public static function getParameterType(\ReflectionParameter $param): ?string
{
return self::getType($param, $param->getType());
}
/**
* @deprecated
*/
public static function getParameterTypes(\ReflectionParameter $param): array
{
$type = Type::fromReflection($param);
return $type ? $type->getNames() : [];
}
/**
* Returns the type of given property and normalizes `self` and `parent` to the actual class names.
* If the property does not have a type, it returns null.
* If the property has union or intersection type, it throws Nette\InvalidStateException.
*/
public static function getPropertyType(\ReflectionProperty $prop): ?string
{
return self::getType($prop, PHP_VERSION_ID >= 70400 ? $prop->getType() : null);
}
/**
* @deprecated
*/
public static function getPropertyTypes(\ReflectionProperty $prop): array
{
$type = Type::fromReflection($prop);
return $type ? $type->getNames() : [];
}
/**
* @param \ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflection
*/
private static function getType($reflection, ?\ReflectionType $type): ?string
{
if ($type === null) {
return null;
} elseif ($type instanceof \ReflectionNamedType) {
return Type::resolve($type->getName(), $reflection);
} elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
throw new Nette\InvalidStateException('The ' . self::toString($reflection) . ' is not expected to have a union or intersection type.');
} else {
throw new Nette\InvalidStateException('Unexpected type of ' . self::toString($reflection));
}
}
/**
* Returns the default value of parameter. If it is a constant, it returns its value.
* @return mixed
* @throws \ReflectionException If the parameter does not have a default value or the constant cannot be resolved
*/
public static function getParameterDefaultValue(\ReflectionParameter $param)
{ {
if ($param->isDefaultValueConstant()) { if ($param->isDefaultValueConstant()) {
$const = $orig = $param->getDefaultValueConstantName(); $const = $orig = $param->getDefaultValueConstantName();
@@ -203,7 +101,7 @@ final class Reflection
$hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()]; $hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()];
if (($alias = $decl->getTraitAliases()[$method->name] ?? null) if (($alias = $decl->getTraitAliases()[$method->name] ?? null)
&& ($m = new \ReflectionMethod($alias)) && ($m = new \ReflectionMethod(...explode('::', $alias, 2)))
&& $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()]
) { ) {
return self::getMethodDeclaringMethod($m); return self::getMethodDeclaringMethod($m);
@@ -228,7 +126,7 @@ final class Reflection
public static function areCommentsAvailable(): bool public static function areCommentsAvailable(): bool
{ {
static $res; static $res;
return $res ?? $res = (bool) (new \ReflectionMethod(__METHOD__))->getDocComment(); return $res ?? $res = (bool) (new \ReflectionMethod(self::class, __FUNCTION__))->getDocComment();
} }
@@ -239,7 +137,7 @@ final class Reflection
} elseif ($ref instanceof \ReflectionMethod) { } elseif ($ref instanceof \ReflectionMethod) {
return $ref->getDeclaringClass()->name . '::' . $ref->name . '()'; return $ref->getDeclaringClass()->name . '::' . $ref->name . '()';
} elseif ($ref instanceof \ReflectionFunction) { } elseif ($ref instanceof \ReflectionFunction) {
return $ref->name . '()'; return $ref->isAnonymous() ? '{closure}()' : $ref->name . '()';
} elseif ($ref instanceof \ReflectionProperty) { } elseif ($ref instanceof \ReflectionProperty) {
return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name; return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name;
} elseif ($ref instanceof \ReflectionParameter) { } elseif ($ref instanceof \ReflectionParameter) {
@@ -261,7 +159,7 @@ final class Reflection
if (empty($name)) { if (empty($name)) {
throw new Nette\InvalidArgumentException('Class name must not be empty.'); throw new Nette\InvalidArgumentException('Class name must not be empty.');
} elseif (isset(self::BuiltinTypes[$lower])) { } elseif (Validators::isBuiltinType($lower)) {
return $lower; return $lower;
} elseif ($lower === 'self' || $lower === 'static') { } elseif ($lower === 'self' || $lower === 'static') {
@@ -291,7 +189,7 @@ final class Reflection
} }
/** @return array of [alias => class] */ /** @return array<string, class-string> of [alias => class] */
public static function getUseStatements(\ReflectionClass $class): array public static function getUseStatements(\ReflectionClass $class): array
{ {
if ($class->isAnonymous()) { if ($class->isAnonymous()) {
@@ -318,22 +216,21 @@ final class Reflection
private static function parseUseStatements(string $code, ?string $forClass = null): array private static function parseUseStatements(string $code, ?string $forClass = null): array
{ {
try { try {
$tokens = token_get_all($code, TOKEN_PARSE); $tokens = \PhpToken::tokenize($code, TOKEN_PARSE);
} catch (\ParseError $e) { } catch (\ParseError $e) {
trigger_error($e->getMessage(), E_USER_NOTICE); trigger_error($e->getMessage(), E_USER_NOTICE);
$tokens = []; $tokens = [];
} }
$namespace = $class = $classLevel = $level = null; $namespace = $class = null;
$classLevel = $level = 0;
$res = $uses = []; $res = $uses = [];
$nameTokens = PHP_VERSION_ID < 80000 $nameTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED];
? [T_STRING, T_NS_SEPARATOR]
: [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED];
while ($token = current($tokens)) { while ($token = current($tokens)) {
next($tokens); next($tokens);
switch (is_array($token) ? $token[0] : $token) { switch ($token->id) {
case T_NAMESPACE: case T_NAMESPACE:
$namespace = ltrim(self::fetch($tokens, $nameTokens) . '\\', '\\'); $namespace = ltrim(self::fetch($tokens, $nameTokens) . '\\', '\\');
$uses = []; $uses = [];
@@ -342,9 +239,7 @@ final class Reflection
case T_CLASS: case T_CLASS:
case T_INTERFACE: case T_INTERFACE:
case T_TRAIT: case T_TRAIT:
case PHP_VERSION_ID < 80100 case T_ENUM:
? T_CLASS
: T_ENUM:
if ($name = self::fetch($tokens, T_STRING)) { if ($name = self::fetch($tokens, T_STRING)) {
$class = $namespace . $name; $class = $namespace . $name;
$classLevel = $level + 1; $classLevel = $level + 1;
@@ -389,13 +284,13 @@ final class Reflection
case T_CURLY_OPEN: case T_CURLY_OPEN:
case T_DOLLAR_OPEN_CURLY_BRACES: case T_DOLLAR_OPEN_CURLY_BRACES:
case '{': case ord('{'):
$level++; $level++;
break; break;
case '}': case ord('}'):
if ($level === $classLevel) { if ($level === $classLevel) {
$class = $classLevel = null; $class = $classLevel = 0;
} }
$level--; $level--;
@@ -406,14 +301,13 @@ final class Reflection
} }
private static function fetch(array &$tokens, $take): ?string private static function fetch(array &$tokens, string|int|array $take): ?string
{ {
$res = null; $res = null;
while ($token = current($tokens)) { while ($token = current($tokens)) {
[$token, $s] = is_array($token) ? $token : [$token, $token]; if ($token->is($take)) {
if (in_array($token, (array) $take, true)) { $res .= $token->text;
$res .= $s; } elseif (!$token->is([T_DOC_COMMENT, T_WHITESPACE, T_COMMENT])) {
} elseif (!in_array($token, [T_DOC_COMMENT, T_WHITESPACE, T_COMMENT], true)) {
break; break;
} }

View File

@@ -9,8 +9,10 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use JetBrains\PhpStorm\Language;
use Nette; use Nette;
use function is_array, is_object, strlen; use function array_keys, array_map, array_shift, array_values, bin2hex, class_exists, defined, extension_loaded, function_exists, htmlspecialchars, htmlspecialchars_decode, iconv, iconv_strlen, iconv_substr, implode, in_array, is_array, is_callable, is_int, is_object, is_string, key, max, mb_convert_case, mb_strlen, mb_strtolower, mb_strtoupper, mb_substr, pack, preg_last_error, preg_last_error_msg, preg_quote, preg_replace, str_contains, str_ends_with, str_repeat, str_replace, str_starts_with, strlen, strpos, strrev, strrpos, strtolower, strtoupper, strtr, substr, trim, unpack, utf8_decode;
use const ENT_IGNORE, ENT_NOQUOTES, ICONV_IMPL, MB_CASE_TITLE, PHP_EOL, PREG_OFFSET_CAPTURE, PREG_PATTERN_ORDER, PREG_SET_ORDER, PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY, PREG_SPLIT_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL;
/** /**
@@ -20,11 +22,14 @@ class Strings
{ {
use Nette\StaticClass; use Nette\StaticClass;
public const TRIM_CHARACTERS = " \t\n\r\0\x0B\u{A0}"; public const TrimCharacters = " \t\n\r\0\x0B\u{A0}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{200B}\u{2028}\u{3000}";
#[\Deprecated('use Strings::TrimCharacters')]
public const TRIM_CHARACTERS = self::TrimCharacters;
/** /**
* Checks if the string is valid in UTF-8 encoding. * @deprecated use Nette\Utils\Validators::isUnicode()
*/ */
public static function checkEncoding(string $s): bool public static function checkEncoding(string $s): bool
{ {
@@ -59,29 +64,47 @@ class Strings
/** /**
* Starts the $haystack string with the prefix $needle? * Returns a code point of specific character in UTF-8 (number in range 0x0000..D7FF or 0xE000..10FFFF).
*/
public static function ord(string $c): int
{
if (!extension_loaded('iconv')) {
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
}
$tmp = iconv('UTF-8', 'UTF-32BE//IGNORE', $c);
if (!$tmp) {
throw new Nette\InvalidArgumentException('Invalid UTF-8 character "' . ($c === '' ? '' : '\x' . strtoupper(bin2hex($c))) . '".');
}
return unpack('N', $tmp)[1];
}
/**
* @deprecated use str_starts_with()
*/ */
public static function startsWith(string $haystack, string $needle): bool public static function startsWith(string $haystack, string $needle): bool
{ {
return strncmp($haystack, $needle, strlen($needle)) === 0; return str_starts_with($haystack, $needle);
} }
/** /**
* Ends the $haystack string with the suffix $needle? * @deprecated use str_ends_with()
*/ */
public static function endsWith(string $haystack, string $needle): bool public static function endsWith(string $haystack, string $needle): bool
{ {
return $needle === '' || substr($haystack, -strlen($needle)) === $needle; return str_ends_with($haystack, $needle);
} }
/** /**
* Does $haystack contain $needle? * @deprecated use str_contains()
*/ */
public static function contains(string $haystack, string $needle): bool public static function contains(string $haystack, string $needle): bool
{ {
return strpos($haystack, $needle) !== false; return str_contains($haystack, $needle);
} }
@@ -116,7 +139,7 @@ class Strings
$s = $n; $s = $n;
} }
$s = self::normalizeNewLines($s); $s = self::unixNewLines($s);
// remove control characters; leave \t + \n // remove control characters; leave \t + \n
$s = self::pcre('preg_replace', ['#[\x00-\x08\x0B-\x1F\x7F-\x9F]+#u', '', $s]); $s = self::pcre('preg_replace', ['#[\x00-\x08\x0B-\x1F\x7F-\x9F]+#u', '', $s]);
@@ -131,12 +154,30 @@ class Strings
} }
/** /** @deprecated use Strings::unixNewLines() */
* Standardize line endings to unix-like.
*/
public static function normalizeNewLines(string $s): string public static function normalizeNewLines(string $s): string
{ {
return str_replace(["\r\n", "\r"], "\n", $s); return self::unixNewLines($s);
}
/**
* Converts line endings to \n used on Unix-like systems.
* Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator.
*/
public static function unixNewLines(string $s): string
{
return preg_replace("~\r\n?|\u{2028}|\u{2029}~", "\n", $s);
}
/**
* Converts line endings to platform-specific, i.e. \r\n on Windows and \n elsewhere.
* Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator.
*/
public static function platformNewLines(string $s): string
{
return preg_replace("~\r\n?|\n|\u{2028}|\u{2029}~", PHP_EOL, $s);
} }
@@ -145,17 +186,12 @@ class Strings
*/ */
public static function toAscii(string $s): string public static function toAscii(string $s): string
{ {
$iconv = defined('ICONV_IMPL') ? trim(ICONV_IMPL, '"\'') : null; if (!extension_loaded('intl')) {
static $transliterator = null; throw new Nette\NotSupportedException(__METHOD__ . '() requires INTL extension that is not loaded.');
if ($transliterator === null) {
if (class_exists('Transliterator', false)) {
$transliterator = \Transliterator::create('Any-Latin; Latin-ASCII');
} else {
trigger_error(__METHOD__ . "(): it is recommended to enable PHP extensions 'intl'.", E_USER_NOTICE);
$transliterator = false;
}
} }
$iconv = defined('ICONV_IMPL') ? trim(ICONV_IMPL, '"\'') : null;
// remove control characters and check UTF-8 validity // remove control characters and check UTF-8 validity
$s = self::pcre('preg_replace', ['#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s]); $s = self::pcre('preg_replace', ['#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s]);
@@ -165,39 +201,15 @@ class Strings
$s = strtr($s, ["\u{AE}" => '(R)', "\u{A9}" => '(c)', "\u{2026}" => '...', "\u{AB}" => '<<', "\u{BB}" => '>>', "\u{A3}" => 'lb', "\u{A5}" => 'yen', "\u{B2}" => '^2', "\u{B3}" => '^3', "\u{B5}" => 'u', "\u{B9}" => '^1', "\u{BA}" => 'o', "\u{BF}" => '?', "\u{2CA}" => "'", "\u{2CD}" => '_', "\u{2DD}" => '"', "\u{1FEF}" => '', "\u{20AC}" => 'EUR', "\u{2122}" => 'TM', "\u{212E}" => 'e', "\u{2190}" => '<-', "\u{2191}" => '^', "\u{2192}" => '->', "\u{2193}" => 'V', "\u{2194}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ € ™ ← ↑ → ↓ ↔ $s = strtr($s, ["\u{AE}" => '(R)', "\u{A9}" => '(c)', "\u{2026}" => '...', "\u{AB}" => '<<', "\u{BB}" => '>>', "\u{A3}" => 'lb', "\u{A5}" => 'yen', "\u{B2}" => '^2', "\u{B3}" => '^3', "\u{B5}" => 'u', "\u{B9}" => '^1', "\u{BA}" => 'o', "\u{BF}" => '?', "\u{2CA}" => "'", "\u{2CD}" => '_', "\u{2DD}" => '"', "\u{1FEF}" => '', "\u{20AC}" => 'EUR', "\u{2122}" => 'TM', "\u{212E}" => 'e', "\u{2190}" => '<-', "\u{2191}" => '^', "\u{2192}" => '->', "\u{2193}" => 'V', "\u{2194}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ € ™ ← ↑ → ↓ ↔
} }
if ($transliterator) { $s = \Transliterator::create('Any-Latin; Latin-ASCII')->transliterate($s);
$s = $transliterator->transliterate($s); // use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ
// use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ if ($iconv === 'glibc') {
if ($iconv === 'glibc') { $s = strtr($s, '?', "\x01"); // temporarily hide ? to distinguish them from the garbage that iconv creates
$s = strtr($s, '?', "\x01"); // temporarily hide ? to distinguish them from the garbage that iconv creates $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); $s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters
$s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters } elseif ($iconv === 'libiconv') {
} elseif ($iconv === 'libiconv') { $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); } else { // null or 'unknown' (#216)
} else { // null or 'unknown' (#216)
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
}
} elseif ($iconv === 'glibc' || $iconv === 'libiconv') {
// temporarily hide these characters to distinguish them from the garbage that iconv creates
$s = strtr($s, '`\'"^~?', "\x01\x02\x03\x04\x05\x06");
if ($iconv === 'glibc') {
// glibc implementation is very limited. transliterate into Windows-1250 and then into ASCII, so most Eastern European characters are preserved
$s = iconv('UTF-8', 'WINDOWS-1250//TRANSLIT//IGNORE', $s);
$s = strtr(
$s,
"\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe\x96\xa0\x8b\x97\x9b\xa6\xad\xb7",
'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.'
);
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]);
} else {
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
}
// remove garbage that iconv creates during transliteration (eg Ý -> Y')
$s = str_replace(['`', "'", '"', '^', '~', '?'], '', $s);
// restore temporarily hidden characters
$s = strtr($s, "\x01\x02\x03\x04\x05\x06", '`\'"^~?');
} else {
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
} }
@@ -357,16 +369,18 @@ class Strings
*/ */
public static function length(string $s): int public static function length(string $s): int
{ {
return function_exists('mb_strlen') return match (true) {
? mb_strlen($s, 'UTF-8') extension_loaded('mbstring') => mb_strlen($s, 'UTF-8'),
: strlen(utf8_decode($s)); extension_loaded('iconv') => iconv_strlen($s, 'UTF-8'),
default => strlen(@utf8_decode($s)), // deprecated
};
} }
/** /**
* Removes all left and right side spaces (or the characters passed as second argument) from a UTF-8 encoded string. * Removes all left and right side spaces (or the characters passed as second argument) from a UTF-8 encoded string.
*/ */
public static function trim(string $s, string $charlist = self::TRIM_CHARACTERS): string public static function trim(string $s, string $charlist = self::TrimCharacters): string
{ {
$charlist = preg_quote($charlist, '#'); $charlist = preg_quote($charlist, '#');
return self::replace($s, '#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', ''); return self::replace($s, '#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', '');
@@ -375,6 +389,7 @@ class Strings
/** /**
* Pads a UTF-8 string to given length by prepending the $pad string to the beginning. * Pads a UTF-8 string to given length by prepending the $pad string to the beginning.
* @param non-empty-string $pad
*/ */
public static function padLeft(string $s, int $length, string $pad = ' '): string public static function padLeft(string $s, int $length, string $pad = ' '): string
{ {
@@ -386,6 +401,7 @@ class Strings
/** /**
* Pads UTF-8 string to given length by appending the $pad string to the end. * Pads UTF-8 string to given length by appending the $pad string to the end.
* @param non-empty-string $pad
*/ */
public static function padRight(string $s, int $length, string $pad = ' '): string public static function padRight(string $s, int $length, string $pad = ' '): string
{ {
@@ -482,73 +498,187 @@ class Strings
/** /**
* Splits a string into array by the regular expression. Parenthesized expression in the delimiter are captured. * Divides the string into arrays according to the regular expression. Expressions in parentheses will be captured and returned as well.
* Parameter $flags can be any combination of PREG_SPLIT_NO_EMPTY and PREG_OFFSET_CAPTURE flags.
*/ */
public static function split(string $subject, string $pattern, int $flags = 0): array public static function split(
string $subject,
#[Language('RegExp')]
string $pattern,
bool|int $captureOffset = false,
bool $skipEmpty = false,
int $limit = -1,
bool $utf8 = false,
): array
{ {
return self::pcre('preg_split', [$pattern, $subject, -1, $flags | PREG_SPLIT_DELIM_CAPTURE]); $flags = is_int($captureOffset) // back compatibility
? $captureOffset
: ($captureOffset ? PREG_SPLIT_OFFSET_CAPTURE : 0) | ($skipEmpty ? PREG_SPLIT_NO_EMPTY : 0);
$pattern .= $utf8 ? 'u' : '';
$m = self::pcre('preg_split', [$pattern, $subject, $limit, $flags | PREG_SPLIT_DELIM_CAPTURE]);
return $utf8 && $captureOffset
? self::bytesToChars($subject, [$m])[0]
: $m;
} }
/** /**
* Checks if given string matches a regular expression pattern and returns an array with first found match and each subpattern. * Searches the string for the part matching the regular expression and returns
* Parameter $flags can be any combination of PREG_OFFSET_CAPTURE and PREG_UNMATCHED_AS_NULL flags. * an array with the found expression and individual subexpressions, or `null`.
*/ */
public static function match(string $subject, string $pattern, int $flags = 0, int $offset = 0): ?array public static function match(
string $subject,
#[Language('RegExp')]
string $pattern,
bool|int $captureOffset = false,
int $offset = 0,
bool $unmatchedAsNull = false,
bool $utf8 = false,
): ?array
{ {
if ($offset > strlen($subject)) { $flags = is_int($captureOffset) // back compatibility
return null; ? $captureOffset
: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
if ($utf8) {
$offset = strlen(self::substring($subject, 0, $offset));
$pattern .= 'u';
} }
return self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset]) if ($offset > strlen($subject)) {
? $m return null;
: null; } elseif (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) {
return null;
} elseif ($utf8 && $captureOffset) {
return self::bytesToChars($subject, [$m])[0];
} else {
return $m;
}
} }
/** /**
* Finds all occurrences matching regular expression pattern and returns a two-dimensional array. Result is array of matches (ie uses by default PREG_SET_ORDER). * Searches the string for all occurrences matching the regular expression and
* Parameter $flags can be any combination of PREG_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL and PREG_PATTERN_ORDER flags. * returns an array of arrays containing the found expression and each subexpression.
* @return ($lazy is true ? \Generator<int, array> : array[])
*/ */
public static function matchAll(string $subject, string $pattern, int $flags = 0, int $offset = 0): array public static function matchAll(
string $subject,
#[Language('RegExp')]
string $pattern,
bool|int $captureOffset = false,
int $offset = 0,
bool $unmatchedAsNull = false,
bool $patternOrder = false,
bool $utf8 = false,
bool $lazy = false,
): array|\Generator
{ {
if ($utf8) {
$offset = strlen(self::substring($subject, 0, $offset));
$pattern .= 'u';
}
if ($lazy) {
$flags = PREG_OFFSET_CAPTURE | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
return (function () use ($utf8, $captureOffset, $flags, $subject, $pattern, $offset) {
$counter = 0;
while (
$offset <= strlen($subject) - ($counter ? 1 : 0)
&& self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])
) {
$offset = $m[0][1] + max(1, strlen($m[0][0]));
if (!$captureOffset) {
$m = array_map(fn($item) => $item[0], $m);
} elseif ($utf8) {
$m = self::bytesToChars($subject, [$m])[0];
}
yield $counter++ => $m;
}
})();
}
if ($offset > strlen($subject)) { if ($offset > strlen($subject)) {
return []; return [];
} }
$flags = is_int($captureOffset) // back compatibility
? $captureOffset
: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0);
self::pcre('preg_match_all', [ self::pcre('preg_match_all', [
$pattern, $subject, &$m, $pattern, $subject, &$m,
($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER), ($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER),
$offset, $offset,
]); ]);
return $m; return $utf8 && $captureOffset
? self::bytesToChars($subject, $m)
: $m;
} }
/** /**
* Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`. * Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`.
* @param string|array $pattern
* @param string|callable $replacement
*/ */
public static function replace(string $subject, $pattern, $replacement = '', int $limit = -1): string public static function replace(
string $subject,
#[Language('RegExp')]
string|array $pattern,
string|callable $replacement = '',
int $limit = -1,
bool $captureOffset = false,
bool $unmatchedAsNull = false,
bool $utf8 = false,
): string
{ {
if (is_object($replacement) || is_array($replacement)) { if (is_object($replacement) || is_array($replacement)) {
if (!is_callable($replacement, false, $textual)) { if (!is_callable($replacement, false, $textual)) {
throw new Nette\InvalidStateException("Callback '$textual' is not callable."); throw new Nette\InvalidStateException("Callback '$textual' is not callable.");
} }
return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit]); $flags = ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
if ($utf8) {
$pattern .= 'u';
if ($captureOffset) {
$replacement = fn($m) => $replacement(self::bytesToChars($subject, [$m])[0]);
}
}
return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit, 0, $flags]);
} elseif (is_array($pattern) && is_string(key($pattern))) { } elseif (is_array($pattern) && is_string(key($pattern))) {
$replacement = array_values($pattern); $replacement = array_values($pattern);
$pattern = array_keys($pattern); $pattern = array_keys($pattern);
} }
if ($utf8) {
$pattern = array_map(fn($item) => $item . 'u', (array) $pattern);
}
return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]); return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]);
} }
private static function bytesToChars(string $s, array $groups): array
{
$lastBytes = $lastChars = 0;
foreach ($groups as &$matches) {
foreach ($matches as &$match) {
if ($match[1] > $lastBytes) {
$lastChars += self::length(substr($s, $lastBytes, $match[1] - $lastBytes));
} elseif ($match[1] < $lastBytes) {
$lastChars -= self::length(substr($s, $match[1], $lastBytes - $match[1]));
}
$lastBytes = $match[1];
$match[1] = $lastChars;
}
}
return $groups;
}
/** @internal */ /** @internal */
public static function pcre(string $func, array $args) public static function pcre(string $func, array $args)
{ {
@@ -560,7 +690,7 @@ class Strings
if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars
&& ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true)) && ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true))
) { ) {
throw new RegexpException((RegexpException::MESSAGES[$code] ?? 'Unknown error') throw new RegexpException(preg_last_error_msg()
. ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code); . ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code);
} }

View File

@@ -10,58 +10,52 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function array_map, array_search, array_splice, count, explode, implode, is_a, is_resource, is_string, strcasecmp, strtolower, substr, trim;
/** /**
* PHP type reflection. * PHP type reflection.
*/ */
final class Type final readonly class Type
{ {
/** @var array */ /** @var array<int, string|self> */
private $types; private array $types;
private bool $simple;
/** @var bool */ private string $kind; // | &
private $single;
/** @var string |, & */
private $kind;
/** /**
* Creates a Type object based on reflection. Resolves self, static and parent to the actual class name. * Creates a Type object based on reflection. Resolves self, static and parent to the actual class name.
* If the subject has no type, it returns null. * If the subject has no type, it returns null.
* @param \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection
*/ */
public static function fromReflection($reflection): ?self public static function fromReflection(
\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection,
): ?self
{ {
if ($reflection instanceof \ReflectionProperty && PHP_VERSION_ID < 70400) { $type = $reflection instanceof \ReflectionFunctionAbstract
return null; ? $reflection->getReturnType() ?? ($reflection instanceof \ReflectionMethod ? $reflection->getTentativeReturnType() : null)
} elseif ($reflection instanceof \ReflectionMethod) { : $reflection->getType();
$type = $reflection->getReturnType() ?? (PHP_VERSION_ID >= 80100 ? $reflection->getTentativeReturnType() : null);
} else {
$type = $reflection instanceof \ReflectionFunctionAbstract
? $reflection->getReturnType()
: $reflection->getType();
}
if ($type === null) { return $type ? self::fromReflectionType($type, $reflection, asObject: true) : null;
return null; }
} elseif ($type instanceof \ReflectionNamedType) {
$name = self::resolve($type->getName(), $reflection); private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject): self|string
return new self($type->allowsNull() && $type->getName() !== 'mixed' ? [$name, 'null'] : [$name]); {
if ($type instanceof \ReflectionNamedType) {
$name = self::resolve($type->getName(), $of);
return $asObject
? new self($type->allowsNull() && $name !== 'mixed' ? [$name, 'null'] : [$name])
: $name;
} elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
return new self( return new self(
array_map( array_map(fn($t) => self::fromReflectionType($t, $of, asObject: false), $type->getTypes()),
function ($t) use ($reflection) { return self::resolve($t->getName(), $reflection); }, $type instanceof \ReflectionUnionType ? '|' : '&',
$type->getTypes()
),
$type instanceof \ReflectionUnionType ? '|' : '&'
); );
} else { } else {
throw new Nette\InvalidStateException('Unexpected type of ' . Reflection::toString($reflection)); throw new Nette\InvalidStateException('Unexpected type of ' . Reflection::toString($of));
} }
} }
@@ -71,37 +65,60 @@ final class Type
*/ */
public static function fromString(string $type): self public static function fromString(string $type): self
{ {
if (!preg_match('#(?: if (!Validators::isTypeDeclaration($type)) {
\?([\w\\\\]+)|
[\w\\\\]+ (?: (&[\w\\\\]+)* | (\|[\w\\\\]+)* )
)()$#xAD', $type, $m)) {
throw new Nette\InvalidArgumentException("Invalid type '$type'."); throw new Nette\InvalidArgumentException("Invalid type '$type'.");
} }
[, $nType, $iType] = $m; if ($type[0] === '?') {
if ($nType) { return new self([substr($type, 1), 'null']);
return new self([$nType, 'null']);
} elseif ($iType) {
return new self(explode('&', $type), '&');
} else {
return new self(explode('|', $type));
} }
$unions = [];
foreach (explode('|', $type) as $part) {
$part = explode('&', trim($part, '()'));
$unions[] = count($part) === 1 ? $part[0] : new self($part, '&');
}
return count($unions) === 1 && $unions[0] instanceof self
? $unions[0]
: new self($unions);
}
/**
* Creates a Type object based on the actual type of value.
*/
public static function fromValue(mixed $value): self
{
$type = get_debug_type($value);
if (is_resource($value)) {
$type = 'mixed';
} elseif (str_ends_with($type, '@anonymous')) {
$parent = substr($type, 0, -10);
$type = $parent === 'class' ? 'object' : $parent;
}
return new self([$type]);
} }
/** /**
* Resolves 'self', 'static' and 'parent' to the actual class name. * Resolves 'self', 'static' and 'parent' to the actual class name.
* @param \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection
*/ */
public static function resolve(string $type, $reflection): string public static function resolve(
string $type,
\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of,
): string
{ {
$lower = strtolower($type); $lower = strtolower($type);
if ($reflection instanceof \ReflectionFunction) { if ($of instanceof \ReflectionFunction) {
return $type; return $type;
} elseif ($lower === 'self' || $lower === 'static') { } elseif ($lower === 'self') {
return $reflection->getDeclaringClass()->name; return $of->getDeclaringClass()->name;
} elseif ($lower === 'parent' && $reflection->getDeclaringClass()->getParentClass()) { } elseif ($lower === 'static') {
return $reflection->getDeclaringClass()->getParentClass()->name; return ($of instanceof ReflectionMethod ? $of->getOriginalClass() : $of->getDeclaringClass())->name;
} elseif ($lower === 'parent' && $of->getDeclaringClass()->getParentClass()) {
return $of->getDeclaringClass()->getParentClass()->name;
} else { } else {
return $type; return $type;
} }
@@ -110,31 +127,57 @@ final class Type
private function __construct(array $types, string $kind = '|') private function __construct(array $types, string $kind = '|')
{ {
if ($types[0] === 'null') { // null as last $o = array_search('null', $types, strict: true);
array_push($types, array_shift($types)); if ($o !== false) { // null as last
array_splice($types, $o, 1);
$types[] = 'null';
} }
$this->types = $types; $this->types = $types;
$this->single = ($types[1] ?? 'null') === 'null'; $this->simple = is_string($types[0]) && ($types[1] ?? 'null') === 'null';
$this->kind = count($types) > 1 ? $kind : ''; $this->kind = count($types) > 1 ? $kind : '';
} }
public function __toString(): string public function __toString(): string
{ {
return $this->single $multi = count($this->types) > 1;
? (count($this->types) > 1 ? '?' : '') . $this->types[0] if ($this->simple) {
: implode($this->kind, $this->types); return ($multi ? '?' : '') . $this->types[0];
}
$res = [];
foreach ($this->types as $type) {
$res[] = $type instanceof self && $multi ? "($type)" : $type;
}
return implode($this->kind, $res);
}
/**
* Returns a type that accepts both the current type and the given type.
*/
public function with(string|self $type): self
{
$type = is_string($type) ? self::fromString($type) : $type;
return match (true) {
$this->allows($type) => $this,
$type->allows($this) => $type,
default => new self(array_unique(
array_merge($this->isIntersection() ? [$this] : $this->types, $type->isIntersection() ? [$type] : $type->types),
SORT_REGULAR,
), '|'),
};
} }
/** /**
* Returns the array of subtypes that make up the compound type as strings. * Returns the array of subtypes that make up the compound type as strings.
* @return string[] * @return array<int, string|string[]>
*/ */
public function getNames(): array public function getNames(): array
{ {
return $this->types; return array_map(fn($t) => $t instanceof self ? $t->getNames() : $t, $this->types);
} }
@@ -144,16 +187,16 @@ final class Type
*/ */
public function getTypes(): array public function getTypes(): array
{ {
return array_map(function ($name) { return self::fromString($name); }, $this->types); return array_map(fn($t) => $t instanceof self ? $t : new self([$t]), $this->types);
} }
/** /**
* Returns the type name for single types, otherwise null. * Returns the type name for simple types, otherwise null.
*/ */
public function getSingleName(): ?string public function getSingleName(): ?string
{ {
return $this->single return $this->simple
? $this->types[0] ? $this->types[0]
: null; : null;
} }
@@ -178,29 +221,36 @@ final class Type
/** /**
* Returns true whether it is a single type. Simple nullable types are also considered to be single types. * Returns true whether it is a simple type. Single nullable types are also considered to be simple types.
*/ */
public function isSimple(): bool
{
return $this->simple;
}
#[\Deprecated('use isSimple()')]
public function isSingle(): bool public function isSingle(): bool
{ {
return $this->single; return $this->simple;
} }
/** /**
* Returns true whether the type is both a single and a PHP built-in type. * Returns true whether the type is both a simple and a PHP built-in type.
*/ */
public function isBuiltin(): bool public function isBuiltin(): bool
{ {
return $this->single && Reflection::isBuiltinType($this->types[0]); return $this->simple && Validators::isBuiltinType($this->types[0]);
} }
/** /**
* Returns true whether the type is both a single and a class name. * Returns true whether the type is both a simple and a class name.
*/ */
public function isClass(): bool public function isClass(): bool
{ {
return $this->single && !Reflection::isBuiltinType($this->types[0]); return $this->simple && !Validators::isBuiltinType($this->types[0]);
} }
@@ -209,44 +259,44 @@ final class Type
*/ */
public function isClassKeyword(): bool public function isClassKeyword(): bool
{ {
return $this->single && Reflection::isClassKeyword($this->types[0]); return $this->simple && Validators::isClassKeyword($this->types[0]);
} }
/** /**
* Verifies type compatibility. For example, it checks if a value of a certain type could be passed as a parameter. * Verifies type compatibility. For example, it checks if a value of a certain type could be passed as a parameter.
*/ */
public function allows(string $type): bool public function allows(string|self $type): bool
{ {
if ($this->types === ['mixed']) { if ($this->types === ['mixed']) {
return true; return true;
} }
$type = self::fromString($type); $type = is_string($type) ? self::fromString($type) : $type;
return $type->isUnion()
? Arrays::every($type->types, fn($t) => $this->allowsAny($t instanceof self ? $t->types : [$t]))
: $this->allowsAny($type->types);
}
if ($this->isIntersection()) {
if (!$type->isIntersection()) {
return false;
}
return Arrays::every($this->types, function ($currentType) use ($type) { private function allowsAny(array $givenTypes): bool
$builtin = Reflection::isBuiltinType($currentType); {
return Arrays::some($type->types, function ($testedType) use ($currentType, $builtin) { return $this->isUnion()
return $builtin ? Arrays::some($this->types, fn($t) => $this->allowsAll($t instanceof self ? $t->types : [$t], $givenTypes))
? strcasecmp($currentType, $testedType) === 0 : $this->allowsAll($this->types, $givenTypes);
: is_a($testedType, $currentType, true); }
});
});
}
$method = $type->isIntersection() ? 'some' : 'every';
return Arrays::$method($type->types, function ($testedType) { private function allowsAll(array $ourTypes, array $givenTypes): bool
$builtin = Reflection::isBuiltinType($testedType); {
return Arrays::some($this->types, function ($currentType) use ($testedType, $builtin) { return Arrays::every(
return $builtin $ourTypes,
? strcasecmp($currentType, $testedType) === 0 fn($ourType) => Arrays::some(
: is_a($testedType, $currentType, true); $givenTypes,
}); fn($givenType) => Validators::isBuiltinType($ourType)
}); ? strcasecmp($ourType, $givenType) === 0
: is_a($givenType, $ourType, allow_string: true),
),
);
} }
} }

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
use function array_key_exists, class_exists, explode, gettype, interface_exists, is_callable, is_float, is_int, is_iterable, is_numeric, is_object, is_string, preg_match, str_ends_with, str_replace, str_starts_with, strlen, strtolower, substr, trait_exists, var_export;
/** /**
@@ -19,6 +20,12 @@ class Validators
{ {
use Nette\StaticClass; use Nette\StaticClass;
private const BuiltinTypes = [
'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1,
'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1,
'never' => 1, 'true' => 1,
];
/** @var array<string,?callable> */ /** @var array<string,?callable> */
protected static $validators = [ protected static $validators = [
// PHP types // PHP types
@@ -87,19 +94,18 @@ class Validators
/** /**
* Verifies that the value is of expected types separated by pipe. * Verifies that the value is of expected types separated by pipe.
* @param mixed $value
* @throws AssertionException * @throws AssertionException
*/ */
public static function assert($value, string $expected, string $label = 'variable'): void public static function assert(mixed $value, string $expected, string $label = 'variable'): void
{ {
if (!static::is($value, $expected)) { if (!static::is($value, $expected)) {
$expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
$translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null']; $translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null'];
$type = $translate[gettype($value)] ?? gettype($value); $type = $translate[gettype($value)] ?? gettype($value);
if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) { if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) {
$type .= ' ' . var_export($value, true); $type .= ' ' . var_export($value, return: true);
} elseif (is_object($value)) { } elseif (is_object($value)) {
$type .= ' ' . get_class($value); $type .= ' ' . $value::class;
} }
throw new AssertionException("The $label expects to be $expected, $type given."); throw new AssertionException("The $label expects to be $expected, $type given.");
@@ -110,15 +116,15 @@ class Validators
/** /**
* Verifies that element $key in array is of expected types separated by pipe. * Verifies that element $key in array is of expected types separated by pipe.
* @param mixed[] $array * @param mixed[] $array
* @param int|string $key
* @throws AssertionException * @throws AssertionException
*/ */
public static function assertField( public static function assertField(
array $array, array $array,
$key, $key,
?string $expected = null, ?string $expected = null,
string $label = "item '%' in array" string $label = "item '%' in array",
): void { ): void
{
if (!array_key_exists($key, $array)) { if (!array_key_exists($key, $array)) {
throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.'); throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.');
@@ -130,18 +136,17 @@ class Validators
/** /**
* Verifies that the value is of expected types separated by pipe. * Verifies that the value is of expected types separated by pipe.
* @param mixed $value
*/ */
public static function is($value, string $expected): bool public static function is(mixed $value, string $expected): bool
{ {
foreach (explode('|', $expected) as $item) { foreach (explode('|', $expected) as $item) {
if (substr($item, -2) === '[]') { if (str_ends_with($item, '[]')) {
if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) { if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) {
return true; return true;
} }
continue; continue;
} elseif (substr($item, 0, 1) === '?') { } elseif (str_starts_with($item, '?')) {
$item = substr($item, 1); $item = substr($item, 1);
if ($value === null) { if ($value === null) {
return true; return true;
@@ -208,9 +213,9 @@ class Validators
/** /**
* Checks if the value is an integer or a float. * Checks if the value is an integer or a float.
* @param mixed $value * @return ($value is int|float ? true : false)
*/ */
public static function isNumber($value): bool public static function isNumber(mixed $value): bool
{ {
return is_int($value) || is_float($value); return is_int($value) || is_float($value);
} }
@@ -218,9 +223,9 @@ class Validators
/** /**
* Checks if the value is an integer or a integer written in a string. * Checks if the value is an integer or a integer written in a string.
* @param mixed $value * @return ($value is non-empty-string ? bool : ($value is int ? true : false))
*/ */
public static function isNumericInt($value): bool public static function isNumericInt(mixed $value): bool
{ {
return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value)); return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value));
} }
@@ -228,9 +233,9 @@ class Validators
/** /**
* Checks if the value is a number or a number written in a string. * Checks if the value is a number or a number written in a string.
* @param mixed $value * @return ($value is non-empty-string ? bool : ($value is int|float ? true : false))
*/ */
public static function isNumeric($value): bool public static function isNumeric(mixed $value): bool
{ {
return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value)); return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value));
} }
@@ -238,19 +243,17 @@ class Validators
/** /**
* Checks if the value is a syntactically correct callback. * Checks if the value is a syntactically correct callback.
* @param mixed $value
*/ */
public static function isCallable($value): bool public static function isCallable(mixed $value): bool
{ {
return $value && is_callable($value, true); return $value && is_callable($value, syntax_only: true);
} }
/** /**
* Checks if the value is a valid UTF-8 string. * Checks if the value is a valid UTF-8 string.
* @param mixed $value
*/ */
public static function isUnicode($value): bool public static function isUnicode(mixed $value): bool
{ {
return is_string($value) && preg_match('##u', $value); return is_string($value) && preg_match('##u', $value);
} }
@@ -258,9 +261,9 @@ class Validators
/** /**
* Checks if the value is 0, '', false or null. * Checks if the value is 0, '', false or null.
* @param mixed $value * @return ($value is 0|''|false|null ? true : false)
*/ */
public static function isNone($value): bool public static function isNone(mixed $value): bool
{ {
return $value == null; // intentionally == return $value == null; // intentionally ==
} }
@@ -275,10 +278,10 @@ class Validators
/** /**
* Checks if a variable is a zero-based integer indexed array. * Checks if a variable is a zero-based integer indexed array.
* @param mixed $value
* @deprecated use Nette\Utils\Arrays::isList * @deprecated use Nette\Utils\Arrays::isList
* @return ($value is list ? true : false)
*/ */
public static function isList($value): bool public static function isList(mixed $value): bool
{ {
return Arrays::isList($value); return Arrays::isList($value);
} }
@@ -287,9 +290,8 @@ class Validators
/** /**
* Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null). * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null).
* Numbers, strings and DateTime objects can be compared. * Numbers, strings and DateTime objects can be compared.
* @param mixed $value
*/ */
public static function isInRange($value, array $range): bool public static function isInRange(mixed $value, array $range): bool
{ {
if ($value === null || !(isset($range[0]) || isset($range[1]))) { if ($value === null || !(isset($range[0]) || isset($range[1]))) {
return false; return false;
@@ -320,14 +322,13 @@ class Validators
$atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part
$alpha = "a-z\x80-\xFF"; // superset of IDN $alpha = "a-z\x80-\xFF"; // superset of IDN
return (bool) preg_match(<<<XX return (bool) preg_match(<<<XX
(^ (^(?n)
("([ !#-[\\]-~]*|\\\\[ -~])+"|$atom+(\\.$atom+)*) # quoted or unquoted ("([ !#-[\\]-~]*|\\\\[ -~])+"|$atom+(\\.$atom+)*) # quoted or unquoted
@ @
([0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)+ # domain - RFC 1034 ([0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)+ # domain - RFC 1034
[$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain [$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain
$)Dix $)Dix
XX XX, $value);
, $value);
} }
@@ -338,20 +339,19 @@ XX
{ {
$alpha = "a-z\x80-\xFF"; $alpha = "a-z\x80-\xFF";
return (bool) preg_match(<<<XX return (bool) preg_match(<<<XX
(^ (^(?n)
https?://( https?://(
(([-_0-9$alpha]+\\.)* # subdomain (([-_0-9$alpha]+\\.)* # subdomain
[0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)? # domain [0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)? # domain
[$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain [$alpha]([-0-9$alpha]{0,17}[$alpha])? # top domain
|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} # IPv4 |\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} # IPv4
|\\[[0-9a-f:]{3,39}\\] # IPv6 |\\[[0-9a-f:]{3,39}\\] # IPv6
)(:\\d{1,5})? # port )(:\\d{1,5})? # port
(/\\S*)? # path (/\\S*)? # path
(\\?\\S*)? # query (\\?\\S*)? # query
(\\#\\S*)? # fragment (\\#\\S*)? # fragment
$)Dix $)Dix
XX XX, $value);
, $value);
} }
@@ -366,6 +366,7 @@ XX
/** /**
* Checks whether the input is a class, interface or trait. * Checks whether the input is a class, interface or trait.
* @deprecated
*/ */
public static function isType(string $type): bool public static function isType(string $type): bool
{ {
@@ -380,4 +381,37 @@ XX
{ {
return preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $value) === 1; return preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $value) === 1;
} }
/**
* Determines if type is PHP built-in type. Otherwise, it is the class name.
*/
public static function isBuiltinType(string $type): bool
{
return isset(self::BuiltinTypes[strtolower($type)]);
}
/**
* Determines if type is special class name self/parent/static.
*/
public static function isClassKeyword(string $name): bool
{
return (bool) preg_match('#^(self|parent|static)$#Di', $name);
}
/**
* Checks whether the given type declaration is syntactically valid.
*/
public static function isTypeDeclaration(string $type): bool
{
return (bool) preg_match(<<<'XX'
~((?n)
\?? (?<type> \\? (?<name> [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) |
(?<intersection> (?&type) (& (?&type))+ ) |
(?<upart> (?&type) | \( (?&intersection) \) ) (\| (?&upart))+
)$~xAD
XX, $type);
}
} }

View File

@@ -11,7 +11,7 @@ namespace Nette\Utils;
/** /**
* The exception that is thrown when an image error occurs. * An error occurred while working with the image.
*/ */
class ImageException extends \Exception class ImageException extends \Exception
{ {
@@ -19,7 +19,7 @@ class ImageException extends \Exception
/** /**
* The exception that indicates invalid image file. * The image file is invalid or in an unsupported format.
*/ */
class UnknownImageFileException extends ImageException class UnknownImageFileException extends ImageException
{ {
@@ -27,31 +27,23 @@ class UnknownImageFileException extends ImageException
/** /**
* The exception that indicates error of JSON encoding/decoding. * JSON encoding or decoding failed.
*/ */
class JsonException extends \Exception class JsonException extends \JsonException
{ {
} }
/** /**
* The exception that indicates error of the last Regexp execution. * Regular expression pattern or execution failed.
*/ */
class RegexpException extends \Exception class RegexpException extends \Exception
{ {
public const MESSAGES = [
PREG_INTERNAL_ERROR => 'Internal error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted',
PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data',
PREG_BAD_UTF8_OFFSET_ERROR => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point',
6 => 'Failed due to limited JIT stack space', // PREG_JIT_STACKLIMIT_ERROR
];
} }
/** /**
* The exception that indicates assertion error. * Type validation failed. The value doesn't match the expected type constraints.
*/ */
class AssertionException extends \Exception class AssertionException extends \Exception
{ {

View File

@@ -11,8 +11,7 @@ namespace Nette;
/** /**
* The exception that is thrown when the value of an argument is * The value is outside the allowed range.
* outside the allowable range of values as defined by the invoked method.
*/ */
class ArgumentOutOfRangeException extends \InvalidArgumentException class ArgumentOutOfRangeException extends \InvalidArgumentException
{ {
@@ -20,8 +19,7 @@ class ArgumentOutOfRangeException extends \InvalidArgumentException
/** /**
* The exception that is thrown when a method call is invalid for the object's * The object is in a state that does not allow the requested operation.
* current state, method has been invoked at an illegal or inappropriate time.
*/ */
class InvalidStateException extends \RuntimeException class InvalidStateException extends \RuntimeException
{ {
@@ -29,7 +27,7 @@ class InvalidStateException extends \RuntimeException
/** /**
* The exception that is thrown when a requested method or operation is not implemented. * The requested feature is not implemented.
*/ */
class NotImplementedException extends \LogicException class NotImplementedException extends \LogicException
{ {
@@ -37,8 +35,7 @@ class NotImplementedException extends \LogicException
/** /**
* The exception that is thrown when an invoked method is not supported. For scenarios where * The requested operation is not supported.
* it is sometimes possible to perform the requested operation, see InvalidStateException.
*/ */
class NotSupportedException extends \LogicException class NotSupportedException extends \LogicException
{ {
@@ -46,7 +43,7 @@ class NotSupportedException extends \LogicException
/** /**
* The exception that is thrown when a requested method or operation is deprecated. * The requested feature is deprecated and no longer available.
*/ */
class DeprecatedException extends NotSupportedException class DeprecatedException extends NotSupportedException
{ {
@@ -54,7 +51,7 @@ class DeprecatedException extends NotSupportedException
/** /**
* The exception that is thrown when accessing a class member (property or method) fails. * Cannot access the requested class property or method.
*/ */
class MemberAccessException extends \Error class MemberAccessException extends \Error
{ {
@@ -62,7 +59,7 @@ class MemberAccessException extends \Error
/** /**
* The exception that is thrown when an I/O error occurs. * Failed to read from or write to a file or stream.
*/ */
class IOException extends \RuntimeException class IOException extends \RuntimeException
{ {
@@ -70,7 +67,7 @@ class IOException extends \RuntimeException
/** /**
* The exception that is thrown when accessing a file that does not exist on disk. * The requested file does not exist.
*/ */
class FileNotFoundException extends IOException class FileNotFoundException extends IOException
{ {
@@ -78,7 +75,7 @@ class FileNotFoundException extends IOException
/** /**
* The exception that is thrown when part of a file or directory cannot be found. * The requested directory does not exist.
*/ */
class DirectoryNotFoundException extends IOException class DirectoryNotFoundException extends IOException
{ {
@@ -86,7 +83,7 @@ class DirectoryNotFoundException extends IOException
/** /**
* The exception that is thrown when an argument does not match with the expected value. * The provided argument has invalid type or format.
*/ */
class InvalidArgumentException extends \InvalidArgumentException class InvalidArgumentException extends \InvalidArgumentException
{ {
@@ -94,7 +91,7 @@ class InvalidArgumentException extends \InvalidArgumentException
/** /**
* The exception that is thrown when an illegal index was requested. * The requested array or collection index does not exist.
*/ */
class OutOfRangeException extends \OutOfRangeException class OutOfRangeException extends \OutOfRangeException
{ {
@@ -102,8 +99,16 @@ class OutOfRangeException extends \OutOfRangeException
/** /**
* The exception that is thrown when a value (typically returned by function) does not match with the expected value. * The returned value has unexpected type or format.
*/ */
class UnexpectedValueException extends \UnexpectedValueException class UnexpectedValueException extends \UnexpectedValueException
{ {
} }
/**
* Houston, we have a problem.
*/
class ShouldNotHappenException extends \LogicException
{
}