diff --git a/lib/Adapter/MysqlAdapter.php b/lib/Adapter/MysqlAdapter.php index ea90b220..11758da9 100644 --- a/lib/Adapter/MysqlAdapter.php +++ b/lib/Adapter/MysqlAdapter.php @@ -41,7 +41,7 @@ public function query_for_tables(): \PDOStatement public function create_column(array $column): Column { $c = new Column(); - $c->inflected_name = Inflector::instance()->variablize($column['field']); + $c->inflected_name = Inflector::variablize($column['field']); $c->name = $column['field']; $c->nullable = ('YES' === $column['null'] ? true : false); $c->pk = ('PRI' === $column['key'] ? true : false); @@ -84,7 +84,7 @@ public function accepts_limit_and_order_for_update_and_delete(): bool } /** - * @return array> + * @return array> */ public function native_database_types(): array { diff --git a/lib/Adapter/PgsqlAdapter.php b/lib/Adapter/PgsqlAdapter.php index 3a9aaaea..9c72baa0 100644 --- a/lib/Adapter/PgsqlAdapter.php +++ b/lib/Adapter/PgsqlAdapter.php @@ -85,7 +85,7 @@ public function query_for_tables(): \PDOStatement public function create_column(array $column): Column { $c = new Column(); - $c->inflected_name = Inflector::instance()->variablize($column['field']); + $c->inflected_name = Inflector::variablize($column['field']); $c->name = $column['field']; $c->nullable = ($column['not_nullable'] ? false : true); $c->pk = ($column['pk'] ? true : false); diff --git a/lib/Adapter/SqliteAdapter.php b/lib/Adapter/SqliteAdapter.php index 128580b5..b23be72c 100644 --- a/lib/Adapter/SqliteAdapter.php +++ b/lib/Adapter/SqliteAdapter.php @@ -19,14 +19,14 @@ */ class SqliteAdapter extends Connection { - public static $datetime_format = 'Y-m-d H:i:s'; + public static string $datetime_format = 'Y-m-d H:i:s'; - protected function __construct(\stdClass $info) + protected function __construct(array $info) { - if (!file_exists($info->host)) { - throw new ConnectionException("Could not find sqlite db: $info->host"); + if (!file_exists($info['host'])) { + throw new ConnectionException('Could not find sqlite db: ' . $info['host']); } - $this->connection = new \PDO("sqlite:$info->host", null, null, static::$PDO_OPTIONS); + $this->connection = new \PDO('sqlite:' . $info['host'], null, null, static::$PDO_OPTIONS); } public function limit(string $sql, int $offset = 0, int $limit = 0) @@ -49,10 +49,10 @@ public function query_for_tables(): \PDOStatement public function create_column(array $column): Column { $c = new Column(); - $c->inflected_name = Inflector::instance()->variablize($column['name']); + $c->inflected_name = Inflector::variablize($column['name']); $c->name = $column['name']; - $c->nullable = $column['notnull'] ? false : true; - $c->pk = $column['pk'] ? true : false; + $c->nullable = !$column['notnull']; + $c->pk = (bool) $column['pk']; $c->auto_increment = in_array( strtoupper($column['type']), ['INT', 'INTEGER'] diff --git a/lib/Cache.php b/lib/Cache.php index b96fd04a..832ef33e 100644 --- a/lib/Cache.php +++ b/lib/Cache.php @@ -54,10 +54,16 @@ public static function initialize(string $url = '', array $options = []): void { if ($url) { $url = parse_url($url); - $file = ucwords(Inflector::instance()->camelize($url['scheme'])); + assert(is_array($url)); + $file = ucwords(Inflector::camelize($url['scheme'] ?? '')); $class = "ActiveRecord\\$file"; require_once __DIR__ . "/cache/$file.php"; - static::$adapter = new $class($url); + + $cache = new $class($url); + + assert($cache instanceof Memcache); + + static::$adapter = $cache; } else { static::$adapter = null; } @@ -85,7 +91,7 @@ public static function get(string $key, \Closure $closure, int $expire = null): $key = static::get_namespace() . $key; if (!($value = static::$adapter->read($key))) { - static::$adapter->write($key, $value = $closure(), $expire ?? static::$options['expire']); + static::$adapter->write($key, $value = $closure(), $expire ?? static::$options['expire'] ?? 0); } return $value; @@ -97,13 +103,9 @@ public static function set(string $key, mixed $var, int $expire = null): void return; } - if (is_null($expire)) { - $expire = static::$options['expire']; - } - $key = static::get_namespace() . $key; - static::$adapter->write($key, $var, $expire); + static::$adapter->write($key, $var, $expire ?? static::$options['expire'] ?? 0); } public static function delete(string $key): void diff --git a/lib/CallBack.php b/lib/CallBack.php index 3826cdca..a611bf4a 100644 --- a/lib/CallBack.php +++ b/lib/CallBack.php @@ -102,7 +102,7 @@ class CallBack /** * Holds data for registered callbacks. * - * @var array + * @var array> */ private array $registry = []; @@ -141,7 +141,7 @@ public function __construct(string $model_class_name) * * @param $name string Name of a callback (see {@link VALID_CALLBACKS $VALID_CALLBACKS}) * - * @return array array of callbacks or null if invalid callback name + * @return array<\Closure|string> array of callbacks or empty array if invalid callback name */ public function get_callbacks(string $name): array { diff --git a/lib/Column.php b/lib/Column.php index 2555757e..1eaecf68 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -130,7 +130,7 @@ public static function castIntegerSafely($value): string|int } // It's just a decimal number - elseif (is_numeric($value) && floor($value) != $value) { + elseif (is_float($value) && floor($value) != $value) { return (int) $value; } diff --git a/lib/Connection.php b/lib/Connection.php index 43236327..7411eb4f 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -13,11 +13,20 @@ use ActiveRecord\Exception\DatabaseException; use Closure; use PDO; +use Psr\Log\LoggerInterface; /** * The base class for database connection adapters. * - * @package ActiveRecord + * @phpstan-type ConnectionInfo array{ + * protocol: string, + * host: string, + * db: string|null, + * user: string|null, + * pass: string|null, + * port?: int|null, + * charset?: string|null + * } */ abstract class Connection { @@ -42,22 +51,16 @@ abstract class Connection public string $last_query; /** * Switch for logging. - * - * @var bool */ - private $logging = false; + private bool $logging = false; /** * Contains a Logger object that must implement a log() method. - * - * @var object */ - private $logger; + private LoggerInterface $logger; /** * The name of the protocol that is used. - * - * @var string */ - public $protocol; + public string $protocol; /** * Database's date format * @@ -66,10 +69,8 @@ abstract class Connection public static $date_format = 'Y-m-d'; /** * Database's datetime format - * - * @var string */ - public static $datetime_format = 'Y-m-d H:i:s'; + public static string $datetime_format = 'Y-m-d H:i:s'; /** * Default PDO options to set for each connection. * @@ -126,16 +127,17 @@ public static function instance(string $connection_string_or_connection_name = n throw new DatabaseException('Empty connection string'); } $info = static::parse_connection_url($connection_string); - $fqclass = static::load_adapter_class($info->protocol); + $fqclass = static::load_adapter_class($info['protocol']); try { $connection = new $fqclass($info); - $connection->protocol = $info->protocol; + assert($connection instanceof Connection); + $connection->protocol = $info['protocol']; $connection->logging = $config->get_logging(); $connection->logger = $connection->logging ? $config->get_logger() : null; - if (isset($info->charset)) { - $connection->set_encoding($info->charset); + if (isset($info['charset'])) { + $connection->set_encoding($info['charset']); } } catch (\PDOException $e) { throw new DatabaseException($e); @@ -162,6 +164,8 @@ protected static function load_adapter_class(string $adapter) } require_once $source; + assert(class_exists($fqclass)); + return $fqclass; } @@ -186,26 +190,26 @@ protected static function load_adapter_class(string $adapter) * * @param string $connection_url A connection URL * - * @return object the parsed URL as an object + * @return ConnectionInfo */ - public static function parse_connection_url(string $connection_url) + public static function parse_connection_url(string $connection_url): array { $url = @parse_url($connection_url); if (!isset($url['host'])) { throw new DatabaseException('Database host must be specified in the connection string. If you want to specify an absolute filename, use e.g. sqlite://unix(/path/to/file)'); } - $info = new \stdClass(); - $info->protocol = $url['scheme']; - $info->host = $url['host']; - $info->db = isset($url['path']) ? substr($url['path'], 1) : null; - $info->user = isset($url['user']) ? $url['user'] : null; - $info->pass = isset($url['pass']) ? $url['pass'] : null; + $host = $url['host']; + $db = isset($url['path']) ? substr($url['path'], 1) : null; + $user = $url['user'] ?? null; + $pass = $url['pass'] ?? null; + $protocol = $url['scheme'] ?? null; + $charset = null; - $allow_blank_db = ('sqlite' == $info->protocol); + $allow_blank_db = ('sqlite' == $protocol); - if ('unix(' == $info->host) { - $socket_database = $info->host . '/' . $info->db; + if ('unix(' == $host) { + $socket_database = $host . '/' . $db; if ($allow_blank_db) { $unix_regex = '/^unix\((.+)\)\/?().*$/'; @@ -214,29 +218,25 @@ public static function parse_connection_url(string $connection_url) } if (preg_match_all($unix_regex, $socket_database, $matches) > 0) { - $info->host = $matches[1][0]; - $info->db = $matches[2][0]; + $host = $matches[1][0]; + $db = $matches[2][0]; } - } elseif ('windows(' == substr($info->host, 0, 8)) { - $info->host = urldecode(substr($info->host, 8) . '/' . substr($info->db, 0, -1)); - $info->db = null; - } - - if ($allow_blank_db && $info->db) { - $info->host .= '/' . $info->db; + } elseif ('windows(' == substr($host, 0, 8)) { + $host = urldecode(substr($host, 8) . '/' . substr($db, 0, -1)); + $db = null; } - if (isset($url['port'])) { - $info->port = $url['port']; + if ($allow_blank_db && $db) { + $host .= '/' . $db; } if (false !== strpos($connection_url, 'decode=true')) { - if ($info->user) { - $info->user = urldecode($info->user); + if ($user) { + $user = urldecode($user); } - if ($info->pass) { - $info->pass = urldecode($info->pass); + if ($pass) { + $pass = urldecode($pass); } } @@ -245,33 +245,45 @@ public static function parse_connection_url(string $connection_url) list($name, $value) = explode('=', $pair); if ('charset' == $name) { - $info->charset = $value; + $charset = $value; } } } - return $info; + assert(!is_null($protocol)); + + return [ + 'charset' => $charset, + 'protocol' => $protocol, + 'host' => $host, + 'db' => $db, + 'user' => $user, + 'pass' => $pass, + 'port' => $url['port'] ?? null + ]; } /** * Class Connection is a singleton. Access it via instance(). + * + * @param ConnectionInfo $info */ - protected function __construct(\stdClass $info) + protected function __construct(array $info) { try { // unix sockets start with a / - if ('/' != $info->host[0]) { - $host = "host=$info->host"; + if ('/' != $info['host'][0]) { + $host = 'host=' . $info['host']; - if (isset($info->port)) { - $host .= ";port=$info->port"; + if (isset($info['port'])) { + $host .= ';port=' . $info['port']; } } else { - $host = "unix_socket=$info->host"; + $host = 'unix_socket=' . $info['host']; } - $dsn = "$info->protocol:$host;dbname=$info->db"; - $this->connection = new \PDO($dsn, $info->user, $info->pass, static::$PDO_OPTIONS); + $dsn = $info['protocol'] . ":$host;dbname=" . $info['db']; + $this->connection = new \PDO($dsn, $info['user'], $info['pass'], static::$PDO_OPTIONS); } catch (\PDOException $e) { throw new Exception\ConnectionException($e); } @@ -484,26 +496,15 @@ public function quote_name($string) $string : static::$QUOTE_CHARACTER . $string . static::$QUOTE_CHARACTER; } - /** - * Return a date time formatted into the database's date format. - * - * @param DateTime $datetime The DateTime object - * - * @return string - */ - public function date_to_string($datetime) + public function date_string(\DateTimeInterface $datetime): string { return $datetime->format(static::$date_format); } /** * Return a date time formatted into the database's datetime format. - * - * @param DateTime $datetime The DateTime object - * - * @return string */ - public function datetime_to_string(\DateTime $datetime) + public function datetime_string(\DateTimeInterface $datetime): string { return $datetime->format(static::$datetime_format); } @@ -524,6 +525,8 @@ public function string_to_datetime(string $string): ?DateTime return null; } + assert($date instanceof \DateTime); + $date_class = Config::instance()->get_date_class(); return $date_class::createFromFormat( diff --git a/lib/Inflector.php b/lib/Inflector.php index 7394d3f7..f394da7d 100644 --- a/lib/Inflector.php +++ b/lib/Inflector.php @@ -10,24 +10,10 @@ */ abstract class Inflector { - /** - * Get an instance of the {@link Inflector} class. - * - * @return object - */ - public static function instance() - { - return new StandardInflector(); - } - /** * Turn a string into its camelized version. - * - * @param string $s string to convert - * - * @return string */ - public function camelize($s) + public static function camelize(string $s): string { $s = preg_replace('/[_-]+/', '_', trim($s)); $s = str_replace(' ', '_', $s); @@ -53,12 +39,8 @@ public function camelize($s) /** * Determines if a string contains all uppercase characters. - * - * @param string $s string to check - * - * @return bool */ - public static function is_upper($s) + public static function is_upper(string $s): bool { return strtoupper($s) === $s; } @@ -66,12 +48,12 @@ public static function is_upper($s) /** * Convert a camelized string to a lowercase, underscored string. */ - public function uncamelize(string $s): string + public static function uncamelize(string $s): string { $normalized = ''; for ($i = 0, $n = strlen($s); $i < $n; ++$i) { - if (ctype_alpha($s[$i]) && self::is_upper($s[$i])) { + if (ctype_alpha($s[$i]) && static::is_upper($s[$i])) { $normalized .= '_' . strtolower($s[$i]); } else { $normalized .= $s[$i]; @@ -84,7 +66,7 @@ public function uncamelize(string $s): string /** * Convert a string with space into a underscored equivalent. */ - public function underscorify(string $s): string + public static function underscorify(string $s): string { $res = preg_replace(['/[_\- ]+/', '/([a-z])([A-Z])/'], ['_', '\\1_\\2'], trim($s)); assert(is_string($res)); @@ -92,25 +74,17 @@ public function underscorify(string $s): string return $res; } - public function keyify(string $class_name): string + public static function keyify(string $class_name): string { - return strtolower($this->underscorify(denamespace($class_name))) . '_id'; + return strtolower(static::underscorify(denamespace($class_name))) . '_id'; } - abstract public function variablize(string $s): string; -} - -/** - * @package ActiveRecord - */ -class StandardInflector extends Inflector -{ - public function tableize(string $s): string + public static function tableize(string $s): string { - return Utils::pluralize(strtolower($this->underscorify($s))); + return Utils::pluralize(strtolower(static::underscorify($s))); } - public function variablize(string $s): string + public static function variablize(string $s): string { return str_replace(['-', ' '], ['_', '_'], strtolower(trim($s))); } diff --git a/lib/Model.php b/lib/Model.php index bde26f2e..51a53559 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -139,7 +139,7 @@ class Model /** * Array of relationship objects as model_attribute_name => relationship * - * @var array + * @var array> */ private array $__relationships = []; @@ -184,7 +184,7 @@ class Model /** * Set this to specify an expiration period for this model. If not set, the expire value you set in your cache options will be used. * - * @var number + * @var int */ public static $cache_expire; @@ -410,7 +410,9 @@ public function &__get($name) $name = strtolower($name); if (method_exists($this, "get_$name")) { $name = "get_$name"; - $res = call_user_func([$this, $name]); + $callable = [$this, $name]; + assert(is_callable($callable)); + $res = call_user_func($callable); return $res; } @@ -519,12 +521,15 @@ public function __set(string $name, mixed $value): void } if ('id' == $name) { - $this->assign_attribute($this->get_primary_key(true), $value); + $this->assign_attribute($this->get_primary_key(), $value); return; } - foreach (static::$delegate as $item) { + foreach (static::$delegate as $key => $item) { + if ('processed' == $key) { + continue; + } if ($delegated_name = $this->is_delegated($name, $item)) { $this->{$item['to']}->$delegated_name = $value; @@ -628,7 +633,7 @@ public function &read_attribute(string $name) } if ('id' == $name) { - $pk = $this->get_primary_key(true); + $pk = $this->get_primary_key(); if (isset($this->attributes[$pk])) { return $this->attributes[$pk]; } @@ -659,7 +664,7 @@ public function &read_attribute(string $name) /** * @throws RelationshipException * - * @return Model|AbstractRelationship|null + * @return Model|array|null */ protected function initRelationships(string $name): mixed { @@ -717,14 +722,11 @@ public function attributes(): array return $this->attributes; } - /** - * @return array|string - */ - public function get_primary_key(bool $first = false): array|string + public function get_primary_key(): string { $pk = static::table()->pk; - return $first ? $pk[0] : $pk; + return $pk[0]; } /** @@ -805,14 +807,14 @@ public static function table_name() * @param string $name Name of an attribute * @param DelegateOptions $delegate An array containing delegate data */ - private function is_delegated(string $name, $delegate): string|null + private function is_delegated(string $name, array $delegate): string|null { if (is_array($delegate)) { - if ('' != $delegate['prefix']) { + if (!empty($delegate['prefix'])) { $name = substr($name, strlen($delegate['prefix']) + 1); } - if (in_array($name, $delegate['delegate'])) { + if (in_array($name, $delegate['delegate'] ?? [])) { return $name; } } @@ -951,7 +953,7 @@ private function insert($validate = true) $attributes = $this->attributes; } - $pk = $this->get_primary_key(true); + $pk = $this->get_primary_key(); $use_sequence = false; if (!empty($table->sequence) && !isset($attributes[$pk])) { @@ -1132,7 +1134,7 @@ public static function update_all(array $options = []): int $conn = static::connection(); $sql = new SQLBuilder($conn, $table->get_fully_qualified_table_name()); - $sql->update($options['set']); + isset($options['set']) && $sql->update($options['set']); if (isset($options['conditions']) && ($conditions = $options['conditions'])) { if (is_array($conditions) && !is_hash($conditions)) { @@ -1399,7 +1401,7 @@ private function set_attributes_via_mass_assignment(array &$attributes, bool $gu * * @internal This should only be used by eager load */ - public function set_relationship_from_eager_load(Model $model = null, string $name): void + public function set_relationship_from_eager_load(?Model $model, string $name): void { $table = static::table(); @@ -1411,8 +1413,9 @@ public function set_relationship_from_eager_load(Model $model = null, string $na return; } - - $this->__relationships[$name][] = $model; + $this->__relationships[$name] ??= []; + assert(is_array($this->__relationships[$name])); + array_push($this->__relationships[$name], $model); return; } @@ -1435,9 +1438,9 @@ public function reload() $this->remove_from_cache(); $this->__relationships = []; - $pk = array_values($this->get_values_for($this->get_primary_key())); - - $this->set_attributes_via_mass_assignment($this->find($pk[0])->attributes, false); + $model = $this->find($this->{static::table()->pk[0]}); + assert($model instanceof static); + $this->set_attributes_via_mass_assignment($model->attributes, false); $this->reset_dirty(); return $this; @@ -1597,11 +1600,12 @@ public function __call($method, $args) * * @see find * - * @return static[] + * @return array */ - public static function all(/* ... */) + public static function all(/* ... */): array { - return call_user_func_array(static::class . '::find', array_merge(['all'], func_get_args())); + /* @phpstan-ignore-next-line */ + return static::find('all', ...func_get_args()); } /** @@ -1615,7 +1619,7 @@ public static function all(/* ... */) * * @return int Number of records that matched the query */ - public static function count(/* ... */) + public static function count(/* ... */): int { $args = func_get_args(); $options = static::extract_and_validate_options($args); @@ -1625,7 +1629,7 @@ public static function count(/* ... */) if (is_hash($args[0])) { $options['conditions'] = $args[0]; } else { - $options['conditions'] = call_user_func_array(static::class . '::pk_conditions', $args); + $options['conditions'] = static::pk_conditions(...$args); } } @@ -1643,41 +1647,43 @@ public static function count(/* ... */) * * ``` * SomeModel::exists(123); - * SomeModel::exists(array('conditions' => array('id=? and name=?', 123, 'Tito'))); - * SomeModel::exists(array('id' => 123, 'name' => 'Tito')); + * SomeModel::exists(['conditions' => ['id=? and name=?', 123, 'Tito']]); + * SomeModel::exists(['id' => 123, 'name' => 'Tito']); * ``` * * @see find - * - * @return bool */ - public static function exists(/* ... */) + public static function exists(/* ... */): bool { - return call_user_func_array(static::class . '::count', func_get_args()) > 0 ? true : false; + return static::count(...func_get_args()) > 0; } /** * Alias for self::find('first'). * * @see find - * - * @return static|null The first matched record or null if not found */ - public static function first(/* ... */) + public static function first(/* ... */): static|null { - return call_user_func_array(static::class . '::find', array_merge(['first'], func_get_args())); + $res = static::find('first', ...func_get_args()); + // this is a workaround for what seems to be a PHPStan bug + assert($res instanceof static || is_null($res)); + + return $res; } /** * Alias for self::find('last') * * @see find - * - * @return Model|null The last matched record or null if not found */ - public static function last(/* ... */) + public static function last(/* ... */): static|null { - return call_user_func_array(static::class . '::find', array_merge(['last'], func_get_args())); + $res = static::find('last', ...func_get_args()); + // this is a workaround for what seems to be a PHPStan bug + assert($res instanceof static || is_null($res)); + + return $res; } /** @@ -1803,7 +1809,7 @@ public static function find(/* $type, $options */): static|array|null $list = static::table()->find($options); } - return $single ? (!empty($list) ? $list[0] : null) : $list; + return $single ? ($list[0] ?? null) : $list; } /** @@ -1855,6 +1861,7 @@ public static function find_by_pk(array|string|int|null $values, array $options, $pks = is_array($values) ? $values : [$values]; $list = static::get_models_from_cache($pks); } else { + // int|string|array $options['conditions'] = static::pk_conditions($values); $list = $table->find($options); } @@ -1935,11 +1942,11 @@ public static function is_options_hash(mixed $options, bool $throw = true): bool /** * Returns a hash containing the names => values of the primary key. * - * @param int|array $args Primary key value(s) + * @param PrimaryKey $args Primary key value(s) * * @return array */ - public static function pk_conditions(int|array $args): array + public static function pk_conditions(mixed $args): array { $table = static::table(); $ret = [$table->pk[0] => $args]; @@ -2056,6 +2063,8 @@ private function serialize(string $type, array $options): string $class = 'ActiveRecord\\Serialize\\' . $type . 'Serializer'; $serializer = new $class($this, $options); + assert($serializer instanceof Serialization); + return $serializer->to_s(); } @@ -2069,7 +2078,7 @@ private function serialize(string $type, array $options): string * * @return bool True if invoked or null if not */ - private function invoke_callback($method_name, $must_exist = true) + private function invoke_callback(string $method_name, bool $must_exist = true) { return static::table()->callback->invoke($this, $method_name, $must_exist); } diff --git a/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php b/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php index b4e3965f..6ff7f7f8 100644 --- a/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php +++ b/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php @@ -3,8 +3,9 @@ namespace ActiveRecord\PhpStan; use ActiveRecord\Model; -use PhpParser\Node\Expr; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ArrayType; @@ -32,12 +33,13 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type { - $subclass = $methodCall->class . ''; + assert($methodCall->class instanceof Name); + $subclass = $methodCall->class->toString(); $args = $methodCall->args; $args = array_map(static function ($arg) use ($scope) { + assert($arg instanceof Arg); $val = $arg->value; - assert($val instanceof Expr); return $scope->getType($val); }, $args); diff --git a/lib/Reflections.php b/lib/Reflections.php index 6aedf58d..224073b6 100644 --- a/lib/Reflections.php +++ b/lib/Reflections.php @@ -29,11 +29,12 @@ class Reflections extends Singleton * * @return Reflections $this so you can chain calls like Reflections::instance()->add('class')->get() */ - public function add(string $class = null): Reflections + public function add(string $class): Reflections { $class = $this->get_class($class); if (!isset($this->reflections[$class])) { + /* @phpstan-ignore-next-line */ $this->reflections[$class] = new \ReflectionClass($class); } @@ -57,19 +58,13 @@ public function destroy(string $class): void /** * Get a cached ReflectionClass. * - * @param class-string|object $className Optional name of a class or an instance of the class + * @param class-string $className Optional name of a class or an instance of the class * * @throws ActiveRecordException if class was not found - * - * @return mixed null or a ReflectionClass instance */ - public function get($className) + public function get(string $className): \ReflectionClass { - if (isset($this->reflections[$className])) { - return $this->reflections[$className]; - } - - throw new ActiveRecordException("Class not found: $className"); + return $this->reflections[$className] ?? throw new ActiveRecordException("Class not found: $className"); } /** @@ -77,7 +72,7 @@ public function get($className) * * @param class-string|object $class An object or name of a class * - * @return string + * @return class-string */ private function get_class(string|object $class = null) { @@ -85,10 +80,6 @@ private function get_class(string|object $class = null) return get_class($class); } - if (!is_null($class)) { - return $class; - } - - return $this->get_called_class(); + return $class; } } diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index 3eddbfdf..fcc4eaef 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -4,7 +4,6 @@ use function ActiveRecord\all; use function ActiveRecord\classify; -use function ActiveRecord\denamespace; use ActiveRecord\Exception\RelationshipException; @@ -36,16 +35,16 @@ abstract class AbstractRelationship /** * Class name of the associated model. * - * @var string + * @var class-string */ public $class_name; /** * Name of the foreign key. * - * @var string|string[] + * @var string[] */ - public mixed $foreign_key = []; + public array $foreign_key = []; /** * Options of the relationship. @@ -54,45 +53,39 @@ abstract class AbstractRelationship */ protected array $options = []; - /** - * Is the relationship single or multi. - */ - protected bool $poly_relationship = false; - /** * List of valid options for relationships. * * @var array */ - protected static $valid_association_options = ['class_name', 'class', 'foreign_key', 'conditions', 'select', 'readonly', 'namespace']; + protected static $valid_association_options = [ + 'class_name', + 'foreign_key', + 'conditions', + 'select', + 'readonly', + 'namespace' + ]; /** * Constructs a relationship. * * @param array $options Options for the relationship (see {@link valid_association_options}) */ - public function __construct(string $attribute_name, $options = []) + public function __construct(string $attribute_name, array $options = []) { $this->attribute_name = $attribute_name; $this->options = $this->merge_association_options($options); - $relationship = strtolower(denamespace(get_called_class())); - - if ('hasmany' === $relationship || 'hasandbelongstomany' === $relationship) { - $this->poly_relationship = true; - } - if (isset($this->options['conditions']) && !is_array($this->options['conditions'])) { $this->options['conditions'] = [$this->options['conditions']]; } - if (isset($this->options['class'])) { - $this->set_class_name($this->options['class']); - } elseif (isset($this->options['class_name'])) { - $this->set_class_name($this->options['class_name']); + if (isset($this->options['class_name'])) { + $this->set_class_name($this->inferred_class_name($this->options['class_name'])); } - $this->attribute_name = strtolower(Inflector::instance()->variablize($this->attribute_name)); + $this->attribute_name = strtolower(Inflector::variablize($this->attribute_name)); if (!$this->foreign_key && isset($this->options['foreign_key'])) { $this->foreign_key = is_array($this->options['foreign_key']) ? $this->options['foreign_key'] : [$this->options['foreign_key']]; @@ -116,7 +109,7 @@ abstract public function load_eagerly(array $models, array $attributes, array $i */ public function is_poly(): bool { - return $this->poly_relationship; + return false; } /** @@ -136,12 +129,11 @@ protected function query_and_attach_related_models_eagerly(Table $table, array $ { $values = []; $options = $this->options; - $inflector = Inflector::instance(); $query_key = $query_keys[0]; $model_values_key = $model_values_keys[0]; foreach ($attributes as $column => $value) { - $values[] = $value[$inflector->variablize($model_values_key)]; + $values[] = $value[Inflector::variablize($model_values_key)]; } $values = [$values]; @@ -192,8 +184,8 @@ protected function query_and_attach_related_models_eagerly(Table $table, array $ $related_models = $class::find('all', $options); $used_models_map = []; $related_models_map = []; - $model_values_key = $inflector->variablize($model_values_key); - $query_key = $inflector->variablize($query_key); + $model_values_key = Inflector::variablize($model_values_key); + $query_key = Inflector::variablize($query_key); foreach ($related_models as $related) { $related_models_map[$related->$query_key][] = $related; @@ -231,7 +223,10 @@ public function build_association(Model $model, array $attributes = [], bool $gu { $class_name = $this->class_name; - return new $class_name($attributes, $guard_attributes); + $model = new $class_name($attributes, $guard_attributes); + assert($model instanceof Model); + + return $model; } /** @@ -253,7 +248,7 @@ protected function append_record_to_associate(Model $associate, Model $record): { $association = &$associate->{$this->attribute_name}; - if ($this->poly_relationship) { + if ($this->is_poly()) { $association[] = $record; } else { $association = $record; @@ -296,24 +291,32 @@ protected function unset_non_finder_options(array $options): array } /** - * Infers the $this->class_name based on $this->attribute_name. - * - * Will try to guess the appropriate class by singularizing and uppercasing $this->attribute_name. - * - * @see attribute_name + * @return class-string */ - protected function set_inferred_class_name(): void + protected function inferred_class_name(string $name): string { - $singularize = ($this instanceof HasMany ? true : false); - $this->set_class_name(classify($this->attribute_name, $singularize)); + if (!has_absolute_namespace($name) && isset($this->options['namespace'])) { + if (!isset($this->options['class_name'])) { + $name = classify($name, $this instanceof HasMany); + } + $name = $this->options['namespace'] . '\\' . $name; + } + + if (!class_exists($name)) { + throw new \ReflectionException('Unknown class name: ' . $name); + } + + return $name; } + /** + * @param class-string $class_name + * + * @throws RelationshipException + * @throws \ActiveRecord\Exception\ActiveRecordException + */ protected function set_class_name(string $class_name): void { - if (!has_absolute_namespace($class_name) && isset($this->options['namespace'])) { - $class_name = $this->options['namespace'] . '\\' . $class_name; - } - $reflection = Reflections::instance()->add($class_name)->get($class_name); if (!$reflection->isSubClassOf('ActiveRecord\\Model')) { diff --git a/lib/Relationship/BelongsTo.php b/lib/Relationship/BelongsTo.php index 3a2af949..297dfd36 100644 --- a/lib/Relationship/BelongsTo.php +++ b/lib/Relationship/BelongsTo.php @@ -41,6 +41,9 @@ */ class BelongsTo extends AbstractRelationship { + /** + * @var class-string + */ public $class_name; /** @@ -63,22 +66,22 @@ public function __construct(string $attributeName, $options = []) parent::__construct($attributeName, $options); if (!$this->class_name) { - $this->set_inferred_class_name(); + $this->set_class_name( + $this->inferred_class_name($this->attribute_name) + ); } // infer from class_name if (!$this->foreign_key) { - $this->foreign_key = [Inflector::instance()->keyify($this->class_name)]; + $this->foreign_key = [Inflector::keyify($this->class_name)]; } } public function load(Model $model): ?Model { $keys = []; - $inflector = Inflector::instance(); - foreach ($this->foreign_key as $key) { - $keys[] = $inflector->variablize($key); + $keys[] = Inflector::variablize($key); } if (!($conditions = $this->create_conditions_from_keys($model, $this->primary_key(), $keys))) { diff --git a/lib/Relationship/HasAndBelongsToMany.php b/lib/Relationship/HasAndBelongsToMany.php index 44811975..67b495eb 100644 --- a/lib/Relationship/HasAndBelongsToMany.php +++ b/lib/Relationship/HasAndBelongsToMany.php @@ -26,6 +26,11 @@ public function __construct($options = []) parent::__construct($options[0], $options); } + public function is_poly(): bool + { + return true; + } + public function load(Model $model): mixed { throw new \Exception("HasAndBelongsToMany doesn't need to load anything."); diff --git a/lib/Relationship/HasMany.php b/lib/Relationship/HasMany.php index 4b1d1d00..7821bad2 100644 --- a/lib/Relationship/HasMany.php +++ b/lib/Relationship/HasMany.php @@ -72,12 +72,17 @@ class HasMany extends AbstractRelationship ]; /** - * @var string|array + * @var array */ - protected string|array $primary_key; + protected array $primary_key; private string $through; + public function is_poly(): bool + { + return true; + } + /** * Constructs a {@link HasMany} relationship. * @@ -91,7 +96,7 @@ public function __construct(string $attribute, array $options = []) $this->through = $this->options['through']; if (isset($this->options['source'])) { - $this->set_class_name($this->options['source']); + $this->set_class_name($this->inferred_class_name($this->options['source'])); } } @@ -100,15 +105,18 @@ public function __construct(string $attribute, array $options = []) } if (!$this->class_name) { - $this->set_inferred_class_name(); + $this->set_class_name($this->inferred_class_name($this->attribute_name)); } } + /** + * @param class-string $model_class_name + */ protected function set_keys(string $model_class_name, bool $override = false): void { // infer from class_name if (!$this->foreign_key || $override) { - $this->foreign_key = [Inflector::instance()->keyify($model_class_name)]; + $this->foreign_key = [Inflector::keyify($model_class_name)]; } if (!isset($this->primary_key) || $override) { @@ -165,7 +173,7 @@ public function load(Model $model): mixed $options = $this->unset_non_finder_options($this->options); $options['conditions'] = $conditions; - $res = $class_name::find($this->poly_relationship ? 'all' : 'first', $options); + $res = $class_name::find($this->is_poly() ? 'all' : 'first', $options); return $res; } @@ -178,7 +186,7 @@ public function load(Model $model): mixed private function get_foreign_key_for_new_association(Model $model): array { $this->set_keys(get_class($model)); - $primary_key = Inflector::instance()->variablize($this->foreign_key[0]); + $primary_key = Inflector::variablize($this->foreign_key[0]); /* * TODO: set up model property reflection stanning diff --git a/lib/Relationship/HasOne.php b/lib/Relationship/HasOne.php index 15698202..9f9cdbd2 100644 --- a/lib/Relationship/HasOne.php +++ b/lib/Relationship/HasOne.php @@ -23,4 +23,8 @@ */ class HasOne extends HasMany { + public function is_poly(): bool + { + return false; + } } diff --git a/lib/SQLBuilder.php b/lib/SQLBuilder.php index be5b58fb..23b57809 100644 --- a/lib/SQLBuilder.php +++ b/lib/SQLBuilder.php @@ -115,7 +115,33 @@ public function get_where_values(): array public function where(/* (conditions, values) || (hash) */): static { - $this->apply_where_conditions(func_get_args()); + $args = func_get_args(); + $num_args = count($args); + + if (1 == $num_args && is_hash($args[0])) { + $hash = empty($this->joins) ? $args[0] : $this->prepend_table_name_to_fields($args[0]); + $e = new Expressions($this->connection, $hash); + $this->where = $e->to_s(); + $this->where_values = array_flatten($e->values()); + } elseif ($num_args > 0) { + // if the values has a nested array then we'll need to use Expressions to expand the bind marker for us + $values = array_slice($args, 1); + + foreach ($values as $name => &$value) { + if (is_array($value)) { + $e = new Expressions($this->connection, $args[0]); + $e->bind_values($values); + $this->where = $e->to_s(); + $this->where_values = array_flatten($e->values()); + + return $this; + } + } + + // no nested array so nothing special to do + $this->where = $args[0] ?? ''; + $this->where_values = $values; + } return $this; } @@ -163,12 +189,7 @@ public function select(string $select): static return $this; } - /** - * @param string|array $joins - * - * @return $this - */ - public function joins(string|array $joins): static + public function joins(string $joins): static { $this->joins = $joins; @@ -220,7 +241,7 @@ public function update(array|string $mixed): static public function delete(): static { $this->operation = 'DELETE'; - $this->apply_where_conditions(func_get_args()); + $this->where(...func_get_args()); return $this; } @@ -256,7 +277,10 @@ public static function reverse_order(string $order = ''): string */ public static function underscored_string_to_parts(string $string, int $flags=PREG_SPLIT_DELIM_CAPTURE): array { - return preg_split('/(_and_|_or_)/i', $string, -1, $flags); + $res = preg_split('/(_and_|_or_)/i', $string, -1, $flags); + assert(is_array($res)); + + return $res; } /** @@ -353,41 +377,6 @@ private function prepend_table_name_to_fields(array $hash = []) return $new; } - /** - * @param array> $args - * - * @throws Exception\ExpressionsException - */ - private function apply_where_conditions(array $args): void - { - $num_args = count($args); - - if (1 == $num_args && is_hash($args[0])) { - $hash = empty($this->joins) ? $args[0] : $this->prepend_table_name_to_fields($args[0]); - $e = new Expressions($this->connection, $hash); - $this->where = $e->to_s(); - $this->where_values = array_flatten($e->values()); - } elseif ($num_args > 0) { - // if the values has a nested array then we'll need to use Expressions to expand the bind marker for us - $values = array_slice($args, 1); - - foreach ($values as $name => &$value) { - if (is_array($value)) { - $e = new Expressions($this->connection, $args[0]); - $e->bind_values($values); - $this->where = $e->to_s(); - $this->where_values = array_flatten($e->values()); - - return; - } - } - - // no nested array so nothing special to do - $this->where = $args[0] ?? ''; - $this->where_values = &$values; - } - } - private function build_delete(): string { $sql = "DELETE FROM $this->table"; diff --git a/lib/Serialize/CsvSerializer.php b/lib/Serialize/CsvSerializer.php index f7e46169..4c55c32e 100644 --- a/lib/Serialize/CsvSerializer.php +++ b/lib/Serialize/CsvSerializer.php @@ -12,7 +12,7 @@ class CsvSerializer extends Serialization public function to_s(): string { - if (true == @$this->options['only_header']) { + if ($this->options['only_header'] ?? false) { return $this->header(); } @@ -35,9 +35,12 @@ private function row(): string private function to_csv(array $arr): string { $outstream = fopen('php://temp', 'w'); + assert(false !== $outstream); fputcsv($outstream, $arr, self::$delimiter, self::$enclosure); rewind($outstream); - $buffer = trim(stream_get_contents($outstream)); + $contents = stream_get_contents($outstream); + assert(is_string($contents)); + $buffer = trim($contents); fclose($outstream); return $buffer; diff --git a/lib/Serialize/JsonSerializer.php b/lib/Serialize/JsonSerializer.php index 6e9ce560..266bb2c4 100644 --- a/lib/Serialize/JsonSerializer.php +++ b/lib/Serialize/JsonSerializer.php @@ -2,6 +2,8 @@ namespace ActiveRecord\Serialize; +use ActiveRecord\Model; + /** * JSON serializer. * @@ -9,10 +11,16 @@ */ class JsonSerializer extends Serialization { + public function __construct(Model $model, $options) + { + parent::__construct($model, $options); + } + public function to_s(): string { - $res = !empty($this->options['include_root']) ? [strtolower(get_class($this->model)) => $this->to_a()] : $this->to_a(); + $res = json_encode(!empty($this->options['include_root']) ? [strtolower(get_class($this->model)) => $this->to_a()] : $this->to_a()); + assert(is_string($res)); - return json_encode($res); + return $res; } } diff --git a/lib/Serialize/Serialization.php b/lib/Serialize/Serialization.php index 07666401..1d6f5c2c 100644 --- a/lib/Serialize/Serialization.php +++ b/lib/Serialize/Serialization.php @@ -2,7 +2,7 @@ namespace ActiveRecord\Serialize; -use ActiveRecord\Config; +use ActiveRecord\DateTimeInterface; use ActiveRecord\Exception\UndefinedPropertyException; use ActiveRecord\Model; use ActiveRecord\Types; @@ -123,9 +123,10 @@ private function parse_options(): void private function check_only(): void { if (isset($this->options['only'])) { - $this->options_to_a('only'); - - $exclude = array_diff(array_keys($this->attributes), $this->options['only']); + $exclude = array_diff( + array_keys($this->attributes), + $this->value_to_a($this->options['only']) + ); $this->attributes = array_diff_key($this->attributes, array_flip($exclude)); } } @@ -133,18 +134,23 @@ private function check_only(): void private function check_except(): void { if (isset($this->options['except']) && !isset($this->options['only'])) { - $this->options_to_a('except'); - $this->attributes = array_diff_key($this->attributes, array_flip($this->options['except'])); + $this->attributes = array_diff_key( + $this->attributes, + array_flip($this->value_to_a($this->options['except'])) + ); } } private function check_methods(): void { if (isset($this->options['methods'])) { - $this->options_to_a('methods'); - - foreach ($this->options['methods'] as $method) { + foreach ($this->value_to_a($this->options['methods']) as $method) { if (method_exists($this->model, $method)) { + /* + * PHPStan complains about this, and I don't know why. Skipping, for now. + * + * @phpstan-ignore-next-line + */ $this->attributes[$method] = $this->model->$method(); } } @@ -164,21 +170,20 @@ private function check_only_method(): void private function check_include(): void { if (isset($this->options['include'])) { - $this->options_to_a('include'); - $serializer_class = get_class($this); - foreach ($this->options['include'] as $association => $options) { + foreach ($this->value_to_a($this->options['include']) as $association => $options) { if (!is_array($options)) { $association = $options; $options = []; } + assert(is_string($association)); try { $assoc = $this->model->$association; if (null === $assoc) { - $this->attributes[$association] = null; + unset($this->attributes[$association]); } elseif (!is_array($assoc)) { $serialized = new $serializer_class($assoc, $options); $this->attributes[$association] = $serialized->to_a(); @@ -189,7 +194,9 @@ private function check_include(): void $serialized = new $serializer_class($a, $options); if ($this->includes_with_class_name_element) { - $includes[strtolower(get_class($a))][] = $serialized->to_a(); + $className = get_class($a); + assert(is_string($className)); + $includes[strtolower($className)][] = $serialized->to_a(); } else { $includes[] = $serialized->to_a(); } @@ -204,11 +211,16 @@ private function check_include(): void } } - final protected function options_to_a(string $key): void + /** + * @return array + */ + final protected function value_to_a(mixed $value): array { - if (!is_array($this->options[$key])) { - $this->options[$key] = [$this->options[$key]]; + if (!is_array($value)) { + return [$value]; } + + return $value; } /** @@ -218,9 +230,8 @@ final protected function options_to_a(string $key): void */ final public function to_a(): array { - $date_class = Config::instance()->get_date_class(); foreach ($this->attributes as &$value) { - if ($value instanceof $date_class) { + if ($value instanceof DateTimeInterface) { $value = $value->format(self::$DATETIME_FORMAT); } } diff --git a/lib/Serialize/XmlSerializer.php b/lib/Serialize/XmlSerializer.php index 6cd5ac34..e9b556c8 100644 --- a/lib/Serialize/XmlSerializer.php +++ b/lib/Serialize/XmlSerializer.php @@ -43,9 +43,9 @@ private function xml_encode(): string $this->write($this->to_a()); $this->writer->endElement(); $this->writer->endDocument(); - $xml = $this->writer->outputMemory(true); + $xml = $this->writer->outputMemory(); - if (true == @$this->options['skip_instruct']) { + if ($this->options['skip_instruct'] ?? false) { $xml = preg_replace('/<\?xml version.*?\?>/', '', $xml); } @@ -59,8 +59,7 @@ private function write(array $data, string $tag = null): void { foreach ($data as $attr => $value) { $attr = $tag ?? $attr; - - if (is_array($value) || is_object($value)) { + if (is_array($value)) { if (!is_int(key($value))) { $this->writer->startElement(denamespace($attr)); $this->write($value); diff --git a/lib/Singleton.php b/lib/Singleton.php index a838a3bd..fe9ee5c0 100644 --- a/lib/Singleton.php +++ b/lib/Singleton.php @@ -44,16 +44,4 @@ final public static function instance(): static private function __clone() { } - - /** - * Similar to a get_called_class() for a child class to invoke. - * - * @return string - */ - final protected function get_called_class() - { - $backtrace = debug_backtrace(); - - return get_class($backtrace[2]['object']); - } } diff --git a/lib/Table.php b/lib/Table.php index 895f8a9d..310873a0 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -20,12 +20,12 @@ * reading and writing to its database table. There is one instance of Table * for every table you have a model for. * - * @package ActiveRecord + * @phpstan-import-type PrimaryKey from Types */ class Table { /** - * @var array + * @var array */ private static array $cache = []; @@ -73,18 +73,17 @@ class Table /** * A instance of CallBack for this model/table - * - * @static - * - * @var object ActiveRecord\CallBack */ - public $callback; + public CallBack $callback; /** * @var array */ private array $relationships = []; + /** + * @param class-string $model_class_name + */ public static function load(string $model_class_name): Table { if (!isset(self::$cache[$model_class_name])) { @@ -106,6 +105,11 @@ public static function clear_cache(string $model_class_name = null): void } } + /** + * @param class-string $class_name + * + * @throws Exception\ActiveRecordException + */ public function __construct(string $class_name) { $this->class = Reflections::instance()->add($class_name)->get($class_name); @@ -262,9 +266,9 @@ public function find(array $options): array } /** - * @param string|array $pk + * @param PrimaryKey $pk */ - public function cache_key_for_model(string|array $pk): string + public function cache_key_for_model(mixed $pk): string { if (is_array($pk)) { $pk = implode('-', $pk); @@ -499,13 +503,13 @@ private function map_names(array &$hash, array &$map): array private function process_data(array|null $hash): array|null { if ($hash) { - $date_class = Config::instance()->get_date_class(); foreach ($hash as $name => $value) { - if ($value instanceof $date_class || $value instanceof \DateTime) { - if (isset($this->columns[$name]) && Column::DATE == $this->columns[$name]->type) { - $hash[$name] = $this->conn->date_to_string($value); + if ($value instanceof \DateTime) { + $column = $this->columns[$name] ?? null; + if (isset($column) && Column::DATE == $column->type) { + $hash[$name] = $this->conn->date_string($value); } else { - $hash[$name] = $this->conn->datetime_to_string($value); + $hash[$name] = $this->conn->datetime_string($value); } } else { $hash[$name] = $value; @@ -537,7 +541,7 @@ private function set_table_name(): void $this->table = $table; } else { // infer table name from the class name - $this->table = Inflector::instance()->tableize($this->class->getName()); + $this->table = Inflector::tableize($this->class->getName()); // strip namespaces from the table name if any $parts = explode('\\', $this->table); @@ -557,7 +561,8 @@ private function set_cache(): void $model_class_name = $this->class->name; $this->cache_individual_model = $model_class_name::$cache; - $this->cache_model_expire = $model_class_name::$cache_expire ?? Cache::$options['expire']; + + $this->cache_model_expire = $model_class_name::$cache_expire ?? Cache::$options['expire'] ?? 0; } private function set_sequence_name(): void diff --git a/lib/Utils.php b/lib/Utils.php index d7a58ed0..5718f3ee 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -34,15 +34,17 @@ namespace ActiveRecord; -function classify(string $class_name, bool $singular = false): string +function classify(string $string, bool $singular = false): string { if ($singular) { - $class_name = Utils::singularize($class_name); + $string = Utils::singularize($string); } - $class_name = Inflector::instance()->camelize($class_name); + $string = Inflector::camelize($string); - return ucfirst($class_name); + $res = ucfirst($string); + + return $res; } /** @@ -196,9 +198,8 @@ public static function add_condition(array &$conditions, string|array $condition public static function human_attribute(string $attr): string { - $inflector = Inflector::instance(); - $inflected = $inflector->variablize($attr); - $normal = $inflector->uncamelize($inflected); + $inflected = Inflector::variablize($attr); + $normal = Inflector::uncamelize($inflected); return ucfirst(str_replace('_', ' ', $normal)); } @@ -366,11 +367,11 @@ public static function singularize(string $string): string return $string; } - /** - * @return string|string[]|null - */ - public static function squeeze(string $char, string $string): mixed + public static function squeeze(string $char, string $string): string { - return preg_replace("/$char+/", $char, $string); + $res = preg_replace("/$char+/", $char, $string); + assert(is_string($res)); + + return $res; } } diff --git a/lib/Validations.php b/lib/Validations.php index f428a363..049efe35 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -115,16 +115,6 @@ class Validations 'validates_uniqueness_of' ]; - /** - * @var ValidationOptions - */ - private static array $DEFAULT_VALIDATION_OPTIONS = [ - 'on' => 'save', - 'allow_null' => false, - 'allow_blank' => false, - 'message' => null, - ]; - /** * @var array */ @@ -148,7 +138,12 @@ public function __construct(Model $model) $this->model = $model; $this->errors = new ValidationErrors($this->model); $this->klass = Reflections::instance()->get(get_class($this->model)); - $this->validators = array_intersect(array_keys($this->klass->getStaticProperties()), self::$VALIDATION_FUNCTIONS); + /** @var array $validators */ + $validators = array_intersect( + array_keys($this->klass->getStaticProperties()), + self::$VALIDATION_FUNCTIONS + ); + $this->validators = $validators; } public function get_errors(): ValidationErrors @@ -314,9 +309,7 @@ public function validates_inclusion_or_exclusion_of(string $type, array $attrs): foreach ($attrs as $attribute => $options) { $var = $this->model->$attribute; - $enum = $options['in'] ?? $options['within']; - assert(isset($enum)); - + $enum = $options['in'] ?? $options['within'] ?? throw new \Exception("Must provide 'in' or 'within'"); $message = str_replace('%s', $var ?? '', $options['message'] ?? ValidationErrors::$DEFAULT_ERROR_MESSAGES[$type]); if ($this->is_null_with_option($var, $options) || $this->is_blank_with_option($var, $options)) { @@ -559,13 +552,13 @@ public function validates_uniqueness_of($attrs): void $options = []; } $pk = $this->model->get_primary_key(); - $pk_value = $this->model->{$pk[0]}; + $pk_value = $this->model->{$pk}; $fields = array_merge([$attr], $options['scope'] ?? []); $add_record = join('_and_', $fields); $conditions = ['']; - $pk_quoted = $connection->quote_name($pk[0]); + $pk_quoted = $connection->quote_name($pk); if (null === $pk_value) { $sql = "{$pk_quoted} IS NOT NULL"; } else { @@ -593,7 +586,7 @@ public function validates_uniqueness_of($attrs): void */ private function is_null_with_option(mixed $var, array &$options): bool { - return is_null($var) && ($options['allow_null'] ?? self::$DEFAULT_VALIDATION_OPTIONS['allow_null']); + return is_null($var) && ($options['allow_null'] ?? false); } /** @@ -601,6 +594,6 @@ private function is_null_with_option(mixed $var, array &$options): bool */ private function is_blank_with_option(mixed $var, array &$options): bool { - return Utils::is_blank($var) && ($options['allow_blank'] ?? self::$DEFAULT_VALIDATION_OPTIONS['allow_blank']); + return Utils::is_blank($var) && ($options['allow_blank'] ?? false); } } diff --git a/lib/cache/Memcache.php b/lib/cache/Memcache.php index 2fe8a0a8..ee4cfbb6 100644 --- a/lib/cache/Memcache.php +++ b/lib/cache/Memcache.php @@ -6,8 +6,8 @@ /** * @phpstan-type MemcacheOptions array{ - * host?: string, - * port?: string + * host: string, + * port?: int * } */ class Memcache @@ -24,13 +24,12 @@ class Memcache public function __construct(array $options) { $this->memcache = new \Memcache(); - $options['port'] = $options['port'] ?? self::DEFAULT_PORT; - - if (!@$this->memcache->connect($options['host'], $options['port'])) { + $port = $options['port'] ?? self::DEFAULT_PORT; + if (!@$this->memcache->connect($options['host'], $port)) { if ($error = error_get_last()) { $message = $error['message']; } else { - $message = sprintf('Could not connect to %s:%s', $options['host'], $options['port']); + $message = sprintf('Could not connect to %s:%s', $options['host'], $port); } throw new CacheException($message); } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5dca0cd0..f421b5f0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,11 +1,12 @@ parameters: - level: 6 + level: 7 tmpDir: .stan-cache paths: - %currentWorkingDirectory%/lib - %currentWorkingDirectory%/test/phpstan reportUnmatchedIgnoredErrors: false + checkGenericClassInNonGenericObjectType: false ignoreErrors: services: diff --git a/test/ActiveRecordFindTest.php b/test/ActiveRecordFindTest.php index a0859875..dd93f9b3 100644 --- a/test/ActiveRecordFindTest.php +++ b/test/ActiveRecordFindTest.php @@ -418,7 +418,7 @@ public function testFindWithSelectNonSelectedFieldsShouldNotHaveAttributes() public function testJoinsOnModelWithAssociationAndExplicitJoins() { JoinBook::$belongs_to = [ - 'author' => true + 'author'=>true ]; JoinBook::first(['joins' => ['author', 'LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)']]); $this->assert_sql_has('INNER JOIN authors ON(books.author_id = authors.author_id)', JoinBook::table()->last_sql); diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php index 159202bc..a8b633e0 100644 --- a/test/ConnectionTest.php +++ b/test/ConnectionTest.php @@ -18,18 +18,18 @@ public function testConnectionInfoFromShouldThrowExceptionWhenNoHost() public function testConnectionInfo() { $info = ActiveRecord\Connection::parse_connection_url('mysql://user:pass@127.0.0.1:3306/dbname'); - $this->assertEquals('mysql', $info->protocol); - $this->assertEquals('user', $info->user); - $this->assertEquals('pass', $info->pass); - $this->assertEquals('127.0.0.1', $info->host); - $this->assertEquals(3306, $info->port); - $this->assertEquals('dbname', $info->db); + $this->assertEquals('mysql', $info['protocol']); + $this->assertEquals('user', $info['user']); + $this->assertEquals('pass', $info['pass']); + $this->assertEquals('127.0.0.1', $info['host']); + $this->assertEquals(3306, $info['port']); + $this->assertEquals('dbname', $info['db']); } public function testGh103SqliteConnectionStringRelative() { $info = ActiveRecord\Connection::parse_connection_url('sqlite://../some/path/to/file.db'); - $this->assertEquals('../some/path/to/file.db', $info->host); + $this->assertEquals('../some/path/to/file.db', $info['host']); } public function testGh103SqliteConnectionStringAbsolute() @@ -41,37 +41,37 @@ public function testGh103SqliteConnectionStringAbsolute() public function testGh103SqliteConnectionStringUnix() { $info = ActiveRecord\Connection::parse_connection_url('sqlite://unix(/some/path/to/file.db)'); - $this->assertEquals('/some/path/to/file.db', $info->host); + $this->assertEquals('/some/path/to/file.db', $info['host']); $info = ActiveRecord\Connection::parse_connection_url('sqlite://unix(/some/path/to/file.db)/'); - $this->assertEquals('/some/path/to/file.db', $info->host); + $this->assertEquals('/some/path/to/file.db', $info['host']); $info = ActiveRecord\Connection::parse_connection_url('sqlite://unix(/some/path/to/file.db)/dummy'); - $this->assertEquals('/some/path/to/file.db', $info->host); + $this->assertEquals('/some/path/to/file.db', $info['host']); } public function testGh103SqliteConnectionStringWindows() { $info = ActiveRecord\Connection::parse_connection_url('sqlite://windows(c%3A/some/path/to/file.db)'); - $this->assertEquals('c:/some/path/to/file.db', $info->host); + $this->assertEquals('c:/some/path/to/file.db', $info['host']); } public function testParseConnectionUrlWithUnixSockets() { $info = ActiveRecord\Connection::parse_connection_url('mysql://user:password@unix(/tmp/mysql.sock)/database'); - $this->assertEquals('/tmp/mysql.sock', $info->host); + $this->assertEquals('/tmp/mysql.sock', $info['host']); } public function testParseConnectionUrlWithDecodeOption() { $info = ActiveRecord\Connection::parse_connection_url('mysql://h%20az:h%40i@127.0.0.1/test?decode=true'); - $this->assertEquals('h az', $info->user); - $this->assertEquals('h@i', $info->pass); + $this->assertEquals('h az', $info['user']); + $this->assertEquals('h@i', $info['pass']); } public function testEncoding() { $info = ActiveRecord\Connection::parse_connection_url('mysql://test:test@127.0.0.1/test?charset=utf8'); - $this->assertEquals('utf8', $info->charset); + $this->assertEquals('utf8', $info['charset']); } } diff --git a/test/InflectorTest.php b/test/InflectorTest.php index e8e09a2a..97ba4278 100644 --- a/test/InflectorTest.php +++ b/test/InflectorTest.php @@ -1,32 +1,26 @@ inflector = ActiveRecord\Inflector::instance(); - } - public function testUnderscorify() { - $this->assertEquals('rm__name__bob', $this->inflector->variablize('rm--name bob')); - $this->assertEquals('One_Two_Three', $this->inflector->underscorify('OneTwoThree')); + $this->assertEquals('rm__name__bob', Inflector::variablize('rm--name bob')); + $this->assertEquals('One_Two_Three', Inflector::underscorify('OneTwoThree')); } public function testTableize() { - $this->assertEquals('angry_people', $this->inflector->tableize('AngryPerson')); - $this->assertEquals('my_sqls', $this->inflector->tableize('MySQL')); + $this->assertEquals('angry_people', Inflector::tableize('AngryPerson')); + $this->assertEquals('my_sqls', Inflector::tableize('MySQL')); } public function testKeyify() { - $this->assertEquals('building_type_id', $this->inflector->keyify('BuildingType')); + $this->assertEquals('building_type_id', Inflector::keyify('BuildingType')); } } diff --git a/test/RelationshipTest.php b/test/RelationshipTest.php index 56375a17..de2f8a11 100644 --- a/test/RelationshipTest.php +++ b/test/RelationshipTest.php @@ -552,7 +552,7 @@ public function testHasManyWithExplicitKeys() public function testHasOneBasic() { - $this->assert_default_has_one($this->get_relationship()); + $this->assert_default_has_one(Employee::find(1)); } public function testHasOneWithExplicitClassName() diff --git a/test/SqliteAdapterTest.php b/test/SqliteAdapterTest.php index f866bc7b..b57758b2 100644 --- a/test/SqliteAdapterTest.php +++ b/test/SqliteAdapterTest.php @@ -63,13 +63,13 @@ public function testGh183SqliteadapterAutoincrement() public function testDatetimeToString() { $datetime = '2009-01-01 01:01:01'; - $this->assertEquals($datetime, $this->connection->datetime_to_string(date_create($datetime))); + $this->assertEquals($datetime, $this->connection->datetime_string(date_create($datetime))); } public function testDateToString() { $datetime = '2009-01-01'; - $this->assertEquals($datetime, $this->connection->date_to_string(date_create($datetime))); + $this->assertEquals($datetime, $this->connection->date_string(date_create($datetime))); } // not supported diff --git a/test/helpers/AdapterTestCase.php b/test/helpers/AdapterTestCase.php index 4bdf6f41..0464a0e5 100644 --- a/test/helpers/AdapterTestCase.php +++ b/test/helpers/AdapterTestCase.php @@ -397,12 +397,12 @@ public function testQuoteNameDoesNotOverQuote() public function testDatetimeToString() { $datetime = '2009-01-01 01:01:01'; - $this->assertEquals($datetime, $this->connection->datetime_to_string(date_create($datetime))); + $this->assertEquals($datetime, $this->connection->datetime_string(date_create($datetime))); } public function testDateToString() { $datetime = '2009-01-01'; - $this->assertEquals($datetime, $this->connection->date_to_string(date_create($datetime))); + $this->assertEquals($datetime, $this->connection->date_string(date_create($datetime))); } }