From 103b8781a1b25afbacf7d94aa45d7c69795bc342 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 16:24:41 -0700 Subject: [PATCH 01/34] stanning on Inflector --- lib/Adapter/MysqlAdapter.php | 2 +- lib/Adapter/PgsqlAdapter.php | 2 +- lib/Adapter/SqliteAdapter.php | 4 +- lib/Cache.php | 2 +- lib/Inflector.php | 48 ++++++----------------- lib/Relationship/AbstractRelationship.php | 9 ++--- lib/Relationship/BelongsTo.php | 6 +-- lib/Relationship/HasMany.php | 7 +++- lib/Table.php | 2 +- lib/Utils.php | 10 +++-- lib/Validations.php | 30 +++++++------- lib/cache/Memcache.php | 11 +++--- phpstan.neon.dist | 2 +- test/InflectorTest.php | 17 +++----- 14 files changed, 61 insertions(+), 91 deletions(-) diff --git a/lib/Adapter/MysqlAdapter.php b/lib/Adapter/MysqlAdapter.php index ea90b220..d98badfd 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); 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..2b51a10d 100644 --- a/lib/Adapter/SqliteAdapter.php +++ b/lib/Adapter/SqliteAdapter.php @@ -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->pk = $column['pk'] ?? false; $c->auto_increment = in_array( strtoupper($column['type']), ['INT', 'INTEGER'] diff --git a/lib/Cache.php b/lib/Cache.php index b96fd04a..27bac538 100644 --- a/lib/Cache.php +++ b/lib/Cache.php @@ -54,7 +54,7 @@ public static function initialize(string $url = '', array $options = []): void { if ($url) { $url = parse_url($url); - $file = ucwords(Inflector::instance()->camelize($url['scheme'])); + $file = ucwords(Inflector::camelize($url['scheme'])); $class = "ActiveRecord\\$file"; require_once __DIR__ . "/cache/$file.php"; static::$adapter = new $class($url); diff --git a/lib/Inflector.php b/lib/Inflector.php index 7394d3f7..a76a6fc7 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) + static public 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) + static public 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 + static public 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 + static public function underscorify(string $s): string { $res = preg_replace(['/[_\- ]+/', '/([a-z])([A-Z])/'], ['_', '\\1_\\2'], trim($s)); assert(is_string($res)); @@ -92,26 +74,20 @@ public function underscorify(string $s): string return $res; } - public function keyify(string $class_name): string + static public 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 + static public 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 + static public function variablize(string $s): string { return str_replace(['-', ' '], ['_', '_'], strtolower(trim($s))); } } + + diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index f218f779..aafd0a7f 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -92,7 +92,7 @@ public function __construct($options = []) $this->set_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']]; @@ -136,12 +136,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 +191,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; diff --git a/lib/Relationship/BelongsTo.php b/lib/Relationship/BelongsTo.php index 676b9a27..9fa8779e 100644 --- a/lib/Relationship/BelongsTo.php +++ b/lib/Relationship/BelongsTo.php @@ -65,17 +65,15 @@ public function __construct($options = []) // 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/HasMany.php b/lib/Relationship/HasMany.php index 92ccac82..07bf4543 100644 --- a/lib/Relationship/HasMany.php +++ b/lib/Relationship/HasMany.php @@ -113,11 +113,14 @@ public function __construct(array $options = []) } } + /** + * @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) { @@ -187,7 +190,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/Table.php b/lib/Table.php index 83e3b0e2..806f5b38 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -537,7 +537,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); diff --git a/lib/Utils.php b/lib/Utils.php index d7a58ed0..d41e773e 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -34,13 +34,16 @@ namespace ActiveRecord; +/** + * @param class-string $class_name + */ function classify(string $class_name, bool $singular = false): string { if ($singular) { $class_name = Utils::singularize($class_name); } - $class_name = Inflector::instance()->camelize($class_name); + $class_name = Inflector::camelize($class_name); return ucfirst($class_name); } @@ -196,9 +199,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)); } diff --git a/lib/Validations.php b/lib/Validations.php index f428a363..3483bbad 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -11,6 +11,7 @@ namespace ActiveRecord; use ActiveRecord\Exception\ValidationsArgumentError; +use function PHPStan\dumpType; /** * Manages validations for a {@link Model}. @@ -115,16 +116,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 +139,16 @@ 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; + + if(!empty($this->validators)) { + xdebug_break(); + } } public function get_errors(): ValidationErrors @@ -314,9 +314,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)) { @@ -593,7 +591,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 +599,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..925c79bc 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 bf5a659d..029d6bbf 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 7 tmpDir: .stan-cache paths: - %currentWorkingDirectory%/lib diff --git a/test/InflectorTest.php b/test/InflectorTest.php index 6762b93c..7da5d273 100644 --- a/test/InflectorTest.php +++ b/test/InflectorTest.php @@ -1,32 +1,27 @@ inflector = ActiveRecord\Inflector::instance(); - } public function test_underscorify() { - $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 test_tableize() { - $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 test_keyify() { - $this->assertEquals('building_type_id', $this->inflector->keyify('BuildingType')); + $this->assertEquals('building_type_id', Inflector::keyify('BuildingType')); } } From d611de8673fa6915ba4bdd6bdbeea7faa01040e3 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 16:25:32 -0700 Subject: [PATCH 02/34] stanning on Inflector --- lib/Inflector.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/Inflector.php b/lib/Inflector.php index a76a6fc7..f394da7d 100644 --- a/lib/Inflector.php +++ b/lib/Inflector.php @@ -13,7 +13,7 @@ abstract class Inflector /** * Turn a string into its camelized version. */ - static public function camelize(string $s): string + public static function camelize(string $s): string { $s = preg_replace('/[_-]+/', '_', trim($s)); $s = str_replace(' ', '_', $s); @@ -40,7 +40,7 @@ static public function camelize(string $s): string /** * Determines if a string contains all uppercase characters. */ - static public function is_upper(string $s): bool + public static function is_upper(string $s): bool { return strtoupper($s) === $s; } @@ -48,7 +48,7 @@ static public function is_upper(string $s): bool /** * Convert a camelized string to a lowercase, underscored string. */ - static public function uncamelize(string $s): string + public static function uncamelize(string $s): string { $normalized = ''; @@ -66,7 +66,7 @@ static public function uncamelize(string $s): string /** * Convert a string with space into a underscored equivalent. */ - static 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)); @@ -74,20 +74,18 @@ static public function underscorify(string $s): string return $res; } - static public function keyify(string $class_name): string + public static function keyify(string $class_name): string { return strtolower(static::underscorify(denamespace($class_name))) . '_id'; } - static public function tableize(string $s): string + public static function tableize(string $s): string { return Utils::pluralize(strtolower(static::underscorify($s))); } - static public function variablize(string $s): string + public static function variablize(string $s): string { return str_replace(['-', ' '], ['_', '_'], strtolower(trim($s))); } } - - From 15cda29e878f14cd470e2ec4fd485edabce1584f Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 16:41:04 -0700 Subject: [PATCH 03/34] stanning on Table --- lib/Model.php | 2 +- lib/Table.php | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/Model.php b/lib/Model.php index 36a64ce9..86de2df5 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -175,7 +175,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; diff --git a/lib/Table.php b/lib/Table.php index 806f5b38..29fbe55a 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -12,6 +12,7 @@ use ActiveRecord\Relationship\HasAndBelongsToMany; use ActiveRecord\Relationship\HasMany; use ActiveRecord\Relationship\HasOne; +use function PHPStan\dumpType; /** * Manages reading and writing to a database table. @@ -25,7 +26,7 @@ class Table { /** - * @var array + * @var array */ private static array $cache = []; @@ -85,6 +86,9 @@ class Table */ 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 +110,10 @@ 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); @@ -502,7 +510,7 @@ private function process_data(array|null $hash): array|null $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) { + if (Column::DATE == $this->columns[$name]->type ?? null) { $hash[$name] = $this->conn->date_to_string($value); } else { $hash[$name] = $this->conn->datetime_to_string($value); @@ -557,6 +565,7 @@ 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']; } From 9e1bb676e3bb77e886b5fce9923deb90d2977204 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 17:25:36 -0700 Subject: [PATCH 04/34] fix column --- lib/Adapter/SqliteAdapter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Adapter/SqliteAdapter.php b/lib/Adapter/SqliteAdapter.php index 2b51a10d..b08404da 100644 --- a/lib/Adapter/SqliteAdapter.php +++ b/lib/Adapter/SqliteAdapter.php @@ -51,8 +51,8 @@ public function create_column(array $column): Column $c = new Column(); $c->inflected_name = Inflector::variablize($column['name']); $c->name = $column['name']; - $c->nullable = $column['notnull'] ? false : true; - $c->pk = $column['pk'] ?? false; + $c->nullable = !$column['notnull']; + $c->pk = !!$column['pk']; $c->auto_increment = in_array( strtoupper($column['type']), ['INT', 'INTEGER'] From 857d9790714a44deec9afc4d5fa8ba2c12a07b54 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 17:54:42 -0700 Subject: [PATCH 05/34] clean up date stuff --- lib/Connection.php | 15 ++------------- lib/Reflections.php | 10 +++------- lib/Serialize/XmlSerializer.php | 4 ++-- lib/Singleton.php | 12 ------------ lib/Table.php | 10 +++++----- lib/Validations.php | 4 ---- test/SqliteAdapterTest.php | 4 ++-- test/helpers/AdapterTestCase.php | 4 ++-- 8 files changed, 16 insertions(+), 47 deletions(-) diff --git a/lib/Connection.php b/lib/Connection.php index 43236327..5e6e6517 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -484,26 +484,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); } diff --git a/lib/Reflections.php b/lib/Reflections.php index 6aedf58d..a711ecc6 100644 --- a/lib/Reflections.php +++ b/lib/Reflections.php @@ -29,7 +29,7 @@ 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); @@ -77,7 +77,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 +85,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/Serialize/XmlSerializer.php b/lib/Serialize/XmlSerializer.php index 6cd5ac34..70a16809 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); } 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 29fbe55a..69fabbb8 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -509,11 +509,11 @@ 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 (Column::DATE == $this->columns[$name]->type ?? null) { - $hash[$name] = $this->conn->date_to_string($value); + if ($value instanceof \DateTime) { + if (Column::DATE == $this->columns[$name]->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; @@ -566,7 +566,7 @@ 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/Validations.php b/lib/Validations.php index 3483bbad..9156f2ae 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -145,10 +145,6 @@ public function __construct(Model $model) self::$VALIDATION_FUNCTIONS ); $this->validators = $validators; - - if(!empty($this->validators)) { - xdebug_break(); - } } public function get_errors(): ValidationErrors diff --git a/test/SqliteAdapterTest.php b/test/SqliteAdapterTest.php index 96e6f935..d0c27c5d 100644 --- a/test/SqliteAdapterTest.php +++ b/test/SqliteAdapterTest.php @@ -64,13 +64,13 @@ public function test_gh183_sqliteadapter_autoincrement() public function test_datetime_to_string() { $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 test_date_to_string() { $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 b3d84153..2328b37f 100644 --- a/test/helpers/AdapterTestCase.php +++ b/test/helpers/AdapterTestCase.php @@ -396,12 +396,12 @@ public function test_quote_name_does_not_over_quote() public function test_datetime_to_string() { $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 test_date_to_string() { $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))); } } From 54043deb4a39513de5f08e7d125118558f32aaa5 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 19:08:03 -0700 Subject: [PATCH 06/34] some serializer linting --- lib/Serialize/XmlSerializer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Serialize/XmlSerializer.php b/lib/Serialize/XmlSerializer.php index 70a16809..ad85c97b 100644 --- a/lib/Serialize/XmlSerializer.php +++ b/lib/Serialize/XmlSerializer.php @@ -5,6 +5,7 @@ use function ActiveRecord\denamespace; use ActiveRecord\Model; +use function PHPStan\dumpType; /** * XML serializer. From f48ca2abb5080bf45587987abf2e3c69500a7325 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 19:14:46 -0700 Subject: [PATCH 07/34] fixer --- lib/Adapter/SqliteAdapter.php | 2 +- lib/Serialize/XmlSerializer.php | 1 - lib/Table.php | 2 +- lib/Validations.php | 1 - lib/cache/Memcache.php | 2 +- 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/Adapter/SqliteAdapter.php b/lib/Adapter/SqliteAdapter.php index b08404da..d3a7444d 100644 --- a/lib/Adapter/SqliteAdapter.php +++ b/lib/Adapter/SqliteAdapter.php @@ -52,7 +52,7 @@ public function create_column(array $column): Column $c->inflected_name = Inflector::variablize($column['name']); $c->name = $column['name']; $c->nullable = !$column['notnull']; - $c->pk = !!$column['pk']; + $c->pk = (bool) $column['pk']; $c->auto_increment = in_array( strtoupper($column['type']), ['INT', 'INTEGER'] diff --git a/lib/Serialize/XmlSerializer.php b/lib/Serialize/XmlSerializer.php index ad85c97b..70a16809 100644 --- a/lib/Serialize/XmlSerializer.php +++ b/lib/Serialize/XmlSerializer.php @@ -5,7 +5,6 @@ use function ActiveRecord\denamespace; use ActiveRecord\Model; -use function PHPStan\dumpType; /** * XML serializer. diff --git a/lib/Table.php b/lib/Table.php index 69fabbb8..e4ec68be 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -12,7 +12,6 @@ use ActiveRecord\Relationship\HasAndBelongsToMany; use ActiveRecord\Relationship\HasMany; use ActiveRecord\Relationship\HasOne; -use function PHPStan\dumpType; /** * Manages reading and writing to a database table. @@ -112,6 +111,7 @@ 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) diff --git a/lib/Validations.php b/lib/Validations.php index 9156f2ae..c7fef099 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -11,7 +11,6 @@ namespace ActiveRecord; use ActiveRecord\Exception\ValidationsArgumentError; -use function PHPStan\dumpType; /** * Manages validations for a {@link Model}. diff --git a/lib/cache/Memcache.php b/lib/cache/Memcache.php index 925c79bc..ee4cfbb6 100644 --- a/lib/cache/Memcache.php +++ b/lib/cache/Memcache.php @@ -25,7 +25,7 @@ public function __construct(array $options) { $this->memcache = new \Memcache(); $port = $options['port'] ?? self::DEFAULT_PORT; - if (!@$this->memcache->connect($options['host'], $port )) { + if (!@$this->memcache->connect($options['host'], $port)) { if ($error = error_get_last()) { $message = $error['message']; } else { From 514bb8ff91e4d81540fadcad92a0fd0242a2c770 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 2 Sep 2023 19:20:08 -0700 Subject: [PATCH 08/34] fix warning --- lib/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Table.php b/lib/Table.php index e4ec68be..85253e5d 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -510,7 +510,7 @@ private function process_data(array|null $hash): array|null $date_class = Config::instance()->get_date_class(); foreach ($hash as $name => $value) { if ($value instanceof \DateTime) { - if (Column::DATE == $this->columns[$name]->type) { + if (Column::DATE == ($this->columns[$name]?->type ?? null)) { $hash[$name] = $this->conn->date_string($value); } else { $hash[$name] = $this->conn->datetime_string($value); From 126acac9f4d337d3108b5f22a226d01da7e97666 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 3 Sep 2023 11:37:45 -0700 Subject: [PATCH 09/34] stanning for Serialization --- lib/Serialize/Serialization.php | 54 ++++++++++++++++++++------------- lib/Serialize/XmlSerializer.php | 4 +-- lib/Table.php | 4 +-- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/lib/Serialize/Serialization.php b/lib/Serialize/Serialization.php index 07666401..4f657af3 100644 --- a/lib/Serialize/Serialization.php +++ b/lib/Serialize/Serialization.php @@ -2,10 +2,11 @@ namespace ActiveRecord\Serialize; -use ActiveRecord\Config; +use ActiveRecord\DateTimeInterface; use ActiveRecord\Exception\UndefinedPropertyException; use ActiveRecord\Model; use ActiveRecord\Types; +use function PHPStan\dumpType; /** * Base class for Model serializers. @@ -123,9 +124,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 +135,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,24 +171,22 @@ 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(); + $this->attributes[$association] = new $serializer_class($assoc, $options); } else { $includes = []; @@ -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,17 @@ private function check_include(): void } } - final protected function options_to_a(string $key): void + /** + * @param mixed $value + * @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 +231,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 70a16809..d90b4886 100644 --- a/lib/Serialize/XmlSerializer.php +++ b/lib/Serialize/XmlSerializer.php @@ -5,6 +5,7 @@ use function ActiveRecord\denamespace; use ActiveRecord\Model; +use function PHPStan\dumpType; /** * XML serializer. @@ -59,8 +60,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/Table.php b/lib/Table.php index 85253e5d..515dd4e3 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -507,10 +507,10 @@ 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 \DateTime) { - if (Column::DATE == ($this->columns[$name]?->type ?? null)) { + $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_string($value); From 18228226fc340677d5f034155381fe6945540cf7 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 3 Sep 2023 20:32:45 -0700 Subject: [PATCH 10/34] more stanning --- lib/Reflections.php | 8 ++------ lib/SQLBuilder.php | 11 ++++------- lib/Serialize/CsvSerializer.php | 7 +++++-- lib/Serialize/JsonSerializer.php | 6 +++--- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/Reflections.php b/lib/Reflections.php index a711ecc6..158aa3fb 100644 --- a/lib/Reflections.php +++ b/lib/Reflections.php @@ -57,7 +57,7 @@ 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 * @@ -65,11 +65,7 @@ public function destroy(string $class): void */ public function get($className) { - 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"); } /** diff --git a/lib/SQLBuilder.php b/lib/SQLBuilder.php index be5b58fb..ce346555 100644 --- a/lib/SQLBuilder.php +++ b/lib/SQLBuilder.php @@ -163,12 +163,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; @@ -256,7 +251,9 @@ 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; } /** diff --git a/lib/Serialize/CsvSerializer.php b/lib/Serialize/CsvSerializer.php index f7e46169..a4f84c7a 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($outstream !== false); 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..e0651f38 100644 --- a/lib/Serialize/JsonSerializer.php +++ b/lib/Serialize/JsonSerializer.php @@ -11,8 +11,8 @@ class JsonSerializer extends Serialization { public function to_s(): string { - $res = !empty($this->options['include_root']) ? [strtolower(get_class($this->model)) => $this->to_a()] : $this->to_a(); - - return json_encode($res); + $res = json_encode(!empty($this->options['include_root']) ? [strtolower(get_class($this->model)) => $this->to_a()] : $this->to_a()); + assert(is_string($res)); + return $res; } } From 362e3df0da9ea072b96135517e9314cb4c8a872e Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 3 Sep 2023 20:43:10 -0700 Subject: [PATCH 11/34] wip --- lib/Reflections.php | 3 +-- lib/Serialize/JsonSerializer.php | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/Reflections.php b/lib/Reflections.php index 158aa3fb..bee4b6e6 100644 --- a/lib/Reflections.php +++ b/lib/Reflections.php @@ -61,9 +61,8 @@ public function destroy(string $class): void * * @throws ActiveRecordException if class was not found * - * @return mixed null or a ReflectionClass instance */ - public function get($className) + public function get(string $className): ReflectionClass { return $this->reflections[$className] ?? throw new ActiveRecordException("Class not found: $className"); } diff --git a/lib/Serialize/JsonSerializer.php b/lib/Serialize/JsonSerializer.php index e0651f38..e611c55e 100644 --- a/lib/Serialize/JsonSerializer.php +++ b/lib/Serialize/JsonSerializer.php @@ -2,6 +2,8 @@ namespace ActiveRecord\Serialize; +use ActiveRecord\Model; + /** * JSON serializer. * @@ -9,6 +11,11 @@ */ class JsonSerializer extends Serialization { + public function __construct(Model $model, $options) + { + parent::__construct($model, $options); + } + public function to_s(): string { $res = json_encode(!empty($this->options['include_root']) ? [strtolower(get_class($this->model)) => $this->to_a()] : $this->to_a()); From e274d12e38c52d2c25a12a830849ef3cee9ae2d2 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 3 Sep 2023 20:45:30 -0700 Subject: [PATCH 12/34] fix test --- lib/Serialize/Serialization.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Serialize/Serialization.php b/lib/Serialize/Serialization.php index 4f657af3..2db7fc81 100644 --- a/lib/Serialize/Serialization.php +++ b/lib/Serialize/Serialization.php @@ -186,7 +186,8 @@ private function check_include(): void if (null === $assoc) { unset($this->attributes[$association]); } elseif (!is_array($assoc)) { - $this->attributes[$association] = new $serializer_class($assoc, $options); + $serialized = new $serializer_class($assoc, $options); + $this->attributes[$association] = $serialized->to_a(); } else { $includes = []; From 55b6f9b8f8587cfdc71283356a11e30f7d941564 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 3 Sep 2023 20:51:45 -0700 Subject: [PATCH 13/34] remove pointless reference --- lib/SQLBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SQLBuilder.php b/lib/SQLBuilder.php index ce346555..6267ccf7 100644 --- a/lib/SQLBuilder.php +++ b/lib/SQLBuilder.php @@ -381,7 +381,7 @@ private function apply_where_conditions(array $args): void // no nested array so nothing special to do $this->where = $args[0] ?? ''; - $this->where_values = &$values; + $this->where_values = $values; } } From 66c8c27c577fbb1557e99dc5aff797c0854f079e Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 3 Sep 2023 22:43:16 -0700 Subject: [PATCH 14/34] stanning for Relationships --- lib/Relationship/AbstractRelationship.php | 21 ++++++++++++++++----- lib/Relationship/BelongsTo.php | 3 +++ lib/Relationship/HasMany.php | 4 ++-- lib/Utils.php | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index 61c5fd92..61ca399f 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -36,16 +36,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. @@ -230,7 +230,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; } /** @@ -303,13 +306,21 @@ protected function unset_non_finder_options(array $options): array */ protected function set_inferred_class_name(): void { - $singularize = ($this instanceof HasMany ? true : false); + $singularize = (bool)($this instanceof HasMany); $this->set_class_name(classify($this->attribute_name, $singularize)); } + /** + * @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'])) { + /** + * @var class-string + */ $class_name = $this->options['namespace'] . '\\' . $class_name; } diff --git a/lib/Relationship/BelongsTo.php b/lib/Relationship/BelongsTo.php index f89bda9e..440decd2 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; /** diff --git a/lib/Relationship/HasMany.php b/lib/Relationship/HasMany.php index 23f7033e..ec9cfdd3 100644 --- a/lib/Relationship/HasMany.php +++ b/lib/Relationship/HasMany.php @@ -72,9 +72,9 @@ class HasMany extends AbstractRelationship ]; /** - * @var string|array + * @var array */ - protected string|array $primary_key; + protected array $primary_key; private string $through; diff --git a/lib/Utils.php b/lib/Utils.php index d41e773e..1dea3e7a 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -35,7 +35,7 @@ namespace ActiveRecord; /** - * @param class-string $class_name + * @return class-string */ function classify(string $class_name, bool $singular = false): string { From a16a4f32e6924163c92cc60284a2957414bfd08c Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 00:44:07 -0700 Subject: [PATCH 15/34] linting --- lib/Model.php | 88 +++++++++++++++++++++++---------------------- lib/Table.php | 11 +++--- lib/Validations.php | 6 ++-- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/lib/Model.php b/lib/Model.php index aa7ec6d4..720ba1e7 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -17,6 +17,7 @@ use ActiveRecord\Relationship\HasMany; use ActiveRecord\Serialize\JsonSerializer; use ActiveRecord\Serialize\Serialization; +use function PHPStan\dumpType; /** * The base class for your models. @@ -139,7 +140,7 @@ class Model /** * Array of relationship objects as model_attribute_name => relationship * - * @var array + * @var array */ private array $__relationships = []; @@ -524,7 +525,10 @@ public function __set(string $name, mixed $value): void return; } - foreach (static::$delegate as $item) { + foreach (static::$delegate as $key => $item) { + if($key == 'processed') { + continue; + } if ($delegated_name = $this->is_delegated($name, $item)) { $this->{$item['to']}->$delegated_name = $value; @@ -717,14 +721,10 @@ 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 +805,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 +951,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])) { @@ -1399,7 +1399,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(); @@ -1408,17 +1408,14 @@ public function set_relationship_from_eager_load(Model $model = null, string $na // if the related model is null and a poly then we should have an empty array if (is_null($model)) { $this->__relationships[$name] = []; - return; } $this->__relationships[$name][] = $model; - return; } $this->__relationships[$name] = $model; - return; } @@ -1435,9 +1432,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; @@ -1581,6 +1578,7 @@ public function __call($method, $args) if (($association = $table->get_relationship($association_name)) || ($association = $table->get_relationship($association_name = Utils::pluralize($association_name)))) { + // access association to ensure that the relationship has been loaded // so that we do not double-up on records if we append a newly created $this->initRelationships($association_name); @@ -1597,11 +1595,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 +1614,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 +1624,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 +1642,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 +1804,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 +1856,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 +1937,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 +2058,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 +2073,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/Table.php b/lib/Table.php index ceb5fef6..01751ff0 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -20,7 +20,7 @@ * 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 { @@ -74,11 +74,8 @@ class Table /** * A instance of CallBack for this model/table * - * @static - * - * @var object ActiveRecord\CallBack */ - public $callback; + public CallBack $callback; /** * @var array @@ -270,9 +267,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); diff --git a/lib/Validations.php b/lib/Validations.php index c7fef099..d205b471 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -551,14 +551,14 @@ public function validates_uniqueness_of($attrs): void } $options = []; } - $pk = $this->model->get_primary_key(); - $pk_value = $this->model->{$pk[0]}; + $pk = $this->model->get_primary_key(true); + $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 { From d913a61a8f9e66e465207ece05b8ef08a6f97ff9 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 00:47:04 -0700 Subject: [PATCH 16/34] linting --- lib/Model.php | 8 +++++--- lib/Reflections.php | 3 +-- lib/Relationship/AbstractRelationship.php | 3 ++- lib/SQLBuilder.php | 1 + lib/Serialize/CsvSerializer.php | 2 +- lib/Serialize/JsonSerializer.php | 1 + lib/Serialize/Serialization.php | 4 +--- lib/Serialize/XmlSerializer.php | 1 - lib/Table.php | 1 - test/InflectorTest.php | 1 - 10 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/Model.php b/lib/Model.php index 720ba1e7..8d988b48 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -17,7 +17,6 @@ use ActiveRecord\Relationship\HasMany; use ActiveRecord\Serialize\JsonSerializer; use ActiveRecord\Serialize\Serialization; -use function PHPStan\dumpType; /** * The base class for your models. @@ -526,7 +525,7 @@ public function __set(string $name, mixed $value): void } foreach (static::$delegate as $key => $item) { - if($key == 'processed') { + if ('processed' == $key) { continue; } if ($delegated_name = $this->is_delegated($name, $item)) { @@ -724,6 +723,7 @@ public function attributes(): array public function get_primary_key(): string { $pk = static::table()->pk; + return $pk[0]; } @@ -1408,14 +1408,17 @@ public function set_relationship_from_eager_load(?Model $model, string $name): v // if the related model is null and a poly then we should have an empty array if (is_null($model)) { $this->__relationships[$name] = []; + return; } $this->__relationships[$name][] = $model; + return; } $this->__relationships[$name] = $model; + return; } @@ -1578,7 +1581,6 @@ public function __call($method, $args) if (($association = $table->get_relationship($association_name)) || ($association = $table->get_relationship($association_name = Utils::pluralize($association_name)))) { - // access association to ensure that the relationship has been loaded // so that we do not double-up on records if we append a newly created $this->initRelationships($association_name); diff --git a/lib/Reflections.php b/lib/Reflections.php index bee4b6e6..10ef3be7 100644 --- a/lib/Reflections.php +++ b/lib/Reflections.php @@ -60,9 +60,8 @@ public function destroy(string $class): void * @param class-string $className Optional name of a class or an instance of the class * * @throws ActiveRecordException if class was not found - * */ - public function get(string $className): ReflectionClass + public function get(string $className): \ReflectionClass { return $this->reflections[$className] ?? throw new ActiveRecordException("Class not found: $className"); } diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index 61ca399f..f52dcfa1 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -306,12 +306,13 @@ protected function unset_non_finder_options(array $options): array */ protected function set_inferred_class_name(): void { - $singularize = (bool)($this instanceof HasMany); + $singularize = (bool) ($this instanceof HasMany); $this->set_class_name(classify($this->attribute_name, $singularize)); } /** * @param class-string $class_name + * * @throws RelationshipException * @throws \ActiveRecord\Exception\ActiveRecordException */ diff --git a/lib/SQLBuilder.php b/lib/SQLBuilder.php index 6267ccf7..3cb823af 100644 --- a/lib/SQLBuilder.php +++ b/lib/SQLBuilder.php @@ -253,6 +253,7 @@ public static function underscored_string_to_parts(string $string, int $flags=PR { $res = preg_split('/(_and_|_or_)/i', $string, -1, $flags); assert(is_array($res)); + return $res; } diff --git a/lib/Serialize/CsvSerializer.php b/lib/Serialize/CsvSerializer.php index a4f84c7a..4c55c32e 100644 --- a/lib/Serialize/CsvSerializer.php +++ b/lib/Serialize/CsvSerializer.php @@ -35,7 +35,7 @@ private function row(): string private function to_csv(array $arr): string { $outstream = fopen('php://temp', 'w'); - assert($outstream !== false); + assert(false !== $outstream); fputcsv($outstream, $arr, self::$delimiter, self::$enclosure); rewind($outstream); $contents = stream_get_contents($outstream); diff --git a/lib/Serialize/JsonSerializer.php b/lib/Serialize/JsonSerializer.php index e611c55e..266bb2c4 100644 --- a/lib/Serialize/JsonSerializer.php +++ b/lib/Serialize/JsonSerializer.php @@ -20,6 +20,7 @@ public function to_s(): string { $res = json_encode(!empty($this->options['include_root']) ? [strtolower(get_class($this->model)) => $this->to_a()] : $this->to_a()); assert(is_string($res)); + return $res; } } diff --git a/lib/Serialize/Serialization.php b/lib/Serialize/Serialization.php index 2db7fc81..1d6f5c2c 100644 --- a/lib/Serialize/Serialization.php +++ b/lib/Serialize/Serialization.php @@ -6,7 +6,6 @@ use ActiveRecord\Exception\UndefinedPropertyException; use ActiveRecord\Model; use ActiveRecord\Types; -use function PHPStan\dumpType; /** * Base class for Model serializers. @@ -147,7 +146,7 @@ private function check_methods(): void if (isset($this->options['methods'])) { 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 @@ -213,7 +212,6 @@ private function check_include(): void } /** - * @param mixed $value * @return array */ final protected function value_to_a(mixed $value): array diff --git a/lib/Serialize/XmlSerializer.php b/lib/Serialize/XmlSerializer.php index d90b4886..e9b556c8 100644 --- a/lib/Serialize/XmlSerializer.php +++ b/lib/Serialize/XmlSerializer.php @@ -5,7 +5,6 @@ use function ActiveRecord\denamespace; use ActiveRecord\Model; -use function PHPStan\dumpType; /** * XML serializer. diff --git a/lib/Table.php b/lib/Table.php index 01751ff0..310873a0 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -73,7 +73,6 @@ class Table /** * A instance of CallBack for this model/table - * */ public CallBack $callback; diff --git a/test/InflectorTest.php b/test/InflectorTest.php index 984ce707..97ba4278 100644 --- a/test/InflectorTest.php +++ b/test/InflectorTest.php @@ -7,7 +7,6 @@ class InflectorTest extends TestCase { - public function testUnderscorify() { $this->assertEquals('rm__name__bob', Inflector::variablize('rm--name bob')); From 4a056803f5cdab11f6116b6f0a62c96958a1d0ca Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 08:32:16 -0700 Subject: [PATCH 17/34] uses of get_primary_key --- lib/Model.php | 4 ++-- lib/Validations.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Model.php b/lib/Model.php index 8d988b48..4625f36d 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -519,7 +519,7 @@ 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; } @@ -631,7 +631,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]; } diff --git a/lib/Validations.php b/lib/Validations.php index d205b471..049efe35 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -551,7 +551,7 @@ public function validates_uniqueness_of($attrs): void } $options = []; } - $pk = $this->model->get_primary_key(true); + $pk = $this->model->get_primary_key(); $pk_value = $this->model->{$pk}; $fields = array_merge([$attr], $options['scope'] ?? []); From 3328a21ae4ec6d2dd813729b6959e1bc7e93e9ae Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 08:34:03 -0700 Subject: [PATCH 18/34] simplify camelize --- lib/Utils.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/Utils.php b/lib/Utils.php index 1dea3e7a..c0a1e129 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -34,18 +34,15 @@ namespace ActiveRecord; -/** - * @return class-string - */ -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::camelize($class_name); + $string = Inflector::camelize($string); - return ucfirst($class_name); + return ucfirst($string); } /** From 6ef0dd162e269035edde8bc2b5fa5db919fb6130 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 09:10:45 -0700 Subject: [PATCH 19/34] merge methods --- lib/Relationship/AbstractRelationship.php | 3 +- lib/SQLBuilder.php | 65 ++++++++++------------- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index f52dcfa1..efa39754 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -307,7 +307,8 @@ protected function unset_non_finder_options(array $options): array protected function set_inferred_class_name(): void { $singularize = (bool) ($this instanceof HasMany); - $this->set_class_name(classify($this->attribute_name, $singularize)); + $class_name = classify($this->attribute_name, $singularize); + $this->set_class_name($class_name); } /** diff --git a/lib/SQLBuilder.php b/lib/SQLBuilder.php index 3cb823af..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; } @@ -215,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; } @@ -351,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"; From 2f9d1c97dded04fbd82040da6b612e02b0b9ce58 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 09:20:51 -0700 Subject: [PATCH 20/34] remove class option --- lib/Relationship/AbstractRelationship.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index efa39754..7950f7da 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -64,14 +64,21 @@ abstract class AbstractRelationship * * @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); @@ -86,9 +93,7 @@ public function __construct(string $attribute_name, $options = []) $this->options['conditions'] = [$this->options['conditions']]; } - if (isset($this->options['class'])) { - $this->set_class_name($this->options['class']); - } elseif (isset($this->options['class_name'])) { + if (isset($this->options['class_name'])) { $this->set_class_name($this->options['class_name']); } From 6c2f3ff4237478f0c85ce83cdc765fe88f53bab4 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 10:05:38 -0700 Subject: [PATCH 21/34] stanning on dynamic extension --- .../FindDynamicMethodReturnTypeReflection.php | 7 +- test/ActiveRecordFindTest.php | 234 ++++++++++-------- 2 files changed, 142 insertions(+), 99 deletions(-) diff --git a/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php b/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php index b4e3965f..bc838d21 100644 --- a/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php +++ b/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php @@ -3,8 +3,10 @@ namespace ActiveRecord\PhpStan; use ActiveRecord\Model; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ArrayType; @@ -32,12 +34,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/test/ActiveRecordFindTest.php b/test/ActiveRecordFindTest.php index a0859875..c232cfb4 100644 --- a/test/ActiveRecordFindTest.php +++ b/test/ActiveRecordFindTest.php @@ -5,97 +5,140 @@ use ActiveRecord\Exception\RecordNotFound; use ActiveRecord\Exception\UndefinedPropertyException; use ActiveRecord\Model; +use ActiveRecord\PhpStan\FindDynamicMethodReturnTypeReflection; +use PHPStan\Command\AnalyseCommand; +use shmax\Environment; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; use test\models\Author; use test\models\HonestLawyer; use test\models\JoinBook; use test\models\Venue; +use function PHPStan\dumpType; class ActiveRecordFindTest extends DatabaseTestCase { - public function testFindWithNoParams() + public function test_find_with_no_params() { $this->expectException(RecordNotFound::class); Author::find(); } - public function testFindReturnsSingleModel() + public function test_phpstan() { - $author = Author::find(3, ['select' => 'author_id']); - $this->assertInstanceOf(Model::class, $author); + $version = "Version unknown"; + try { + $version = \Jean85\PrettyVersions::getVersion( + "phpstan/phpstan" + )->getPrettyVersion(); + } catch (\OutOfBoundsException $e) { + } + + $application = new _PHPStan_a4fa95a42\Symfony\Component\Console\Application( + "PHPStan - PHP Static Analysis Tool", + $version + ); + $application->setAutoExit(false); + $application->add(new AnalyseCommand([])); + // $application->add(new DumpDependenciesCommand()); + try { + $res = $application->run( + new \_PHPStan_a4fa95a42\Symfony\Component\Console\Input\ArgvInput( + [ + "vendor/phpstan/phpstan/bin/phpstan", + "analyse", + "--xdebug", + "--debug", + "-c", + "d:/repos/shmax/activerecord/phpstan.neon.dist", + "d:/repos/shmax/activerecord/test/phpstan/PhpStanReflectionTests.php", + ] + ) + ); + } catch (\Throwable $e) { + xdebug_break(); + } + } + + public function test_find_returns_single_model() { + $author = Author::find(3); + $this->assertInstanceOf(Model::class, $author ); $author = Author::find('3'); - $this->assertInstanceOf(Model::class, $author); + $this->assertInstanceOf(Model::class, $author ); + + $author = Author::find(["name"=>"Bill Clinton"]); + $this->assertInstanceOf(Model::class, $author ); - $author = Author::find(['name'=>'Bill Clinton']); - $this->assertInstanceOf(Model::class, $author); + $author = Author::find(["name"=>"Bill Clinton"]); + $this->assertInstanceOf(Model::class, $author ); - $author = Author::find_by_name('Bill Clinton'); - $this->assertInstanceOf(Model::class, $author); + $author = Author::find_by_name("Bill Clinton"); + $this->assertInstanceOf(Model::class, $author ); $author = Author::first(); - $this->assertInstanceOf(Model::class, $author); + $this->assertInstanceOf(Model::class, $author ); - $author = Author::find('first', ['name'=>'Bill Clinton']); - $this->assertInstanceOf(Model::class, $author); + $author = Author::find("first", ["name"=>"Bill Clinton"]); + $this->assertInstanceOf(Model::class, $author ); $author = Author::last(); - $this->assertInstanceOf(Model::class, $author); + $this->assertInstanceOf(Model::class, $author ); - $author = Author::find('last', ['name'=>'Bill Clinton']); - $this->assertInstanceOf(Model::class, $author); + $author = Author::find("last", ["name"=>"Bill Clinton"]); + $this->assertInstanceOf(Model::class, $author ); } - public function testFindReturnsArrayOfModels() + public function test_find_returns_array_of_models() { $authors = Author::all(); $this->assertIsArray($authors); - $authors = Author::find('all', ['limit' => 1]); + $authors = Author::find("all"); $this->assertIsArray($authors); - $authors = Author::find('all', ['name' => 'Bill Clinton']); + $authors = Author::find("all", ["name" => "Bill Clinton"]); $this->assertIsArray($authors); - $authors = Author::find_all_by_name('Bill Clinton'); + $authors = Author::find_all_by_name("Bill Clinton"); $this->assertIsArray($authors); - $authors = Author::find(1, 2, 3); + $authors = Author::find(1,2,3); $this->assertIsArray($authors); - $authors = Author::find([1, 2, 3]); + $authors = Author::find([1,2,3]); $this->assertIsArray($authors); - $authors = Author::find(['conditions'=> ['name' => 'Bill Clinton']]); + $authors = Author::find(["conditions"=> ["name" => "Bill Clinton"]]); $this->assertIsArray($authors); - $authors = Author::find(['conditions'=>['author_id = ?', 3]]); + $authors = Author::find(['conditions'=>["author_id = ?", 3]]); $this->assertIsArray($authors); } - public function testFindReturnsNull() - { + public function test_find_returns_null() { $lawyer = HonestLawyer::first(); $this->assertNull($lawyer); $lawyer = HonestLawyer::last(); $this->assertNull($lawyer); - $lawyer = HonestLawyer::find('first', ['name'=>'Abe']); + $lawyer = HonestLawyer::find("first", ["name"=>"Abe"]); $this->assertNull($lawyer); - $lawyer = HonestLawyer::find('last', ['name'=>'Abe']); + $lawyer = HonestLawyer::find("last", ["name"=>"Abe"]); $this->assertNull($lawyer); } - public static function noReturnValues(): array + static public function noReturnValues(): array { return [ [ -1, null, - ['first', ['name'=>'Abe']], - ['last', ['name'=>'Abe']], - ['conditions'=> ['name' => 'Bill Clinton']] + ["first", ["name"=>"Abe"]], + ["last", ["name"=>"Abe"]], + ["conditions"=> ["name" => "Bill Clinton"]] ] ]; } @@ -103,25 +146,24 @@ public static function noReturnValues(): array /** * @dataProvider noReturnValues */ - public function testFindDoesntReturn($badValue) - { + public function test_find_doesnt_return($badValue) { $this->expectException(RecordNotFound::class); Author::find($badValue); } - public function testFindByPk() + public function test_find_by_pk() { $author = Author::find(3); $this->assertEquals(3, $author->id); } - public function testFindByPknoResults() + public function test_find_by_pkno_results() { $this->expectException(RecordNotFound::class); Author::find(99999999); } - public function testFindByMultiplePkWithPartialMatch() + public function test_find_by_multiple_pk_with_partial_match() { try { Author::find(1, 999999999); @@ -131,14 +173,14 @@ public function testFindByMultiplePkWithPartialMatch() } } - public function testFindByPkWithOptions() + public function test_find_by_pk_with_options() { $author = Author::find(3, ['order' => 'name']); $this->assertEquals(3, $author->id); $this->assertTrue(false !== strpos(Author::table()->last_sql, 'ORDER BY name')); } - public function testFindByPkArray() + public function test_find_by_pk_array() { $authors = Author::find(1, '2'); $this->assertEquals(2, count($authors)); @@ -146,150 +188,150 @@ public function testFindByPkArray() $this->assertEquals(2, $authors[1]->id); } - public function testFindByPkArrayWithOptions() + public function test_find_by_pk_array_with_options() { $authors = Author::find(1, '2', ['order' => 'name']); $this->assertEquals(2, count($authors)); $this->assertTrue(false !== strpos(Author::table()->last_sql, 'ORDER BY name')); } - public function testFindNothingWithSqlInString() + public function test_find_nothing_with_sql_in_string() { $this->expectException(RecordNotFound::class); Author::first('name = 123123123'); } - public function testFindAll() + public function test_find_all() { $authors = Author::find('all', ['conditions' => ['author_id IN(?)', [1, 2, 3]]]); $this->assertTrue(count($authors) >= 3); } - public function testFindAllWithNoBindValues() + public function test_find_all_with_no_bind_values() { $authors = Author::find('all', ['conditions' => ['author_id IN(1,2,3)']]); $this->assertEquals(1, $authors[0]->author_id); } - public function testFindAllWithEmptyArrayBindValueThrowsException() + public function test_find_all_with_empty_array_bind_value_throws_exception() { $this->expectException(DatabaseException::class); $authors = Author::find('all', ['conditions' => ['author_id IN(?)', []]]); $this->assertCount(0, $authors); } - public function testFindHashUsingAlias() + public function test_find_hash_using_alias() { $venues = Venue::all(['conditions' => ['marquee' => 'Warner Theatre', 'city' => ['Washington', 'New York']]]); $this->assertTrue(count($venues) >= 1); } - public function testFindHashUsingAliasWithNull() + public function test_find_hash_using_alias_with_null() { $venues = Venue::all(['conditions' => ['marquee' => null]]); $this->assertEquals(0, count($venues)); } - public function testDynamicFinderUsingAlias() + public function test_dynamic_finder_using_alias() { $this->assertNotNull(Venue::find_by_marquee('Warner Theatre')); } - public function testFindAllHash() + public function test_find_all_hash() { $books = \test\models\Book::find('all', ['conditions' => ['author_id' => 1]]); $this->assertTrue(count($books) > 0); } - public function testFindAllHashWithOrder() + public function test_find_all_hash_with_order() { $books = \test\models\Book::find('all', ['conditions' => ['author_id' => 1], 'order' => 'name DESC']); $this->assertTrue(count($books) > 0); } - public function testFindAllNoArgs() + public function test_find_all_no_args() { $author = Author::all(); $this->assertTrue(count($author) > 1); } - public function testFindAllNoResults() + public function test_find_all_no_results() { $authors = Author::find('all', ['conditions' => ['author_id IN(11111111111,22222222222,333333333333)']]); $this->assertEquals([], $authors); } - public function testFindFirst() + public function test_find_first() { $author = Author::find('first', ['conditions' => ['author_id IN(?)', [1, 2, 3]]]); $this->assertEquals(1, $author->author_id); $this->assertEquals('Tito', $author->name); } - public function testFindFirstNoResults() + public function test_find_first_no_results() { $this->assertNull(Author::find('first', ['conditions' => 'author_id=1111111'])); } - public function testFindFirstUsingPk() + public function test_find_first_using_pk() { $author = Author::find('first', 3); $this->assertEquals(3, $author->author_id); } - public function testFindFirstWithConditionsAsString() + public function test_find_first_with_conditions_as_string() { $author = Author::find('first', ['conditions' => 'author_id=3']); $this->assertEquals(3, $author->author_id); } - public function testFindAllWithConditionsAsString() + public function test_find_all_with_conditions_as_string() { $author = Author::find('all', ['conditions' => 'author_id in(2,3)']); $this->assertEquals(2, count($author)); } - public function testFindBySql() + public function test_find_by_sql() { $author = Author::find_by_sql('SELECT * FROM authors WHERE author_id in(1,2)'); $this->assertEquals(1, $author[0]->author_id); $this->assertEquals(2, count($author)); } - public function testFindBySqltakesValuesArray() + public function test_find_by_sqltakes_values_array() { $author = Author::find_by_sql('SELECT * FROM authors WHERE author_id=?', [1]); $this->assertNotNull($author); } - public function testFindWithConditions() + public function test_find_with_conditions() { $author = Author::find('first', ['conditions' => ['author_id=? and name=?', 1, 'Tito']]); $this->assertEquals(1, $author->author_id); } - public function testFindLast() + public function test_find_last() { $author = Author::last(); $this->assertEquals(4, $author->author_id); $this->assertEquals('Uncle Bob', $author->name); } - public function testFindLastUsingStringCondition() + public function test_find_last_using_string_condition() { $author = Author::find('last', ['conditions' => 'author_id IN(1,2,3,4)']); $this->assertEquals(4, $author->author_id); $this->assertEquals('Uncle Bob', $author->name); } - public function testLimitBeforeOrder() + public function test_limit_before_order() { $authors = Author::all(['limit' => 2, 'order' => 'author_id desc', 'conditions' => 'author_id in(1,2)']); $this->assertEquals(2, $authors[0]->author_id); $this->assertEquals(1, $authors[1]->author_id); } - public function testForEach() + public function test_for_each() { $i = 0; $res = Author::all(); @@ -301,7 +343,7 @@ public function testForEach() $this->assertTrue($i > 0); } - public function testFetchAll() + public function test_fetch_all() { $i = 0; @@ -312,7 +354,7 @@ public function testFetchAll() $this->assertTrue($i > 0); } - public function testCount() + public function test_count() { $this->assertEquals(1, Author::count(1)); $this->assertEquals(2, Author::count([1, 2])); @@ -322,14 +364,14 @@ public function testCount() $this->assertEquals(1, Author::count(['name' => 'Tito', 'author_id' => 1])); } - public function testGh149EmptyCount() + public function test_gh149_empty_count() { $total = Author::count(); $this->assertEquals($total, Author::count(null)); $this->assertEquals($total, Author::count([])); } - public function testExists() + public function test_exists() { $this->assertTrue(Author::exists(1)); $this->assertTrue(Author::exists(['conditions' => 'author_id=1'])); @@ -338,7 +380,7 @@ public function testExists() $this->assertFalse(Author::exists(['conditions' => 'author_id=999999'])); } - public function testFindByCallStatic() + public function test_find_by_call_static() { $this->assertEquals('Tito', Author::find_by_name('Tito')->name); $this->assertEquals('Tito', Author::find_by_author_id_and_name(1, 'Tito')->name); @@ -346,19 +388,19 @@ public function testFindByCallStatic() $this->assertEquals('Tito', Author::find_by_name(['Tito', 'George W. Bush'], ['order' => 'name desc'])->name); } - public function testFindByCallStaticNoResults() + public function test_find_by_call_static_no_results() { $this->assertNull(Author::find_by_name('SHARKS WIT LASERZ')); $this->assertNull(Author::find_by_name_or_author_id()); } - public function testFindByCallStaticInvalidColumnName() + public function test_find_by_call_static_invalid_column_name() { $this->expectException(DatabaseException::class); Author::find_by_sharks(); } - public function testFindAllByCallStatic() + public function test_find_all_by_call_static() { $x = Author::find_all_by_name('Tito'); $this->assertEquals('Tito', $x[0]->name); @@ -369,45 +411,45 @@ public function testFindAllByCallStatic() $this->assertEquals('George W. Bush', $x[0]->name); } - public function testFindAllByCallStaticNoResults() + public function test_find_all_by_call_static_no_results() { $x = Author::find_all_by_name('SHARKSSSSSSS'); $this->assertEquals(0, count($x)); } - public function testFindAllByCallStaticWithArrayValuesAndOptions() + public function test_find_all_by_call_static_with_array_values_and_options() { $author = Author::find_all_by_name(['Tito', 'Bill Clinton'], ['order' => 'name desc']); $this->assertEquals('Tito', $author[0]->name); $this->assertEquals('Bill Clinton', $author[1]->name); } - public function testFindAllByCallStaticUndefinedMethod() + public function test_find_all_by_call_static_undefined_method() { $this->expectException(ActiveRecordException::class); Author::find_sharks('Tito'); } - public function testFindAllTakesLimitOptions() + public function test_find_all_takes_limit_options() { $authors = Author::all(['limit' => 1, 'offset' => 2, 'order' => 'name desc']); $this->assertEquals('George W. Bush', $authors[0]->name); } - public function testFindByCallStaticWithInvalidFieldName() + public function test_find_by_call_static_with_invalid_field_name() { $this->expectException(ActiveRecordException::class); Author::find_by_some_invalid_field_name('Tito'); } - public function testFindWithSelect() + public function test_find_with_select() { $author = Author::first(['select' => 'name, 123 as bubba', 'order' => 'name desc']); $this->assertEquals('Uncle Bob', $author->name); $this->assertEquals(123, $author->bubba); } - public function testFindWithSelectNonSelectedFieldsShouldNotHaveAttributes() + public function test_find_with_select_non_selected_fields_should_not_have_attributes() { $this->expectException(UndefinedPropertyException::class); $author = Author::first(['select' => 'name, 123 as bubba']); @@ -415,30 +457,28 @@ public function testFindWithSelectNonSelectedFieldsShouldNotHaveAttributes() $this->fail('expected ActiveRecord\UndefinedPropertyExecption'); } - public function testJoinsOnModelWithAssociationAndExplicitJoins() + public function test_joins_on_model_with_association_and_explicit_joins() { - JoinBook::$belongs_to = [ - 'author' => true - ]; + JoinBook::$belongs_to = [['author']]; 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); $this->assert_sql_has('LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)', JoinBook::table()->last_sql); } - public function testJoinsOnModelWithExplicitJoins() + public function test_joins_on_model_with_explicit_joins() { JoinBook::first(['joins' => ['LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)']]); $this->assert_sql_has('LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)', JoinBook::table()->last_sql); } - public function testGroup() + public function test_group() { $venues = Venue::all(['select' => 'state', 'group' => 'state']); $this->assertTrue(count($venues) > 0); $this->assert_sql_has('GROUP BY state', ActiveRecord\Table::load(Venue::class)->last_sql); } - public function testGroupWithOrderAndLimitAndHaving() + public function test_group_with_order_and_limit_and_having() { $venues = Venue::all(['select' => 'state', 'group' => 'state', 'having' => 'length(state) = 2', 'order' => 'state', 'limit' => 2]); $this->assertTrue(count($venues) > 0); @@ -446,13 +486,13 @@ public function testGroupWithOrderAndLimitAndHaving() 'SELECT state FROM venues GROUP BY state HAVING length(state) = 2 ORDER BY state', 0, 2), Venue::table()->last_sql); } - public function testEscapeQuotes() + public function test_escape_quotes() { $author = Author::find_by_name("Tito's"); $this->assertNotEquals("Tito's", Author::table()->last_sql); } - public function testFrom() + public function test_from() { $author = Author::find('first', ['from' => 'books', 'order' => 'author_id asc']); $this->assertTrue($author instanceof Author); @@ -463,7 +503,7 @@ public function testFrom() $this->assertEquals(1, $author->id); } - public function testHaving() + public function test_having() { Author::first([ 'select' => 'date(created_at) as created_at', @@ -472,13 +512,13 @@ public function testHaving() $this->assert_sql_has("GROUP BY date(created_at) HAVING date(created_at) > '2009-01-01'", Author::table()->last_sql); } - public function testFromWithInvalidTable() + public function test_from_with_invalid_table() { $this->expectException(DatabaseException::class); Author::find('first', ['from' => 'wrong_authors_table']); } - public function testFindWithHash() + public function test_find_with_hash() { $this->assertNotNull(Author::find(['name' => 'Tito'])); $this->assertNotNull(Author::find('first', ['name' => 'Tito'])); @@ -486,50 +526,50 @@ public function testFindWithHash() $this->assertEquals(1, count(Author::all(['name' => 'Tito']))); } - public function testFindOrCreateByOnExistingRecord() + public function test_find_or_create_by_on_existing_record() { $this->assertNotNull(Author::find_or_create_by_name('Tito')); } - public function testFindOrCreateByCreatesNewRecord() + public function test_find_or_create_by_creates_new_record() { $author = Author::find_or_create_by_name_and_encrypted_password('New Guy', 'pencil'); $this->assertTrue($author->author_id > 0); $this->assertEquals('pencil', $author->encrypted_password); } - public function testFindOrCreateByThrowsExceptionWhenUsingOr() + public function test_find_or_create_by_throws_exception_when_using_or() { $this->expectException(ActiveRecordException::class); Author::find_or_create_by_name_or_encrypted_password('New Guy', 'pencil'); } - public function testFindByZero() + public function test_find_by_zero() { $this->expectException(RecordNotFound::class); Author::find(0); } - public function testFindByNull() + public function test_find_by_null() { $this->expectException(RecordNotFound::class); Author::find(null); } - public function testCountBy() + public function test_count_by() { $this->assertEquals(2, Venue::count_by_state('VA')); $this->assertEquals(3, Venue::count_by_state_or_name('VA', 'Warner Theatre')); $this->assertEquals(0, Venue::count_by_state_and_name('VA', 'zzzzzzzzzzzzz')); } - public function testFindByPkShouldNotUseLimit() + public function test_find_by_pk_should_not_use_limit() { Author::find(1); $this->assert_sql_has('SELECT * FROM authors WHERE author_id=?', Author::table()->last_sql); } - public function testFindByDatetime() + public function test_find_by_datetime() { $now = new DateTime(); $arnow = new ActiveRecord\DateTime(); From b0de19dbab0f99751d8bd09f3b8ebd2887beb72d Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 11:59:51 -0700 Subject: [PATCH 22/34] start on Connection --- lib/Connection.php | 22 ++-- test/ActiveRecordFindTest.php | 234 ++++++++++++++-------------------- 2 files changed, 108 insertions(+), 148 deletions(-) diff --git a/lib/Connection.php b/lib/Connection.php index 5e6e6517..fcf22f6d 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -13,6 +13,7 @@ use ActiveRecord\Exception\DatabaseException; use Closure; use PDO; +use Psr\Log\LoggerInterface; /** * The base class for database connection adapters. @@ -43,21 +44,19 @@ abstract class Connection /** * 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 * @@ -67,9 +66,8 @@ abstract class Connection /** * 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. * @@ -186,9 +184,10 @@ protected static function load_adapter_class(string $adapter) * * @param string $connection_url A connection URL * - * @return object the parsed URL as an object + * @return \stdClass + * */ - public static function parse_connection_url(string $connection_url) + public static function parse_connection_url(string $connection_url): stdClass { $url = @parse_url($connection_url); @@ -199,8 +198,8 @@ public static function parse_connection_url(string $connection_url) $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; + $info->user = $url['user'] ?? null; + $info->pass = $url['pass'] ?? null; $allow_blank_db = ('sqlite' == $info->protocol); @@ -507,6 +506,7 @@ public function datetime_string(\DateTimeInterface $datetime): string public function string_to_datetime(string $string): ?DateTime { $date = date_create($string); + assert($date instanceof DateTimeInterface); $errors = \DateTime::getLastErrors(); if (is_array($errors) && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) { diff --git a/test/ActiveRecordFindTest.php b/test/ActiveRecordFindTest.php index c232cfb4..dd93f9b3 100644 --- a/test/ActiveRecordFindTest.php +++ b/test/ActiveRecordFindTest.php @@ -5,140 +5,97 @@ use ActiveRecord\Exception\RecordNotFound; use ActiveRecord\Exception\UndefinedPropertyException; use ActiveRecord\Model; -use ActiveRecord\PhpStan\FindDynamicMethodReturnTypeReflection; -use PHPStan\Command\AnalyseCommand; -use shmax\Environment; -use Symfony\Component\Console\Application; -use Symfony\Component\Console\Command\Command; use test\models\Author; use test\models\HonestLawyer; use test\models\JoinBook; use test\models\Venue; -use function PHPStan\dumpType; class ActiveRecordFindTest extends DatabaseTestCase { - public function test_find_with_no_params() + public function testFindWithNoParams() { $this->expectException(RecordNotFound::class); Author::find(); } - public function test_phpstan() + public function testFindReturnsSingleModel() { - $version = "Version unknown"; - try { - $version = \Jean85\PrettyVersions::getVersion( - "phpstan/phpstan" - )->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { - } - - $application = new _PHPStan_a4fa95a42\Symfony\Component\Console\Application( - "PHPStan - PHP Static Analysis Tool", - $version - ); - $application->setAutoExit(false); - $application->add(new AnalyseCommand([])); - // $application->add(new DumpDependenciesCommand()); - try { - $res = $application->run( - new \_PHPStan_a4fa95a42\Symfony\Component\Console\Input\ArgvInput( - [ - "vendor/phpstan/phpstan/bin/phpstan", - "analyse", - "--xdebug", - "--debug", - "-c", - "d:/repos/shmax/activerecord/phpstan.neon.dist", - "d:/repos/shmax/activerecord/test/phpstan/PhpStanReflectionTests.php", - ] - ) - ); - } catch (\Throwable $e) { - xdebug_break(); - } - } - - public function test_find_returns_single_model() { - $author = Author::find(3); - $this->assertInstanceOf(Model::class, $author ); + $author = Author::find(3, ['select' => 'author_id']); + $this->assertInstanceOf(Model::class, $author); $author = Author::find('3'); - $this->assertInstanceOf(Model::class, $author ); - - $author = Author::find(["name"=>"Bill Clinton"]); - $this->assertInstanceOf(Model::class, $author ); + $this->assertInstanceOf(Model::class, $author); - $author = Author::find(["name"=>"Bill Clinton"]); - $this->assertInstanceOf(Model::class, $author ); + $author = Author::find(['name'=>'Bill Clinton']); + $this->assertInstanceOf(Model::class, $author); - $author = Author::find_by_name("Bill Clinton"); - $this->assertInstanceOf(Model::class, $author ); + $author = Author::find_by_name('Bill Clinton'); + $this->assertInstanceOf(Model::class, $author); $author = Author::first(); - $this->assertInstanceOf(Model::class, $author ); + $this->assertInstanceOf(Model::class, $author); - $author = Author::find("first", ["name"=>"Bill Clinton"]); - $this->assertInstanceOf(Model::class, $author ); + $author = Author::find('first', ['name'=>'Bill Clinton']); + $this->assertInstanceOf(Model::class, $author); $author = Author::last(); - $this->assertInstanceOf(Model::class, $author ); + $this->assertInstanceOf(Model::class, $author); - $author = Author::find("last", ["name"=>"Bill Clinton"]); - $this->assertInstanceOf(Model::class, $author ); + $author = Author::find('last', ['name'=>'Bill Clinton']); + $this->assertInstanceOf(Model::class, $author); } - public function test_find_returns_array_of_models() + public function testFindReturnsArrayOfModels() { $authors = Author::all(); $this->assertIsArray($authors); - $authors = Author::find("all"); + $authors = Author::find('all', ['limit' => 1]); $this->assertIsArray($authors); - $authors = Author::find("all", ["name" => "Bill Clinton"]); + $authors = Author::find('all', ['name' => 'Bill Clinton']); $this->assertIsArray($authors); - $authors = Author::find_all_by_name("Bill Clinton"); + $authors = Author::find_all_by_name('Bill Clinton'); $this->assertIsArray($authors); - $authors = Author::find(1,2,3); + $authors = Author::find(1, 2, 3); $this->assertIsArray($authors); - $authors = Author::find([1,2,3]); + $authors = Author::find([1, 2, 3]); $this->assertIsArray($authors); - $authors = Author::find(["conditions"=> ["name" => "Bill Clinton"]]); + $authors = Author::find(['conditions'=> ['name' => 'Bill Clinton']]); $this->assertIsArray($authors); - $authors = Author::find(['conditions'=>["author_id = ?", 3]]); + $authors = Author::find(['conditions'=>['author_id = ?', 3]]); $this->assertIsArray($authors); } - public function test_find_returns_null() { + public function testFindReturnsNull() + { $lawyer = HonestLawyer::first(); $this->assertNull($lawyer); $lawyer = HonestLawyer::last(); $this->assertNull($lawyer); - $lawyer = HonestLawyer::find("first", ["name"=>"Abe"]); + $lawyer = HonestLawyer::find('first', ['name'=>'Abe']); $this->assertNull($lawyer); - $lawyer = HonestLawyer::find("last", ["name"=>"Abe"]); + $lawyer = HonestLawyer::find('last', ['name'=>'Abe']); $this->assertNull($lawyer); } - static public function noReturnValues(): array + public static function noReturnValues(): array { return [ [ -1, null, - ["first", ["name"=>"Abe"]], - ["last", ["name"=>"Abe"]], - ["conditions"=> ["name" => "Bill Clinton"]] + ['first', ['name'=>'Abe']], + ['last', ['name'=>'Abe']], + ['conditions'=> ['name' => 'Bill Clinton']] ] ]; } @@ -146,24 +103,25 @@ static public function noReturnValues(): array /** * @dataProvider noReturnValues */ - public function test_find_doesnt_return($badValue) { + public function testFindDoesntReturn($badValue) + { $this->expectException(RecordNotFound::class); Author::find($badValue); } - public function test_find_by_pk() + public function testFindByPk() { $author = Author::find(3); $this->assertEquals(3, $author->id); } - public function test_find_by_pkno_results() + public function testFindByPknoResults() { $this->expectException(RecordNotFound::class); Author::find(99999999); } - public function test_find_by_multiple_pk_with_partial_match() + public function testFindByMultiplePkWithPartialMatch() { try { Author::find(1, 999999999); @@ -173,14 +131,14 @@ public function test_find_by_multiple_pk_with_partial_match() } } - public function test_find_by_pk_with_options() + public function testFindByPkWithOptions() { $author = Author::find(3, ['order' => 'name']); $this->assertEquals(3, $author->id); $this->assertTrue(false !== strpos(Author::table()->last_sql, 'ORDER BY name')); } - public function test_find_by_pk_array() + public function testFindByPkArray() { $authors = Author::find(1, '2'); $this->assertEquals(2, count($authors)); @@ -188,150 +146,150 @@ public function test_find_by_pk_array() $this->assertEquals(2, $authors[1]->id); } - public function test_find_by_pk_array_with_options() + public function testFindByPkArrayWithOptions() { $authors = Author::find(1, '2', ['order' => 'name']); $this->assertEquals(2, count($authors)); $this->assertTrue(false !== strpos(Author::table()->last_sql, 'ORDER BY name')); } - public function test_find_nothing_with_sql_in_string() + public function testFindNothingWithSqlInString() { $this->expectException(RecordNotFound::class); Author::first('name = 123123123'); } - public function test_find_all() + public function testFindAll() { $authors = Author::find('all', ['conditions' => ['author_id IN(?)', [1, 2, 3]]]); $this->assertTrue(count($authors) >= 3); } - public function test_find_all_with_no_bind_values() + public function testFindAllWithNoBindValues() { $authors = Author::find('all', ['conditions' => ['author_id IN(1,2,3)']]); $this->assertEquals(1, $authors[0]->author_id); } - public function test_find_all_with_empty_array_bind_value_throws_exception() + public function testFindAllWithEmptyArrayBindValueThrowsException() { $this->expectException(DatabaseException::class); $authors = Author::find('all', ['conditions' => ['author_id IN(?)', []]]); $this->assertCount(0, $authors); } - public function test_find_hash_using_alias() + public function testFindHashUsingAlias() { $venues = Venue::all(['conditions' => ['marquee' => 'Warner Theatre', 'city' => ['Washington', 'New York']]]); $this->assertTrue(count($venues) >= 1); } - public function test_find_hash_using_alias_with_null() + public function testFindHashUsingAliasWithNull() { $venues = Venue::all(['conditions' => ['marquee' => null]]); $this->assertEquals(0, count($venues)); } - public function test_dynamic_finder_using_alias() + public function testDynamicFinderUsingAlias() { $this->assertNotNull(Venue::find_by_marquee('Warner Theatre')); } - public function test_find_all_hash() + public function testFindAllHash() { $books = \test\models\Book::find('all', ['conditions' => ['author_id' => 1]]); $this->assertTrue(count($books) > 0); } - public function test_find_all_hash_with_order() + public function testFindAllHashWithOrder() { $books = \test\models\Book::find('all', ['conditions' => ['author_id' => 1], 'order' => 'name DESC']); $this->assertTrue(count($books) > 0); } - public function test_find_all_no_args() + public function testFindAllNoArgs() { $author = Author::all(); $this->assertTrue(count($author) > 1); } - public function test_find_all_no_results() + public function testFindAllNoResults() { $authors = Author::find('all', ['conditions' => ['author_id IN(11111111111,22222222222,333333333333)']]); $this->assertEquals([], $authors); } - public function test_find_first() + public function testFindFirst() { $author = Author::find('first', ['conditions' => ['author_id IN(?)', [1, 2, 3]]]); $this->assertEquals(1, $author->author_id); $this->assertEquals('Tito', $author->name); } - public function test_find_first_no_results() + public function testFindFirstNoResults() { $this->assertNull(Author::find('first', ['conditions' => 'author_id=1111111'])); } - public function test_find_first_using_pk() + public function testFindFirstUsingPk() { $author = Author::find('first', 3); $this->assertEquals(3, $author->author_id); } - public function test_find_first_with_conditions_as_string() + public function testFindFirstWithConditionsAsString() { $author = Author::find('first', ['conditions' => 'author_id=3']); $this->assertEquals(3, $author->author_id); } - public function test_find_all_with_conditions_as_string() + public function testFindAllWithConditionsAsString() { $author = Author::find('all', ['conditions' => 'author_id in(2,3)']); $this->assertEquals(2, count($author)); } - public function test_find_by_sql() + public function testFindBySql() { $author = Author::find_by_sql('SELECT * FROM authors WHERE author_id in(1,2)'); $this->assertEquals(1, $author[0]->author_id); $this->assertEquals(2, count($author)); } - public function test_find_by_sqltakes_values_array() + public function testFindBySqltakesValuesArray() { $author = Author::find_by_sql('SELECT * FROM authors WHERE author_id=?', [1]); $this->assertNotNull($author); } - public function test_find_with_conditions() + public function testFindWithConditions() { $author = Author::find('first', ['conditions' => ['author_id=? and name=?', 1, 'Tito']]); $this->assertEquals(1, $author->author_id); } - public function test_find_last() + public function testFindLast() { $author = Author::last(); $this->assertEquals(4, $author->author_id); $this->assertEquals('Uncle Bob', $author->name); } - public function test_find_last_using_string_condition() + public function testFindLastUsingStringCondition() { $author = Author::find('last', ['conditions' => 'author_id IN(1,2,3,4)']); $this->assertEquals(4, $author->author_id); $this->assertEquals('Uncle Bob', $author->name); } - public function test_limit_before_order() + public function testLimitBeforeOrder() { $authors = Author::all(['limit' => 2, 'order' => 'author_id desc', 'conditions' => 'author_id in(1,2)']); $this->assertEquals(2, $authors[0]->author_id); $this->assertEquals(1, $authors[1]->author_id); } - public function test_for_each() + public function testForEach() { $i = 0; $res = Author::all(); @@ -343,7 +301,7 @@ public function test_for_each() $this->assertTrue($i > 0); } - public function test_fetch_all() + public function testFetchAll() { $i = 0; @@ -354,7 +312,7 @@ public function test_fetch_all() $this->assertTrue($i > 0); } - public function test_count() + public function testCount() { $this->assertEquals(1, Author::count(1)); $this->assertEquals(2, Author::count([1, 2])); @@ -364,14 +322,14 @@ public function test_count() $this->assertEquals(1, Author::count(['name' => 'Tito', 'author_id' => 1])); } - public function test_gh149_empty_count() + public function testGh149EmptyCount() { $total = Author::count(); $this->assertEquals($total, Author::count(null)); $this->assertEquals($total, Author::count([])); } - public function test_exists() + public function testExists() { $this->assertTrue(Author::exists(1)); $this->assertTrue(Author::exists(['conditions' => 'author_id=1'])); @@ -380,7 +338,7 @@ public function test_exists() $this->assertFalse(Author::exists(['conditions' => 'author_id=999999'])); } - public function test_find_by_call_static() + public function testFindByCallStatic() { $this->assertEquals('Tito', Author::find_by_name('Tito')->name); $this->assertEquals('Tito', Author::find_by_author_id_and_name(1, 'Tito')->name); @@ -388,19 +346,19 @@ public function test_find_by_call_static() $this->assertEquals('Tito', Author::find_by_name(['Tito', 'George W. Bush'], ['order' => 'name desc'])->name); } - public function test_find_by_call_static_no_results() + public function testFindByCallStaticNoResults() { $this->assertNull(Author::find_by_name('SHARKS WIT LASERZ')); $this->assertNull(Author::find_by_name_or_author_id()); } - public function test_find_by_call_static_invalid_column_name() + public function testFindByCallStaticInvalidColumnName() { $this->expectException(DatabaseException::class); Author::find_by_sharks(); } - public function test_find_all_by_call_static() + public function testFindAllByCallStatic() { $x = Author::find_all_by_name('Tito'); $this->assertEquals('Tito', $x[0]->name); @@ -411,45 +369,45 @@ public function test_find_all_by_call_static() $this->assertEquals('George W. Bush', $x[0]->name); } - public function test_find_all_by_call_static_no_results() + public function testFindAllByCallStaticNoResults() { $x = Author::find_all_by_name('SHARKSSSSSSS'); $this->assertEquals(0, count($x)); } - public function test_find_all_by_call_static_with_array_values_and_options() + public function testFindAllByCallStaticWithArrayValuesAndOptions() { $author = Author::find_all_by_name(['Tito', 'Bill Clinton'], ['order' => 'name desc']); $this->assertEquals('Tito', $author[0]->name); $this->assertEquals('Bill Clinton', $author[1]->name); } - public function test_find_all_by_call_static_undefined_method() + public function testFindAllByCallStaticUndefinedMethod() { $this->expectException(ActiveRecordException::class); Author::find_sharks('Tito'); } - public function test_find_all_takes_limit_options() + public function testFindAllTakesLimitOptions() { $authors = Author::all(['limit' => 1, 'offset' => 2, 'order' => 'name desc']); $this->assertEquals('George W. Bush', $authors[0]->name); } - public function test_find_by_call_static_with_invalid_field_name() + public function testFindByCallStaticWithInvalidFieldName() { $this->expectException(ActiveRecordException::class); Author::find_by_some_invalid_field_name('Tito'); } - public function test_find_with_select() + public function testFindWithSelect() { $author = Author::first(['select' => 'name, 123 as bubba', 'order' => 'name desc']); $this->assertEquals('Uncle Bob', $author->name); $this->assertEquals(123, $author->bubba); } - public function test_find_with_select_non_selected_fields_should_not_have_attributes() + public function testFindWithSelectNonSelectedFieldsShouldNotHaveAttributes() { $this->expectException(UndefinedPropertyException::class); $author = Author::first(['select' => 'name, 123 as bubba']); @@ -457,28 +415,30 @@ public function test_find_with_select_non_selected_fields_should_not_have_attrib $this->fail('expected ActiveRecord\UndefinedPropertyExecption'); } - public function test_joins_on_model_with_association_and_explicit_joins() + public function testJoinsOnModelWithAssociationAndExplicitJoins() { - JoinBook::$belongs_to = [['author']]; + JoinBook::$belongs_to = [ + '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); $this->assert_sql_has('LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)', JoinBook::table()->last_sql); } - public function test_joins_on_model_with_explicit_joins() + public function testJoinsOnModelWithExplicitJoins() { JoinBook::first(['joins' => ['LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)']]); $this->assert_sql_has('LEFT JOIN authors a ON(books.secondary_author_id=a.author_id)', JoinBook::table()->last_sql); } - public function test_group() + public function testGroup() { $venues = Venue::all(['select' => 'state', 'group' => 'state']); $this->assertTrue(count($venues) > 0); $this->assert_sql_has('GROUP BY state', ActiveRecord\Table::load(Venue::class)->last_sql); } - public function test_group_with_order_and_limit_and_having() + public function testGroupWithOrderAndLimitAndHaving() { $venues = Venue::all(['select' => 'state', 'group' => 'state', 'having' => 'length(state) = 2', 'order' => 'state', 'limit' => 2]); $this->assertTrue(count($venues) > 0); @@ -486,13 +446,13 @@ public function test_group_with_order_and_limit_and_having() 'SELECT state FROM venues GROUP BY state HAVING length(state) = 2 ORDER BY state', 0, 2), Venue::table()->last_sql); } - public function test_escape_quotes() + public function testEscapeQuotes() { $author = Author::find_by_name("Tito's"); $this->assertNotEquals("Tito's", Author::table()->last_sql); } - public function test_from() + public function testFrom() { $author = Author::find('first', ['from' => 'books', 'order' => 'author_id asc']); $this->assertTrue($author instanceof Author); @@ -503,7 +463,7 @@ public function test_from() $this->assertEquals(1, $author->id); } - public function test_having() + public function testHaving() { Author::first([ 'select' => 'date(created_at) as created_at', @@ -512,13 +472,13 @@ public function test_having() $this->assert_sql_has("GROUP BY date(created_at) HAVING date(created_at) > '2009-01-01'", Author::table()->last_sql); } - public function test_from_with_invalid_table() + public function testFromWithInvalidTable() { $this->expectException(DatabaseException::class); Author::find('first', ['from' => 'wrong_authors_table']); } - public function test_find_with_hash() + public function testFindWithHash() { $this->assertNotNull(Author::find(['name' => 'Tito'])); $this->assertNotNull(Author::find('first', ['name' => 'Tito'])); @@ -526,50 +486,50 @@ public function test_find_with_hash() $this->assertEquals(1, count(Author::all(['name' => 'Tito']))); } - public function test_find_or_create_by_on_existing_record() + public function testFindOrCreateByOnExistingRecord() { $this->assertNotNull(Author::find_or_create_by_name('Tito')); } - public function test_find_or_create_by_creates_new_record() + public function testFindOrCreateByCreatesNewRecord() { $author = Author::find_or_create_by_name_and_encrypted_password('New Guy', 'pencil'); $this->assertTrue($author->author_id > 0); $this->assertEquals('pencil', $author->encrypted_password); } - public function test_find_or_create_by_throws_exception_when_using_or() + public function testFindOrCreateByThrowsExceptionWhenUsingOr() { $this->expectException(ActiveRecordException::class); Author::find_or_create_by_name_or_encrypted_password('New Guy', 'pencil'); } - public function test_find_by_zero() + public function testFindByZero() { $this->expectException(RecordNotFound::class); Author::find(0); } - public function test_find_by_null() + public function testFindByNull() { $this->expectException(RecordNotFound::class); Author::find(null); } - public function test_count_by() + public function testCountBy() { $this->assertEquals(2, Venue::count_by_state('VA')); $this->assertEquals(3, Venue::count_by_state_or_name('VA', 'Warner Theatre')); $this->assertEquals(0, Venue::count_by_state_and_name('VA', 'zzzzzzzzzzzzz')); } - public function test_find_by_pk_should_not_use_limit() + public function testFindByPkShouldNotUseLimit() { Author::find(1); $this->assert_sql_has('SELECT * FROM authors WHERE author_id=?', Author::table()->last_sql); } - public function test_find_by_datetime() + public function testFindByDatetime() { $now = new DateTime(); $arnow = new ActiveRecord\DateTime(); From 6e8bd6021f694731f2d760823bfb58f516729216 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 13:16:00 -0700 Subject: [PATCH 23/34] fix squeeze --- lib/Adapter/SqliteAdapter.php | 10 ++-- lib/Connection.php | 102 ++++++++++++++++++++-------------- lib/Utils.php | 9 ++- test/ConnectionTest.php | 30 +++++----- 4 files changed, 84 insertions(+), 67 deletions(-) diff --git a/lib/Adapter/SqliteAdapter.php b/lib/Adapter/SqliteAdapter.php index d3a7444d..8584d25a 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) diff --git a/lib/Connection.php b/lib/Connection.php index fcf22f6d..21f1db64 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -18,7 +18,15 @@ /** * 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 { @@ -124,16 +132,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); @@ -160,6 +169,7 @@ protected static function load_adapter_class(string $adapter) } require_once $source; + assert(class_exists($fqclass)); return $fqclass; } @@ -184,27 +194,27 @@ protected static function load_adapter_class(string $adapter) * * @param string $connection_url A connection URL * - * @return \stdClass + * @return ConnectionInfo * */ - public static function parse_connection_url(string $connection_url): stdClass + 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 = $url['user'] ?? null; - $info->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\((.+)\)\/?().*$/'; @@ -213,29 +223,25 @@ public static function parse_connection_url(string $connection_url): stdClass } 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); } } @@ -244,33 +250,44 @@ public static function parse_connection_url(string $connection_url): stdClass 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); } @@ -506,13 +523,14 @@ public function datetime_string(\DateTimeInterface $datetime): string public function string_to_datetime(string $string): ?DateTime { $date = date_create($string); - assert($date instanceof DateTimeInterface); $errors = \DateTime::getLastErrors(); if (is_array($errors) && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) { return null; } + assert($date instanceof \DateTime); + $date_class = Config::instance()->get_date_class(); return $date_class::createFromFormat( diff --git a/lib/Utils.php b/lib/Utils.php index c0a1e129..af0585b1 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -365,11 +365,10 @@ 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/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']); } } From 5208fac820f659eb1a7c0fbb944a2ad954470d13 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 13:18:27 -0700 Subject: [PATCH 24/34] linting --- lib/Adapter/SqliteAdapter.php | 4 ++-- lib/Connection.php | 14 +++++--------- .../FindDynamicMethodReturnTypeReflection.php | 3 +-- lib/Utils.php | 1 + 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/Adapter/SqliteAdapter.php b/lib/Adapter/SqliteAdapter.php index 8584d25a..b23be72c 100644 --- a/lib/Adapter/SqliteAdapter.php +++ b/lib/Adapter/SqliteAdapter.php @@ -24,9 +24,9 @@ class SqliteAdapter extends Connection protected function __construct(array $info) { if (!file_exists($info['host'])) { - throw new ConnectionException("Could not find sqlite db: " . $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) diff --git a/lib/Connection.php b/lib/Connection.php index 21f1db64..7411eb4f 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -51,18 +51,14 @@ abstract class Connection public string $last_query; /** * Switch for logging. - * */ private bool $logging = false; /** * Contains a Logger object that must implement a log() method. - * */ private LoggerInterface $logger; /** * The name of the protocol that is used. - * - * @var string */ public string $protocol; /** @@ -73,7 +69,6 @@ abstract class Connection public static $date_format = 'Y-m-d'; /** * Database's datetime format - * */ public static string $datetime_format = 'Y-m-d H:i:s'; /** @@ -170,6 +165,7 @@ protected static function load_adapter_class(string $adapter) require_once $source; assert(class_exists($fqclass)); + return $fqclass; } @@ -195,7 +191,6 @@ protected static function load_adapter_class(string $adapter) * @param string $connection_url A connection URL * * @return ConnectionInfo - * */ public static function parse_connection_url(string $connection_url): array { @@ -270,6 +265,7 @@ public static function parse_connection_url(string $connection_url): array /** * Class Connection is a singleton. Access it via instance(). + * * @param ConnectionInfo $info */ protected function __construct(array $info) @@ -277,13 +273,13 @@ protected function __construct(array $info) try { // unix sockets start with a / if ('/' != $info['host'][0]) { - $host = "host=" . $info['host']; + $host = 'host=' . $info['host']; if (isset($info['port'])) { - $host .= ";port=" . $info['port']; + $host .= ';port=' . $info['port']; } } else { - $host = "unix_socket=" . $info['host']; + $host = 'unix_socket=' . $info['host']; } $dsn = $info['protocol'] . ":$host;dbname=" . $info['db']; diff --git a/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php b/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php index bc838d21..6ff7f7f8 100644 --- a/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php +++ b/lib/PhpStan/FindDynamicMethodReturnTypeReflection.php @@ -4,7 +4,6 @@ use ActiveRecord\Model; use PhpParser\Node\Arg; -use PhpParser\Node\Expr; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; @@ -34,7 +33,7 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type { - assert($methodCall->class instanceof Name ); + assert($methodCall->class instanceof Name); $subclass = $methodCall->class->toString(); $args = $methodCall->args; diff --git a/lib/Utils.php b/lib/Utils.php index af0585b1..2c1bc9ca 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -369,6 +369,7 @@ public static function squeeze(string $char, string $string): string { $res = preg_replace("/$char+/", $char, $string); assert(is_string($res)); + return $res; } } From bd5491a31d6c72c8207c1445bf14f8a74ce17bee Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 13:33:35 -0700 Subject: [PATCH 25/34] stanning for Cache --- lib/Cache.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/Cache.php b/lib/Cache.php index 27bac538..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::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 From 3b055a8e4110e5227bed8979a9974ec19fb7cb02 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 13:42:48 -0700 Subject: [PATCH 26/34] stanning for Callback --- lib/Adapter/MysqlAdapter.php | 2 +- lib/CallBack.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Adapter/MysqlAdapter.php b/lib/Adapter/MysqlAdapter.php index d98badfd..11758da9 100644 --- a/lib/Adapter/MysqlAdapter.php +++ b/lib/Adapter/MysqlAdapter.php @@ -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/CallBack.php b/lib/CallBack.php index 3826cdca..6d03a001 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 array of callbacks or empty array if invalid callback name */ public function get_callbacks(string $name): array { From ad81af3e429abd40290e0c044bc3a2f737ccce59 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 13:44:54 -0700 Subject: [PATCH 27/34] fix column --- lib/Column.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Column.php b/lib/Column.php index 2555757e..005a8713 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -130,6 +130,7 @@ public static function castIntegerSafely($value): string|int } // It's just a decimal number + /* @phpstan-ignore-next-line */ elseif (is_numeric($value) && floor($value) != $value) { return (int) $value; } From 6f162ffcc682760a4fbbf16e1e23b2b049d089ee Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 13:46:24 -0700 Subject: [PATCH 28/34] linting --- lib/CallBack.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/CallBack.php b/lib/CallBack.php index 6d03a001..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 empty array 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 { From 9c8d4ca4aa5fed8839f4ca06b113e7d7af07f2cd Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 13:51:02 -0700 Subject: [PATCH 29/34] better fix for Column --- lib/Column.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Column.php b/lib/Column.php index 005a8713..1eaecf68 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -130,8 +130,7 @@ public static function castIntegerSafely($value): string|int } // It's just a decimal number - /* @phpstan-ignore-next-line */ - elseif (is_numeric($value) && floor($value) != $value) { + elseif (is_float($value) && floor($value) != $value) { return (int) $value; } From 2708b935d5b9b515cad4fa5b6f23643aee3a1530 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 14:49:15 -0700 Subject: [PATCH 30/34] finish model --- lib/Model.php | 14 ++++++++------ lib/Relationship/AbstractRelationship.php | 15 ++------------- lib/Relationship/HasAndBelongsToMany.php | 5 +++++ lib/Relationship/HasMany.php | 7 ++++++- lib/Relationship/HasOne.php | 4 ++++ test/RelationshipTest.php | 2 +- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/Model.php b/lib/Model.php index 4625f36d..3d7eb022 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 = []; @@ -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; } @@ -662,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 { @@ -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)) { @@ -1408,11 +1410,11 @@ public function set_relationship_from_eager_load(?Model $model, string $name): v // if the related model is null and a poly then we should have an empty array if (is_null($model)) { $this->__relationships[$name] = []; - return; } - $this->__relationships[$name][] = $model; + assert(is_array($this->__relationships[$name])); + array_push($this->__relationships[$name], $model); return; } diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index 7950f7da..5f80ca1a 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -54,11 +54,6 @@ abstract class AbstractRelationship */ protected array $options = []; - /** - * Is the relationship single or multi. - */ - protected bool $poly_relationship = false; - /** * List of valid options for relationships. * @@ -83,12 +78,6 @@ 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']]; } @@ -121,7 +110,7 @@ abstract public function load_eagerly(array $models, array $attributes, array $i */ public function is_poly(): bool { - return $this->poly_relationship; + return false; } /** @@ -260,7 +249,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; 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 ec9cfdd3..ee7dfa5d 100644 --- a/lib/Relationship/HasMany.php +++ b/lib/Relationship/HasMany.php @@ -78,6 +78,11 @@ class HasMany extends AbstractRelationship private string $through; + public function is_poly(): bool + { + return true; + } + /** * Constructs a {@link HasMany} relationship. * @@ -168,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; } 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/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() From 1bbd1bd0c44002f901aa27e3aef3a81122f02cce Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 14:54:26 -0700 Subject: [PATCH 31/34] linting --- lib/Model.php | 1 + lib/Relationship/AbstractRelationship.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model.php b/lib/Model.php index 3d7eb022..c05d8de0 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -1410,6 +1410,7 @@ public function set_relationship_from_eager_load(?Model $model, string $name): v // if the related model is null and a poly then we should have an empty array if (is_null($model)) { $this->__relationships[$name] = []; + return; } diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index 5f80ca1a..a0d03555 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; From 41efd77dcaef05c9ff4af4452c4e31cd0871a43d Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 15:46:04 -0700 Subject: [PATCH 32/34] linting --- lib/Model.php | 2 +- lib/Reflections.php | 1 + lib/Relationship/AbstractRelationship.php | 28 +++++++++++++++-------- lib/Utils.php | 3 ++- phpstan.neon.dist | 1 + 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/Model.php b/lib/Model.php index c05d8de0..51a53559 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -1413,7 +1413,7 @@ public function set_relationship_from_eager_load(?Model $model, string $name): v return; } - + $this->__relationships[$name] ??= []; assert(is_array($this->__relationships[$name])); array_push($this->__relationships[$name], $model); diff --git a/lib/Reflections.php b/lib/Reflections.php index 10ef3be7..224073b6 100644 --- a/lib/Reflections.php +++ b/lib/Reflections.php @@ -34,6 +34,7 @@ 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); } diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index a0d03555..75c6bfbf 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -82,7 +82,7 @@ public function __construct(string $attribute_name, array $options = []) } if (isset($this->options['class_name'])) { - $this->set_class_name($this->options['class_name']); + $this->set_class_name($this->inferred_class_name($this->options['class_name'])); } $this->attribute_name = strtolower(Inflector::variablize($this->attribute_name)); @@ -299,11 +299,26 @@ protected function unset_non_finder_options(array $options): array */ protected function set_inferred_class_name(): void { - $singularize = (bool) ($this instanceof HasMany); - $class_name = classify($this->attribute_name, $singularize); + $class_name = $this->inferred_class_name($this->attribute_name); $this->set_class_name($class_name); } + /** + * @param string $name + * @return class-string + */ + protected function inferred_class_name(string $name): string { + 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; + } + + assert(class_exists($name)); + return $name; + } + /** * @param class-string $class_name * @@ -312,13 +327,6 @@ protected function set_inferred_class_name(): void */ protected function set_class_name(string $class_name): void { - if (!has_absolute_namespace($class_name) && isset($this->options['namespace'])) { - /** - * @var class-string - */ - $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/Utils.php b/lib/Utils.php index 2c1bc9ca..72dfb3cc 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -42,7 +42,8 @@ function classify(string $string, bool $singular = false): string $string = Inflector::camelize($string); - return ucfirst($string); + $res = ucfirst($string); + return $res; } /** diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a19668ee..f421b5f0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,6 +6,7 @@ parameters: - %currentWorkingDirectory%/test/phpstan reportUnmatchedIgnoredErrors: false + checkGenericClassInNonGenericObjectType: false ignoreErrors: services: From c6abfadfb5bf790516a23837bf6161a930b236a0 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 15:48:51 -0700 Subject: [PATCH 33/34] linting --- lib/Relationship/AbstractRelationship.php | 5 +++-- lib/Utils.php | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index 75c6bfbf..b0ca86da 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -304,10 +304,10 @@ protected function set_inferred_class_name(): void } /** - * @param string $name * @return class-string */ - protected function inferred_class_name(string $name): string { + protected function inferred_class_name(string $name): string + { if (!has_absolute_namespace($name) && isset($this->options['namespace'])) { if (!isset($this->options['class_name'])) { $name = classify($name, $this instanceof HasMany); @@ -316,6 +316,7 @@ protected function inferred_class_name(string $name): string { } assert(class_exists($name)); + return $name; } diff --git a/lib/Utils.php b/lib/Utils.php index 72dfb3cc..5718f3ee 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -43,6 +43,7 @@ function classify(string $string, bool $singular = false): string $string = Inflector::camelize($string); $res = ucfirst($string); + return $res; } From beaf06d080180e58b18f8fd86d2c21e4bc1b0920 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 4 Sep 2023 16:03:13 -0700 Subject: [PATCH 34/34] fix tests --- lib/Relationship/AbstractRelationship.php | 17 +++-------------- lib/Relationship/BelongsTo.php | 4 +++- lib/Relationship/HasMany.php | 4 ++-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/lib/Relationship/AbstractRelationship.php b/lib/Relationship/AbstractRelationship.php index b0ca86da..fcc4eaef 100644 --- a/lib/Relationship/AbstractRelationship.php +++ b/lib/Relationship/AbstractRelationship.php @@ -290,19 +290,6 @@ protected function unset_non_finder_options(array $options): array return $options; } - /** - * 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 - */ - protected function set_inferred_class_name(): void - { - $class_name = $this->inferred_class_name($this->attribute_name); - $this->set_class_name($class_name); - } - /** * @return class-string */ @@ -315,7 +302,9 @@ protected function inferred_class_name(string $name): string $name = $this->options['namespace'] . '\\' . $name; } - assert(class_exists($name)); + if (!class_exists($name)) { + throw new \ReflectionException('Unknown class name: ' . $name); + } return $name; } diff --git a/lib/Relationship/BelongsTo.php b/lib/Relationship/BelongsTo.php index 440decd2..297dfd36 100644 --- a/lib/Relationship/BelongsTo.php +++ b/lib/Relationship/BelongsTo.php @@ -66,7 +66,9 @@ 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 diff --git a/lib/Relationship/HasMany.php b/lib/Relationship/HasMany.php index ee7dfa5d..7821bad2 100644 --- a/lib/Relationship/HasMany.php +++ b/lib/Relationship/HasMany.php @@ -96,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'])); } } @@ -105,7 +105,7 @@ 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)); } }