加入後台選單建立機制,後台設定選項建立機制

This commit is contained in:
kroutony 2020-02-22 20:43:41 +08:00
parent 568d481742
commit 51f9e57e6a
40 changed files with 1859 additions and 4 deletions

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class AdminMenuItemArgInvalidException extends Exception
{
}

View File

@ -0,0 +1,192 @@
<?php
namespace App\Http\Controllers\Admin\Menu;
use Auth;
use View;
use Arr;
use Gate;
use Str;
use Illuminate\Http\Request;
/**
* Class BaseMenuItemController
* @package App\Http\Controllers\Admin\Menu
*/
abstract class BaseMenuItemController
{
/**
* 顯示名稱
*
* @var string
*/
protected $name;
/**
* Slug
*
* @var string
*/
protected $slug;
/**
* 所需權限,有一個符合即可
* 如沒有填入,代表不設限
* 子選單預設會沿用父選單的權限,如有填入,則再加上自己的權限
*
* @var array
*/
protected $permissions = [];
/**
* 選單項目的Icon類別
*
* @var array
*/
protected $iconClasses = [];
/**
* @var array
*/
protected $badge;
/**
* Default route handler
*/
public function handle(Request $request)
{
abort(404);
}
/**
* @return string
*/
public function getName()
{
return trans($this->name);
}
/**
* @param string $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* @return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* @param string $slug
*/
public function setSlug($slug): void
{
$this->slug = $slug;
}
/**
* @return array
*/
public function getPermissions(): array
{
return $this->permissions;
}
/**
* @param array $permissions
*/
public function setPermissions(array $permissions): void
{
$this->permissions = $permissions;
}
/**
* Get bootstrap badge arguments
*
* @return array|null
*/
public function getBadge()
{
if(!empty($this->badge['label']) && !empty($this->badge['type'])) {
return Arr::add($this->badge, 'isPill', false);
} else {
return null;
}
}
/**
* Set bootstrap badge arguments
*
* @param array $badge
*/
public function setBadge(array $badge): void
{
$this->badge = $badge;
}
/**
* @return string|array
*/
public function getIconClass(): string
{
if(is_array($this->iconClasses)) {
return implode(' ', $this->iconClasses);
} else {
return $this->iconClasses;
}
}
/**
* @param array|string $iconClasses
*/
public function setIconClasses($iconClasses): void
{
$this->iconClasses = $iconClasses;
}
/**
* @return bool
*/
public function hasIcon()
{
return !empty($this->getIconClass());
}
/**
* @return bool
*/
public function hasBadge()
{
return !empty($this->badge);
}
/**
* @return bool
* @throws \Exception
*/
public function userHasPermission()
{
$hasAnyPermission = false;
foreach ($this->permissions as $permission) {
$hasAnyPermission = $hasAnyPermission || Gate::allows('permission:' . Str::kebab($permission));
}
return Auth::check() &&
((!empty($this->permissions) && $hasAnyPermission) || empty($this->permissions));
}
/**
* Valid if both name and slug are not empty
*
* @return bool
*/
public function isValid()
{
return $this->getName() && $this->getSlug();
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Options\Children;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
use Gate;
use Illuminate\Http\Request;
class DevelopmentMenuItemController extends BaseMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.options.development';
$this->slug = 'development';
$this->permissions = ['admin manage options development'];
$this->iconClasses = 'nav-icon icon-settings';
}
public function handle(Request $request)
{
return view('admin.menu.options.development');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Options\Children;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
use Gate;
use Illuminate\Http\Request;
class GeneralMenuItemController extends BaseMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.options.general';
$this->slug = 'general';
$this->permissions = ['admin manage options general'];
$this->iconClasses = 'nav-icon icon-settings';
}
public function handle(Request $request)
{
return view('admin.menu.options.general');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Options\Children;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
use Gate;
use Illuminate\Http\Request;
class PlatformMenuItemController extends BaseMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.options.platform';
$this->slug = 'platform';
$this->permissions = ['admin manage options platform'];
$this->iconClasses = 'nav-icon icon-settings';
}
public function handle(Request $request)
{
return view('admin.menu.options.platform');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Options;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
class OptionsMenuItemController extends BaseMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.options.options';
$this->slug = 'options';
$this->permissions = ['admin view options'];
$this->iconClasses = 'nav-icon icon-settings';
}
}

View File

