1
0
mirror of https://github.com/flarum/core.git synced 2025-07-18 15:21:16 +02:00

Major refactor and improvements

- Reorganised all namespaces and class names for consistency and structure. Following PSR bylaws (Abstract prefix, Interface/Trait suffix).
  - Move models into root of Core, because writing `use Flarum\Core\Discussion` is nice. Namespace the rest by type. (Namespacing by entity was too arbitrary.)
  - Moved some non-domain stuff out of Core: Database, Formatter, Settings.
  - Renamed config table and all references to "settings" for consistency.
  - Remove Core class and add url()/isInstalled()/inDebugMode() as instance methods of Foundation\Application.
  - Cleanup, docblocking, etc.

- Improvements to HTTP architecture
  - API and forum/admin Actions are now actually all the same thing (simple PSR-7 Request handlers), renamed to Controllers.
  - Upgrade to tobscure/json-api 0.2 branch.
  - Where possible, moved generic functionality to tobscure/json-api (e.g. pagination links). I'm quite happy with the backend balance now re: #262

- Improvements to other architecture
  - Use Illuminate's Auth\Access\Gate interface/implementation instead of our old Locked trait. We still use events to actually determine the permissions though. Our Policy classes are actually glorified event subscribers.
  - Extract model validation into Core\Validator classes.
  - Make post visibility permission stuff much more efficient and DRY.

- Renamed Flarum\Event classes for consistency. ref #246
  - `Configure` prefix for events dedicated to configuring an object.
  - `Get` prefix for events whose listeners should return something.
  - `Prepare` prefix when a variable is passed by reference so it can be modified.
  - `Scope` prefix when a query builder is passed.

- Miscellaneous improvements/bug-fixes. I'm easily distracted!
  - Increase default height of post composer.
  - Improve post stream redraw flickering in Safari by keying loading post placeholders with their IDs. ref #451
  - Use a PHP JavaScript minification library for minifying TextFormatter's JavaScript, instead of ClosureCompilerService (can't rely on external service!)
  - Use UrlGenerator properly in various places. closes #123
  - Make Api\Client return Response object. closes #128
  - Allow extensions to specify custom icon images.
  - Allow external API/admin URLs to be optionally specified in config.php. If the value or "url" is an array, we look for the corresponding path inside. Otherwise, we append the path to the base URL, using the corresponding value in "paths" if present. closes #244
This commit is contained in:
Toby Zerner
2015-10-08 14:28:02 +10:30
parent 8c7cdb184f
commit dd67291ce0
434 changed files with 8676 additions and 7997 deletions

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Database;
use Illuminate\Database\Schema\Builder;
use Flarum\Settings\SettingsRepository;
abstract class AbstractMigration
{
/**
* @var Builder
*/
protected $schema;
/**
* @var SettingsRepository
*/
protected $settings;
/**
* @param Builder $schema
* @param SettingsRepository $settings
*/
public function __construct(Builder $schema, SettingsRepository $settings)
{
$this->schema = $schema;
$this->settings = $settings;
}
/**
* Run the migrations.
*/
abstract public function up();
/**
* Reverse the migrations.
*/
abstract public function down();
}

187
src/Database/AbstractModel.php Executable file
View File

