Skip to content

Commit

Permalink
Adds: RemoteModel
Browse files Browse the repository at this point in the history
  • Loading branch information
DaltonMcCleery committed Jul 9, 2024
1 parent ce454ae commit a8a7ccc
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 0 deletions.
229 changes: 229 additions & 0 deletions src/RemoteModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<?php

namespace RemoteModels;

use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

trait RemoteModel
{
protected static $remoteModelConnection;

public static function bootRemoteModel(): void
{
$instance = (new static);

$cachePath = $instance->remoteModelCachePath();
$dataPath = $instance->remoteModelCacheReferencePath();

$states = [
'cache-file-found-and-up-to-date' => function () use ($cachePath) {
static::setSqliteConnection($cachePath);
},
'cache-file-not-found-or-stale' => function () use ($cachePath, $dataPath, $instance) {
static::cacheFileNotFoundOrStale($cachePath, $dataPath, $instance);
},
'no-caching-capabilities' => function () use ($instance) {
static::setSqliteConnection(':memory:');

$instance->migrate();
},
];

switch (true) {
case \file_exists($cachePath) && \filemtime($dataPath) <= \filemtime($cachePath):
$states['cache-file-found-and-up-to-date']();
break;

case \file_exists($instance->remoteModelCacheDirectory()) && \is_writable($instance->remoteModelCacheDirectory()):
$states['cache-file-not-found-or-stale']();
break;

default:
$states['no-caching-capabilities']();
break;
}
}

public function getEndpoint(): ?string
{
if ($this->endpoint) {
$domain = config('remote-models.domain', '');
if (Str::endsWith($domain, '/')) {
$domain = \rtrim($domain, '/');
}

return $domain . $this->endpoint;
}

return null;
}

protected function remoteModelCachePath(): string
{
return \implode(DIRECTORY_SEPARATOR, [
$this->remoteModelCacheDirectory(),
$this->remoteModelCacheFileName(),
]);
}

protected function remoteModelCacheFileName(): string
{
return config('remote-models.cache-prefix', 'remote').'-'.Str::kebab(\str_replace('\\', '', static::class)).'.sqlite';
}

protected function remoteModelCacheDirectory(): false|string
{
return realpath(config('remote-models.cache-path', storage_path('framework/cache')));
}

protected function remoteModelCacheReferencePath(): false|string
{
return (new \ReflectionClass(static::class))->getFileName();
}

protected static function cacheFileNotFoundOrStale($cachePath, $dataPath, $instance): void
{
\file_put_contents($cachePath, '');

static::setSqliteConnection($cachePath);

$instance->migrate();

\touch($cachePath, \filemtime($dataPath));
}

public static function resolveConnection($connection = null)
{
return static::$remoteModelConnection;
}

protected static function setSqliteConnection($database): void
{
$config = [
'driver' => 'sqlite',
'database' => $database,
];

static::$remoteModelConnection = app(ConnectionFactory::class)->make($config);

app('config')->set('database.connections.'.static::class, $config);
}

public function getConnectionName(): string
{
return static::class;
}

public function migrate(): void
{
/** @var \Illuminate\Database\Schema\SQLiteBuilder $schemaBuilder */
$schemaBuilder = static::resolveConnection()->getSchemaBuilder();

try {
$schemaBuilder->create($this->getTable(), function (BluePrint $table) {
$schema = [];

// Load initial data from endpoint to gather columns.
$response = $this->callRemoteModelEndpoint();

if (\array_key_exists('data', $response)) {
// Paginated
if (\count($response['data']) > 0) {
$schema = \array_keys($response['data'][0]);
}
} else if (\count($response) > 0) {
// Default to array of data.
$schema = \array_keys($response[0]);
}

if (\count($schema) === 0) {
throw new \Exception('No data returned from Remote Model `$endpoint`.');
}

$table->id();

$schema = collect($schema)
->filter(fn ($column) => ! \in_array($column, ['id', 'created_at', 'updated_at']))
->toArray();

foreach ($schema as $type => $column) {
if ($column === 'id' || $type === 'id') {
continue;
}

if (\gettype($type) === 'integer') {
// Default to string column.
$table->string($column)->nullable();
} else if (Str::endsWith($column, ['_at', '_on'])) {
$table->dateTime($column)->nullable();
} else {
$table->{$type}($column)->nullable();
}
}

$table->timestamps();
});

$this->loadRemoteModelData();
} catch (QueryException $e) {
if (Str::contains($e->getMessage(), [
'already exists (SQL: create table',
\sprintf('table "%s" already exists', $this->getTable()),
])) {
// This error can happen in rare circumstances due to a race condition.
// Concurrent requests may both see the necessary preconditions for
// the table creation, but only one can actually succeed.
return;
}

throw $e;
}
}

public function loadRemoteModelData(int $page = 1): void
{
$response = $this->callRemoteModelEndpoint($page);

if (\array_key_exists('data', $response)) {
// Paginated
foreach (\array_chunk($response['data'], $response['per_page'] ?? 15) ?? [] as $inserts) {
if (! empty($inserts)) {
static::insert($inserts);
}
}

if (\array_key_exists('current_page', $response) && \array_key_exists('last_page', $response)) {
if ((int) $response['current_page'] < (int) $response['last_page']) {
$this->loadRemoteModelData((int) $response['current_page'] + 1);
}
}
} else if (\count($response) > 0) {
// Default to array of data.
foreach (\array_chunk($response, 15) ?? [] as $inserts) {
if (! empty($inserts)) {
static::insert($inserts);
}
}
}
}

public function callRemoteModelEndpoint(int $page = 1): array
{
// Load first result from endpoint
if (! $this->getEndpoint()) {
throw new \Exception('Remote Model property `$endpoint` cannot be empty.');
}

$response = Http::timeout(10)->get($this->getEndpoint() . '?page=' . $page);

if ($response->failed()) {
throw new \Exception('Access to Remote Model `$endpoint` failed.');
}

return $response->json();
}
}
37 changes: 37 additions & 0 deletions src/RemoteModelServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace RemoteModels;

use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Support\ServiceProvider;

/**
* Class RemoteModelServiceProvider.
*/
class RemoteModelServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/config/remote-models.php' => config_path('remote-models.php'),
], 'config');
}

AboutCommand::add('Remote Models', 'Version', '0.1.0');
}

/**
* Register the application services.
*/
public function register(): void
{
$this->mergeConfigFrom(
__DIR__.'/config/remote-models.php',
'remote-models'
);
}
}
9 changes: 9 additions & 0 deletions src/config/remote-models.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

return [
'domain' => null,

'cache-path' => null,

'cache-prefix' => 'remote',
];

0 comments on commit a8a7ccc

Please sign in to comment.