@ -0,0 +1,120 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Exceptions\AdminOptionsInvalidException;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Gate;
use Arr;
use mysql_xdevapi\Exception;
use Str;
use Validator;
/**
* 網站設定選項
* Class OptionsController
* @package App\Http\Controllers\Admin
*/
class OptionsController extends Controller
{
private $request;
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* 檢查權限
*
* @param $gateName
*/
private function gate($gateName)
{
if(Gate::denies($gateName)) {
abort(403);
}
}
/**
* 驗證欄位
*
* @param $options
* @throws \Illuminate\Validation\ValidationException
*/
private function validateOptions($options)
{
$rules = collect($options['fields'])
->filter(function($item, $key){
return isset($item['validator']);
})
->map(function($item, $key){
return $item['validator'];
});
$this->validate($this->request, $rules->all());
}
/**
* 更新欄位資料
*
* @param $options
*/
private function update($options)
{
$fields = $this->request->only(array_keys($options['fields']));
$checkboxOptions = collect($options['fields'])
->filter(function($item, $key){
return isset($item['type']) && $item['type'] = 'checkbox';
});
foreach ($fields as $key => $value) {
app('Option')->$key = $value;
}
foreach ($checkboxOptions as $key => $checkboxOption) {
if(!$this->request->has($key)) {
app('Option')->$key = '';
}
}
}
/**
* 處理更新選項的動作並回應
*
* @param $options
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Validation\ValidationException
*/
private function generateResponse($options)
{
$this->validateOptions($options);
$this->update($options);
return back()->with(['updated' => trans('message.success')]);
}
/**
* 處理更新選項的動作
*
* @param string $method
* @param array $args
* @return \Illuminate\Http\RedirectResponse|mixed
* @throws \Exception
*/
public function __call($method, $args = [])
{
$config = config('admin.options');
if(preg_match('/^update/', $method)) {
$page = strtolower(preg_replace('/^update/', '', $method));
if(in_array($page, array_keys($config))) {
if(!empty($config[$page]['permission'])) {
$this->gate('permission:' . Str::kebab($config[$page]['permission']));
}
return $this->generateResponse($config[$page]);
}
}
throw new \Exception("method: $method dos not exist");
}
}

View File