@@ -0,0 +1,187 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Database;
use Flarum\Event\ConfigureModelDates;
use Flarum\Event\GetModelRelationship;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Relations\Relation;
use LogicException;
/**
* Base model class, building on Eloquent.
*
* Adds the ability for custom relations to be added to a model during runtime.
* These relations behave in the same way that you would expect; they can be
* queried, eager loaded, and accessed as an attribute.
*/
abstract class AbstractModel extends Eloquent
{
/**
* Indicates if the model should be timestamped. Turn off by default.
*
* @var boolean
*/
public $timestamps = false;
/**
* An array of callbacks to be run once after the model is saved.
*
* @var callable[]
*/
protected $afterSaveCallbacks = [];
/**
* An array of callbacks to be run once after the model is deleted.
*
* @var callable[]
*/
protected $afterDeleteCallbacks = [];
/**
* {@inheritdoc}
*/
public static function boot()
{
parent::boot();
static::saved(function (AbstractModel $model) {
foreach ($model->releaseAfterSaveCallbacks() as $callback) {
$callback($model);
}
});
static::deleted(function (AbstractModel $model) {
foreach ($model->releaseAfterDeleteCallbacks() as $callback) {
$callback($model);
}
});
}
/**
* Get the attributes that should be converted to dates.
*
* @return array
*/
public function getDates()
{
static $dates = [];
$class = get_class($this);
if (! isset($dates[$class])) {
static::$dispatcher->fire(
new ConfigureModelDates($this, $this->dates)
);
$dates[$class] = $this->dates;
}
return $dates[$class];
}
/**
* Get an attribute from the model. If nothing is found, attempt to load
* a custom relation method with this key.
*
* @param string $key
* @return mixed
*/
public function getAttribute($key)
{
if (! is_null($value = parent::getAttribute($key))) {
return $value;
}
// If a custom relation with this key has been set up, then we will load
// and return results from the query and hydrate the relationship's
// value on the "relationships" array.
if (! $this->relationLoaded($key) && ($relation = $this->getCustomRelation($key))) {
if (! $relation instanceof Relation) {
throw new LogicException('Relationship method must return an object of type '
. 'Illuminate\Database\Eloquent\Relations\Relation');
}
return $this->relations[$key] = $relation->getResults();
}
}
/**
* Get a custom relation object.
*
* @param string $name
* @return mixed
*/
protected function getCustomRelation($name)
{
return static::$dispatcher->until(
new GetModelRelationship($this, $name)
);
}
/**
* Register a callback to be run once after the model is saved.
*
* @param callable $callback
* @return void
*/
public function afterSave($callback)
{
$this->afterSaveCallbacks[] = $callback;
}
/**
* Register a callback to be run once after the model is deleted.
*
* @param callable $callback
* @return void
*/
public function afterDelete($callback)
{
$this->afterDeleteCallbacks[] = $callback;
}
/**
* @return callable[]
*/
public function releaseAfterSaveCallbacks()
{
$callbacks = $this->afterSaveCallbacks;
$this->afterSaveCallbacks = [];
return $callbacks;
}
/**
* @return callable[]
*/
public function releaseAfterDeleteCallbacks()
{
$callbacks = $this->afterDeleteCallbacks;
$this->afterDeleteCallbacks = [];
return $callbacks;
}
/**
* {@inheritdoc}
*/
public function __call($method, $arguments)
{
if ($relation = $this->getCustomRelation($method)) {
return $relation;
}
return parent::__call($method, $arguments);
}
}

View File

