diff --git a/composer.json b/composer.json index 079dd4e..437a5d3 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "Squire\\AirlinesServiceProvider", "Squire\\CountriesServiceProvider", "Squire\\RegionsEnServiceProvider", + "Squire\\ModelServiceProvider", "Squire\\GbCountiesEnServiceProvider", "Squire\\AirlinesEnServiceProvider", "Squire\\CountriesEnServiceProvider", diff --git a/packages/airlines/src/Models/Airline.php b/packages/airlines/src/Models/Airline.php index ad15ee5..e2358cc 100644 --- a/packages/airlines/src/Models/Airline.php +++ b/packages/airlines/src/Models/Airline.php @@ -6,6 +6,16 @@ class Airline extends Model { + public static $schema = [ + 'id' => 'string', + 'alias' => 'string', + 'call_sign' => 'string', + 'code_iata' => 'string', + 'code_icao' => 'string', + 'country_id' => 'string', + 'name' => 'string', + ]; + public function country() { return $this->belongsTo(Country::class); diff --git a/packages/airports/src/Models/Airport.php b/packages/airports/src/Models/Airport.php index 75df2be..6bb8d98 100644 --- a/packages/airports/src/Models/Airport.php +++ b/packages/airports/src/Models/Airport.php @@ -6,6 +6,17 @@ class Airport extends Model { + public static $schema = [ + 'id' => 'string', + 'code_gps' => 'string', + 'code_iata' => 'string', + 'code_local' => 'string', + 'municipality' => 'string', + 'name' => 'string', + 'region_id' => 'string', + 'type' => 'string', + ]; + public function country() { return $this->hasOneThrough(Country::class, Region::class); diff --git a/packages/continents/src/Models/Continent.php b/packages/continents/src/Models/Continent.php index 5080e33..7571b55 100644 --- a/packages/continents/src/Models/Continent.php +++ b/packages/continents/src/Models/Continent.php @@ -6,6 +6,12 @@ class Continent extends Model { + public static $schema = [ + 'id' => 'string', + 'code' => 'string', + 'name' => 'string', + ]; + public function countries() { return $this->hasMany(Country::class); diff --git a/packages/countries/src/Models/Country.php b/packages/countries/src/Models/Country.php index b6ef7e0..5988d19 100644 --- a/packages/countries/src/Models/Country.php +++ b/packages/countries/src/Models/Country.php @@ -6,8 +6,16 @@ class Country extends Model { - protected $schema = [ + public static $schema = [ + 'id' => 'string', 'calling_code' => 'string', + 'capital_city' => 'string', + 'code_2' => 'string', + 'code_3' => 'string', + 'continent_id' => 'string', + 'currency_id' => 'string', + 'flag' => 'string', + 'name' => 'string', ]; public function airlines() diff --git a/packages/currencies/src/Models/Currency.php b/packages/currencies/src/Models/Currency.php index b4c322f..917bf48 100644 --- a/packages/currencies/src/Models/Currency.php +++ b/packages/currencies/src/Models/Currency.php @@ -6,6 +6,18 @@ class Currency extends Model { + public static $schema = [ + 'id' => 'string', + 'code_alphabetic' => 'string', + 'code_numeric' => 'integer', + 'decimal_digits' => 'integer', + 'name' => 'string', + 'name_plural' => 'string', + 'rounding' => 'integer', + 'symbol' => 'string', + 'symbol_native' => 'string', + ]; + public function countries() { return $this->hasMany(Country::class); diff --git a/packages/gb-counties/src/Models/GbCounty.php b/packages/gb-counties/src/Models/GbCounty.php index 432adbd..947ef98 100644 --- a/packages/gb-counties/src/Models/GbCounty.php +++ b/packages/gb-counties/src/Models/GbCounty.php @@ -6,6 +6,13 @@ class GbCounty extends Model { + public static $schema = [ + 'id' => 'string', + 'code' => 'string', + 'name' => 'string', + 'region_id' => 'string', + ]; + public function region() { return $this->belongsTo(Region::class); diff --git a/packages/model/composer.json b/packages/model/composer.json index 3e7de34..61ffba6 100644 --- a/packages/model/composer.json +++ b/packages/model/composer.json @@ -24,6 +24,13 @@ "Squire\\": "src" } }, + "extra": { + "laravel": { + "providers": [ + "Squire\\ModelServiceProvider" + ] + } + }, "config": { "sort-packages": true }, diff --git a/packages/model/config/squire.php b/packages/model/config/squire.php new file mode 100644 index 0000000..1fefddd --- /dev/null +++ b/packages/model/config/squire.php @@ -0,0 +1,9 @@ + storage_path('framework/cache'), + + 'cache-prefix' => 'squire', + +]; \ No newline at end of file diff --git a/packages/model/src/Model.php b/packages/model/src/Model.php index 53ddb26..46cb212 100644 --- a/packages/model/src/Model.php +++ b/packages/model/src/Model.php @@ -2,99 +2,160 @@ namespace Squire; +use Illuminate\Database\Connectors\ConnectionFactory; use Illuminate\Database\Eloquent; use Illuminate\Support\Str; -use Sushi\Sushi; class Model extends Eloquent\Model { - use Sushi; - public $incrementing = false; - protected $map; - - protected $rawData; + public static $schema = []; - protected $schema; + protected static $sqliteConnection; - public static function bootSushi() + protected static function boot() { - $instance = (new static); + parent::boot(); - $cacheFileName = config('sushi.cache-prefix', 'sushi').'-'.Str::kebab(str_replace('\\', '', static::class)).'-'.Repository::getLocale(static::class).'.sqlite'; - $cacheDirectory = realpath(config('sushi.cache-path', storage_path('framework/cache'))); - $cachePath = $cacheDirectory.'/'.$cacheFileName; - $sourcePath = Repository::getSource(static::class); + if (static::hasValidCache()) { + static::setSqliteConnection(static::getCachePath()); - $states = [ - 'cache-file-found-and-up-to-date' => function () use ($cachePath) { - static::setSqliteConnection($cachePath); - }, - 'cache-file-not-found-or-stale' => function () use ($cachePath, $sourcePath, $instance) { - file_put_contents($cachePath, ''); + return; + } - static::setSqliteConnection($cachePath); + if (static::isCacheable()) { + static::cache([ + Repository::getLocale(static::class), + ]); - $instance->migrate(); + return; + } - touch($cachePath, filemtime($sourcePath)); - } - ]; + static::setSqliteConnection(':memory:'); - switch (true) { - case file_exists($cachePath) && filemtime($sourcePath) <= filemtime($cachePath): - $states['cache-file-found-and-up-to-date'](); - break; + static::migrate(); + } - case file_exists($cacheDirectory) && is_writable($cacheDirectory): - $states['cache-file-not-found-or-stale'](); - break; + public static function cache($locales = []) + { + if (! static::isCacheable()) return false; - default: - $states['cache-file-not-found-or-stale'](); - break; + if (! count($locales)) { + $locales = array_keys(Repository::getSources(static::class)); } + + collect($locales)->filter(function ($locale) { + return Repository::sourceIsRegistered(static::class, $locale); + })->each(function ($locale) { + $cachePath = static::getCachePath($locale); + + file_put_contents($cachePath, ''); + + static::setSqliteConnection($cachePath); + + static::migrate($locale); + + $modelUpdatedAt = static::getModelUpdatedAt(); + $sourceUpdatedAt = static::getSourceUpdatedAt($locale); + + touch($cachePath, $modelUpdatedAt >= $sourceUpdatedAt ? $modelUpdatedAt : $sourceUpdatedAt); + }); + + return false; } - public function getMap() + protected static function getCachedAt($locale = null) { - $map = collect($this->map); + $cachePath = static::getCachePath($locale); - if ($map->count()) return $map; + return file_exists($cachePath) ? filemtime($cachePath) : 0; + } - return collect($this->rawData->first())->mapWithKeys(function ($value, $columnName) { - return [$columnName => $columnName]; - }); + protected static function getCacheDirectory() + { + return realpath(config('squire.cache-path', storage_path('framework/cache'))); + } + + protected static function getCacheFileName($locale = null) + { + $kebabCaseLocale = Str::of($locale ?? Repository::getLocale(static::class))->replace('_', '-')->lower(); + $kebabCaseModelClassName = Str::of( static::class)->replace('\\', '')->kebab(); + + return config('squire.cache-prefix', 'squire').'-'.$kebabCaseModelClassName.'-'.$kebabCaseLocale.'.sqlite'; + } + + protected static function getCachePath($locale = null) + { + return static::getCacheDirectory().'/'.static::getCacheFileName($locale); + } + + protected static function getModelUpdatedAt() + { + return filemtime((new \ReflectionClass(static::class))->getFileName()); + } + + protected static function getSourceUpdatedAt($locale = null) + { + return filemtime(Repository::getSource(static::class, $locale)); + } + + protected static function hasValidCache($locale = null) + { + $cachedAt = static::getCachedAt($locale); + $modelUpdatedAt = static::getModelUpdatedAt(); + $sourceUpdatedAt = static::getSourceUpdatedAt($locale); + + return $modelUpdatedAt <= $cachedAt && $sourceUpdatedAt <= $cachedAt; } - public function getRows() + protected static function isCacheable() { - $this->rawData = collect(Repository::fetchData(static::class)); + $cacheDirectory = static::getCacheDirectory(); - $map = $this->getMap(); + return file_exists($cacheDirectory) && is_writable($cacheDirectory); + } - return $this->rawData->map(function ($row) use ($map) { - $record = []; + public static function migrate($locale = null) + { + $tableName = (new static)->getTable(); - foreach ($map as $columnName => $sourceColumnName) { - $record[$columnName] = $row[$sourceColumnName]; + static::resolveConnection()->getSchemaBuilder()->create($tableName, function ($table) { + foreach (static::$schema as $name => $type) { + $table->{$type}($name)->nullable(); } + }); - return $record; - })->toArray(); + $data = collect(Repository::fetchData(static::class)); + + $schema = collect(str_getcsv($data->first())); + + $data->transform(function ($line) use ($schema) { + return $schema->combine(str_getcsv($line)); + }); + + $data->shift(); + + foreach (array_chunk($data->toArray(), 100) ?? [] as $dataToInsert) { + if (! empty($dataToInsert)) static::insert($dataToInsert); + } + } + + public static function resolveConnection($connection = null) + { + return static::$sqliteConnection; + } + + protected static function setSqliteConnection($database) + { + static::$sqliteConnection = app(ConnectionFactory::class)->make([ + 'driver' => 'sqlite', + 'database' => $database, + ]); } - public function getSchema() + public function usesTimestamps() { - $schema = collect($this->schema ?? []); - - return $this->getMap() - ->filter(function ($sourceColumnName, $columnName) use ($schema) { - return $schema->get($sourceColumnName, false); - }) - ->mapWithKeys(function ($sourceColumnName, $columnName) use ($schema) { - return [$columnName => $schema->get($sourceColumnName)]; - })->toArray(); + return false; } } \ No newline at end of file diff --git a/packages/model/src/ModelServiceProvider.php b/packages/model/src/ModelServiceProvider.php new file mode 100644 index 0000000..1057c05 --- /dev/null +++ b/packages/model/src/ModelServiceProvider.php @@ -0,0 +1,19 @@ +publishes([ + __DIR__.'/../config/squire.php' => config_path('squire.php'), + ]); + + $this->mergeConfigFrom( + __DIR__.'/../config/squire.php', 'squire' + ); + } +} \ No newline at end of file diff --git a/packages/regions/src/Models/Region.php b/packages/regions/src/Models/Region.php index 6655d4f..70710f3 100644 --- a/packages/regions/src/Models/Region.php +++ b/packages/regions/src/Models/Region.php @@ -6,6 +6,13 @@ class Region extends Model { + public static $schema = [ + 'id' => 'string', + 'code' => 'string', + 'country_id' => 'string', + 'name' => 'string', + ]; + public function airports() { return $this->hasMany(Airport::class); diff --git a/packages/repository/src/RepositoryManager.php b/packages/repository/src/RepositoryManager.php index ae26953..f11a5c9 100644 --- a/packages/repository/src/RepositoryManager.php +++ b/packages/repository/src/RepositoryManager.php @@ -19,21 +19,10 @@ public function fetchData($name, $locale = null) public function fetchDataFromSource($source) { - $data = collect(file($source)) - ->map(function ($line) { - return collect(str_getcsv($line)); - }); - - $data = $data->map(function ($line) use ($data) { - return $data->first()->combine($line); - }); - - $data->shift(); - - return $data; + return file($source); } - public function getLocale($name = null) + public function getLocale($name) { $appLocale = App::getLocale(); if ($this->sourceIsRegistered($name, $appLocale)) return $appLocale; @@ -68,7 +57,7 @@ public function registerSource($name, $locale, $path) { if (! $this->sourceIsRegistered($name)) $this->sources[$name] = []; - $this->sources[$name][$locale] = $path; + $this->sources[$name][$locale] = realpath($path); } public function sourceIsRegistered($name, $locale = null) diff --git a/packages/rule/src/Rule.php b/packages/rule/src/Rule.php index 0276e87..3a5920d 100644 --- a/packages/rule/src/Rule.php +++ b/packages/rule/src/Rule.php @@ -20,10 +20,7 @@ protected function getQueryBuilder() {} public function message() { - return __($this->message, [ - 'attribute' => $this->attribute, - 'column' => $this->column, - ]); + return __($this->message); } public function passes($attribute, $value) diff --git a/tests/ModelTest.php b/tests/ModelTest.php index b37a4d8..861bb29 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -3,86 +3,45 @@ namespace Squire\Tests; use Illuminate\Support\Facades\App; -use Squire\Model; -use Squire\Models; use Squire\Repository; +use Squire\Tests\Models; class ModelTest extends TestCase { /** @test */ - public function basic_usage() + public function can_query_models() { - Repository::registerSource(Foo::class, App::getLocale(), __DIR__.'/data/foo.csv'); - - $this->assertEquals(Foo::count(), 2); - $this->assertEquals(Foo::first()->foo, 'bar'); - $this->assertEquals(Foo::where('bob', 'law')->first()->foo, 'baz'); + Repository::registerSource(Models\Foo::class, App::getLocale(), __DIR__.'/data/foo-en.csv'); + + $this->assertEquals(2, Models\Foo::count()); + $this->assertEquals('bar', Models\Foo::first()->foo); + + $this->testModel(\Squire\Models\Airline::class); + $this->testModel(\Squire\Models\Airport::class); + $this->testModel(\Squire\Models\Continent::class); + $this->testModel(\Squire\Models\Country::class); + $this->testModel(\Squire\Models\Currency::class); + $this->testModel(\Squire\Models\GbCounty::class); + $this->testModel(\Squire\Models\Region::class); } /** @test */ - public function custom_column_map() + public function can_translate_models() { - Repository::registerSource(Bar::class, App::getLocale(), __DIR__.'/data/bar.csv'); + Repository::registerSource(Models\Foo::class, 'en', __DIR__.'/data/foo-en.csv'); + Repository::registerSource(Models\Foo::class, 'es', __DIR__.'/data/foo-es.csv'); - $this->assertEquals(Bar::first()->new_foo, 'bar'); - $this->assertEquals(Bar::where('new_bob', 'law')->first()->new_foo, 'baz'); - } + App::setLocale('en'); + $this->assertEquals('en', Models\Foo::first()->lang); - /** @test */ - public function custom_column_map_and_schema() - { - Repository::registerSource(Baz::class, App::getLocale(), __DIR__.'/data/baz.csv'); + Models\Foo::clearBootedModels(); - $this->assertEquals(Baz::first()->new_foo, '1.0'); - $this->assertEquals(Baz::first()->new_bob, '1'); + App::setLocale('es'); + $this->assertEquals('es', Models\Foo::first()->lang); } - /** @test */ - public function models() + protected function testModel($model) { - $this->assertIsObject(Models\Airline::first()); - $this->assertIsObject(Models\Airport::first()); - $this->assertIsObject(Models\Continent::first()); - $this->assertIsObject(Models\Country::first()); - $this->assertIsObject(Models\Currency::first()); - $this->assertIsObject(Models\GbCounty::first()); - $this->assertIsObject(Models\Region::first()); + $this->assertIsObject($model::all()); } -} - -class Foo extends Model -{ - protected $rawData = [ - ['foo' => 'bar', 'bob' => 'lob'], - ['foo' => 'baz', 'bob' => 'law'], - ]; -} - -class Bar extends Model -{ - protected $map = [ - 'new_foo' => 'foo', - 'new_bob' => 'bob', - ]; - - protected $rawData = [ - ['foo' => 'bar', 'bob' => 'lob'], - ['foo' => 'baz', 'bob' => 'law'], - ]; -} - -class Baz extends Model -{ - protected $map = [ - 'new_foo' => 'foo', - 'new_bob' => 'bob', - ]; - - protected $rawData = [ - ['foo' => '1.0', 'bob' => '1.0'], - ]; - - protected $schema = [ - 'bob' => 'integer', - ]; } \ No newline at end of file diff --git a/tests/Models/Foo.php b/tests/Models/Foo.php new file mode 100644 index 0000000..ced168a --- /dev/null +++ b/tests/Models/Foo.php @@ -0,0 +1,13 @@ + 'string', + 'lang' => 'string', + ]; +} \ No newline at end of file diff --git a/tests/RuleTest.php b/tests/RuleTest.php new file mode 100644 index 0000000..9dcc6b8 --- /dev/null +++ b/tests/RuleTest.php @@ -0,0 +1,50 @@ +testRule(Rules\Foo::class, Models\Foo::class); + + $this->testRule(\Squire\Rules\Airline::class, \Squire\Models\Airline::class); + $this->testRule(\Squire\Rules\Airport::class, \Squire\Models\Airport::class); + $this->testRule(\Squire\Rules\Continent::class, \Squire\Models\Continent::class); + $this->testRule(\Squire\Rules\Country::class, \Squire\Models\Country::class); + $this->testRule(\Squire\Rules\Currency::class, \Squire\Models\Currency::class); + $this->testRule(\Squire\Rules\GbCounty::class, \Squire\Models\GbCounty::class); + $this->testRule(\Squire\Rules\Region::class, \Squire\Models\Region::class); + } + + protected function testRule($rule, $model) + { + $primaryKey = array_keys($model::$schema)[0]; + $secondaryKey = array_keys($model::$schema)[1]; + + $this->assertTrue(Validator::make([ + $primaryKey => $model::first()->{$primaryKey}, + $secondaryKey => $model::first()->{$secondaryKey}, + ], [ + $primaryKey => [new $rule()], + $secondaryKey => [new $rule($secondaryKey)], + ])->passes()); + + $this->assertTrue(Validator::make([ + $primaryKey => null, + $secondaryKey => null, + ], [ + $primaryKey => [new $rule()], + $secondaryKey => [new $rule($secondaryKey)], + ])->fails()); + } +} \ No newline at end of file diff --git a/tests/Rules/Foo.php b/tests/Rules/Foo.php new file mode 100644 index 0000000..75ca678 --- /dev/null +++ b/tests/Rules/Foo.php @@ -0,0 +1,18 @@ +