@ -66,6 +66,7 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'admin.area' => \App\Http\Middleware\AdminAreaGuard::class,
'admin.menu' => \App\Http\Middleware\CheckAdminMenuPagePermission::class,
];
/**

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Route;
use Arr;
/**
* 檢查Admin選單項目的route權限
*
* Class CheckAdminMenuPagePermission
* @package App\Http\Middleware
*/
class CheckAdminMenuPagePermission
{
/**
* 處理並檢查Admin選單項目的route權限
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
//取得目前的Route
$currentRoute = Route::current();
//取得目前的Route Controller
$menuController = $currentRoute->controller;
//取得目前的Route Method
$method = $currentRoute->getActionMethod();
//如果Controller為選單項目的子類別
if(is_subclass_of($menuController, \App\Http\Controllers\Admin\Menu\BaseMenuItemController::class)) {
//如果方法為預設的route方法
if($method == 'handle') {
//取得slug組成的route name
$routeName = str_replace(config('admin.route_name_prefix') . config('admin.menu.route_name_prefix'), '', Route::currentRouteName());
//取得Admin選單項目
$adminMenu = app('AdminMenu')->getMenu();
//分解Slug
$slugs = explode('.', $routeName);
//如果slug數量大於1則為子選單controller
if(sizeof($slugs) > 1) {
$permission = Arr::get($adminMenu, "{$slugs[0]}.children.{$slugs[1]}.userHasPermission");
} else {
$permission = Arr::get($adminMenu, "{$slugs[0]}.userHasPermission");
}
if(!$permission) {
abort(403);
}
}
}
return $next($request);
}
}

15
app/Option.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Option extends Model
{
protected $fillable = ['key', 'value'];
public function scopeKey($query, $key)
{
return $query->where('key', $key);
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Presenters\Admin;
use App\Presenters\Html\HtmlPresenter;
class MainMenuItemPresenter
{
private $htmlPresenter;
public function __construct(HtmlPresenter $htmlPresenter)
{
$this->htmlPresenter = $htmlPresenter;
}
/**
* 檢查選單項目權限
*
* @param array $item
* @return mixed
*/
private function checkMenuItemPermission($item)
{
return !empty($item['userHasPermission']) && $item['userHasPermission'];
}
/**
* Check if any child has permission
*
* @param $item
* @return bool
*/
private function checkAnyChildPermission($item)
{
if(isset($item['children'])) {
$anyChildHasPermission = false;
foreach ($item['children'] as $childMenu) {
$anyChildHasPermission = $anyChildHasPermission || $childMenu['userHasPermission'];
}
return $anyChildHasPermission;
} else {
return true;
}
}
/**
* 回傳選單項目Html
*
* return string
*/
public function render()
{
$menu = app('AdminMenu')->getMenu();
$html = '';
$htmlPresenter = $this->htmlPresenter;
foreach ($menu as $parentKey => $item) {
switch($item['type']) {
case 'title':
$html .= $htmlPresenter->li([
'class' => 'nav-title',
'html' => trans('menu.titles.' . $item['name'])
]);
break;
case 'item':
if($this->checkMenuItemPermission($item) && $this->checkAnyChildPermission($item)) {
$hasChildren = !empty($item['children']);
$html .= $htmlPresenter->li([
'class' => [
'nav-item',
$hasChildren ? 'nav-dropdown has-children' : '',
"slug-{$item['slug']}"
],
'html' => [
$htmlPresenter->a([
'class' => [
'nav-link',
$hasChildren ? 'nav-dropdown-toggle' : '',
],
'href' => $item['link'],
'html' => [
!empty($item['iconClass']) ? $htmlPresenter->i(['class' => $item['iconClass']]) : '',
$item['name'],
!empty($item['badge']) ? $htmlPresenter->bs()->badge($item['badge']['label'], $item['badge']['type'], $item['badge']['isPill']) : ''
],
]),
$hasChildren ? $htmlPresenter->ul([
'class' => 'nav-dropdown-items submenu',
'html' => function() use ($hasChildren, $item, $htmlPresenter) {
$html = '';
if($hasChildren) {
foreach ($item['children'] as $childItem) {
if($this->checkMenuItemPermission($childItem)) {
$html .= $htmlPresenter->li([
'class' => [
'nav-item',
"slug-{$childItem['slug']}",
],
'html' => $htmlPresenter->a([
'class' => 'nav-link',
'href' => $childItem['link'],
'html' => [
!empty($childItem['iconClass']) ? $htmlPresenter->i(['class' => $childItem['iconClass']]) : '',
$childItem['name'],
!empty($childItem['badge']) ? $htmlPresenter->bs()->badge($childItem['badge']['label'], $childItem['badge']['type'], $childItem['badge']['isPill']) : ''
]
])
]);
}
}
}
return $html;
}
]) : ''
]
]);
}
break;
case 'divider':
$html .= $htmlPresenter->li([
'class' => 'nav-divider'
]);
break;
}
}
echo $html;
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace App\Presenters\Admin;
use Arr;
class OptionFormFieldsPresenter
{
public function render($page)
{
$options = config('admin.options.' . $page);
$presenter = app('Html');
$optionRepo = app('Option');
$html = '';
if(!empty($options['fields'])) {
foreach($options['fields'] as $key => $option) {
$type = !empty($option['type']) ? $option['type'] : 'text';
$required = !empty($option['required']) && $option['required'] ? true : false;
$label = !empty($option['label']) ? $option['label'] : $key;
$html .= $presenter->div([
'class' => 'form-group row',
'html' => [
$presenter->div([
'class' => 'col-12 col-md-6',
'html' => $presenter->label([
'for' => 'option-' . $key,
'html' => [
trans('adminOptionLabels.' . $label),
$required ? $presenter->span([
'class' => 'required',
'html' => '*'
]) : ''
]
])
]),
$presenter->div([
'class' => 'col-12 col-md-6',
'html' => function() use ($key, $type, $required, $presenter, $optionRepo) {
$html = '';
$bastHtmlArgs = [
'name' => $key,
'id' => 'option-' . $key,
];
switch ($type) {
case 'text':
case 'password':
case 'email':
case 'number':
$htmlArgs = array_merge([
'type' => $type,
'value' => $optionRepo->$key,
'class' => 'form-control',
], $bastHtmlArgs);
if($required)
$htmlArgs['required'] = null;
$html .= $presenter->input($htmlArgs);
break;
case 'checkbox':
$htmlArgs = array_merge([
'type' => 'checkbox',
'value' => 1,
], $bastHtmlArgs);
if($optionRepo->$key) {
$htmlArgs['checked'] = 'checked';
}
$html .= $presenter->input($htmlArgs);
break;
case 'textarea':
$htmlArgs = array_merge([
'html' => $optionRepo->$key,
'class' => 'form-control',
], $bastHtmlArgs);
if($required)
$htmlArgs['required'] = null;
if(!empty($option['row']))
$htmlArgs['row'] = $option['row'];
$html .= $presenter->textarea($htmlArgs);
break;
case 'select':
$selectOptions = !empty($option['options']) ? $option['options'] : null;
$htmlArgs = array_merge([
'class' => 'form-control',
], $bastHtmlArgs);
$currentValue = $optionRepo->$key;
if(!empty($selectOptions)) {
$htmlArgs['html'] = function() use ($currentValue, $selectOptions, $presenter) {
$html = '';
foreach ($selectOptions as $value => $label) {
$htmlArgs = [
'value' => $value,
'html' => trans('form.options.' . $label),
];
if($currentValue == $value) {
$htmlArgs['selected'] = 'selected';
}
$html .= $presenter->option($htmlArgs);
}
return $html;
};
}
if($required)
$htmlArgs['required'] = null;
$html .= $presenter->select($htmlArgs);
break;
case 'radio':
$radioOptions = !empty($option['options']) ? $option['options'] : null;
$currentValue = $optionRepo->$key;
$defaultValue = !empty($option['default']) ? $option['default'] : null;
if(!empty($radioOptions)) {
foreach ($radioOptions as $value => $label) {
$html .= $presenter->div([
'class' => 'form-check',
'html' => function() use ($key, $currentValue, $defaultValue, $label, $value, $radioOptions, $presenter) {
$html = '';
$inputId = 'option-' . $key . '-' . $value;
$inputHtmlArgs = [
'type' => 'radio',
'class' => 'form-check-input',
'name' => $key,
'id' => $inputId,
'value' => $value
];
if($currentValue === null) {
if($defaultValue && $defaultValue == $value) {
$inputHtmlArgs['checked'] = 'checked';
} elseif(array_key_first($radioOptions) == $value) {
$inputHtmlArgs['checked'] = 'checked';
}
} elseif($currentValue == $value) {
$inputHtmlArgs['checked'] = 'checked';
}
$html .= $presenter->input($inputHtmlArgs);
$html .= $presenter->label([
'class' => 'form-check-label',
'for' => $inputId,
'html' => $label
]);
return $html;
}
]);
}
}
break;
}
return $html;
}
]),
]
]);
}
}
return $html;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace App\Presenters\Html;
use Arr;
trait BaseHtmlPresenter
{
public function __call($name, $arguments)
{
return $this->tag($name, isset($arguments[0]) ? $arguments[0] : []);
}
public function getAttribute($key, $value = null)
{
if($value === null) {
return "$key";
} else{
return "$key=\"{$value}\"";
}
}
public function getAttributes($fieldArgs = [])
{
$attributes = [];
$multiValueAttributes = [
'class',
'style',
];
Arr::forget($fieldArgs, 'html');
foreach ($fieldArgs as $name => $value) {
if(in_array($name, $multiValueAttributes)) {
if(is_array($value)) {
$value = array_filter($value);
$values = trim(implode(' ', $value));
} else {
$values = $value;
}
array_push($attributes, $this->getAttribute($name, $values));
} else {
array_push($attributes, $this->getAttribute($name, $value));
}
}
return trim(implode(' ', $attributes));
}
public function tag($tag, $fieldArgs = [], $echo = false)
{
$selfClosingTags = array(
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr',
);
$htmlElements = !isset($fieldArgs['html']) ? '' : $fieldArgs['html'];
$htmlContent = '';
if(is_array($htmlElements)) {
foreach ($htmlElements as $htmlElement) {
if($htmlElement instanceof \Closure) {
$htmlContent .= call_user_func($htmlElement);
} else {
$htmlContent .= $htmlElement;
}
}
} elseif($htmlElements instanceof \Closure) {
$htmlContent = call_user_func($htmlElements);
} else {
$htmlContent = $htmlElements;
}
$isSelfClosing = in_array($tag, $selfClosingTags);
return '<' . trim(implode(' ', [$tag, $this->getAttributes($fieldArgs)])) . '>' .
(!$isSelfClosing ? $htmlContent . '</' . $tag . '>' : '');
}
public function readonly($readonly = true)
{
return $readonly ? $this->getAttribute('readonly') : '';
}
public function disabled($disabled = true)
{
return $disabled ? $this->getAttribute('disabled', 'disabled') : '';
}
public function required($required = true)
{
return $required ? $this->getAttribute('required') : '';
}
public function checked($checked = true)
{
return $checked ? $this->getAttribute('checked', 'checked') : '';
}
public function checkChecked($currentValue, $comparedValue)
{
if(is_array($comparedValue)) {
return in_array($currentValue, $comparedValue) ? $this->checked() : '';
} else {
return $currentValue == $comparedValue ? $this->checked() : '';
}
}
public function selected($selected = true)
{
return $selected ? $this->getAttribute('selected', 'selected') : '';
}
public function selectSelected($currentValue, $comparedValue)
{
return $currentValue == $comparedValue ? $this->selected() : '';
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace App\Presenters\Html;
class Bootstrap4HtmlPresenter
{
use BaseHtmlPresenter;
/**
* 返回Badge元件的Html
*
* @param string $content 內容
* @param string $type 類型
* @param bool $isPill 膠囊型態
* @param bool $isLink 連結
* @return string
*/
public function badge(string $content = '', string $type = 'primary', $isPill = false, string $link = '')
{
$allowedTypes = [
'primary',
'secondary',
'success',
'danger',
'warning',
'info',
'light',
'dark',
];
if(in_array(strtolower($type), $allowedTypes)) {
$tag = $link ? 'a' : 'span';
$htmlArgs = [
'class' => [
'badge',
'badge-' . $type,
],
'role' => 'alert',
'html' => $content
];
if($isPill)
$htmlArgs['class'][] = 'badge-pill';
if($link)
$htmlArgs['href'] = $link;
return $this->tag($tag, $htmlArgs);
} else {
return '';
}
}
/**
* 返回Alert元件的Html
*
* @param string $content 內容,可包含連結,連結格式為:key再於alertLink參數使用陣列指定傳入網址
* @param $type string 類型
* @param bool $dismiss 是否有關閉的按鈕
* @param array $alertLinks 文字中連結,陣列格式為 key => url
* @return string
*/
public function alert(string $content = '', string $type = 'primary', $dismiss = true, array $alertLinks = [])
{
$allowedTypes = [
'primary',
'secondary',
'success',
'danger',
'warning',
'info',
'light',
'dark',
];
if(in_array(strtolower($type), $allowedTypes)) {
if(!empty($alertLinks)) {
foreach ($alertLinks as $key => $link) {
$linkHtmlArgs = [
'href' => $link['url'],
'class' => 'alert-link',
'html' => $link['content']
];
if(!empty($link['target'])) {
$linkHtmlArgs['target'] = $link['target'];
}
$linkHtml = $this->tag('a', $linkHtmlArgs);
$content = str_replace(":$key", $linkHtml, $content);
}
}
$dismissButtonHtml = '';
if($dismiss) {
$dismissButtonHtml = $this->tag('button', [
'type' => 'button',
'class' => 'close',
'data-dismiss' => 'alert',
'aria-label' => 'Close',
'html' => $this->tag('span', [
'aria-hidden' => 'true',
'html' => '&times;'
])
]);
}
$htmlArgs = [
'class' => [
'alert',
'alert-' . $type,
],
'html' => $content . $dismissButtonHtml
];
if($dismiss) {
$htmlArgs['class'][] = 'alert-dismissible fade show';
}
return $this->tag('div', $htmlArgs);
} else {
return '';
}
}
/**
* 返回Breadcrumb元件的Html
*
* @param array $items
* @return string
*/
public function breadcrumb(array $items = [])
{
$html = $this->tag('nav', [
'aria-label' => 'breadcrumb',
'html' => $this->tag('ol', [
'class' => 'breadcrumb',
'html' => function() use ($items) {
$itemsHtml = '';
foreach ((array)$items as $item) {
$args = [
'class' => [
'breadcrumb-item'
],
'html' => !empty($item['content']) ? $item['content'] : ''
];
if(!empty($item['active'])) {
$args['class'][] = 'active';
$args['aria-current'] = 'page';
$args['html'] = $this->tag('a', [
'href' => !empty($item['href']) ? $item['href'] : '#',
'html' => !empty($item['content']) ? $item['content'] : ''
]);
}
$itemsHtml .= $this->tag('li', $args);
}
return $itemsHtml;
}
])
]);
return $html;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Presenters\Html;
class FontAwesomeHtmlPresenter
{
use BaseHtmlPresenter;
public function icon($name = '')
{
return $this->tag('i', [
'class' => [
'fa',
'fa-' . $name
]
]);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Presenters\Html;
/**
* Class HtmlPresenter
* @package App\Presenters\Html
*
* @property Bootstrap4HtmlPresenter $bootstrap
* @property FontAwesomeHtmlPresenter $fontawesome
*/
class HtmlPresenter
{
use BaseHtmlPresenter;
private $bootstrap;
private $fontawesome;
public function __construct()
{
$this->bootstrap = app()->make(Bootstrap4HtmlPresenter::class);
$this->fontawesome = app()->make(FontAwesomeHtmlPresenter::class);
}
public function bootstrap()
{
return $this->bootstrap;
}
public function bs()
{
return $this->bootstrap();
}
public function fontawesome()
{
return $this->fontawesome;
}
public function fa()
{
return $this->fontawesome();
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Route;
use Auth;
/**
* 從Config檔案建立Menu Item並註冊相關Route
*
* Class AdminMenuServiceProvider
* @package App\Providers
*/
class AdminMenuServiceProvider extends ServiceProvider
{
protected $namespace = '\\';
/**
* Register services.
*
* @return void
*/
public function register()
{
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
/** 從Config建Admin Menu物件內容 */
Route::group(['prefix' => config('admin.route') . '/main', 'middleware' => ['web', 'admin.area', 'admin.menu'], 'as' => config('admin.route_name_prefix')], function(){
//Config的Menu組態設定
$menuItems = config('admin.menuItems');
//Menu Route Name的前綴
$menuRouteNamePrefix = config('admin.menu.route_name_prefix');
//MenuItemContoller的Route處理函式
$menuItemControllerHandlingMethod = 'handle';
/**
* MenuItem的處理物件為全域Singleton物件
* @var \App\Services\Admin\MainMenuItemService $adminMenu
*/
$adminMenu = app('AdminMenu');
foreach ($menuItems as $menuItem) {
if($menuItem['type'] == 'item') {
if(class_exists($menuItem['controller'])) {
/**
* @var \App\Http\Controllers\Admin\Menu\BaseMenuItemController $parentItem
*/
$parentItem = new $menuItem['controller'];
if(!$parentItem->isValid()) {
continue;
}
$hasChildren = !empty($menuItem['children']);
$routeName = false;
$link = '#';
if(!$hasChildren) {
$routeName = "{$menuRouteNamePrefix}{$parentItem->getSlug()}";
$action = "{$menuItem['controller']}@$menuItemControllerHandlingMethod";
//註冊Route
Route::get($parentItem->getSlug(), $action)->name($routeName);
}
//註冊選單項目
$adminMenu->addItem([
'type' => 'item',
'parentSlug' => false,
'routeName' => $routeName,
], $parentItem);
if($hasChildren) {
foreach((array)$menuItem['children'] as $childMenuItem) {
if(class_exists($childMenuItem)) {
/**
* @var \App\Http\Controllers\Admin\Menu\BaseMenuItemController $childItem
*/
$childItem = new $childMenuItem;
if(!$childItem->isValid()) {
continue;
}
$routeName = "{$menuRouteNamePrefix}{$parentItem->getSlug()}.{$childItem->getSlug()}";
$action = "$childMenuItem@$menuItemControllerHandlingMethod";
//註冊Route
Route::get("{$parentItem->getSlug()}/{$childItem->getSlug()}", $action)->name($routeName);
//註冊選單項目
$adminMenu->addItem([
'type' => 'item',
'parentSlug' => $parentItem->getSlug(),
'routeName' => $routeName,
], $childItem);
}
}
}
}
} elseif($menuItem['type'] == 'title') {
$adminMenu->addItem([
'type' => 'title',
'name' => $menuItem['name']
]);
} elseif($menuItem['type'] == 'divider') {
$adminMenu->addItem([
'type' => 'divider',
]);
}
}
});
}
}

View File

@ -4,6 +4,9 @@ namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Str;
class AuthServiceProvider extends ServiceProvider
{
@ -25,6 +28,38 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
//
//administrator不受Gate限制
Gate::before(function($user, $ability) {
/** @var \App\User $user */
if($user->hasRole('administrator')) {
return true;
}
});
try {
// Register all permissions as gate ability
$allPermissions = Permission::all();
foreach ($allPermissions as $permission) {
/** @var Permission $permission */
$permissionName = $permission->name;
$gateName = 'permission:' . Str::kebab($permissionName);
Gate::define($gateName, function ($user) use ($permissionName) {
/** @var \App\User $user */
return $user->hasPermissionTo($permissionName);
});
}
// Register all roles as gate ability
$allRoles = Role::all();
foreach ($allRoles as $role) {
/** @var Role $permission */
$roleName = $role->name;
$gateName = 'role:' . Str::kebab($roleName);
Gate::define($gateName, function ($user) use ($roleName) {
/** @var \App\User $user */
return $user->hasRole($roleName);
});
}
} catch (\Exception $e) {}
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Providers;
use App\Http\Controllers\Admin\OptionsController;
use Illuminate\Support\ServiceProvider;
use Route;
/**
* 建立後台設定頁面的route
*
* Class OptionRouteServiceProvider
* @package App\Providers
*/
class OptionRouteServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
Route::group(['prefix' => config('admin.route'), 'middleware' => ['web', 'admin.area'], 'as' => config('admin.route_name_prefix')], function(){
$config = config('admin.options');
if(empty($config)) return;
$options = array_keys(config('admin.options'));
foreach ($options as $page) {
$action = OptionsController::class;
$action .= '@update' . ucfirst($page);
Route::match(['put', 'patch'], "/options/{$page}/update", $action)
->name("options.{$page}.update");
}
});
}
}

View File

@ -2,7 +2,6 @@
namespace App\Providers;
use App\Option;
use Illuminate\Support\ServiceProvider;
/**
@ -25,6 +24,25 @@ class SingletonServiceProvider extends ServiceProvider
$this->app->singleton(\App\Services\SiteStateService::class, function($app){
return new \App\Services\SiteStateService;
});
$this->app->alias(\App\Services\Admin\MainMenuItemService::class, 'AdminMenu');
$this->app->singleton(\App\Services\Admin\MainMenuItemService::class, function($app){
return new \App\Services\Admin\MainMenuItemService;
});
//Repositories
$this->app->alias(\App\Repositories\OptionRepository::class, 'Option');
$this->app->singleton(\App\Repositories\OptionRepository::class, function($app){
return new \App\Repositories\OptionRepository($app->make(\App\Option::class));
});
// Presenters
$this->app->alias(\App\Presenters\Html\HtmlPresenter::class, 'Html');
$this->app->singleton(\App\Presenters\Html\HtmlPresenter::class, function($app){
return new \App\Presenters\Html\HtmlPresenter;
});
}
/**

View File

@ -0,0 +1,125 @@
<?php
namespace App\Repositories;
use App\Option;
use Illuminate\Support\Facades\Cache;
/**
* Class OptionRepository
* @package App\Repositories
*
* @property \App\Option $model
* @method \App\Option getModel()
*/
class OptionRepository extends BaseRepository
{
private $cacheKeyPrefix = 'options_';
public function __construct(Option $option)
{
$this->setModel($option);
}
public function __get($key)
{
return $this->getOption($key);
}
public function __set($key, $value)
{
$this->setOption($key, $value);
}
/**
* 新增Option
*
* @param $key
* @param $value
* @return void
*/
public function addOption($key, $value)
{
if(is_string($key) && $key) {
if(!is_string($value)) {
if(is_bool($value) || is_null($value)) {
$value = (string) $value;
} else {
$value = serialize($value);
}
}
$this->model->updateOrInsert(['key' => $key], ['value' => $value]);
Cache::forget($this->cacheKeyPrefix . $key);
}
}
/**
* 更新Option
*
* @param $key
* @param $value
* @return void
*/
public function setOption($key, $value)
{
$this->addOption($key, $value);
}
/**
* 取得Option
*
* @param $key
* @return mixed
*/
public function getOption($key)
{
if(Cache::has($this->cacheKeyPrefix . $key)) {
return Cache::get($this->cacheKeyPrefix . $key);
}
$value = null;
try {
$option = $this->model->where(['key' => $key])->first();
if($option) {
$value = @unserialize($option->value);
if($value === false) {
$value = $option->value;
}
} else {
$value = null;
}
Cache::put($this->cacheKeyPrefix . $key, $value);
} catch(\Exception $e) {}
return $value;
}
/**
* 刪除Option
*
* @param $key
* @throws \Exception
* @return void
*/
public function deleteOption($key)
{
$option = $this->model->where('key', $key)->first();
if($option) {
$option->delete();
Cache::forget($this->cacheKeyPrefix . $key);
}
}
/**
* 批次更新Options
*
* @param $options
* @return void
*/
public function setOptions($options)
{
if(is_array($options)) {
foreach ($options as $key => $value) {
$this->setOption($key, $value);
}
}
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace App\Services\Admin;
use App\Exceptions\AdminMenuItemArgInvalidException;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
use Composer\Util\Git;
use Validator;
use Arr;
use Route;
/**
* 存放Admin主選單項目
*
* Class MainMenuItemHandler
* @package App\Handlers\Admin
*/
class MainMenuItemService
{
private $menu = [];
private $allowedItemTypes = [
'item',
'title',
'divider'
];
private $adminRouteNamePrefix;
public function __construct()
{
$this->adminRouteNamePrefix = config('admin.route_name_prefix');
}
/**
* 驗證新增選單項目方法參數
*
* @param $args
* @throws AdminMenuItemArgInvalidException
*/
private function validateItemArgs($args)
{
$validator = Validator::make($args, [
'type' => 'required',
'name' => 'required_if:type,title',
'slug' => 'required_if:type,item',
'controller' => 'required_if:type,item',
'routeName' => 'required_if:type,item',
]);
if($validator->fails()) {
throw new AdminMenuItemArgInvalidException($validator->getMessageBag()->first());
}
}
/**
* 新增選單項目
*
* @param array $givenArgs
* @param BaseMenuItemController $menuItemController;
* @throws AdminMenuItemArgInvalidException
*/
public function addItem($givenArgs, BaseMenuItemController $menuItemController = null)
{
$args = [
'slug' => null,
'routeName' => null,
'link' => null,
'iconClass' => null,
'badge' => null,
'controller' => null,
'parentSlug' => null,
];
if($givenArgs['type'] == 'item' && $menuItemController) {
$givenArgs['slug'] = $menuItemController->getSlug();
$givenArgs['iconClass'] = $menuItemController->getIconClass();
$givenArgs['badge'] = $menuItemController->getBadge();
$givenArgs['controller'] = $menuItemController;
}
$this->validateItemArgs($givenArgs);
foreach ($givenArgs as $key => $value) {
$args = Arr::add($args, $key, $value);
}
if(in_array($args['type'], $this->allowedItemTypes)) {
if($args['parentSlug']) {
Arr::set($this->menu, "{$args['parentSlug']}.children.{$args['slug']}", $args);
} else {
if($args['type'] == 'item') {
$this->menu[$args['slug']] = $args;
} else {
$this->menu[] = $args;
}
}
}
}
/**
* 取得選單項目
*
* @return array
*/
public function getMenu()
{
foreach ($this->menu as $parentSlug => $parentItem) {
if($parentItem['controller']) {
$userHasParentPermission = $parentItem['controller']->userHasPermission();
Arr::set($this->menu, "$parentSlug.userHasPermission", $userHasParentPermission);
Arr::set($this->menu, "$parentSlug.name", $parentItem['controller']->getName());
$parentRouteName = $this->adminRouteNamePrefix . $parentItem['routeName'];
$link = Route::has($parentRouteName) ? route($parentRouteName) : '#';
Arr::set($this->menu, "$parentSlug.link", $link);
if(!empty($parentItem['children'])) {
foreach ($parentItem['children'] as $childSlug => $childItem) {
if($childItem['controller']) {
Arr::set($this->menu,
"$parentSlug.children.$childSlug.userHasPermission",
$userHasParentPermission && $childItem['controller']->userHasPermission());
Arr::set($this->menu, "$parentSlug.children.$childSlug.name", $childItem['controller']->getName());
$childRouteName = $this->adminRouteNamePrefix . $childItem['routeName'];
$link = Route::has($childRouteName) ? route($childRouteName) : '#';
Arr::set($this->menu, "$parentSlug.children.$childSlug.link", $link);
}
}
}
}
}
return $this->menu;
}
/**
* 設定選單項目
*
* @param $menu
*/
public function setMenu($menu)
{
$this->menu = $menu;
}
}

View File

@ -6,4 +6,51 @@ return [
'route' => 'adm',
// 後台的Route Name前綴
'route_name_prefix' => 'admin.',
// 後台選單項目
'menuItems' => [
[
'type' => 'item',
'controller' => Menu\Options\OptionsMenuItemController::class,
'children' => [
Menu\Options\Children\GeneralMenuItemController::class,
Menu\Options\Children\PlatformMenuItemController::class,
Menu\Options\Children\DevelopmentMenuItemController::class,
]
],
],
// 設定的項目
'options' => [
'general' => [
'permission' => 'admin manage options general',
'fields' => [
'site_name' => [
'label' => 'siteName',
'validator' => 'required',
'required' => true
],
'site_description' => [
'label' => 'siteDescription'
],
'block_search_indexing' => [
'label' => 'blockSearchEngineIndexing',
'type' => 'checkbox'
],
'google_api_key' => [
'label' => 'googleApiKey',
],
'fb_app_id' => [
'label' => 'fbAppId'
],
]
],
'platform' => [
'permission' => 'admin manage options platform',
'fields' => [
]
],
'development' => [
'permission' => 'admin manage options development'
],
]
];

View File

@ -177,6 +177,8 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\SingletonServiceProvider::class,
App\Providers\AdminMenuServiceProvider::class,
App\Providers\OptionRouteServiceProvider::class,
],
/*

View File

@ -10,8 +10,37 @@ return [
'name' => 'admin area',
'displayName' => 'adminArea',
'assignTo' => [
'administrator', 'editor'
'editor'
]
],
]
[
//檢視設定頁面
'name' => 'admin view options',
'displayName' => 'adminViewOptions',
'assignTo' => []
],
[
//管理一般設定
'name' => 'admin manage options general',
'displayName' => 'adminManageOptionsGeneral',
'assignTo' => []
],
[
//管理平台設定
'name' => 'admin manage options platform',
'displayName' => 'adminManageOptionsPlatform',
'assignTo' => []
],
[
//管理開發設定的權限
'name' => 'admin manage options development',
'displayName' => 'adminManageOptionsDevelopment',
'assignTo' => []
],
],
// 預設的設定值
'options' => [
'site_name' => config('app.name'),
'block_search_indexing' => true
],
];

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOptionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('options', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('key')->unique()->comment('選項名稱');
$table->longText('value')->comment('選項值');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('options');
}
}

View File

@ -12,5 +12,6 @@ class DatabaseSeeder extends Seeder
public function run()
{
$this->call(RolesAndPermissionsSeeder::class);
$this->call(OptionsSeeder::class);
}
}

View File

@ -0,0 +1,21 @@
<?php
use Illuminate\Database\Seeder;
use \App\Repositories\OptionRepository;
class OptionsSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$options = config('data-presets.options');
$optionRepo = app(OptionRepository::class);
$optionRepo->setOptions($options);
}
}

View File

@ -0,0 +1,11 @@
<?php
return array(
'items' => [
'options' => [
'options' => 'Options',
'general' => 'General',
'development' => 'Development',
'platform' => 'Platform',
],
],
);

View File

@ -0,0 +1,12 @@
<?php
return [
'siteName' => 'Site Name',
'siteDescription' => 'Site Description',
'blockSearchEngineIndexing' => 'Block Search Engine Indexing',
'googleApiKey' => 'Google API Key',
'fbAppId' => 'Facebook APP ID',
'categoryName' => 'Category Name',
'parentCategory' => 'Parent Category',
'featureImage' => 'Feature Image',
'showInFrontSearch' => 'Show In Searchbar List',
];

View File

@ -0,0 +1,9 @@
<?php
return [
'options' => [
'general' => 'General Settings',
'development' => 'Development Settings',
'platform' => 'Platform Settings',
],
];

View File

@ -0,0 +1,6 @@
<?php
return [
'buttons' => [
'update' => 'Update'
]
];

View File

@ -0,0 +1,4 @@
<?php
return [
'success' => 'Success'
];

View File

@ -0,0 +1,2 @@
@inject('adminMenuItemPresenter', App\Presenters\Admin\MainMenuItemPresenter)
{{ $adminMenuItemPresenter->render() }}

View File

@ -1,3 +1,9 @@
@extends('admin.layouts.app')
@section('title', 'Admin Area')
@section('admin-page-content')
<pre>
{{ print_r(app('AdminMenu')->getMenu()) }}
</pre>
@endsection

View File

@ -51,6 +51,7 @@
<div class="sidebar">
<nav class="sidebar-nav">
<ul class="nav">
@include('admin.components.navItems')
</ul>
</nav>
<button class="sidebar-minimizer brand-minimizer" type="button"></button>

View File

@ -0,0 +1,3 @@
@include('admin.menu.options.layout', [
'slug' => 'development'
])

View File

@ -0,0 +1,3 @@
@include('admin.menu.options.layout', [
'slug' => 'general'
])

View File

@ -0,0 +1,37 @@
@extends('admin.layouts.app')
@section('admin-page-content')
<div class="row">
<div class="col">
<h1>@lang('adminPageHeader.options.' . $slug)</h1>
</div>
</div>
@if($errors->any())
<div class="row">
<div class="col">
{!! app('Html')->bs()->alert($errors->first(), 'danger') !!}
</div>
</div>
@endif
@if(session()->has('updated'))
<div class="row">
<div class="col">
{!! app('Html')->bs()->alert(session()->get('updated'), 'success') !!}
</div>
</div>
@endif
<div class="row">
<div class="col-12 col-md-6">
<form action="{{ route('admin.options.' . $slug . '.update') }}" method="post">
@method('put')
@csrf
@inject('html', \App\Presenters\Admin\OptionFormFieldsPresenter)
{!! $html->render($slug) !!}
<div class="form-group row">
<div class="col">
<button type="submit" class="btn btn-primary">@lang('form.buttons.update')</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,3 @@
@include('admin.menu.options.layout', [
'slug' => 'platform'
])