@@ -0,0 +1,166 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Database;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
class DatabaseMigrationRepository implements MigrationRepositoryInterface
{
/**
* The database connection resolver instance.
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected $resolver;
/**
* The name of the migration table.
*
* @var string
*/
protected $table;
/**
* The name of the database connection to use.
*
* @var string
*/
protected $connection;
/**
* Create a new database migration repository instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param string $table
* @return void
*/
public function __construct(Resolver $resolver, $table)
{
$this->table = $table;
$this->resolver = $resolver;
}
/**
* Get the ran migrations.
*
* @return array
*/
public function getRan($extension = null)
{
return $this->table()
->where('extension', $extension)
->orderBy('migration', 'asc')
->lists('migration');
}
/**
* Log that a migration was run.
*
* @param string $file
* @param string $extension
* @return void
*/
public function log($file, $extension = null)
{
$record = ['migration' => $file, 'extension' => $extension];
$this->table()->insert($record);
}
/**
* Remove a migration from the log.
*
* @param string $file
* @param string $extension
* @return void
*/
public function delete($file, $extension = null)
{
$query = $this->table()->where('migration', $file);
if (is_null($extension)) {
$query->whereNull('extension');
} else {
$query->where('extension', $extension);
}
$query->delete();
}
/**
* Create the migration repository data store.
*
* @return void
*/
public function createRepository()
{
$schema = $this->getConnection()->getSchemaBuilder();
$schema->create($this->table, function ($table) {
$table->string('migration');
$table->string('extension')->nullable();
});
}
/**
* Determine if the migration repository exists.
*
* @return bool
*/
public function repositoryExists()
{
$schema = $this->getConnection()->getSchemaBuilder();
return $schema->hasTable($this->table);
}
/**
* Get a query builder for the migration table.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function table()
{
return $this->getConnection()->table($this->table);
}
/**
* Get the connection resolver instance.
*
* @return \Illuminate\Database\ConnectionResolverInterface
*/
public function getConnectionResolver()
{
return $this->resolver;
}
/**
* Resolve the database connection instance.
*
* @return \Illuminate\Database\Connection
*/
public function getConnection()
{
return $this->resolver->connection($this->connection);
}
/**
* Set the information source to gather data.
*
* @param string $name
* @return void
*/
public function setSource($name)
{
$this->connection = $name;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Database;
use Flarum\Core;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Application;
use Illuminate\Database\ConnectionResolver;
use Illuminate\Database\Connectors\ConnectionFactory;
use PDO;
class DatabaseServiceProvider extends AbstractServiceProvider
{
/**
* {@inheritdoc}
*/
public function register()
{
$this->app->singleton('flarum.db', function () {
$factory = new ConnectionFactory($this->app);
$connection = $factory->make($this->app->make('flarum.config')['database']);
$connection->setEventDispatcher($this->app->make('Illuminate\Contracts\Events\Dispatcher'));
$connection->setFetchMode(PDO::FETCH_CLASS);
return $connection;
});
$this->app->alias('flarum.db', 'Illuminate\Database\ConnectionInterface');
$this->app->singleton('Illuminate\Database\ConnectionResolverInterface', function () {
$resolver = new ConnectionResolver([
'flarum' => $this->app->make('flarum.db'),
]);
$resolver->setDefaultConnection('flarum');
return $resolver;
});
$this->app->alias('Illuminate\Database\ConnectionResolverInterface', 'db');
$this->app->singleton('Flarum\Database\MigrationRepositoryInterface', function ($app) {
return new DatabaseMigrationRepository($app['db'], 'migrations');
});
$this->app->bind(MigrationCreator::class, function (Application $app) {
return new MigrationCreator($app->make('Illuminate\Filesystem\Filesystem'), $app->basePath());
});
}
/**
* {@inheritdoc}
*/
public function boot()
{
if ($this->app->isInstalled()) {
AbstractModel::setConnectionResolver($this->app->make('Illuminate\Database\ConnectionResolverInterface'));
AbstractModel::setEventDispatcher($this->app->make('events'));
}
}
}

148
src/Database/MigrationCreator.php Executable file
View File

@@ -0,0 +1,148 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Database;
use Illuminate\Support\Str;
use Illuminate\Filesystem\Filesystem;
class MigrationCreator
{
/**
* The filesystem instance.
*
* @var Filesystem
*/
protected $files;
/**
* @var string
*/
protected $publicPath;
/**
* Create a new migrator instance.
*
* @param Filesystem $files
* @param string $publicPath
*/
public function __construct(Filesystem $files, $publicPath)
{
$this->files = $files;
$this->publicPath = $publicPath;
}
/**
* Create a new migration for the given extension.
*
* @param string $name
* @param string $path
* @param string $table
* @param bool $create
* @return string
*/
public function create($name, $extension = null, $table = null, $create = false)
{
$migrationPath = $this->getMigrationPath($extension);
$path = $this->getPath($name, $migrationPath);
$stub = $this->getStub($table, $create);
$this->files->put($path, $this->populateStub($extension, $name, $stub, $table));
return $path;
}
/**
* Get the migration stub file.
*
* @param string $table
* @param bool $create
* @return string
*/
protected function getStub($table, $create)
{
if (is_null($table)) {
return $this->files->get($this->getStubPath().'/blank.stub');
}
// We also have stubs for creating new tables and modifying existing tables
// to save the developer some typing when they are creating a new tables
// or modifying existing tables. We'll grab the appropriate stub here.
$stub = $create ? 'create.stub' : 'update.stub';
return $this->files->get($this->getStubPath()."/{$stub}");
}
/**
* Populate the place-holders in the migration stub.
*
* @param string $name
* @param string $stub
* @param string $table
* @return string
*/
protected function populateStub($extension, $name, $stub, $table)
{
$replacements = [
'{{namespace}}' => Str::studly($extension) ?: 'Flarum\Core',
'{{name}}' => Str::studly($name),
'{{table}}' => $table
];
return str_replace(array_keys($replacements), array_values($replacements), $stub);
}
/**
* Get the full path name to the migration directory.
*
* @param string $extension
* @return string
*/
protected function getMigrationPath($extension)
{
$parent = $extension ? public_path().'/extensions/'.$extension : __DIR__.'/../..';
return $parent.'/migrations';
}
/**
* Get the full path name to the migration.
*
* @param string $name
* @param string $path
* @return string
*/
protected function getPath($name, $path)
{
return $path.'/'.$this->getDatePrefix().'_'.$name.'.php';
}
/**
* Get the date prefix for the migration.
*
* @return string
*/
protected function getDatePrefix()
{
return date('Y_m_d_His');
}
/**
* Get the path to the stubs.
*
* @return string
*/
protected function getStubPath()
{
return __DIR__.'/../../stubs/migrations';
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Database;
interface MigrationRepositoryInterface
{
/**
* Get the ran migrations for the given extension.
*
* @return array
*/
public function getRan($extension = null);
/**
* Log that a migration was run.
*
* @param string $file
* @param string $extension
* @return void
*/
public function log($file, $extension = null);
/**
* Remove a migration from the log.
*
* @param string $file
* @param string $extension
* @return void
*/
public function delete($file, $extension = null);
/**
* Create the migration repository data store.
*
* @return void
*/
public function createRepository();
/**
* Determine if the migration repository exists.
*
* @return bool
*/
public function repositoryExists();
/**
* Set the information source to gather data.
*
* @param string $name
* @return void
*/
public function setSource($name);
}

334
src/Database/Migrator.php Executable file
View File

@@ -0,0 +1,334 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Database;
use Illuminate\Support\Str;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
class Migrator
{
/**
* The migration repository implementation.
*
* @var \Flarum\Database\MigrationRepositoryInterface
*/
protected $repository;
/**
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* The connection resolver instance.
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected $resolver;
/**
* The name of the default connection.
*
* @var string
*/
protected $connection;
/**
* The notes for the current operation.
*
* @var array
*/
protected $notes = [];
/**
* Create a new migrator instance.
*
* @param \Flarum\Database\MigrationRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function __construct(
MigrationRepositoryInterface $repository,
Resolver $resolver,
Filesystem $files
) {
$this->files = $files;
$this->resolver = $resolver;
$this->repository = $repository;
}
/**
* Run the outstanding migrations at a given path.
*
* @param string $path
* @param string $extension
* @return void
*/
public function run($path, $extension = null)
{
$this->notes = [];
$files = $this->getMigrationFiles($path);
$ran = $this->repository->getRan($extension);
$migrations = array_diff($files, $ran);
$this->requireFiles($path, $migrations);
$this->runMigrationList($migrations, $extension);
}
/**
* Run an array of migrations.
*
* @param array $migrations
* @param bool $pretend
* @return void
*/
public function runMigrationList($migrations, $extension)
{
// First we will just make sure that there are any migrations to run. If there
// aren't, we will just make a note of it to the developer so they're aware
// that all of the migrations have been run against this database system.
if (count($migrations) == 0) {
$this->note('<info>Nothing to migrate.</info>');
return;
}
// Once we have the array of migrations, we will spin through them and run the
// migrations "up" so the changes are made to the databases. We'll then log
// that the migration was run so we don't repeat it next time we execute.
foreach ($migrations as $file) {
$this->runUp($file, $extension);
}
}
/**
* Run "up" a migration instance.
*
* @param string $file
* @param string $extension
* @return void
*/
protected function runUp($file, $extension)
{
// First we will resolve a "real" instance of the migration class from this
// migration file name. Once we have the instances we can run the actual
// command such as "up" or "down", or we can just simulate the action.
$migration = $this->resolve($file, $extension);
$migration->up();
// Once we have run a migrations class, we will log that it was run in this
// repository so that we don't try to run it next time we do a migration
// in the application. A migration repository keeps the migrate order.
$this->repository->log($file, $extension);
$this->note("<info>Migrated:</info> $file");
}
/**
* Rolls all of the currently applied migrations back.
*
* @param bool $pretend
* @return int
*/
public function reset($path, $extension = null)
{
$this->notes = [];
$migrations = array_reverse($this->repository->getRan($extension));
$this->requireFiles($path, $migrations);
$count = count($migrations);
if ($count === 0) {
$this->note('<info>Nothing to rollback.</info>');
} else {
foreach ($migrations as $migration) {
$this->runDown($migration, $extension);
}
}
return $count;
}
/**
* Run "down" a migration instance.
*
* @param string $file
* @param string $extension
* @return void
*/
protected function runDown($file, $extension = null)
{
// First we will get the file name of the migration so we can resolve out an
// instance of the migration. Once we get an instance we can either run a
// pretend execution of the migration or we can run the real migration.
$instance = $this->resolve($file, $extension);
$instance->down();
// Once we have successfully run the migration "down" we will remove it from
// the migration repository so it will be considered to have not been run
// by the application then will be able to fire by any later operation.
$this->repository->delete($file, $extension);
$this->note("<info>Rolled back:</info> $file");
}
/**
* Get all of the migration files in a given path.
*
* @param string $path
* @return array
*/
public function getMigrationFiles($path)
{
$files = $this->files->glob($path.'/*_*.php');
// Once we have the array of files in the directory we will just remove the
// extension and take the basename of the file which is all we need when
// finding the migrations that haven't been run against the databases.
if ($files === false) {
return [];
}
$files = array_map(function ($file) {
return str_replace('.php', '', basename($file));
}, $files);
// Once we have all of the formatted file names we will sort them and since
// they all start with a timestamp this should give us the migrations in
// the order they were actually created by the application developers.
sort($files);
return $files;
}
/**
* Require in all the migration files in a given path.
*
* @param string $path
* @param array $files
* @return void
*/
public function requireFiles($path, array $files)
{
foreach ($files as $file) {
$this->files->requireOnce($path.'/'.$file.'.php');
}
}
/**
* Resolve a migration instance from a file.
*
* @param string $file
* @return object
*/
public function resolve($file, $extension = null)
{
$file = implode('_', array_slice(explode('_', $file), 4));
$class = ($extension ? Str::studly($extension) : 'Flarum\\Core') . '\\Migration\\';
$class .= Str::studly($file);
return app()->make($class);
}
/**
* Raise a note event for the migrator.
*
* @param string $message
* @return void
*/
protected function note($message)
{
$this->notes[] = $message;
}
/**
* Get the notes for the last operation.
*
* @return array
*/
public function getNotes()
{
return $this->notes;
}
/**
* Resolve the database connection instance.
*
* @param string $connection
* @return \Illuminate\Database\Connection
*/
public function resolveConnection($connection)
{
return $this->resolver->connection($connection);
}
/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setConnection($name)
{
if (!is_null($name)) {
$this->resolver->setDefaultConnection($name);
}
$this->repository->setSource($name);
$this->connection = $name;
}
/**
* Get the migration repository instance.
*
* @return \Illuminate\Database\Migrations\MigrationRepositoryInterface
*/
public function getRepository()
{
return $this->repository;
}
/**
* Determine if the migration repository exists.
*
* @return bool
*/
public function repositoryExists()
{
return $this->repository->repositoryExists();
}
/**
* Get the file system instance.
*
* @return \Illuminate\Filesystem\Filesystem
*/
public function getFilesystem()
{
return $this->files;
}
}