diff --git a/src/Application.php b/src/Application.php index d40018d..8a9159c 100644 --- a/src/Application.php +++ b/src/Application.php @@ -55,11 +55,14 @@ use Framework\Supports\Facades\File; use Framework\Supports\Str; use Framework\Supports\Traits\Macroable; -use Exception; use Framework\Supports\Arr; use InvalidArgumentException; use RuntimeException; +use function Framework\throw_anyway; +use function Framework\throw_if; +use function Framework\throw_unless; + class Application extends Container { use Macroable; @@ -497,9 +500,7 @@ public static function configure(string $base_path) */ public function use_routing(string $path) { - if (!file_exists($path)) { - throw new Exception("Route file not found: $path"); - } + throw_unless(file_exists($path), "Route file not found: $path"); include $path; @@ -914,15 +915,15 @@ protected function register_app_defined_providers() } foreach ($providers as $provider) { - if (!class_exists($provider) || !is_subclass_of($provider, ServiceProvider::class)) { - throw new InvalidArgumentException( - sprintf( - 'Class %s must be a subclass of %s.', - $provider, - ServiceProvider::class - ) - ); - } + throw_if( + !class_exists($provider) || !is_subclass_of($provider, ServiceProvider::class), + sprintf( + 'Class %s must be a subclass of %s.', + $provider, + ServiceProvider::class + ), + InvalidArgumentException::class + ); $this->register(new $provider($this)); } @@ -1084,7 +1085,7 @@ public function get_namespace_for_path($relative_path) } } - throw new RuntimeException(sprintf('Unable to detect namespace for path [%s].', $relative_path)); + throw_anyway(sprintf('Unable to detect namespace for path [%s].', $relative_path), RuntimeException::class); } /** diff --git a/src/Cache/CacheManager.php b/src/Cache/CacheManager.php index f97adf9..80b0c72 100644 --- a/src/Cache/CacheManager.php +++ b/src/Cache/CacheManager.php @@ -28,6 +28,8 @@ use function Framework\app; use function Framework\config; +use function Framework\throw_anyway; +use function Framework\throw_if; class CacheManager { @@ -202,11 +204,11 @@ public function store_config(string $name) $configured = config('cache.stores'); $configured = is_array($configured) ? $configured : []; - if (!isset($defaults[$name]) && !isset($configured[$name])) { - throw new InvalidArgumentException( - sprintf('Cache store [%s] is not configured. Check the "cache.stores" configuration.', $name) - ); - } + throw_if( + !isset($defaults[$name]) && !isset($configured[$name]), + sprintf('Cache store [%s] is not configured. Check the "cache.stores" configuration.', $name), + InvalidArgumentException::class + ); return array_merge($defaults[$name] ?? [], $configured[$name] ?? []); } @@ -278,8 +280,9 @@ protected function resolve(string $name) return $this->create_file_repository($name, $config); } - throw new InvalidArgumentException( - sprintf('Unsupported cache driver [%s] for store [%s].', (string) $driver, $name) + throw_anyway( + sprintf('Unsupported cache driver [%s] for store [%s].', (string) $driver, $name), + InvalidArgumentException::class ); } diff --git a/src/Cache/Repository.php b/src/Cache/Repository.php index dc19ee8..dd60826 100644 --- a/src/Cache/Repository.php +++ b/src/Cache/Repository.php @@ -27,6 +27,8 @@ use Throwable; use function Framework\app; +use function Framework\throw_if; +use function Framework\throw_unless; use function Framework\value; class Repository implements ArrayAccess @@ -234,9 +236,7 @@ public function string($key, $default = null) { $value = $this->get($key, $default); - if (!is_string($value)) { - throw new InvalidArgumentException($this->type_error($key, 'a string', $value)); - } + throw_unless(is_string($value), $this->type_error($key, 'a string', $value), InvalidArgumentException::class); return $value; } @@ -257,9 +257,11 @@ public function integer($key, $default = null) { $value = $this->get($key, $default); - if (filter_var($value, FILTER_VALIDATE_INT) === false) { - throw new InvalidArgumentException($this->type_error($key, 'an integer', $value)); - } + throw_if( + filter_var($value, FILTER_VALIDATE_INT) === false, + $this->type_error($key, 'an integer', $value), + InvalidArgumentException::class + ); return (int) $value; } @@ -280,9 +282,11 @@ public function float($key, $default = null) { $value = $this->get($key, $default); - if (filter_var($value, FILTER_VALIDATE_FLOAT) === false) { - throw new InvalidArgumentException($this->type_error($key, 'a float', $value)); - } + throw_if( + filter_var($value, FILTER_VALIDATE_FLOAT) === false, + $this->type_error($key, 'a float', $value), + InvalidArgumentException::class + ); return (float) $value; } @@ -303,9 +307,7 @@ public function boolean($key, $default = null) { $value = $this->get($key, $default); - if (!is_bool($value)) { - throw new InvalidArgumentException($this->type_error($key, 'a boolean', $value)); - } + throw_unless(is_bool($value), $this->type_error($key, 'a boolean', $value), InvalidArgumentException::class); return $value; } @@ -326,9 +328,7 @@ public function array($key, $default = null) { $value = $this->get($key, $default); - if (!is_array($value)) { - throw new InvalidArgumentException($this->type_error($key, 'an array', $value)); - } + throw_unless(is_array($value), $this->type_error($key, 'an array', $value), InvalidArgumentException::class); return $value; } @@ -612,11 +612,11 @@ public function flexible($key, array $ttl, Closure $callback) { $key = (string) $key; - if (count($ttl) !== 2) { - throw new InvalidArgumentException( - sprintf('The flexible lifetime for key [%s] must be a fresh and stale pair.', $key) - ); - } + throw_if( + count($ttl) !== 2, + sprintf('The flexible lifetime for key [%s] must be a fresh and stale pair.', $key), + InvalidArgumentException::class + ); $fresh = (int) $this->seconds_until($ttl[0]); $stale = (int) $this->seconds_until($ttl[1]); diff --git a/src/Cache/Stores/FileStore.php b/src/Cache/Stores/FileStore.php index f3a090e..f545bc5 100644 --- a/src/Cache/Stores/FileStore.php +++ b/src/Cache/Stores/FileStore.php @@ -21,6 +21,8 @@ use Framework\Filesystem\Filesystem; use Throwable; +use function Framework\throw_if; + class FileStore implements Store, CacheEntryProvider { use HashesKeys; @@ -142,11 +144,11 @@ public static function guard_supported() $method = get_filesystem_method(); - if ($method !== 'direct') { - throw new StoreUnavailableException( - sprintf('The file cache store requires direct filesystem access, got [%s].', (string) $method) - ); - } + throw_if( + $method !== 'direct', + sprintf('The file cache store requires direct filesystem access, got [%s].', (string) $method), + StoreUnavailableException::class + ); } /** diff --git a/src/Collections/Concerns/EnumeratesValues.php b/src/Collections/Concerns/EnumeratesValues.php index 9c0c26f..7242a31 100644 --- a/src/Collections/Concerns/EnumeratesValues.php +++ b/src/Collections/Concerns/EnumeratesValues.php @@ -14,6 +14,7 @@ use Framework\Collections\HigherOrderCollectionProxy; use function Framework\deep_get; +use function Framework\throw_unless; /** * Trait to enumerate values of the collection. @@ -113,9 +114,11 @@ protected function value_retriever($value) */ public function __get($key) { - if (!in_array($key, static::$proxies, true)) { - throw new Exception(sprintf('Property [%s] does not exist on this collection.', $key)); - } + throw_unless( + in_array($key, static::$proxies, true), + sprintf('Property [%s] does not exist on this collection.', $key), + Exception::class + ); return new HigherOrderCollectionProxy($this, $key); } diff --git a/src/Concerns/DependencyResolvable.php b/src/Concerns/DependencyResolvable.php index dfcf185..5277c69 100644 --- a/src/Concerns/DependencyResolvable.php +++ b/src/Concerns/DependencyResolvable.php @@ -18,6 +18,7 @@ use ReflectionParameter; use function Framework\app; +use function Framework\throw_anyway; trait DependencyResolvable { @@ -38,11 +39,11 @@ protected function resolve_primitive(ReflectionParameter $parameter) return $parameter->getDefaultValue(); } - throw new ReflectionException(sprintf( + throw_anyway(sprintf( 'Unable to resolve primitive parameter "%s" in class "%s".', $parameter->getName(), $parameter->getDeclaringClass()->getName() - )); + ), ReflectionException::class); } /** diff --git a/src/Console/CommandManager.php b/src/Console/CommandManager.php index 99ee483..21abc46 100644 --- a/src/Console/CommandManager.php +++ b/src/Console/CommandManager.php @@ -15,6 +15,7 @@ use RuntimeException; use function Framework\app; +use function Framework\throw_unless; class CommandManager { @@ -59,18 +60,20 @@ public function register(string $name, $command) protected function resolve($command) { if (is_object($command)) { - if (!$command instanceof CommandBase) { - throw new RuntimeException( - sprintf("Command [%s] must extend [%s]", get_class($command), CommandBase::class) - ); - } + throw_unless( + $command instanceof CommandBase, + sprintf("Command [%s] must extend [%s]", get_class($command), CommandBase::class), + RuntimeException::class + ); return $command; } - if (!class_exists($command)) { - throw new RuntimeException(sprintf("Command class [%s] not found", $command)); - } + throw_unless( + class_exists($command), + sprintf("Command class [%s] not found", $command), + RuntimeException::class + ); return app()->make($command); } diff --git a/src/Container.php b/src/Container.php index 0cd07aa..2dfe021 100644 --- a/src/Container.php +++ b/src/Container.php @@ -21,6 +21,9 @@ use LogicException; use ReflectionNamedType; +use function Framework\throw_anyway; +use function Framework\throw_if; + class Container implements ContainerContract { /** @@ -188,9 +191,7 @@ public function instance(string $abstract, $instance) */ public function alias(string $alias, string $abstract) { - if ($alias === $abstract) { - throw new LogicException(sprintf('[%s] is aliased to itself.', $abstract)); - } + throw_if($alias === $abstract, sprintf('[%s] is aliased to itself.', $abstract), LogicException::class); $this->aliases[$alias] = $abstract; } @@ -328,7 +329,7 @@ protected function autowire(string $class, array $parameters = []) // Check for circular dependencies if ($this->resolved($class)) { $chain = implode(' → ', $this->resolved) . " → {$class}"; - throw new Exception(sprintf( + throw_anyway(sprintf( 'Circular dependency detected: %s', $chain )); @@ -416,7 +417,7 @@ protected function resolve_primitive(ReflectionParameter $parameter, array $prim return $parameter->getDefaultValue(); } - throw new Exception(sprintf( + throw_anyway(sprintf( 'Unable to resolve primitive parameter "%s" in class "%s".', $param_name, $parameter->getDeclaringClass()->getName() diff --git a/src/CoreServiceProvider.php b/src/CoreServiceProvider.php index 0f84530..4993dfe 100644 --- a/src/CoreServiceProvider.php +++ b/src/CoreServiceProvider.php @@ -37,6 +37,7 @@ use Framework\View\ViewContext; use function Framework\config; +use function Framework\throw_anyway; class CoreServiceProvider extends ServiceProvider { @@ -146,8 +147,9 @@ protected function register_session_services() return new ArraySessionHandler(); } - throw new InvalidArgumentException( - sprintf('Unsupported session driver [%s]. Use "database" or "array".', (string) $driver) + throw_anyway( + sprintf('Unsupported session driver [%s]. Use "database" or "array".', (string) $driver), + InvalidArgumentException::class ); }); diff --git a/src/Database/Concerns/HasAttributes.php b/src/Database/Concerns/HasAttributes.php index 6b3abe9..40c1e87 100644 --- a/src/Database/Concerns/HasAttributes.php +++ b/src/Database/Concerns/HasAttributes.php @@ -28,6 +28,8 @@ use function Framework\collection; use function Framework\Polyfill\str_contains; use function Framework\tap; +use function Framework\throw_anyway; +use function Framework\throw_if; trait HasAttributes { @@ -199,20 +201,18 @@ protected function get_relationship_from_method($method) $relation = $this->$method(); if (!$relation instanceof Relation) { - if (is_null($relation)) { - throw new LogicException(sprintf( - '%s::%s must return a relationship instance, ' - . 'but "null" was returned. Was the "return" keyword used?', - static::class, - $method, - )); - } + throw_if(is_null($relation), sprintf( + '%s::%s must return a relationship instance, ' + . 'but "null" was returned. Was the "return" keyword used?', + static::class, + $method, + ), LogicException::class); - throw new LogicException(sprintf( + throw_anyway(sprintf( '%s::%s must return a relationship instance.', static::class, $method, - )); + ), LogicException::class); } return tap($relation->get_results(), function ($results) use ($method) { @@ -547,9 +547,7 @@ protected function cast_attribute_as_json($key, $value) { $value = $this->as_json($value); - if ($value === false) { - throw new InvalidCastException($this, $key, 'json'); - } + throw_if($value === false, $this, InvalidCastException::class, $key, 'json'); return $value; } @@ -992,7 +990,7 @@ protected function is_class_castable($key) return true; } - throw new InvalidCastException($this, $key, $cast_type); + throw_anyway($this, InvalidCastException::class, $key, $cast_type); } /** diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 292a5a4..3d8d902 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -106,6 +106,7 @@ protected function belongs_to($related, $foreign_key = null, $owner_key = null, */ protected function guess_belongs_to_relation() { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Introspects the calling method name to guess a relation; not debugging output. [, , $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); return $caller['function']; @@ -173,6 +174,7 @@ protected function belongs_to_many( */ protected function guess_belongs_to_many_relation() { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Introspects the calling method name to guess a relation; not debugging output. $caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($trace) { return !in_array( $trace['function'], diff --git a/src/Database/Connection/Connection.php b/src/Database/Connection/Connection.php index 1a602f1..13a038d 100644 --- a/src/Database/Connection/Connection.php +++ b/src/Database/Connection/Connection.php @@ -28,6 +28,8 @@ use function Framework\collection; use function Framework\Polyfill\str_contains; +use function Framework\throw_anyway; +use function Framework\throw_if; class Connection { @@ -111,7 +113,7 @@ protected function connect() global $wpdb; $this->db = $wpdb; } catch (Exception $error) { - throw new Exception("Database connection failed: " . $error->getMessage()); + throw_anyway("Database connection failed: " . $error->getMessage()); } } @@ -358,16 +360,17 @@ protected function run_query_callback($query, array $bindings, Closure $callback return $result; } catch (Exception $error) { - if ($this->is_unique_constraint_error($error)) { - throw new UniqueConstraintViolationException( - $query, - $this->prepare_bindings($bindings), - $error - ); - } + throw_if( + $this->is_unique_constraint_error($error), + $query, + UniqueConstraintViolationException::class, + $this->prepare_bindings($bindings), + $error + ); - throw new QueryException( + throw_anyway( $query, + QueryException::class, $this->prepare_bindings($bindings), $error ); diff --git a/src/Database/Migrations/Migrator.php b/src/Database/Migrations/Migrator.php index 0222773..ee70d7e 100644 --- a/src/Database/Migrations/Migrator.php +++ b/src/Database/Migrations/Migrator.php @@ -16,6 +16,8 @@ use Framework\Supports\Facades\Schema; use Exception; +use function Framework\throw_unless; + class Migrator { /** @@ -244,23 +246,23 @@ protected function get_short_name(string $class_name) */ protected function validate_migration($migration, string $class_name) { - if (!class_exists($class_name)) { - throw new Exception( - sprintf( - 'Class [%s] does not exist', - $class_name - ) - ); - } - - if (!$migration instanceof Migration) { - throw new Exception( - sprintf( - 'Class [%s] must implements [%s]', - $class_name, - Migration::class - ) - ); - } + throw_unless( + class_exists($class_name), + sprintf( + 'Class [%s] does not exist', + $class_name + ), + Exception::class + ); + + throw_unless( + $migration instanceof Migration, + sprintf( + 'Class [%s] must implements [%s]', + $class_name, + Migration::class + ), + Exception::class + ); } } diff --git a/src/Database/Query/Model.php b/src/Database/Query/Model.php index 1fb49eb..08d7e6b 100644 --- a/src/Database/Query/Model.php +++ b/src/Database/Query/Model.php @@ -33,6 +33,7 @@ use function Framework\app; use function Framework\Polyfill\str_contains; +use function Framework\throw_anyway; abstract class Model implements Arrayable, Jsonable, ArrayAccess, JsonSerializable { @@ -859,12 +860,13 @@ public function fill(array $attributes) if (isset(static::$discarded_attribute_callback)) { call_user_func(static::$discarded_attribute_callback, $this, [$key]); } else { - throw new MassAssignmentException( + throw_anyway( sprintf( 'Add [%s] to fillable array to allow mass assignment on [%s].', $key, get_class($this) - ) + ), + MassAssignmentException::class ); } } @@ -876,12 +878,13 @@ public function fill(array $attributes) if (isset(static::$discarded_attribute_callback)) { call_user_func(static::$discarded_attribute_callback, $this, $keys); } else { - throw new MassAssignmentException( + throw_anyway( sprintf( 'Add [%s] to fillable array to allow mass assignment on [%s].', implode(', ', $keys), get_class($this) - ) + ), + MassAssignmentException::class ); } } diff --git a/src/Database/Query/QueryBuilder.php b/src/Database/Query/QueryBuilder.php index 8756d05..d0fb8a8 100644 --- a/src/Database/Query/QueryBuilder.php +++ b/src/Database/Query/QueryBuilder.php @@ -37,6 +37,9 @@ use function Framework\Polyfill\array_last; use function Framework\Polyfill\str_contains; use function Framework\tap; +use function Framework\throw_anyway; +use function Framework\throw_if; +use function Framework\throw_unless; use function Framework\value; /** @@ -482,9 +485,11 @@ public function from_raw($expression, array $bindings = []) */ public function set_bindings(array $bindings, $type = 'where') { - if (!array_key_exists($type, $this->bindings)) { - throw new InvalidArgumentException("Invalid binding type: $type"); - } + throw_unless( + array_key_exists($type, $this->bindings), + "Invalid binding type: $type", + InvalidArgumentException::class + ); $this->bindings[$type] = $bindings; @@ -507,9 +512,11 @@ public function add_bindings($value, $type = 'where') { $type = strtolower($type); - if (!array_key_exists($type, $this->bindings)) { - throw new InvalidArgumentException(sprintf('Invalid binding type: %s', $type)); - } + throw_unless( + array_key_exists($type, $this->bindings), + sprintf('Invalid binding type: %s', $type), + InvalidArgumentException::class + ); if (is_array($value)) { $this->bindings[$type] = array_values( @@ -2828,7 +2835,7 @@ public function first_or_fail($columns = ['*']) return $model; } - throw new ModelNotFoundException(get_class($this->model)); + throw_anyway(get_class($this->model), ModelNotFoundException::class); } /** @@ -3591,9 +3598,7 @@ public function sole($columns = ['*']) throw new RecordNotFoundException(); } - if ($count > 1) { - throw new MultipleRecordsFoundException($count); - } + throw_if($count > 1, $count, MultipleRecordsFoundException::class); return $result->first(); } diff --git a/src/Database/Query/Relations/BelongsToMany.php b/src/Database/Query/Relations/BelongsToMany.php index 0a4c16d..597ebc8 100644 --- a/src/Database/Query/Relations/BelongsToMany.php +++ b/src/Database/Query/Relations/BelongsToMany.php @@ -1035,7 +1035,7 @@ protected function update_existing_pivot($id, array $attributes) */ protected function add_timestamps_to_pivot(array &$record, $update = false) { - $timestamp = date('Y-m-d H:i:s'); + $timestamp = gmdate('Y-m-d H:i:s'); if (!$update && !isset($record['created_at'])) { $record['created_at'] = $timestamp; diff --git a/src/Database/Schema/Compiler.php b/src/Database/Schema/Compiler.php index 710a8a2..4b989c2 100644 --- a/src/Database/Schema/Compiler.php +++ b/src/Database/Schema/Compiler.php @@ -18,6 +18,10 @@ use Exception; use Framework\Database\Connection\Connection; +use function Framework\throw_anyway; +use function Framework\throw_if; +use function Framework\throw_unless; + class Compiler { /** @@ -340,14 +344,14 @@ public function compile_alter(Structure $structure) $this->compile_alter_drops($structure) ); - if (empty($clauses)) { - throw new Exception( - sprintf( - 'No changes were defined for table [%s].', - $structure->get_table() - ) - ); - } + throw_if( + empty($clauses), + sprintf( + 'No changes were defined for table [%s].', + $structure->get_table() + ), + Exception::class + ); return sprintf( 'ALTER TABLE %s %s', @@ -521,12 +525,13 @@ protected function get_existing_column_definition(Structure $structure, string $ } } - throw new Exception( + throw_anyway( sprintf( 'Column [%s] does not exist on table [%s].', $column, $structure->get_table() - ) + ), + Exception::class ); } @@ -938,9 +943,11 @@ protected function get_column_type($column) $column_type = $column->type; $getter_method = 'type_' . strtolower($column_type); - if (!method_exists($this, $getter_method)) { - throw new Exception(sprintf('Method %s not found.', $getter_method)); - } + throw_unless( + method_exists($this, $getter_method), + sprintf('Method %s not found.', $getter_method), + Exception::class + ); return $this->$getter_method($column); } diff --git a/src/Database/Schema/SchemaManager.php b/src/Database/Schema/SchemaManager.php index cf44af2..379cf79 100644 --- a/src/Database/Schema/SchemaManager.php +++ b/src/Database/Schema/SchemaManager.php @@ -16,6 +16,8 @@ use Framework\Database\Connection\Connection; use Exception; +use function Framework\throw_if; + class SchemaManager { /** @@ -61,9 +63,11 @@ public function create($table, Closure $callback) $create_sql = $structure->get_table_structure(); $this->connection->get_db()->query($create_sql); - if (!empty($this->connection->get_db()->last_error)) { - throw new Exception($this->connection->get_db()->last_error); - } + throw_if( + !empty($this->connection->get_db()->last_error), + $this->connection->get_db()->last_error, + Exception::class + ); } /** @@ -86,9 +90,11 @@ public function table($table, Closure $callback) $alter_sql = $structure->get_table_structure(); $this->connection->get_db()->query($alter_sql); - if (!empty($this->connection->get_db()->last_error)) { - throw new Exception($this->connection->get_db()->last_error); - } + throw_if( + !empty($this->connection->get_db()->last_error), + $this->connection->get_db()->last_error, + Exception::class + ); } /** @@ -115,9 +121,11 @@ public function rename(string $from, string $to) ) ); - if (!empty($this->connection->get_db()->last_error)) { - throw new Exception($this->connection->get_db()->last_error); - } + throw_if( + !empty($this->connection->get_db()->last_error), + $this->connection->get_db()->last_error, + Exception::class + ); } /** diff --git a/src/Database/Schema/Structure.php b/src/Database/Schema/Structure.php index 438d9f2..80828af 100644 --- a/src/Database/Schema/Structure.php +++ b/src/Database/Schema/Structure.php @@ -18,6 +18,8 @@ use Framework\Database\Schema\Definitions\ForeignKeyDefinition; use Exception; +use function Framework\throw_unless; + class Structure { /** @@ -267,9 +269,7 @@ public function engine(string $engine) { $available_engines = ['InnoDB', 'MyISAM']; - if (!in_array($engine, $available_engines)) { - throw new Exception("Invalid engine: $engine"); - } + throw_unless(in_array($engine, $available_engines), "Invalid engine: $engine"); $this->engine = $engine; } @@ -869,14 +869,14 @@ public function foreign(string $column, $name = null) */ protected function guard_altering(string $operation) { - if (!$this->is_altering()) { - throw new Exception( - sprintf( - 'The [%s] operation is only available when altering an existing table.', - $operation - ) - ); - } + throw_unless( + $this->is_altering(), + sprintf( + 'The [%s] operation is only available when altering an existing table.', + $operation + ), + Exception::class + ); } /** diff --git a/src/Discovery/ListenerDiscovery.php b/src/Discovery/ListenerDiscovery.php index a685db7..da71b36 100644 --- a/src/Discovery/ListenerDiscovery.php +++ b/src/Discovery/ListenerDiscovery.php @@ -234,6 +234,7 @@ public function cache(?string $path = null) $path, "listeners(), true) . ';' ); diff --git a/src/Discovery/PolicyDiscovery.php b/src/Discovery/PolicyDiscovery.php index e6e2410..7fe423f 100644 --- a/src/Discovery/PolicyDiscovery.php +++ b/src/Discovery/PolicyDiscovery.php @@ -208,6 +208,7 @@ public function cache(?string $path = null) $path, "policies(), true) . ';' ); diff --git a/src/Filesystem/File.php b/src/Filesystem/File.php index 49cdb3b..f23f4d5 100644 --- a/src/Filesystem/File.php +++ b/src/Filesystem/File.php @@ -16,6 +16,10 @@ use InvalidArgumentException; use SplFileInfo; +use function Framework\throw_anyway; +use function Framework\throw_if; +use function Framework\throw_unless; + class File extends SplFileInfo { /** @@ -32,9 +36,11 @@ class File extends SplFileInfo */ public function __construct(string $path, bool $check_path = true) { - if ($check_path && !file_exists($path)) { - throw new InvalidArgumentException("File does not exist at path: {$path}"); - } + throw_if( + $check_path && !file_exists($path), + "File does not exist at path: {$path}", + InvalidArgumentException::class + ); parent::__construct($path); } @@ -55,6 +61,7 @@ public function move(string $directory, ?string $name = null) { $target = $this->get_target_file($directory, $name); + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- Captures a PHP warning from rename()/move_uploaded_file() into a catchable value; restored in the finally block immediately after. set_error_handler(static function ($type, $msg) use (&$error) { $error = $msg; }); @@ -65,16 +72,16 @@ public function move(string $directory, ?string $name = null) restore_error_handler(); } - if (!$renamed) { - throw new Exception( - sprintf( - 'Could not move the file "%s" to "%s" (%s).', - $this->getPathname(), - $target, - strip_tags($error ?? '') - ) - ); - } + throw_unless( + $renamed, + sprintf( + 'Could not move the file "%s" to "%s" (%s).', + $this->getPathname(), + $target, + strip_tags($error ?? '') + ), + Exception::class + ); @chmod($target, 0666 & ~umask()); @@ -94,9 +101,7 @@ public function get_content() { $content = file_get_contents($this->getPathname()); - if ($content === false) { - throw new Exception(sprintf('Unable to read the file "%s".', $this->getPathname())); - } + throw_if($content === false, sprintf('Unable to read the file "%s".', $this->getPathname())); return $content; } @@ -116,15 +121,15 @@ public function get_content() protected function get_target_file(string $directory, ?string $name = null) { if (!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory)) { - if (is_file($directory)) { - throw new Exception( - sprintf('Unable to create the "%s" directory. A similar named file exists.', $directory) - ); - } + throw_if( + is_file($directory), + sprintf('Unable to create the "%s" directory. A similar named file exists.', $directory), + Exception::class + ); - throw new Exception(sprintf('Unable to create the "%s" directory.', $directory)); + throw_anyway(sprintf('Unable to create the "%s" directory.', $directory)); } elseif (!is_writable($directory)) { - throw new Exception(sprintf('Unable to write in the "%s" directory.', $directory)); + throw_anyway(sprintf('Unable to write in the "%s" directory.', $directory)); } $target = rtrim($directory, '/\\') diff --git a/src/Filesystem/Fileable.php b/src/Filesystem/Fileable.php index 01525ff..6d4a954 100644 --- a/src/Filesystem/Fileable.php +++ b/src/Filesystem/Fileable.php @@ -10,6 +10,8 @@ defined('ABSPATH') || exit; +use function Framework\throw_unless; + /** * Fileable interface for filesystem operations. * @@ -115,9 +117,11 @@ public function __call($method, $parameters) { $filesystem = new Filesystem(); - if (!method_exists($filesystem, $method)) { - throw new \BadMethodCallException("Method [$method] does not exist on [Filesystem]."); - } + throw_unless( + method_exists($filesystem, $method), + "Method [$method] does not exist on [Filesystem].", + \BadMethodCallException::class + ); return $filesystem->{$method}(...$this->parameters($method, $parameters)); } diff --git a/src/Filesystem/Filesystem.php b/src/Filesystem/Filesystem.php index 11d6d08..e29363d 100644 --- a/src/Filesystem/Filesystem.php +++ b/src/Filesystem/Filesystem.php @@ -12,7 +12,6 @@ defined('ABSPATH') || exit; -use Exception; use Framework\Exceptions\AuthorizationException; use Framework\Exceptions\NotFoundException; use Framework\Sanitizer; @@ -22,6 +21,8 @@ use function Framework\Polyfill\str_starts_with; use function Framework\message; +use function Framework\throw_if; +use function Framework\throw_unless; class Filesystem { @@ -319,15 +320,11 @@ public function put($path, $data) */ public function get($path) { - if (!$this->is_file($path)) { - throw new NotFoundException(message('filesystem.file_not_found', $path)); - } + throw_unless($this->is_file($path), message('filesystem.file_not_found', $path), NotFoundException::class); $contents = $this->filesystem->get_contents($path); - if ($contents === false) { - throw new NotFoundException(message('filesystem.file_not_found', $path)); - } + throw_if($contents === false, message('filesystem.file_not_found', $path), NotFoundException::class); return $contents; } @@ -566,9 +563,11 @@ public function make(string $path) */ public function upload(string $path, UploadedFile $file, $name = null) { - if (!current_user_can(Capabilities::UPLOAD_FILES)) { - throw new AuthorizationException(message('auth.upload_forbidden')); - } + throw_unless( + current_user_can(Capabilities::UPLOAD_FILES), + message('auth.upload_forbidden'), + AuthorizationException::class + ); if (is_null($name)) { $name = $file->get_client_original_name(); @@ -604,17 +603,17 @@ protected function make_upload_directory(string $path) $upload_directory = wp_upload_dir()['basedir']; $base = realpath($upload_directory); - if ($base === false) { - throw new Exception(message('upload.directory_unavailable')); - } + throw_if($base === false, message('upload.directory_unavailable')); $relative = Path::normalize($path); $directory = str_replace('\\', '/', Path::join($base, $relative)); $base_normalized = str_replace('\\', '/', $base); - if ($directory !== $base_normalized && !str_starts_with($directory, $base_normalized . '/')) { - throw new AuthorizationException(message('auth.invalid_upload_path')); - } + throw_if( + $directory !== $base_normalized && !str_starts_with($directory, $base_normalized . '/'), + message('auth.invalid_upload_path'), + AuthorizationException::class + ); return str_replace('/', DIRECTORY_SEPARATOR, $directory); } diff --git a/src/Filesystem/UploadedFile.php b/src/Filesystem/UploadedFile.php index 07a886a..c87fbeb 100644 --- a/src/Filesystem/UploadedFile.php +++ b/src/Filesystem/UploadedFile.php @@ -20,6 +20,8 @@ use function Framework\Polyfill\str_starts_with; use function Framework\message; +use function Framework\throw_anyway; +use function Framework\throw_unless; class UploadedFile extends File implements JsonSerializable { @@ -260,6 +262,7 @@ public function move(string $directory, ?string $name = null) if ($this->is_valid()) { $target = $this->get_target_file($directory, $name); + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- Captures a PHP warning from rename()/move_uploaded_file() into a catchable value; restored in the finally block immediately after. set_error_handler(static function ($type, $msg) use (&$error) { $error = $msg; }); @@ -270,23 +273,23 @@ public function move(string $directory, ?string $name = null) restore_error_handler(); } - if (!$moved) { - throw new Exception( - message( - 'upload.move_failed', - $this->getPathname(), - $target, - strip_tags($error ?? '') - ) - ); - } + throw_unless( + $moved, + message( + 'upload.move_failed', + $this->getPathname(), + $target, + strip_tags($error ?? '') + ), + Exception::class + ); @chmod($target, 0666 & ~umask()); return $target; } - throw new Exception($this->get_error_message()); + throw_anyway($this->get_error_message()); } /** diff --git a/src/Http/Client/MultipartStream.php b/src/Http/Client/MultipartStream.php index d67965f..3557fb9 100644 --- a/src/Http/Client/MultipartStream.php +++ b/src/Http/Client/MultipartStream.php @@ -16,6 +16,7 @@ use UnexpectedValueException; use function Framework\collection; +use function Framework\throw_unless; class MultipartStream { @@ -119,9 +120,7 @@ protected function create_stream(array $data) protected function create_stream_item(array $item) { foreach (['contents', 'name'] as $key) { - if (!array_key_exists($key, $item)) { - throw new UnexpectedValueException("Missing {$key} in item"); - } + throw_unless(array_key_exists($key, $item), "Missing {$key} in item", UnexpectedValueException::class); } $name = $item['name']; diff --git a/src/Http/Client/Request.php b/src/Http/Client/Request.php index 536006a..4bda12c 100644 --- a/src/Http/Client/Request.php +++ b/src/Http/Client/Request.php @@ -22,6 +22,8 @@ use function Framework\collection; use function Framework\Polyfill\str_contains; +use function Framework\throw_anyway; +use function Framework\throw_unless; class Request { @@ -729,9 +731,11 @@ public function send(string $method, string $url, array $options = []) */ protected function send_request(string $method, string $url, array $options = []) { - if (!$this->is_valid_method($method)) { - throw new RuntimeException(sprintf('Invalid HTTP method: %s', $method)); - } + throw_unless( + $this->is_valid_method($method), + sprintf('Invalid HTTP method: %s', $method), + RuntimeException::class + ); $data = $this->parse_request_data($method, $url, $options); $url = $this->prepare_request_url($method, $url, $data); @@ -840,9 +844,7 @@ protected function prepare_request_body(string $method, array $data) case 'multipart': return $this->make_multipart_body($data); default: - throw new RuntimeException( - sprintf('Invalid body format: %s', $this->body_format) - ); + throw_anyway(sprintf('Invalid body format: %s', $this->body_format), RuntimeException::class); } } @@ -951,8 +953,6 @@ public function __call($method, $parameters) return $this->call_macro($method, $parameters); } - throw new BadMethodCallException( - sprintf('Call to undefined method %s::%s', static::class, $method) - ); + throw_anyway(sprintf('Call to undefined method %s::%s', static::class, $method), BadMethodCallException::class); } } diff --git a/src/Http/Concerns/InteractsWithFiles.php b/src/Http/Concerns/InteractsWithFiles.php index dfc33ad..3769e54 100644 --- a/src/Http/Concerns/InteractsWithFiles.php +++ b/src/Http/Concerns/InteractsWithFiles.php @@ -14,6 +14,7 @@ use Framework\Collections\Collection; use Framework\Filesystem\UploadedFile; +use Framework\Http\Superglobals; use SplFileInfo; use function Framework\deep_get; @@ -67,8 +68,7 @@ public function file(?string $key = null, $default = null) */ protected function load_files_from_global() { - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $files = $_FILES ?? []; + $files = Superglobals::files(); $keys = array_keys($files); $files = array_map(function ($file) { diff --git a/src/Http/Cookie.php b/src/Http/Cookie.php index ff325ab..a68334f 100644 --- a/src/Http/Cookie.php +++ b/src/Http/Cookie.php @@ -14,6 +14,9 @@ use InvalidArgumentException; +use function Framework\throw_if; +use function Framework\throw_unless; + class Cookie { /** @@ -425,11 +428,11 @@ protected function normalize_same_site($same_site) $normalized = strtolower((string) $same_site); - if (!isset($supported[$normalized])) { - throw new InvalidArgumentException( - sprintf('The same site attribute "%s" is invalid.', $same_site) - ); - } + throw_unless( + isset($supported[$normalized]), + sprintf('The same site attribute "%s" is invalid.', $same_site), + InvalidArgumentException::class + ); return $supported[$normalized]; } @@ -451,10 +454,10 @@ protected function validate_name(string $name) throw new InvalidArgumentException('The cookie name cannot be empty.'); } - if (strpbrk($name, static::RESERVED_CHARACTERS) !== false) { - throw new InvalidArgumentException( - sprintf('The cookie name "%s" contains invalid characters.', $name) - ); - } + throw_if( + strpbrk($name, static::RESERVED_CHARACTERS) !== false, + sprintf('The cookie name "%s" contains invalid characters.', $name), + InvalidArgumentException::class + ); } } diff --git a/src/Http/JsonResponse.php b/src/Http/JsonResponse.php index d5ba9fb..4a9a9ad 100644 --- a/src/Http/JsonResponse.php +++ b/src/Http/JsonResponse.php @@ -20,6 +20,8 @@ use JsonSerializable; use WP_REST_Response; +use function Framework\throw_unless; + class JsonResponse extends WP_REST_Response { use InteractsWithCookies; @@ -163,9 +165,7 @@ public function set_content($data) break; } - if (!$this->is_valid_json(json_last_error())) { - throw new InvalidArgumentException(json_last_error_msg()); - } + throw_unless($this->is_valid_json(json_last_error()), json_last_error_msg(), InvalidArgumentException::class); return $this; } diff --git a/src/Http/Request.php b/src/Http/Request.php index 93af0e1..c59cfd9 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -26,6 +26,7 @@ use function Framework\app; use function Framework\config; use function Framework\message; +use function Framework\throw_unless; use function Framework\user; use function Framework\value; @@ -212,11 +213,11 @@ public function __call(string $name, array $arguments) { $name = strtolower($name); - if (!in_array($name, static::$types, true)) { - throw new BadMethodCallException( - sprintf('Method %s::%s does not exist.', static::class, $name) - ); - } + throw_unless( + in_array($name, static::$types, true), + sprintf('Method %s::%s does not exist.', static::class, $name), + BadMethodCallException::class + ); $method_name = 'get_' . $name; @@ -260,8 +261,7 @@ public function make_request(WP_REST_Request $request) $this->route_params = $request->get_url_params(); // WP_REST_Request carries no cookie params, so read them from the superglobal. - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $this->cookies = $this->unslash_array($_COOKIE ?? []); + $this->cookies = Superglobals::cookie(); return $this; } @@ -275,8 +275,14 @@ public function make_request(WP_REST_Request $request) */ public static function capture() { - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - return (new static())->make_from_http($_GET, $_POST, $_FILES, $_SERVER, [], $_COOKIE); + return (new static())->make_from_http( + Superglobals::query(), + Superglobals::post(), + Superglobals::files(), + Superglobals::server(), + [], + Superglobals::cookie() + ); } /** @@ -305,8 +311,8 @@ public function make_from_http( $body = $this->unslash_array($body); $this->attributes = array_merge($query, $body, $route_params); - $this->method = strtoupper($server['REQUEST_METHOD'] ?? 'GET'); - $this->route = $this->resolve_request_path($server); + $this->method = static::resolve_method($server); + $this->route = static::resolve_request_path($server); $this->headers = $this->extract_headers($server); $this->server = $server; $this->route_params = $route_params; @@ -382,13 +388,13 @@ protected function unslash_array(array $values) * * @since 1.0.0 */ - protected function resolve_request_path(array $server) + public static function resolve_request_path(array $server) { $request_uri = isset($server['REQUEST_URI']) ? (string) $server['REQUEST_URI'] : ''; - $path = (string) parse_url($request_uri, PHP_URL_PATH); + $path = (string) wp_parse_url($request_uri, PHP_URL_PATH); if (function_exists('home_url')) { - $home_path = (string) parse_url(home_url(), PHP_URL_PATH); + $home_path = (string) wp_parse_url(home_url(), PHP_URL_PATH); if ($home_path !== '' && $home_path !== '/' && strpos($path, $home_path) === 0) { $path = substr($path, strlen($home_path)); @@ -398,6 +404,20 @@ protected function resolve_request_path(array $server) return trim($path, '/'); } + /** + * Resolve the HTTP method from server parameters. + * + * @param array $server Server parameters. + * + * @return string + * + * @since 1.0.0 + */ + public static function resolve_method(array $server) + { + return strtoupper($server['REQUEST_METHOD'] ?? 'GET'); + } + /** * Extract HTTP headers from server parameters. * @@ -894,8 +914,7 @@ public function attributes() */ public function ip() { - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $server = !empty($this->server) ? $this->server : $_SERVER; + $server = !empty($this->server) ? $this->server : Superglobals::server(); $remote = isset($server['REMOTE_ADDR']) ? trim((string) $server['REMOTE_ADDR']) : null; if (empty($remote) || !$this->is_trusted_proxy($remote)) { @@ -1066,9 +1085,7 @@ protected function ip_in_range(string $ip, string $range) */ public function authorize_request() { - if (!$this->authorize()) { - throw new AuthorizationException(message('auth.unauthorized_request')); - } + throw_unless($this->authorize(), message('auth.unauthorized_request'), AuthorizationException::class); return $this; } diff --git a/src/Http/Superglobals.php b/src/Http/Superglobals.php new file mode 100644 index 0000000..94eb5d8 --- /dev/null +++ b/src/Http/Superglobals.php @@ -0,0 +1,154 @@ +get_current_user(); - if (!$user->is_logged_in()) { - throw new AuthorizationException(message('auth.logged_in_required')); - } + throw_unless( + $user->is_logged_in(), + message('auth.logged_in_required'), + AuthorizationException::class + ); $policy = $this->resolve_policy($model); - if (!$policy) { - throw new AuthorizationException(message('auth.no_policy')); - } + throw_unless( + $policy, + message('auth.no_policy'), + AuthorizationException::class + ); if (method_exists($policy, 'before')) { $before_result = $policy->before($user, $ability); @@ -192,19 +198,19 @@ public function authorize(string $ability, $model = null, ...$arguments) return true; } - if ($before_result === false) { - throw new AuthorizationException( - message('auth.unauthorized_action', $ability) - ); - } - } - - if (!method_exists($policy, $ability)) { - throw new AuthorizationException( - message('auth.ability_not_defined', $ability) + throw_if( + $before_result === false, + message('auth.unauthorized_action', $ability), + AuthorizationException::class ); } + throw_unless( + method_exists($policy, $ability), + message('auth.ability_not_defined', $ability), + AuthorizationException::class + ); + $dependencies = $this->resolve_method_dependencies( $policy, $ability, @@ -213,11 +219,11 @@ public function authorize(string $ability, $model = null, ...$arguments) $can_perform = $policy->$ability(...$dependencies); - if (!$can_perform) { - throw new AuthorizationException( - message('auth.unauthorized_action', $ability) - ); - } + throw_unless( + $can_perform, + message('auth.unauthorized_action', $ability), + AuthorizationException::class + ); return true; } diff --git a/src/Managers/SessionManager.php b/src/Managers/SessionManager.php index 134ed87..2977c24 100644 --- a/src/Managers/SessionManager.php +++ b/src/Managers/SessionManager.php @@ -14,6 +14,8 @@ use Exception; use Framework\Contracts\SessionHandler; +use Framework\Http\Superglobals; +use Framework\Sanitizer; use Framework\Supports\Arr; use function Framework\app; @@ -274,14 +276,7 @@ protected function request_id() protected function request_cookie(string $name) { // Reading the session id cookie; the value is format-validated before use. - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $value = $_COOKIE[$name] ?? null; - - if (!is_string($value)) { - return null; - } - - return function_exists('wp_unslash') ? wp_unslash($value) : $value; + return Superglobals::cookie($name, null, Sanitizer::KEY); } /** diff --git a/src/Middlewares/AdminMiddleware.php b/src/Middlewares/AdminMiddleware.php index 4e8022c..02aea9b 100644 --- a/src/Middlewares/AdminMiddleware.php +++ b/src/Middlewares/AdminMiddleware.php @@ -18,6 +18,7 @@ use Framework\Wordpress\Constants\Capabilities; use function Framework\message; +use function Framework\throw_anyway; class AdminMiddleware implements Middleware { @@ -40,6 +41,6 @@ public function handle(Request $request, callable $next) return $next($request); }; - throw new AuthorizationException(message('auth.admin_required'), Response::FORBIDDEN); + throw_anyway(message('auth.admin_required'), AuthorizationException::class, Response::FORBIDDEN); } } diff --git a/src/Middlewares/AuthMiddleware.php b/src/Middlewares/AuthMiddleware.php index 0287264..d53b263 100644 --- a/src/Middlewares/AuthMiddleware.php +++ b/src/Middlewares/AuthMiddleware.php @@ -16,6 +16,7 @@ use Framework\Exceptions\AuthorizationException; use function Framework\message; +use function Framework\throw_anyway; class AuthMiddleware implements Middleware { @@ -38,6 +39,6 @@ public function handle(Request $request, callable $next) return $next($request); }; - throw new AuthorizationException(message('auth.logged_in_required')); + throw_anyway(message('auth.logged_in_required'), AuthorizationException::class); } } diff --git a/src/Middlewares/ThrottleRequests.php b/src/Middlewares/ThrottleRequests.php index 8ed7805..4f6f1f5 100644 --- a/src/Middlewares/ThrottleRequests.php +++ b/src/Middlewares/ThrottleRequests.php @@ -24,6 +24,8 @@ use InvalidArgumentException; use function Framework\app; +use function Framework\throw_exception; +use function Framework\throw_if; class ThrottleRequests implements Middleware { @@ -104,13 +106,13 @@ protected function enforce(Request $request, Limit $limit) $key = $this->resolve_key($request, $limit); if ($limiter->too_many_attempts($key, $limit->max_attempts)) { - throw $this->rejection($request, $limit, $key); + throw_exception($this->rejection($request, $limit, $key)); } $hits = $limiter->increment($key, $limit->decay_seconds); if ($hits > $limit->max_attempts) { - throw $this->rejection($request, $limit, $key); + throw_exception($this->rejection($request, $limit, $key)); } $this->record_headers([ @@ -186,11 +188,11 @@ protected function resolve_limits(Request $request, array $parameters) $callback = $this->limiter()->limiter((string) $first); - if (is_null($callback)) { - throw new InvalidArgumentException( - sprintf('Rate limiter [%s] is not registered.', $first) - ); - } + throw_if( + is_null($callback), + sprintf('Rate limiter [%s] is not registered.', $first), + InvalidArgumentException::class + ); $limits = $callback($request); diff --git a/src/Route.php b/src/Route.php index 69c18b9..eab1e19 100644 --- a/src/Route.php +++ b/src/Route.php @@ -23,6 +23,7 @@ use Framework\Exceptions\InvalidRoutActionException; use Framework\Exceptions\ModelNotFoundException; use Framework\Http\Request; +use Framework\Http\Superglobals; use Framework\Routing\CurrentRoute; use Framework\Routing\RouteParser; use Framework\Routing\SiteRouter; @@ -39,6 +40,8 @@ use function Framework\app; use function Framework\Polyfill\array_first; use function Framework\Polyfill\array_last; +use function Framework\throw_if; +use function Framework\throw_unless; class Route { @@ -1198,19 +1201,21 @@ protected function make(string $abstract, array $resolving = []) return $this->get_cached($abstract); } - if (in_array($abstract, $resolving, true)) { - throw new Exception(sprintf('Circular dependency detected for class "%s".', $abstract)); - } + throw_if( + in_array($abstract, $resolving, true), + sprintf('Circular dependency detected for class "%s".', $abstract), + Exception::class + ); - if (!class_exists($abstract)) { - throw new Exception(sprintf('Class "%s" does not exist.', $abstract)); - } + throw_unless(class_exists($abstract), sprintf('Class "%s" does not exist.', $abstract)); $reflector = new ReflectionClass($abstract); - if ($reflector->isAbstract()) { - throw new Exception(sprintf('Class "%s" is abstract and cannot be instantiated.', $abstract)); - } + throw_if( + $reflector->isAbstract(), + sprintf('Class "%s" is abstract and cannot be instantiated.', $abstract), + Exception::class + ); $constructor = $reflector->getConstructor(); @@ -1218,11 +1223,11 @@ protected function make(string $abstract, array $resolving = []) return new $abstract(); } - if (!$constructor->isPublic()) { - throw new Exception( - sprintf('Class "%s" has a non-public constructor and cannot be instantiated.', $abstract) - ); - } + throw_unless( + $constructor->isPublic(), + sprintf('Class "%s" has a non-public constructor and cannot be instantiated.', $abstract), + Exception::class + ); $dependencies = []; $resolving[] = $abstract; @@ -1230,23 +1235,23 @@ protected function make(string $abstract, array $resolving = []) foreach ($constructor->getParameters() as $parameter) { $type = $parameter->getType(); - if (!$type) { - throw new Exception( - sprintf( - 'Parameter "%s" is missing a type hint in the constructor. Please add a class type hint.', - $parameter->getName() - ) - ); - } + throw_unless( + $type !== null, + sprintf( + 'Parameter "%s" is missing a type hint in the constructor. Please add a class type hint.', + $parameter->getName() + ), + Exception::class + ); - if ($type->isBuiltin()) { - throw new Exception( - sprintf( - 'Parameter "%s" must be a class type, not a built-in type. Please specify a valid class dependency.', // phpcs:ignore Generic.Files.LineLength.TooLong - $parameter->getName() - ) - ); - } + throw_if( + $type->isBuiltin(), + sprintf( + 'Parameter "%s" must be a class type, not a built-in type. Please specify a valid class dependency.', // phpcs:ignore Generic.Files.LineLength.TooLong + $parameter->getName() + ), + Exception::class + ); $dependencies[] = $this->is_cached($type->getName()) ? $this->get_cached($type->getName()) @@ -1276,9 +1281,11 @@ protected function resolve_method_dependencies($abstract, $method) { $method_reflection = new ReflectionMethod($abstract, $method); - if (!$method_reflection->isPublic()) { - throw new Exception(sprintf('Method "%s" is not public and cannot be called.', $method)); - } + throw_unless( + $method_reflection->isPublic(), + sprintf('Method "%s" is not public and cannot be called.', $method), + Exception::class + ); $dependencies = $this->categorize_parameters($method_reflection->getParameters()); $this->assert_single_request_dependency($dependencies, $method); @@ -1361,17 +1368,17 @@ protected function categorize_parameters(array $parameters) */ protected function assert_single_request_dependency(array $dependencies, string $handler) { - if (count($dependencies['requests']) < 1) { - throw new InvalidArgumentException( - sprintf('The method "%s" must have at least one request dependency.', $handler) - ); - } + throw_if( + count($dependencies['requests']) < 1, + sprintf('The method "%s" must have at least one request dependency.', $handler), + InvalidArgumentException::class + ); - if (count($dependencies['requests']) > 1) { - throw new InvalidArgumentException( - sprintf('The method "%s" must have only one request dependency.', $handler) - ); - } + throw_if( + count($dependencies['requests']) > 1, + sprintf('The method "%s" must have only one request dependency.', $handler), + InvalidArgumentException::class + ); } /** @@ -1714,17 +1721,12 @@ public function dispatch_site(array $route_params = []) { $request_class = $this->resolve_request_class(); $request = app()->make($request_class)->make_from_http( - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $_GET, - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $_POST, - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $_FILES, - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $_SERVER, + Superglobals::query(), + Superglobals::post(), + Superglobals::files(), + Superglobals::server(), $route_params, - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $_COOKIE + Superglobals::cookie() ); $request->authorize_request(); @@ -1895,31 +1897,33 @@ protected function dispatch_with_request(Request $request) */ protected function resolve_controller(Request $request) { - if (!is_array($this->action)) { - throw new InvalidRoutActionException( - sprintf('Invalid method registered for the route %s', $this->endpoint) - ); - } + throw_unless( + is_array($this->action), + sprintf('Invalid method registered for the route %s', $this->endpoint), + InvalidRoutActionException::class + ); - if (count($this->action) !== 2) { - throw new InvalidRoutActionException( - sprintf('Invalid controller syntax for the route %s', $this->endpoint) - ); - } + throw_if( + count($this->action) !== 2, + sprintf('Invalid controller syntax for the route %s', $this->endpoint), + InvalidRoutActionException::class + ); [$controller, $method] = $this->action; - if (!class_exists($controller)) { - throw new InvalidRoutActionException(sprintf('Controller %s not found', $controller)); - } + throw_unless( + class_exists($controller), + sprintf('Controller %s not found', $controller), + InvalidRoutActionException::class + ); $controller_instance = $this->make($controller); - if (!method_exists($controller_instance, $method)) { - throw new InvalidRoutActionException( - sprintf('The method %s is missing in the controller %s', $method, $controller) - ); - } + throw_unless( + method_exists($controller_instance, $method), + sprintf('The method %s is missing in the controller %s', $method, $controller), + InvalidRoutActionException::class + ); $dependencies = $this->resolve_method_dependencies($controller_instance, $method); $first_request = array_first($dependencies['requests']); @@ -1952,11 +1956,11 @@ function ($next, $middleware) { return function ($request) use ($next, $middleware) { [$class, $parameters] = static::parse_middleware($middleware); - if (!is_subclass_of($class, Middleware::class)) { - throw new InvalidArgumentException( - sprintf('Middleware %s must implement the %s interface.', $class, Middleware::class) - ); - } + throw_unless( + is_subclass_of($class, Middleware::class), + sprintf('Middleware %s must implement the %s interface.', $class, Middleware::class), + InvalidArgumentException::class + ); return (new $class())->handle($request, $next, ...$parameters); }; @@ -2030,11 +2034,11 @@ protected static function parse_middleware($middleware) return [static::$middleware_aliases[$name], $parameters]; } - if (!class_exists($name)) { - throw new InvalidMiddlewareException( - sprintf('Middleware [%s] is not a registered alias and is not a resolvable class.', $name) - ); - } + throw_unless( + class_exists($name), + sprintf('Middleware [%s] is not a registered alias and is not a resolvable class.', $name), + InvalidMiddlewareException::class + ); return [$name, $parameters]; } diff --git a/src/Routing/SiteRouter.php b/src/Routing/SiteRouter.php index e1b41fb..5f59b50 100644 --- a/src/Routing/SiteRouter.php +++ b/src/Routing/SiteRouter.php @@ -13,6 +13,8 @@ use Framework\Http\JsonResponse; use Framework\Http\RedirectResponse; +use Framework\Http\Request; +use Framework\Http\Superglobals; use Framework\Managers\CookieManager; use Framework\Managers\SessionManager; use Framework\Route; @@ -268,7 +270,7 @@ public function intercept_parse_request($wp) { $path = isset($wp->request) && $wp->request !== '' ? trim((string) $wp->request, '/') - : $this->resolve_request_path(); + : Request::resolve_request_path(Superglobals::server()); foreach ($this->routes as $id => $route) { if ($route->get_match_using() === Route::MATCH_PAGE) { @@ -466,8 +468,7 @@ protected function match_current_request(string $expected_hook_name, int $expect return null; } - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); + $method = Request::resolve_method(Superglobals::server()); if (strtoupper($route->get_method()) !== $method) { SiteExceptionHandler::handle(new Exception('Method Not Allowed', 405)); @@ -662,8 +663,7 @@ protected function collect_route_params(Route $route) */ protected function get_matched_route(Route $route) { - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $request_method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); + $request_method = Request::resolve_method(Superglobals::server()); foreach ($this->routes as $route_entry) { if ( @@ -804,27 +804,6 @@ protected function send_route_redirect(Route $route, array $params) exit; } - /** - * Resolve the current request path relative to the site home path. - * - * @return string - * - * @since 1.0.0 - */ - protected function resolve_request_path() - { - // phpcs:ignore Framework.NamingConventions.SnakeCaseVariable.NotSnakeCase - $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : ''; - $path = (string) parse_url($request_uri, PHP_URL_PATH); - $home_path = (string) parse_url(home_url(), PHP_URL_PATH); - - if ($home_path !== '' && $home_path !== '/' && strpos($path, $home_path) === 0) { - $path = substr($path, strlen($home_path)); - } - - return trim($path, '/'); - } - /** * Build a stable internal route id. * diff --git a/src/SiteExceptionHandler.php b/src/SiteExceptionHandler.php index 74c784b..a044b48 100644 --- a/src/SiteExceptionHandler.php +++ b/src/SiteExceptionHandler.php @@ -15,6 +15,7 @@ use Framework\Exceptions\ModelNotFoundException; use Framework\Exceptions\ValidationException; use Framework\Http\Response; +use Framework\Supports\Facades\Log; use Exception; class SiteExceptionHandler @@ -57,9 +58,7 @@ public static function handle(Exception $exception) $status = Response::INTERNAL_SERVER_ERROR; } - if (function_exists('error_log')) { - error_log($exception->getMessage()); - } + Log::error($exception->getMessage()); static::fail($status, $exception->getMessage() ?: 'Internal Server Error'); } @@ -101,6 +100,6 @@ protected static function fail(int $status, string $message) { status_header($status); nocache_headers(); - wp_die(esc_html($message), esc_html($message), ['response' => $status]); + wp_die(esc_html($message), esc_html($message), ['response' => absint($status)]); } } diff --git a/src/Supports/Somoy.php b/src/Supports/Somoy.php index 3d8bee2..d582984 100644 --- a/src/Supports/Somoy.php +++ b/src/Supports/Somoy.php @@ -36,6 +36,10 @@ use Framework\Exceptions\InvalidDateFormatException; use InvalidArgumentException; +use function Framework\throw_anyway; +use function Framework\throw_if; +use function Framework\throw_unless; + class Somoy extends DateTime implements SomoyInterface { /** @@ -303,12 +307,11 @@ public static function parse($time = null, $timezone = null) $time = (string) $time; } - if ($time !== null && !is_string($time)) { - throw new InvalidDateFormatException(sprintf( - 'Could not parse a value of type %s as a date.', - gettype($time) - )); - } + throw_if( + $time !== null && !is_string($time), + sprintf('Could not parse a value of type %s as a date.', gettype($time)), + InvalidDateFormatException::class + ); try { return new static( @@ -316,8 +319,9 @@ public static function parse($time = null, $timezone = null) static::resolve_timezone($timezone) ); } catch (Exception $exception) { - throw new InvalidDateFormatException( + throw_anyway( sprintf('Could not parse "%s" as a date.', $time), + InvalidDateFormatException::class, 0, $exception ); @@ -386,13 +390,15 @@ public static function create_from_format($format, $time, $timezone = null) ? DateTime::createFromFormat($format, $time) : DateTime::createFromFormat($format, $time, $timezone); - if (!$date instanceof DateTimeInterface) { - throw new InvalidDateFormatException(sprintf( + throw_unless( + $date instanceof DateTimeInterface, + sprintf( 'Could not parse "%s" using the format "%s".', is_scalar($time) ? $time : gettype($time), $format - )); - } + ), + InvalidDateFormatException::class + ); return static::instance($date); } @@ -1421,12 +1427,11 @@ public function set_time_from_time_string($time) $modified = false; } - if ($modified === false) { - throw new InvalidDateFormatException(sprintf( - 'Could not read "%s" as a time.', - is_scalar($given) ? $given : gettype($given) - )); - } + throw_if( + $modified === false, + sprintf('Could not read "%s" as a time.', is_scalar($given) ? $given : gettype($given)), + InvalidDateFormatException::class + ); return $this; } @@ -1468,13 +1473,11 @@ public function __toString() */ public function __get($name) { - if (!isset(static::$readable_units[$name])) { - throw new InvalidArgumentException(sprintf( - 'Undefined property %s::$%s.', - static::class, - $name - )); - } + throw_unless( + isset(static::$readable_units[$name]), + sprintf('Undefined property %s::$%s.', static::class, $name), + InvalidArgumentException::class + ); return (int) $this->format(static::$readable_units[$name]); } @@ -1510,11 +1513,10 @@ public function __isset($name) */ public function __call($method, $parameters) { - throw new BadMethodCallException(sprintf( - 'Call to undefined method %s::%s(). Date methods are snake_case.', - static::class, - $method - )); + throw_anyway( + sprintf('Call to undefined method %s::%s(). Date methods are snake_case.', static::class, $method), + BadMethodCallException::class + ); } /** @@ -1531,11 +1533,10 @@ public function __call($method, $parameters) */ public static function __callStatic($method, $parameters) { - throw new BadMethodCallException(sprintf( - 'Call to undefined method %s::%s(). Date methods are snake_case.', - static::class, - $method - )); + throw_anyway( + sprintf('Call to undefined method %s::%s(). Date methods are snake_case.', static::class, $method), + BadMethodCallException::class + ); } /** @@ -1591,8 +1592,9 @@ protected static function resolve_timezone($timezone) try { return new DateTimeZone($timezone); } catch (Exception $exception) { - throw new InvalidDateFormatException( + throw_anyway( sprintf('Unknown timezone "%s".', is_scalar($timezone) ? $timezone : gettype($timezone)), + InvalidDateFormatException::class, 0, $exception ); @@ -1637,18 +1639,18 @@ protected static function format_human_diff_unit($unit, $value, $short) */ protected static function from_timestamp($timestamp) { - if (!is_numeric($timestamp)) { - throw new InvalidDateFormatException(sprintf( - 'Could not create a date from a non numeric timestamp of type %s.', - gettype($timestamp) - )); - } + throw_unless( + is_numeric($timestamp), + sprintf('Could not create a date from a non numeric timestamp of type %s.', gettype($timestamp)), + InvalidDateFormatException::class + ); try { return new static('@' . sprintf('%.6F', (float) $timestamp)); } catch (Exception $exception) { - throw new InvalidDateFormatException( + throw_anyway( sprintf('Could not create a date from the timestamp "%s".', $timestamp), + InvalidDateFormatException::class, 0, $exception ); diff --git a/src/Validation/RuleFactory.php b/src/Validation/RuleFactory.php index a9e2086..1f4af17 100644 --- a/src/Validation/RuleFactory.php +++ b/src/Validation/RuleFactory.php @@ -21,6 +21,7 @@ use function Framework\Polyfill\array_last; use function Framework\Polyfill\str_contains; use function Framework\Polyfill\str_starts_with; +use function Framework\throw_if; class RuleFactory { @@ -202,9 +203,11 @@ protected function build_rules_chain(array $rules) $rule_class = $this->get_rule_class($rule_name, $arguments); - if ($rule_class === null && $last_base_rule === null) { - throw new InvalidValidationRuleException(message('validator.invalid_rule', [$rule])); - } + throw_if( + $rule_class === null && $last_base_rule === null, + message('validator.invalid_rule', [$rule]), + InvalidValidationRuleException::class + ); if ($rule_class !== null) { $last_base_rule = $rule_class->get_rule_name(); diff --git a/src/Validation/Rules/FileRule.php b/src/Validation/Rules/FileRule.php index 28c2394..cd65575 100644 --- a/src/Validation/Rules/FileRule.php +++ b/src/Validation/Rules/FileRule.php @@ -18,6 +18,8 @@ use Framework\Supports\Str; use InvalidArgumentException; +use function Framework\throw_anyway; + /** * File rule class. @@ -256,7 +258,7 @@ protected function to_kilobytes($size) case Str::ends_with($size, 'tb'): return $value * 1024 * 1024 * 1024; default: - throw new InvalidArgumentException('Invalid file size: ' . $size); + throw_anyway('Invalid file size: ' . $size, InvalidArgumentException::class); } } @@ -323,7 +325,7 @@ protected function make_file($value) return new FilesystemFile($value, false); } - throw new InvalidArgumentException('Invalid file value: ' . $value); + throw_anyway('Invalid file value: ' . $value, InvalidArgumentException::class); } diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 2459546..6beaa63 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -19,6 +19,7 @@ use function Framework\deep_get; use function Framework\message; +use function Framework\throw_exception; defined('ABSPATH') || exit; @@ -353,7 +354,7 @@ public function errors() public function validate() { if ($this->fails()) { - throw ValidationException::with_errors($this->errors()); + throw_exception(ValidationException::with_errors($this->errors())); } return $this->validated(); @@ -373,7 +374,7 @@ public function validated() } if (!empty($this->errors())) { - throw ValidationException::with_errors($this->errors()); + throw_exception(ValidationException::with_errors($this->errors())); } $results = []; diff --git a/src/View/SectionManager.php b/src/View/SectionManager.php index 1e0177a..33d66f1 100644 --- a/src/View/SectionManager.php +++ b/src/View/SectionManager.php @@ -15,6 +15,8 @@ use RuntimeException; +use function Framework\throw_if; + class SectionManager { /** @@ -48,15 +50,15 @@ class SectionManager */ public function start(string $name) { - if ($this->active_section !== null) { - throw new RuntimeException( - sprintf( - 'Cannot start section [%s] while section [%s] is already being captured.', - $name, - $this->active_section - ) - ); - } + throw_if( + $this->active_section !== null, + sprintf( + 'Cannot start section [%s] while section [%s] is already being captured.', + $name, + $this->active_section + ), + RuntimeException::class + ); $this->active_section = $name; diff --git a/src/View/TemplateEngine.php b/src/View/TemplateEngine.php index 2c6a850..7cc9bc2 100644 --- a/src/View/TemplateEngine.php +++ b/src/View/TemplateEngine.php @@ -14,6 +14,7 @@ use RuntimeException; use function Framework\app; +use function Framework\throw_if; class TemplateEngine { @@ -88,9 +89,7 @@ public function render(string $view, array $data = [], $layout = true) { $path = $this->resolve_path($view); - if ($path === '') { - throw new RuntimeException(sprintf('View [%s] not found.', $view)); - } + throw_if($path === '', sprintf('View [%s] not found.', $view), RuntimeException::class); $merged = array_merge($this->shared, $data); $context = app(ViewContext::class); @@ -138,9 +137,7 @@ protected function render_with_master_layout(string $child_path, string $master_ { $master_path = $this->resolve_path($master_view); - if ($master_path === '') { - throw new RuntimeException(sprintf('Master layout [%s] not found.', $master_view)); - } + throw_if($master_path === '', sprintf('Master layout [%s] not found.', $master_view), RuntimeException::class); $sections = app(SectionManager::class); $sections->clear(); @@ -349,9 +346,7 @@ public function include(string $view, array $data = [], bool $once = true) { $path = $this->resolve_path($view); - if ($path === '') { - throw new RuntimeException(sprintf('View [%s] not found.', $view)); - } + throw_if($path === '', sprintf('View [%s] not found.', $view), RuntimeException::class); $merged = array_merge($this->shared, $data); $context = app(ViewContext::class); diff --git a/src/View/ViewContext.php b/src/View/ViewContext.php index d070488..c8f021d 100644 --- a/src/View/ViewContext.php +++ b/src/View/ViewContext.php @@ -216,6 +216,7 @@ protected function authorized_frame() protected function trace_files() { $files = []; + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Collects calling file paths to scope view-data access; not debugging output. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); foreach ($trace as $frame) { diff --git a/src/View/layout-wrapper.php b/src/View/layout-wrapper.php index 4ac4ca4..b78cf02 100644 --- a/src/View/layout-wrapper.php +++ b/src/View/layout-wrapper.php @@ -19,47 +19,47 @@ use function Framework\app; -$context = app(ViewContext::class); -$active = $context->get_active(); +$framework_context = app(ViewContext::class); +$framework_active = $framework_context->get_active(); -if ($active === null || empty($active['resolved_path'])) { +if ($framework_active === null || empty($framework_active['resolved_path'])) { return; } -$path = $active['resolved_path']; -$engine = app(TemplateEngine::class); +$framework_path = $framework_active['resolved_path']; +$framework_engine = app(TemplateEngine::class); // Master layout: child populates sections, then master layout renders around them. -if (!empty($active['master_layout'])) { - $master_path = $engine->resolve_path($active['master_layout']); +if (!empty($framework_active['master_layout'])) { + $framework_master_path = $framework_engine->resolve_path($framework_active['master_layout']); - if ($master_path === '') { + if ($framework_master_path === '') { return; } - $sections = app(SectionManager::class); - $sections->clear(); + $framework_sections = app(SectionManager::class); + $framework_sections->clear(); // Execute the child template to populate sections. ob_start(); - require $path; + require $framework_path; ob_end_clean(); // Render the master layout which yields the captured sections. ob_start(); - require $master_path; + require $framework_master_path; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Assembled layout HTML; dynamic data is escaped in view templates via esc_*. echo (string) ob_get_clean(); - $sections->clear(); + $framework_sections->clear(); return; } // Standard theme layout: wrap with header/footer. ob_start(); -require $path; -$content = (string) ob_get_clean(); +require $framework_path; +$framework_content = (string) ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Assembled layout HTML; dynamic data is escaped in view templates via esc_*. -echo $engine->wrap_layout($content); +echo $framework_engine->wrap_layout($framework_content); diff --git a/src/Wordpress/Hooks/Actions/RegisterAdminMenu.php b/src/Wordpress/Hooks/Actions/RegisterAdminMenu.php index b86356e..0e09a67 100644 --- a/src/Wordpress/Hooks/Actions/RegisterAdminMenu.php +++ b/src/Wordpress/Hooks/Actions/RegisterAdminMenu.php @@ -19,6 +19,7 @@ use Exception; use function Framework\config; +use function Framework\throw_if; class RegisterAdminMenu extends BaseHook { @@ -66,9 +67,11 @@ public function handle(...$args) } foreach ($menus as $menu) { - if (!class_exists($menu) || !is_subclass_of($menu, Menu::class)) { - throw new Exception(sprintf('Menu class %s does not exist.', $menu)); - } + throw_if( + !class_exists($menu) || !is_subclass_of($menu, Menu::class), + sprintf('Menu class %s does not exist.', $menu), + Exception::class + ); $menu_instance = new $menu(); diff --git a/src/Wordpress/User.php b/src/Wordpress/User.php index 380be9d..a694a95 100644 --- a/src/Wordpress/User.php +++ b/src/Wordpress/User.php @@ -17,6 +17,7 @@ use Exception; use function Framework\Polyfill\str_starts_with; +use function Framework\throw_unless; use function Framework\with_prefix; /** @@ -390,9 +391,11 @@ public function cannot($ability, $model = null) */ public function __call($name, $arguments = []) { - if (!str_starts_with($name, 'can_')) { - throw new Exception(sprintf('Method %s does not exist', $name)); - } + throw_unless( + str_starts_with($name, 'can_'), + sprintf('Method %s does not exist', $name), + Exception::class + ); $action = preg_replace('/^can_/', '', $name); diff --git a/src/helpers.php b/src/helpers.php index 8633993..85af7ae 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1066,32 +1066,53 @@ function message($key, ...$args) /** * Throw an exception unconditionally * - * @param string $message The message to attach + * @param mixed $message The message to attach, or the primary constructor argument for exceptions with a non-string leading parameter * @param string $exception_class The exception class * @param array $params The rest of the params - * + * * @throws Exception */ - function throw_anyway(string $message = "", $exception_class = Exception::class, ...$params) + function throw_anyway($message = "", $exception_class = Exception::class, ...$params) { + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Escaping is handled centrally by the framework's exception handler at the point of output, not at the point of throwing. throw new $exception_class($message, ...$params); } } +if (!function_exists('Framework\throw_exception')) { + /** + * Throw an already-built exception instance unconditionally. + * + * For call sites that build the exception ahead of time (e.g. a helper method that may + * return one of several pre-existing exception instances) rather than constructing one + * from a class and arguments, so throw_anyway()'s `new $exception_class(...)` shape + * doesn't apply. + * + * @param Throwable $exception The exception instance to throw. + * + * @throws Throwable + */ + function throw_exception($exception) + { + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Escaping is handled centrally by the framework's exception handler at the point of output, not at the point of throwing. + throw $exception; + } +} + if (!function_exists('Framework\throw_if')) { /** * Throw an exception if the condition is satisfied. * * @param bool $condition The condition to satisfy - * @param string $message The message to show with the exception + * @param mixed $message The message to show with the exception, or the primary constructor argument for exceptions with a non-string leading parameter * @param string $exception_class The exception class * @param array $params The other params - * + * * @throws Exception * * @since 1.0.0 */ - function throw_if(bool $condition, string $message = "", $exception_class = Exception::class, ...$params) + function throw_if(bool $condition, $message = "", $exception_class = Exception::class, ...$params) { if ($condition) { throw_anyway($message, $exception_class, ...$params); @@ -1104,15 +1125,15 @@ function throw_if(bool $condition, string $message = "", $exception_class = Exce * Throw an exception if the condition is satisfied. * * @param bool $condition The condition to satisfy - * @param string $message The message to show with the exception + * @param mixed $message The message to show with the exception, or the primary constructor argument for exceptions with a non-string leading parameter * @param string $exception_class The exception class * @param array $params The other params - * + * * @throws Exception * * @since 1.0.0 */ - function throw_unless(bool $condition, string $message = "", $exception_class = Exception::class, ...$params) + function throw_unless(bool $condition, $message = "", $exception_class = Exception::class, ...$params) { if (!$condition) { throw_anyway($message, $exception_class, ...$params); diff --git a/tests/Support/StubsWordPressFunctions.php b/tests/Support/StubsWordPressFunctions.php index 33f5a67..f25ebdd 100644 --- a/tests/Support/StubsWordPressFunctions.php +++ b/tests/Support/StubsWordPressFunctions.php @@ -98,6 +98,13 @@ function home_url($path = '') } } +if (!function_exists('wp_parse_url')) { + function wp_parse_url($url, $component = -1) + { + return parse_url($url, $component); + } +} + if (!function_exists('apply_filters')) { function apply_filters($hook_name, $value, ...$args) { @@ -123,6 +130,13 @@ function wp_unslash($value) } } +if (!function_exists('absint')) { + function absint($value) + { + return abs((int) $value); + } +} + if (!function_exists('sanitize_key')) { function sanitize_key($key) { diff --git a/tests/Unit/Http/SuperglobalsTest.php b/tests/Unit/Http/SuperglobalsTest.php new file mode 100644 index 0000000..0e035ce --- /dev/null +++ b/tests/Unit/Http/SuperglobalsTest.php @@ -0,0 +1,83 @@ + "O\\'Brien test"]; + + $this->assertSame('O\'Brien test', Superglobals::post('name')); + + $_POST = []; + } + + public function test_single_key_read_uses_text_sanitization_by_default(): void + { + $_GET = ['q' => ' hello ']; + + $this->assertSame('alert(1)hello', Superglobals::query('q')); + + $_GET = []; + } + + public function test_missing_key_returns_default_unsanitized(): void + { + $_POST = []; + + $this->assertSame('fallback value', Superglobals::post('missing', 'fallback value')); + $this->assertNull(Superglobals::post('missing')); + } + + public function test_reading_does_not_mutate_the_superglobal(): void + { + $_COOKIE = ['session' => "abc\\'123"]; + + Superglobals::cookie('session', null, Sanitizer::KEY); + $first = $_COOKIE; + + Superglobals::cookie('session', null, Sanitizer::KEY); + $second = $_COOKIE; + + $this->assertSame(['session' => "abc\\'123"], $first); + $this->assertSame($first, $second); + + $_COOKIE = []; + } + + public function test_whole_array_read_is_unslashed_but_not_type_sanitized(): void + { + $_SERVER['REQUEST_URI'] = "/path?q=raw&name=O\\'Brien"; + + $server = Superglobals::server(); + + $this->assertSame("/path?q=raw&name=O'Brien", $server['REQUEST_URI']); + + unset($_SERVER['REQUEST_URI']); + } + + public function test_non_scalar_value_is_treated_as_absent(): void + { + $_POST = ['name' => ['unexpected' => 'array']]; + + $this->assertSame('fallback', Superglobals::post('name', 'fallback')); + + $_POST = []; + } + + public function test_files_returns_unslashed_whole_array(): void + { + $_FILES = ['upload' => ['name' => "photo\\'s.png", 'error' => 0]]; + + $files = Superglobals::files(); + + $this->assertSame("photo's.png", $files['upload']['name']); + + $_FILES = []; + } +}