diff --git a/Herbert/Framework/API.php b/Herbert/Framework/API.php index f1cee73..aee15b7 100644 --- a/Herbert/Framework/API.php +++ b/Herbert/Framework/API.php @@ -1,6 +1,6 @@ methods[$method] = $fn; } + /** + * Gets a method. + * + * @param string $method + * @return Callable + */ + public function get($method) + { + return array_get($this->methods, $method); + } + /** * Magic call from the function collection. * @@ -46,9 +57,9 @@ public function add($method, $fn) */ public function __call($method, $params) { - if (!isset($this->methods[$method])) + if ( ! isset($this->methods[$method])) { - throw new WP_Error('broke', "Method '{$method}' not set!"); + throw new Exception("Method '{$method}' not set!"); } return $this->app->call( diff --git a/Herbert/Framework/Application.php b/Herbert/Framework/Application.php index 211a51e..9f9ae25 100644 --- a/Herbert/Framework/Application.php +++ b/Herbert/Framework/Application.php @@ -3,16 +3,26 @@ use Illuminate\Support\ServiceProvider; use vierbergenlars\SemVer\version as SemVersion; use vierbergenlars\SemVer\expression as SemVersionExpression; +use Illuminate\Database\Capsule\Manager as CapsuleManager; +use Illuminate\Database\Schema\Blueprint as SchemaBlueprint; /** * @see http://getherbert.com */ class Application extends \Illuminate\Container\Container implements \Illuminate\Contracts\Foundation\Application { + public function getCachedPackagesPath() + { + + } + public function runningInConsole() + { + + } /** * The application's version. */ - const VERSION = '0.9.2'; + const VERSION = '0.9.13'; /** * The application's version. @@ -96,6 +106,13 @@ class Application extends \Illuminate\Container\Container implements \Illuminate */ protected $matched = []; + /** + * The plugin apis. + * + * @var array + */ + protected $apis = []; + /** * The plugin configurations. * @@ -144,11 +161,11 @@ public function __construct() /** * Added to satisfy interface * - * @return null + * @return string */ public function basePath() { - return; + return content_directory() . '/herbert-cache'; } /** @@ -441,6 +458,7 @@ protected function loadPluginAPIs($requires = []) { global $$name; $api = $$name = new API($this); + $this->apis[] = [$name, $api]; require "$require"; } @@ -481,6 +499,24 @@ public function activatePlugin($root) $config = $this->getPluginConfig($root); + foreach (array_get($config, 'tables', []) as $table => $class) + { + if ( ! class_exists($class)) + { + continue; + } + + if (CapsuleManager::schema()->hasTable($table)) + { + continue; + } + + CapsuleManager::schema()->create($table, function (SchemaBlueprint $table) use ($class) + { + $this->call($class . '@activate', ['table' => $table, 'app' => $this]); + }); + } + foreach (array_get($config, 'activators', []) as $activator) { if ( ! file_exists($activator)) @@ -535,6 +571,85 @@ public function deactivatePlugin($root) 'widget' ]); } + + foreach (array_get($config, 'tables', []) as $table => $class) + { + if ( ! class_exists($class)) + { + continue; + } + + if ( ! CapsuleManager::schema()->hasTable($table)) + { + continue; + } + + CapsuleManager::schema()->table($table, function (SchemaBlueprint $table) use ($class) + { + $this->call($class . '@deactivate', ['table' => $table, 'app' => $this]); + }); + } + } + + /** + * Deletes a plugin. + * + * @see register_uninstall_hook + * @param $root + */ + public function deletePlugin($root) + { + $plugins = array_filter($this->plugins, function (Plugin $plugin) use ($root) + { + return $plugin->getBasePath() === $root; + }); + + foreach ($plugins as $plugin) + { + if ( ! method_exists($plugin, 'delete')) + { + continue; + } + + $plugin->deactivate(); + } + + $config = $this->getPluginConfig($root); + + foreach (array_get($config, 'deleters', []) as $deleter) + { + if ( ! file_exists($deleter)) + { + continue; + } + + $this->loadWith($deleter, [ + 'http', + 'router', + 'enqueue', + 'panel', + 'shortcode', + 'widget' + ]); + } + + foreach (array_get($config, 'tables', []) as $table => $class) + { + if ( ! class_exists($class)) + { + continue; + } + + if ( ! CapsuleManager::schema()->hasTable($table)) + { + continue; + } + + CapsuleManager::schema()->table($table, function (SchemaBlueprint $table) use ($class) + { + $this->call($class . '@delete', ['table' => $table, 'app' => $this]); + }); + } } /** @@ -1032,6 +1147,13 @@ protected function buildViewGlobals() $globals = array_merge($globals, $val); } + foreach ($this->apis as $api) + { + list($name, $instance) = $api; + + $globals[$name] = $instance; + } + $this->builtViewGlobals = $globals; } @@ -1140,4 +1262,24 @@ public static function getInstance() return static::$instance; } + /** + * Get the path to the cached "compiled.php" file. + * + * @return string + */ + public function getCachedCompilePath() + { + return $this->basePath() . '/vendor/compiled.php'; + } + + /** + * Get the path to the cached services.json file. + * + * @return string + */ + public function getCachedServicesPath() + { + return $this->basePath() . '/vendor/services.json'; + } + } diff --git a/Herbert/Framework/Enqueue.php b/Herbert/Framework/Enqueue.php index 2fbaf8c..c0cf67a 100644 --- a/Herbert/Framework/Enqueue.php +++ b/Herbert/Framework/Enqueue.php @@ -73,6 +73,10 @@ public function buildInclude($attrs, $footer) else { wp_enqueue_script($attrs['as'], $attrs['src'], [], false, $footer); + + if(isset($attrs['localize'])) { + wp_localize_script( $attrs['as'], $attrs['as'], $attrs['localize'] ); + } } } @@ -183,32 +187,23 @@ public function filterHook($attrs, $filterWith) public function filterPanel($attrs, $filterWith) { $panels = $this->app['panel']->getPanels(); - $http = $this->app['http']; + $page = $this->app['http']->get('page', false); - if ($filterWith[0] === '*') { - if (!$http->has('page')) - { - return false; - } - - foreach ($panels as $panel) - { - if ($panel['slug'] === $http->get('page')) - { - return true; - } - } + if (!$page && function_exists('get_current_screen')) + { + $page = object_get(get_current_screen(), 'id', $page); } - else + + foreach ($filterWith as $filter) { - foreach ($filterWith as $filter) + $filtered = array_filter($panels, function ($panel) use ($page, $filter) { + return $page === $panel['slug'] && str_is($filter, $panel['slug']); + }); + + if (count($filtered) > 0) { - if ($panels[$filter]['slug'] === $http->get('page')) - { - return true; - } + return true; } - } return false; @@ -332,7 +327,7 @@ public function filterSearch($attrs, $filterWith) */ public function filterPostType($attrs, $filterWith) { - return array_search(get_post_type(), $filterWith) !== null; + return array_search(get_post_type(), $filterWith) !== FALSE; } } diff --git a/Herbert/Framework/Exceptions/HttpErrorException.php b/Herbert/Framework/Exceptions/HttpErrorException.php new file mode 100644 index 0000000..4bc1a69 --- /dev/null +++ b/Herbert/Framework/Exceptions/HttpErrorException.php @@ -0,0 +1,57 @@ +status = $status; + + if ( ! is_string($message)) + { + $this->response = $message; + } + } + + /** + * Gets the Http status code. + * + * @return integer + */ + public function getStatus() + { + return $this->status; + } + + /** + * Gets the response. + * + * @return mixed + */ + public function getResponse() + { + return $this->response; + } + +} diff --git a/Herbert/Framework/Models/Comment.php b/Herbert/Framework/Models/Comment.php index 3cfa9f3..4e7364f 100644 --- a/Herbert/Framework/Models/Comment.php +++ b/Herbert/Framework/Models/Comment.php @@ -21,6 +21,20 @@ class Comment extends Model { */ protected $primaryKey = 'comment_ID'; + /** + * The name of the "created at" column. + * + * @var string + */ + const CREATED_AT = 'comment_date'; + + /** + * The name of the "updated at" column. + * + * @var string + */ + const UPDATED_AT = null; + /** * The attributes that are mass assignable. * @@ -50,7 +64,7 @@ class Comment extends Model { */ public function post() { - return $this->belongsTo(__NAMESPACE__ . '/Post', 'comment_post_ID'); + return $this->belongsTo(__NAMESPACE__ . '\Post', 'comment_post_ID'); } /** @@ -63,4 +77,33 @@ public function meta() return $this->hasMany(__NAMESPACE__ . '\CommentMeta', 'comment_id'); } + /** + * Set the value of the "created at" attribute. + * + * @param mixed $value + * @return void + */ + public function setCreatedAt($value) + { + $this->{static::CREATED_AT} = $value; + + if ( ! $value instanceof Carbon) + { + $value = new Carbon($value); + } + + $this->{static::CREATED_AT . '_gmt'} = $value->timezone('GMT'); + } + + /** + * Set the value of the "updated at" attribute. + * + * @param mixed $value + * @return void + */ + public function setUpdatedAt($value) + { + // + } + } diff --git a/Herbert/Framework/Models/CommentMeta.php b/Herbert/Framework/Models/CommentMeta.php index 3ac0835..3cd4faf 100644 --- a/Herbert/Framework/Models/CommentMeta.php +++ b/Herbert/Framework/Models/CommentMeta.php @@ -7,6 +7,13 @@ */ class CommentMeta extends Model { + /** + * Disable timestamps. + * + * @var boolean + */ + public $timestamps = false; + /** * The table associated with the model. * diff --git a/Herbert/Framework/Models/Option.php b/Herbert/Framework/Models/Option.php index 4a1ba65..628c9ae 100644 --- a/Herbert/Framework/Models/Option.php +++ b/Herbert/Framework/Models/Option.php @@ -7,6 +7,13 @@ */ class Option extends Model { + /** + * Disable timestamps. + * + * @var boolean + */ + public $timestamps = false; + /** * The table associated with the model. * diff --git a/Herbert/Framework/Models/Post.php b/Herbert/Framework/Models/Post.php index 59dcf3a..9f9857c 100644 --- a/Herbert/Framework/Models/Post.php +++ b/Herbert/Framework/Models/Post.php @@ -1,12 +1,16 @@ where('post_type', $type); } + /** + * Set the value of the "created at" attribute. + * + * @param mixed $value + * @return void + */ + public function setCreatedAt($value) + { + $this->{static::CREATED_AT} = $value; + + if ( ! $value instanceof Carbon) + { + $value = new Carbon($value); + } + + $this->{static::CREATED_AT . '_gmt'} = $value->timezone('GMT'); + } + + /** + * Set the value of the "updated at" attribute. + * + * @param mixed $value + * @return void + */ + public function setUpdatedAt($value) + { + $this->{static::UPDATED_AT} = $value; + + if ( ! $value instanceof Carbon) + { + $value = new Carbon($value); + } + + $this->{static::UPDATED_AT . '_gmt'} = $value->timezone('GMT'); + } + } diff --git a/Herbert/Framework/Models/PostMeta.php b/Herbert/Framework/Models/PostMeta.php index d63afa7..a4380c1 100644 --- a/Herbert/Framework/Models/PostMeta.php +++ b/Herbert/Framework/Models/PostMeta.php @@ -7,6 +7,13 @@ */ class PostMeta extends Model { + /** + * Disable timestamps. + * + * @var boolean + */ + public $timestamps = false; + /** * The table associated with the model. * diff --git a/Herbert/Framework/Models/SoftDeletes/SoftDeletes.php b/Herbert/Framework/Models/SoftDeletes/SoftDeletes.php new file mode 100644 index 0000000..b9676fd --- /dev/null +++ b/Herbert/Framework/Models/SoftDeletes/SoftDeletes.php @@ -0,0 +1,95 @@ +newQuery()->where($this->getKeyName(), $this->getKey()); + + $this->{$this->getDeletedAtColumn()} = 'trash'; + + $query->update([$this->getDeletedAtColumn() => 'trash']); + } + + /** + * Restore a soft-deleted model instance. + * + * @return bool|null + */ + public function restore() + { + // If the restoring event does not return false, we will proceed with this + // restore operation. Otherwise, we bail out so the developer will stop + // the restore totally. We will clear the deleted timestamp and save. + if ($this->fireModelEvent('restoring') === false) { + return false; + } + + $this->{$this->getDeletedAtColumn()} = 'publish'; + + // Once we have saved the model, we will fire the "restored" event so this + // developer will do anything they need to after a restore operation is + // totally finished. Then we will return the result of the save call. + $this->exists = true; + + $result = $this->save(); + + $this->fireModelEvent('restored', false); + + return $result; + } + + /** + * Determine if the model instance has been soft-deleted. + * + * @return bool + */ + public function trashed() + { + return $this->{$this->getDeletedAtColumn()} === 'trash'; + } + + /** + * Get a new query builder that includes soft deletes. + * + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public static function withTrashed() + { + return (new static)->newQueryWithoutScope(new SoftDeletingScope)->withTrashed(); + } + + /** + * Get a new query builder that only includes soft deletes. + * + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public static function onlyTrashed() + { + $instance = new static; + + $column = $instance->getQualifiedDeletedAtColumn(); + + return $instance->newQueryWithoutScope(new SoftDeletingScope)->onlyTrashed(); + } + +} diff --git a/Herbert/Framework/Models/SoftDeletes/SoftDeletingScope.php b/Herbert/Framework/Models/SoftDeletes/SoftDeletingScope.php new file mode 100644 index 0000000..a9a89bc --- /dev/null +++ b/Herbert/Framework/Models/SoftDeletes/SoftDeletingScope.php @@ -0,0 +1,130 @@ +where($model->getQualifiedDeletedAtColumn(), '!=', 'trash'); + + $this->extend($builder); + } + + /** + * Remove the scope from the given Eloquent query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function remove(Builder $builder, Model $model) + { + $column = $model->getQualifiedDeletedAtColumn(); + + $query = $builder->getQuery(); + + $query->wheres = collect($query->wheres)->reject(function ($where) use ($column) { + return $this->isSoftDeleteConstraint($where, $column); + })->values()->all(); + + $bindings = $query->getRawBindings(); + + foreach ($bindings['where'] as $k => $v) + { + if ($v !== 'trash') + { + continue; + } + + unset($bindings['where'][$k]); + + break; + } + + $query->setBindings($bindings['where']); + } + + /** + * Extend the query builder with the needed functions. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function extend(Builder $builder) + { + foreach ($this->extensions as $extension) { + $this->{"add{$extension}"}($builder); + } + + $builder->onDelete(function (Builder $builder) { + $column = $this->getDeletedAtColumn($builder); + + return $builder->update([ + $column => 'trash', + ]); + }); + } + + /** + * Add the restore extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addRestore(Builder $builder) + { + $builder->macro('restore', function (Builder $builder) { + $builder->withTrashed(); + + return $builder->update([$builder->getModel()->getDeletedAtColumn() => 'publish']); + }); + } + + /** + * Add the only-trashed extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addOnlyTrashed(Builder $builder) + { + $builder->macro('onlyTrashed', function (Builder $builder) { + $model = $builder->getModel(); + + $this->remove($builder, $model); + + $builder->getQuery()->where($model->getQualifiedDeletedAtColumn(), 'trash'); + + return $builder; + }); + } + + /** + * Determine if the given where clause is a soft delete constraint. + * + * @param array $where + * @param string $column + * @return bool + */ + protected function isSoftDeleteConstraint(array $where, $column) + { + return $where === [ + 'type' => 'Basic', + 'column' => $column, + 'operator' => '!=', + 'value' => 'trash', + 'boolean' => 'and' + ]; + } + +} diff --git a/Herbert/Framework/Models/Taxonomy.php b/Herbert/Framework/Models/Taxonomy.php index fc621d9..109d2ef 100644 --- a/Herbert/Framework/Models/Taxonomy.php +++ b/Herbert/Framework/Models/Taxonomy.php @@ -7,6 +7,13 @@ */ class Taxonomy extends Model { + /** + * Disable timestamps. + * + * @var boolean + */ + public $timestamps = false; + /** * The table associated with the model. * diff --git a/Herbert/Framework/Models/Term.php b/Herbert/Framework/Models/Term.php index b3ea6e6..8c69039 100644 --- a/Herbert/Framework/Models/Term.php +++ b/Herbert/Framework/Models/Term.php @@ -7,6 +7,13 @@ */ class Term extends Model { + /** + * Disable timestamps. + * + * @var boolean + */ + public $timestamps = false; + /** * The table associated with the model. * diff --git a/Herbert/Framework/Notifier.php b/Herbert/Framework/Notifier.php index c72d98a..7563eb3 100644 --- a/Herbert/Framework/Notifier.php +++ b/Herbert/Framework/Notifier.php @@ -8,7 +8,7 @@ class Notifier { /** * The notifier instance. * - * @var \Herbet\Framework\Notifier + * @var \Herbert\Framework\Notifier */ protected static $instance; @@ -24,51 +24,74 @@ class Notifier { */ public function __construct() { + if ( ! self::$instance) + { + self::$instance = $this; + + $this->gatherFlashed(); + } + add_action('admin_notices', [$this, 'sendNotices']); + add_action('shutdown', [$this, 'sendNotices']); } /** * Adds a notice. * - * @param $message - * @param string $class + * @param string $message + * @param string $class + * @param boolean $flash */ - protected function notify($message, $class = 'updated') + protected function notify($message, $class = 'updated', $flash = false) { - $this->notices[] = [ + $notification = [ 'message' => $message, 'class' => $class ]; + + if ( ! $flash) + { + $this->notices[] = $notification; + + return; + } + + $notices = session('__notifier_flashed', []); + $notices[] = $notification; + session()->getFlashBag()->set('__notifier_flashed', $notices); } /** * Adds a success notice. * - * @param $message + * @param string $message + * @param boolean $flash */ - protected function success($message) + protected function success($message, $flash = false) { - $this->notify($message, 'updated'); + $this->notify($message, 'updated', $flash); } /** * Adds a warning notice. * - * @param $message + * @param string $message + * @param boolean $flash */ - protected function warning($message) + protected function warning($message, $flash = false) { - $this->notify($message, 'update-nag'); + $this->notify($message, 'update-nag', $flash); } /** * Adds an error notice. * - * @param $message + * @param string $message + * @param boolean $flash */ - protected function error($message) + protected function error($message, $flash = false) { - $this->notify($message, 'error'); + $this->notify($message, 'error', $flash); } /** @@ -82,6 +105,18 @@ public function sendNotices() { echo "

{$notice['message']}

"; } + + $this->notices = []; + } + + /** + * Gathers all the flashed notify messages. + * + * @return void + */ + protected function gatherFlashed() + { + $this->notices = session()->getFlashBag()->get('__notifier_flashed', []); } /** @@ -95,7 +130,7 @@ public static function __callStatic($name, $arguments) { if ( ! self::$instance) { - self::$instance = new self; + new self; } return call_user_func_array([self::$instance, $name], $arguments); diff --git a/Herbert/Framework/Panel.php b/Herbert/Framework/Panel.php index e16aaac..c23d43b 100644 --- a/Herbert/Framework/Panel.php +++ b/Herbert/Framework/Panel.php @@ -1,14 +1,27 @@ app = $app; + $this->http = $http; + + if ( ! is_admin()) + { + return; + } add_action('admin_menu', [$this, 'boot']); + + $http->setMethod($http->get('_method'), $old = $http->method()); + + if ( ! in_array($http->method(), self::$methods)) + { + $http->setMethod($old); + } + + if ($http->method() !== 'GET') + { + add_action('init', [$this, 'bootEarly']); + } } /** @@ -74,6 +111,31 @@ public function boot() } } + /** + * Boots early. + * + * @return void + */ + public function bootEarly() + { + if (($slug = $this->http->get('page')) === null) + { + return; + } + + if (($panel = $this->getPanel($slug, true)) === null) + { + return; + } + + if ( ! $this->handler($panel, true)) + { + return; + } + + die; + } + /** * Adds a panel. * @@ -125,7 +187,7 @@ public function add($data, $uses = null) $data['as'] = $this->namespaceAs($data['as']); } - if (isset($data['parent'])) + if ($data['type'] === 'sub-panel' && isset($data['parent'])) { $data['parent'] = $this->namespaceAs($data['parent']); } @@ -144,10 +206,11 @@ protected function addPanel($panel) add_menu_page( $panel['title'], $panel['title'], - 'manage_options', + isset($panel['capability']) && $panel['capability'] ? $panel['capability'] : 'manage_options', $panel['slug'], - $this->makeCallable($panel['uses']), - isset($panel['icon']) ? $this->fetchIcon($panel['icon']) : '' + $this->makeCallable($panel), + isset($panel['icon']) ? $this->fetchIcon($panel['icon']) : '', + isset($panel['order']) ? $panel['order'] : null ); if (isset($panel['rename']) && !empty($panel['rename'])) @@ -183,9 +246,9 @@ protected function addSubPanel($panel) $panel['parent'], $panel['title'], $panel['title'], - 'manage_options', + isset($panel['capability']) && $panel['capability'] ? $panel['capability'] : 'manage_options', $panel['slug'], - isset($panel['rename']) && $panel['rename'] ? null : $this->makeCallable($panel['uses']) + isset($panel['rename']) && $panel['rename'] ? null : $this->makeCallable($panel) ); } @@ -202,8 +265,8 @@ protected function fetchIcon($icon) return ''; } - if (substr($icon, 0, 9) === "dashicons" || substr($icon, 0, 5) === "data:" - || substr($icon, 0, 2) === "//" || $icon == 'none') + if (substr($icon, 0, 9) === 'dashicons' || substr($icon, 0, 5) === 'data:' + || substr($icon, 0, 2) === '//' || $icon == 'none') { return $icon; } @@ -214,13 +277,13 @@ protected function fetchIcon($icon) /** * Makes a callable for the panel hook. * - * @param $callable + * @param $panel * @return callable */ - protected function makeCallable($callable) + protected function makeCallable($panel) { - return function () use ($callable) { - $this->call($callable); + return function () use ($panel) { + return $this->handler($panel); }; } @@ -237,8 +300,20 @@ protected function call($callable) ['app' => $this->app] ); + if ($response instanceof RedirectResponse) + { + $response->flash(); + } + if ($response instanceof Response) { + status_header($response->getStatusCode()); + + foreach ($response->getHeaders() as $key => $value) + { + @header($key . ': ' . $value); + } + echo $response->getBody(); return; @@ -257,29 +332,67 @@ protected function call($callable) return; } + + throw new Exception('Unknown response type!'); } /** - * Get the URL to a panel. + * Gets a panel. * - * @param string $name - * @return string + * @param string $name + * @param boolean $slug + * @return array */ - public function url($name) + protected function getPanel($name, $slug = false) { + $slug = $slug ? 'slug' : 'as'; + foreach ($this->panels as $panel) { - if (array_get($panel, 'as') !== $name) + if (array_get($panel, $slug) !== $name) { continue; } - return menu_page_url(array_get($panel, 'slug'), false); + return $panel; } return null; } + /** + * Gets the panels. + * + * @return array + */ + public function getPanels() + { + return array_values($this->panels); + } + + /** + * Get the URL to a panel. + * + * @param string $name + * @return string + */ + public function url($name) + { + if (($panel = $this->getPanel($name)) === null) + { + return null; + } + + $slug = array_get($panel, 'slug'); + + if (array_get($panel, 'type') === 'wp-sub-panel') + { + return admin_url(add_query_arg('page', $slug, array_get($panel, 'parent'))); + } + + return admin_url('admin.php?page=' . $slug); + } + /** * Sets the current namespace. * @@ -317,4 +430,67 @@ protected function namespaceAs($as) return $this->namespace . '::' . $as; } + + /** + * Return the correct callable based on action + * + * @param array $panel + * @param boolean $strict + * @return void + */ + protected function handler($panel, $strict = false) + { + $callable = $uses = $panel['uses']; + $method = strtolower($this->http->method()); + $action = strtolower($this->http->get('action', 'uses')); + + $callable = array_get($panel, $method, false) ?: $callable; + + if ($callable === $uses || is_array($callable)) + { + $callable = array_get($panel, $action, false) ?: $callable; + } + + if ($callable === $uses || is_array($callable)) + { + $callable = array_get($panel, "{$method}.{$action}", false) ?: $callable; + } + + if (is_array($callable)) + { + $callable = $uses; + } + + if ($strict && $uses === $callable) + { + return false; + } + + try { + $this->call($callable); + } catch (HttpErrorException $e) { + if ($e->getStatus() === 301 || $e->getStatus() === 302) + { + $this->call(function () use (&$e) + { + return $e->getResponse(); + }); + } + + global $wp_query; + $wp_query->set_404(); + + status_header($e->getStatus()); + + define('HERBERT_HTTP_ERROR_CODE', $e->getStatus()); + define('HERBERT_HTTP_ERROR_MESSAGE', $e->getMessage()); + + Notifier::error('' . $e->getStatus() . ': ' . $e->getMessage()); + + do_action('admin_notices'); + } + + return true; + } + } diff --git a/Herbert/Framework/Providers/HerbertServiceProvider.php b/Herbert/Framework/Providers/HerbertServiceProvider.php index 5b8c476..6bb5826 100644 --- a/Herbert/Framework/Providers/HerbertServiceProvider.php +++ b/Herbert/Framework/Providers/HerbertServiceProvider.php @@ -2,6 +2,8 @@ use Illuminate\Database\Capsule\Manager as Capsule; use Illuminate\Support\ServiceProvider; +use Illuminate\Cookie\CookieJar; +use Herbert\Framework\Session; /** * @see http://getherbert.com @@ -83,6 +85,36 @@ public function register() 'widget', 'Herbert\Framework\Widget' ); + + $this->app->instance( + 'session', + $this->app->make('Herbert\Framework\Session', ['app' => $this->app]) + ); + + $this->app->alias( + 'session', + 'Herbert\Framework\Session' + ); + + $this->app->instance( + 'notifier', + $this->app->make('Herbert\Framework\Notifier', ['app' => $this->app]) + ); + + $this->app->alias( + 'notifier', + 'Herbert\Framework\Notifier' + ); + + $this->app->singleton( + 'errors', + function () + { + return session_flashed('__validation_errors', []); + } + ); + + $_GLOBALS['errors'] = $this->app['errors']; } /** @@ -103,7 +135,7 @@ protected function registerEloquent() 'username' => DB_USER, 'password' => DB_PASSWORD, 'charset' => DB_CHARSET, - 'collation' => 'utf8_unicode_ci', + 'collation' => DB_COLLATE ?: $wpdb->collate, 'prefix' => $wpdb->prefix ]); @@ -111,4 +143,14 @@ protected function registerEloquent() $capsule->bootEloquent(); } + /** + * Boots the service provider. + * + * @return void + */ + public function boot() + { + $this->app['session']->start(); + } + } diff --git a/Herbert/Framework/Providers/TwigServiceProvider.php b/Herbert/Framework/Providers/TwigServiceProvider.php index b1ef8e6..cce2573 100644 --- a/Herbert/Framework/Providers/TwigServiceProvider.php +++ b/Herbert/Framework/Providers/TwigServiceProvider.php @@ -20,7 +20,7 @@ public function register() { $this->app->singleton('twig.loader', function () { - $loader = new Twig_Loader_Filesystem('/'); + $loader = new Twig_Loader_Filesystem(); foreach ($this->app->getPlugins() as $plugin) { @@ -35,7 +35,7 @@ public function register() return [ 'debug' => $this->app->environment() === 'local', 'charset' => 'utf-8', - 'cache' => ABSPATH . 'wp-content/twig-cache', + 'cache' => content_directory() . '/twig-cache', 'auto_reload' => true, 'strict_variables' => false, 'autoescape' => true, @@ -49,8 +49,13 @@ public function register() 'dd', 'herbert', 'view', + 'content_directory', + 'plugin_directory', 'panel_url', - 'route_url' + 'route_url', + 'session', + 'session_flashed', + 'errors' ]; }); @@ -84,6 +89,8 @@ public function constructTwig() $twig->addGlobal($key, $value); } + $twig->addGlobal('errors', $this->app['errors']); + foreach ((array) $this->app['twig.functions'] as $function) { $twig->addFunction(new Twig_SimpleFunction($function, $function)); diff --git a/Herbert/Framework/RedirectResponse.php b/Herbert/Framework/RedirectResponse.php new file mode 100644 index 0000000..8ce9829 --- /dev/null +++ b/Herbert/Framework/RedirectResponse.php @@ -0,0 +1,108 @@ +target = $url; + $this->updateBody(); + $this->updateHeaders(); + } + + /** + * Update the body. + * + * @return void + */ + protected function updateBody() + { + $this->body = sprintf(' + + + + + + Redirecting to %1$s + + + Redirecting to %1$s. + + + +', str_replace('"', '\\"', $this->target)); + } + + /** + * Update the headers. + * + * @return void + */ + protected function updateHeaders() + { + array_set($this->headers, 'Location', $this->target); + } + + /** + * Flashes the :key of value :val to the session. + * + * @param string $key + * @param mixed $val + * @return \Herbert\Framework\RedirectResponse + */ + public function with($key, $val = null) + { + if ( ! is_array($key)) + { + $key = [$key => $val]; + } + + foreach ($key as $k => $v) + { + array_set($this->data, $k, $v); + } + + return $this; + } + + /** + * Actually flashes in the session data. + * + * @return \Herbert\Framework\RedirectResponse + */ + public function flash() + { + $bag = session()->getFlashBag(); + + foreach ($this->data as $key => $val) + { + $bag->add($key, $val); + } + + return $this; + } + +} diff --git a/Herbert/Framework/Route.php b/Herbert/Framework/Route.php index 82b6c23..457fd73 100644 --- a/Herbert/Framework/Route.php +++ b/Herbert/Framework/Route.php @@ -1,6 +1,6 @@ app = $app; $this->parameters = $parameters; $this->uri = $data['uri']; - $this->name = isset($data['as']) ? $data['as'] : $this->uri; + $this->name = array_get($data, 'as', $this->uri); $this->uses = $data['uses']; } @@ -73,7 +73,7 @@ public function handle() return new JsonResponse($response); } - var_dump($response); + throw new Exception('Unknown response type!'); } /** @@ -85,7 +85,7 @@ public function handle() */ public function parameter($name, $default = null) { - if (!isset($this->parameters[$name])) + if ( ! isset($this->parameters[$name])) { return $default; } diff --git a/Herbert/Framework/Router.php b/Herbert/Framework/Router.php index 8455f35..0af15af 100644 --- a/Herbert/Framework/Router.php +++ b/Herbert/Framework/Router.php @@ -2,6 +2,7 @@ use Closure; use InvalidArgumentException; +use Herbert\Framework\Exceptions\HttpErrorException; /** * @see http://getherbert.com @@ -93,11 +94,15 @@ public function __construct(Application $app, Http $http) public function boot() { add_rewrite_tag('%herbert_route%', '(.+)'); - - foreach ($this->routes[$this->http->method()] as $id => $route) + + if(is_array($this->routes[$this->http->method()])) { - $this->addRoute($route, $id, $this->http->method()); + foreach ($this->routes[$this->http->method()] as $id => $route) + { + $this->addRoute($route, $id, $this->http->method()); + } } + } /** @@ -232,12 +237,41 @@ public function parseRequest($wp) $data['parameters'][$key] = $wp->query_vars['herbert_param_' . $key]; } - $this->processRequest( - $this->buildRoute( - $route, - $data['parameters'] - ) - ); + try { + $this->processRequest( + $this->buildRoute( + $route, + $data['parameters'] + ) + ); + } catch (HttpErrorException $e) { + if ($e->getStatus() === 301 || $e->getStatus() === 302) + { + $this->processResponse($e->getResponse()); + + die; + } + + if ($e->getStatus() === 404) + { + global $wp_query; + $wp_query->set_404(); + } + + status_header($e->getStatus()); + + define('HERBERT_HTTP_ERROR_CODE', $e->getStatus()); + define('HERBERT_HTTP_ERROR_MESSAGE', $e->getMessage()); + + if ($e->getStatus() === 404) + { + @include get_404_template(); + } + else + { + echo $e->getMessage(); + } + } die; } @@ -258,11 +292,25 @@ protected function buildRoute($data, $params) * Processes a request. * * @param \Herbert\Framework\Route $route - * @return mixed + * @return void */ protected function processRequest(Route $route) { - $response = $route->handle(); + $this->processResponse($route->handle()); + } + + /** + * Processes a response. + * + * @param \Herbert\Framework\Response $response + * @return void + */ + protected function processResponse(Response $response) + { + if ($response instanceof RedirectResponse) + { + $response->flash(); + } status_header($response->getStatusCode()); diff --git a/Herbert/Framework/Session.php b/Herbert/Framework/Session.php new file mode 100644 index 0000000..815f499 --- /dev/null +++ b/Herbert/Framework/Session.php @@ -0,0 +1,9 @@ +renameArguments($arguments, $attributes); } - return $this->app->call( + if (is_string($callable) && strpos($callable, '::') !== false) + { + list($api, $method) = explode('::', $callable); + + global $$api; + + if ($$api === null) + { + throw new Exception("API '{$api}' not set!"); + } + + $callable = $$api->get($method); + + if ($callable === null) + { + throw new Exception("Method '{$method}' not set!"); + } + } + + $response = $this->app->call( $callable, array_merge([ '_attributes' => $attributes, '_content' => $content ], $attributes) ); + + if ($response instanceof RedirectResponse) + { + $response->flash(); + } + + if ($response instanceof Response) + { + status_header($response->getStatusCode()); + + foreach ($response->getHeaders() as $key => $value) + { + @header($key . ': ' . $value); + } + + return $response->getBody(); + } + + if (is_null($response) || is_string($response)) + { + return $response; + } + + if (is_array($response) || $response instanceof Jsonable || $response instanceof JsonSerializable) + { + return (new JsonResponse($response))->getBody(); + } + + throw new Exception('Unknown response type!'); }); } diff --git a/Herbert/Framework/Widget.php b/Herbert/Framework/Widget.php index 5a2d775..c4e9a92 100644 --- a/Herbert/Framework/Widget.php +++ b/Herbert/Framework/Widget.php @@ -60,7 +60,7 @@ public function boot() public function add($widget, Plugin $plugin = null) { $this->widgets[] = [ - 'widget' => $widget, + 'class' => $widget, 'plugin' => $plugin ]; } diff --git a/bootstrap/autoload.php b/bootstrap/autoload.php index adc77e3..ae2859d 100644 --- a/bootstrap/autoload.php +++ b/bootstrap/autoload.php @@ -28,7 +28,8 @@ /** * Load all herbert.php files in plugin roots. */ -$iterator = new DirectoryIterator(ABSPATH . 'wp-content/plugins'); +$iterator = new DirectoryIterator(plugin_directory()); + foreach ($iterator as $directory) { @@ -46,7 +47,10 @@ $config = $herbert->getPluginConfig($root); - register_activation_hook($root . '/plugin.php', function () use ($herbert, $config, $root) + $plugin = substr($root . '/plugin.php', strlen(plugin_directory())); + $plugin = ltrim($plugin, '/'); + + register_activation_hook($plugin, function () use ($herbert, $config, $root) { if ( ! $herbert->pluginMatches($config)) { @@ -58,12 +62,16 @@ $herbert->activatePlugin($root); }); - register_deactivation_hook($root . '/plugin.php', function () use ($herbert, $root) + register_deactivation_hook($plugin, function () use ($herbert, $root) { $herbert->deactivatePlugin($root); }); - if ( ! is_plugin_active(substr($root, strlen(ABSPATH . 'wp-content/plugins/')) . '/plugin.php')) + // Ugly hack to make the install hook work correctly + // as WP doesn't allow closures to be passed here + register_uninstall_hook($plugin, create_function('', 'herbert()->deletePlugin(\'' . $root . '\');')); + + if ( ! is_plugin_active($plugin)) { continue; } @@ -76,6 +84,9 @@ } $herbert->pluginMatched($root); + + @require_once $root.'/plugin.php'; + $herbert->loadPlugin($config); } diff --git a/bootstrap/helpers.php b/bootstrap/helpers.php index 9aa15f8..2ea9328 100644 --- a/bootstrap/helpers.php +++ b/bootstrap/helpers.php @@ -15,6 +15,32 @@ function dd() } } +if ( ! function_exists('content_directory')) +{ + /** + * Gets the content directory. + * + * @return string + */ + function content_directory() + { + return WP_CONTENT_DIR; + } +} + +if ( ! function_exists('plugin_directory')) +{ + /** + * Gets the plugin directory. + * + * @return string + */ + function plugin_directory() + { + return WP_PLUGIN_DIR; + } +} + if ( ! function_exists('response')) { /** @@ -47,6 +73,22 @@ function json_response($jsonable, $status = 200, $headers = null) } } +if ( ! function_exists('redirect_response')) +{ + /** + * Generates a redirect response. + * + * @param string $url + * @param integer $status + * @param array $headers + * @return \Herbert\Framework\Response + */ + function redirect_response($url, $status = 302, $headers = null) + { + return new Herbert\Framework\RedirectResponse($url, $status, $headers); + } +} + if ( ! function_exists('herbert')) { /** @@ -68,6 +110,68 @@ function herbert($binding = null) } } +if ( ! function_exists('errors')) +{ + /** + * Get the errors. + * + * @param string key + * @return array + */ + function errors($key = null) + { + $errors = herbert('errors'); + $errors = isset($errors[0]) ? $errors[0] : $errors; + + if (!$key) + { + return $errors; + } + + return array_get($errors, $key); + } +} + +if ( ! function_exists('session')) +{ + /** + * Gets the session or a key from the session. + * + * @param string $key + * @param mixed $default + * @return \Illuminate\Session\Store|mixed + */ + function session($key = null, $default = null) + { + if ($key === null) + { + return herbert('session'); + } + + return herbert('session')->get($key, $default); + } +} + +if ( ! function_exists('session_flashed')) +{ + /** + * Gets the session flashbag or a key from the session flashbag. + * + * @param string $key + * @param mixed $default + * @return \Illuminate\Session\Store|mixed + */ + function session_flashed($key = null, $default = []) + { + if ($key === null) + { + return herbert('session')->getFlashBag(); + } + + return herbert('session')->getFlashBag()->get($key, $default); + } +} + if ( ! function_exists('view')) { /** @@ -79,7 +183,7 @@ function herbert($binding = null) */ function view($name, $context = []) { - return herbert('Twig_Environment')->render($name, $context); + return response(herbert('Twig_Environment')->render($name, $context)); } } @@ -89,11 +193,12 @@ function view($name, $context = []) * Gets the url to a panel. * * @param string $name + * @param array $query * @return string */ - function panel_url($name) + function panel_url($name, $query = []) { - return herbert('panel')->url($name); + return add_query_arg($query, herbert('panel')->url($name)); } } @@ -104,10 +209,11 @@ function panel_url($name) * * @param string $name * @param array $args + * @param array $query * @return string */ - function route_url($name, $args = []) + function route_url($name, $args = [], $query = []) { - return herbert('router')->url($name, $args); + return add_query_arg($query, herbert('router')->url($name, $args)); } } diff --git a/composer.json b/composer.json index ab2e78a..274ddd8 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,17 @@ { - "name": "getherbert/herbert", + "name": "lexisvar/framework", "description": "Herbert", "license": "MIT", "require": { "twig/twig": "~1.16", - "illuminate/database": "~5.1", + "illuminate/database": "~5.0", "vierbergenlars/php-semver": "~3.0", "symfony/var-dumper": "~3.0", - "illuminate/http": "~5.1" + "illuminate/http": "~5.0" }, "autoload": { "psr-4": { - "Herbert\\Framework\\": "Herbert/Framework/" + "Herbert\\Framework\\": "Lexisvar/Framework/" } }, "config": {