From 51f9e57e6adae92bd1d65f35a63ff296ac18eb5a Mon Sep 17 00:00:00 2001 From: kroutony Date: Sat, 22 Feb 2020 20:43:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=BE=8C=E5=8F=B0=E9=81=B8?= =?UTF-8?q?=E5=96=AE=E5=BB=BA=E7=AB=8B=E6=A9=9F=E5=88=B6=EF=BC=8C=E5=BE=8C?= =?UTF-8?q?=E5=8F=B0=E8=A8=AD=E5=AE=9A=E9=81=B8=E9=A0=85=E5=BB=BA=E7=AB=8B?= =?UTF-8?q?=E6=A9=9F=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminMenuItemArgInvalidException.php | 10 + .../Admin/Menu/BaseMenuItemController.php | 192 ++++++++++++++++++ .../DevelopmentMenuItemController.php | 26 +++ .../Children/GeneralMenuItemController.php | 26 +++ .../Children/PlatformMenuItemController.php | 26 +++ .../Options/OptionsMenuItemController.php | 19 ++ app/Http/Controllers/OptionsController.php | 120 +++++++++++ app/Http/Kernel.php | 1 + .../CheckAdminMenuPagePermission.php | 56 +++++ app/Option.php | 15 ++ .../Admin/MainMenuItemPresenter.php | 129 ++++++++++++ .../Admin/OptionFormFieldsPresenter.php | 168 +++++++++++++++ app/Presenters/Html/BaseHtmlPresenter.php | 128 ++++++++++++ .../Html/Bootstrap4HtmlPresenter.php | 158 ++++++++++++++ .../Html/FontAwesomeHtmlPresenter.php | 18 ++ app/Presenters/Html/HtmlPresenter.php | 47 +++++ app/Providers/AdminMenuServiceProvider.php | 113 +++++++++++ app/Providers/AuthServiceProvider.php | 37 +++- app/Providers/OptionRouteServiceProvider.php | 47 +++++ app/Providers/SingletonServiceProvider.php | 20 +- app/Reposotories/OptionRepository.php | 125 ++++++++++++ app/Services/Admin/MainMenuItemService.php | 149 ++++++++++++++ config/admin.php | 47 +++++ config/app.php | 2 + config/data-presets.php | 33 ++- ...2020_02_22_120952_create_options_table.php | 32 +++ database/seeds/DatabaseSeeder.php | 1 + database/seeds/Preset/OptionsSeeder.php | 21 ++ resources/lang/en/adminMenu.php | 11 + resources/lang/en/adminOptionLabels.php | 12 ++ resources/lang/en/adminPageHeader.php | 9 + resources/lang/en/form.php | 6 + resources/lang/en/message.php | 4 + .../views/admin/components/navItems.blade.php | 2 + resources/views/admin/index.blade.php | 6 + resources/views/admin/layouts/app.blade.php | 1 + .../admin/menu/options/development.blade.php | 3 + .../admin/menu/options/general.blade.php | 3 + .../views/admin/menu/options/layout.blade.php | 37 ++++ .../admin/menu/options/platform.blade.php | 3 + 40 files changed, 1859 insertions(+), 4 deletions(-) create mode 100644 app/Exceptions/AdminMenuItemArgInvalidException.php create mode 100644 app/Http/Controllers/Admin/Menu/BaseMenuItemController.php create mode 100644 app/Http/Controllers/Admin/Menu/Options/Children/DevelopmentMenuItemController.php create mode 100644 app/Http/Controllers/Admin/Menu/Options/Children/GeneralMenuItemController.php create mode 100644 app/Http/Controllers/Admin/Menu/Options/Children/PlatformMenuItemController.php create mode 100644 app/Http/Controllers/Admin/Menu/Options/OptionsMenuItemController.php create mode 100644 app/Http/Controllers/OptionsController.php create mode 100644 app/Http/Middleware/CheckAdminMenuPagePermission.php create mode 100644 app/Option.php create mode 100644 app/Presenters/Admin/MainMenuItemPresenter.php create mode 100644 app/Presenters/Admin/OptionFormFieldsPresenter.php create mode 100644 app/Presenters/Html/BaseHtmlPresenter.php create mode 100644 app/Presenters/Html/Bootstrap4HtmlPresenter.php create mode 100644 app/Presenters/Html/FontAwesomeHtmlPresenter.php create mode 100644 app/Presenters/Html/HtmlPresenter.php create mode 100644 app/Providers/AdminMenuServiceProvider.php create mode 100644 app/Providers/OptionRouteServiceProvider.php create mode 100644 app/Reposotories/OptionRepository.php create mode 100644 app/Services/Admin/MainMenuItemService.php create mode 100644 database/migrations/2020_02_22_120952_create_options_table.php create mode 100644 database/seeds/Preset/OptionsSeeder.php create mode 100644 resources/lang/en/adminMenu.php create mode 100644 resources/lang/en/adminOptionLabels.php create mode 100644 resources/lang/en/adminPageHeader.php create mode 100644 resources/lang/en/form.php create mode 100644 resources/lang/en/message.php create mode 100644 resources/views/admin/components/navItems.blade.php create mode 100644 resources/views/admin/menu/options/development.blade.php create mode 100644 resources/views/admin/menu/options/general.blade.php create mode 100644 resources/views/admin/menu/options/layout.blade.php create mode 100644 resources/views/admin/menu/options/platform.blade.php diff --git a/app/Exceptions/AdminMenuItemArgInvalidException.php b/app/Exceptions/AdminMenuItemArgInvalidException.php new file mode 100644 index 0000000..b9fa39b --- /dev/null +++ b/app/Exceptions/AdminMenuItemArgInvalidException.php @@ -0,0 +1,10 @@ +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(); + } +} diff --git a/app/Http/Controllers/Admin/Menu/Options/Children/DevelopmentMenuItemController.php b/app/Http/Controllers/Admin/Menu/Options/Children/DevelopmentMenuItemController.php new file mode 100644 index 0000000..90efc4d --- /dev/null +++ b/app/Http/Controllers/Admin/Menu/Options/Children/DevelopmentMenuItemController.php @@ -0,0 +1,26 @@ +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'); + } +} diff --git a/app/Http/Controllers/Admin/Menu/Options/Children/GeneralMenuItemController.php b/app/Http/Controllers/Admin/Menu/Options/Children/GeneralMenuItemController.php new file mode 100644 index 0000000..11a51a8 --- /dev/null +++ b/app/Http/Controllers/Admin/Menu/Options/Children/GeneralMenuItemController.php @@ -0,0 +1,26 @@ +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'); + } +} diff --git a/app/Http/Controllers/Admin/Menu/Options/Children/PlatformMenuItemController.php b/app/Http/Controllers/Admin/Menu/Options/Children/PlatformMenuItemController.php new file mode 100644 index 0000000..d6f6890 --- /dev/null +++ b/app/Http/Controllers/Admin/Menu/Options/Children/PlatformMenuItemController.php @@ -0,0 +1,26 @@ +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'); + } +} diff --git a/app/Http/Controllers/Admin/Menu/Options/OptionsMenuItemController.php b/app/Http/Controllers/Admin/Menu/Options/OptionsMenuItemController.php new file mode 100644 index 0000000..8ccc5b1 --- /dev/null +++ b/app/Http/Controllers/Admin/Menu/Options/OptionsMenuItemController.php @@ -0,0 +1,19 @@ +name = 'adminMenu.items.options.options'; + + $this->slug = 'options'; + + $this->permissions = ['admin view options']; + + $this->iconClasses = 'nav-icon icon-settings'; + } +} diff --git a/app/Http/Controllers/OptionsController.php b/app/Http/Controllers/OptionsController.php new file mode 100644 index 0000000..15b24b0 --- /dev/null +++ b/app/Http/Controllers/OptionsController.php @@ -0,0 +1,120 @@ +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"); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 48d5a69..e501d3f 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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, ]; /** diff --git a/app/Http/Middleware/CheckAdminMenuPagePermission.php b/app/Http/Middleware/CheckAdminMenuPagePermission.php new file mode 100644 index 0000000..cba7f99 --- /dev/null +++ b/app/Http/Middleware/CheckAdminMenuPagePermission.php @@ -0,0 +1,56 @@ +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); + } +} diff --git a/app/Option.php b/app/Option.php new file mode 100644 index 0000000..b7f8201 --- /dev/null +++ b/app/Option.php @@ -0,0 +1,15 @@ +where('key', $key); + } +} diff --git a/app/Presenters/Admin/MainMenuItemPresenter.php b/app/Presenters/Admin/MainMenuItemPresenter.php new file mode 100644 index 0000000..e84d216 --- /dev/null +++ b/app/Presenters/Admin/MainMenuItemPresenter.php @@ -0,0 +1,129 @@ +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; + } +} diff --git a/app/Presenters/Admin/OptionFormFieldsPresenter.php b/app/Presenters/Admin/OptionFormFieldsPresenter.php new file mode 100644 index 0000000..8cf6ad0 --- /dev/null +++ b/app/Presenters/Admin/OptionFormFieldsPresenter.php @@ -0,0 +1,168 @@ + $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; + } +} diff --git a/app/Presenters/Html/BaseHtmlPresenter.php b/app/Presenters/Html/BaseHtmlPresenter.php new file mode 100644 index 0000000..3cc1ba8 --- /dev/null +++ b/app/Presenters/Html/BaseHtmlPresenter.php @@ -0,0 +1,128 @@ +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 . '' : ''); + } + + 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() : ''; + } +} diff --git a/app/Presenters/Html/Bootstrap4HtmlPresenter.php b/app/Presenters/Html/Bootstrap4HtmlPresenter.php new file mode 100644 index 0000000..5f73645 --- /dev/null +++ b/app/Presenters/Html/Bootstrap4HtmlPresenter.php @@ -0,0 +1,158 @@ + [ + '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' => '×' + ]) + ]); + } + $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; + } +} diff --git a/app/Presenters/Html/FontAwesomeHtmlPresenter.php b/app/Presenters/Html/FontAwesomeHtmlPresenter.php new file mode 100644 index 0000000..8be6fdc --- /dev/null +++ b/app/Presenters/Html/FontAwesomeHtmlPresenter.php @@ -0,0 +1,18 @@ +tag('i', [ + 'class' => [ + 'fa', + 'fa-' . $name + ] + ]); + } +} diff --git a/app/Presenters/Html/HtmlPresenter.php b/app/Presenters/Html/HtmlPresenter.php new file mode 100644 index 0000000..a329604 --- /dev/null +++ b/app/Presenters/Html/HtmlPresenter.php @@ -0,0 +1,47 @@ +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(); + } +} diff --git a/app/Providers/AdminMenuServiceProvider.php b/app/Providers/AdminMenuServiceProvider.php new file mode 100644 index 0000000..d36feea --- /dev/null +++ b/app/Providers/AdminMenuServiceProvider.php @@ -0,0 +1,113 @@ + 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', + ]); + } + } + }); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 3049068..1c3415d 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -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) {} } } diff --git a/app/Providers/OptionRouteServiceProvider.php b/app/Providers/OptionRouteServiceProvider.php new file mode 100644 index 0000000..aa99fe0 --- /dev/null +++ b/app/Providers/OptionRouteServiceProvider.php @@ -0,0 +1,47 @@ + 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"); + } + }); + } +} diff --git a/app/Providers/SingletonServiceProvider.php b/app/Providers/SingletonServiceProvider.php index d842e2a..22561dc 100644 --- a/app/Providers/SingletonServiceProvider.php +++ b/app/Providers/SingletonServiceProvider.php @@ -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; + }); + + } /** diff --git a/app/Reposotories/OptionRepository.php b/app/Reposotories/OptionRepository.php new file mode 100644 index 0000000..405271b --- /dev/null +++ b/app/Reposotories/OptionRepository.php @@ -0,0 +1,125 @@ +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); + } + } + } +} diff --git a/app/Services/Admin/MainMenuItemService.php b/app/Services/Admin/MainMenuItemService.php new file mode 100644 index 0000000..8293daf --- /dev/null +++ b/app/Services/Admin/MainMenuItemService.php @@ -0,0 +1,149 @@ +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; + } + +} diff --git a/config/admin.php b/config/admin.php index 4d986e4..187e4b4 100644 --- a/config/admin.php +++ b/config/admin.php @@ -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' + ], + ] + ]; diff --git a/config/app.php b/config/app.php index 1083f63..6927fdb 100644 --- a/config/app.php +++ b/config/app.php @@ -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, ], /* diff --git a/config/data-presets.php b/config/data-presets.php index f3ea333..86f3c35 100644 --- a/config/data-presets.php +++ b/config/data-presets.php @@ -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 + ], ]; diff --git a/database/migrations/2020_02_22_120952_create_options_table.php b/database/migrations/2020_02_22_120952_create_options_table.php new file mode 100644 index 0000000..f2e79ec --- /dev/null +++ b/database/migrations/2020_02_22_120952_create_options_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->string('key')->unique()->comment('選項名稱'); + $table->longText('value')->comment('選項值'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('options'); + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index 6d8eef7..25330db 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -12,5 +12,6 @@ class DatabaseSeeder extends Seeder public function run() { $this->call(RolesAndPermissionsSeeder::class); + $this->call(OptionsSeeder::class); } } diff --git a/database/seeds/Preset/OptionsSeeder.php b/database/seeds/Preset/OptionsSeeder.php new file mode 100644 index 0000000..abc0357 --- /dev/null +++ b/database/seeds/Preset/OptionsSeeder.php @@ -0,0 +1,21 @@ +setOptions($options); + } +} diff --git a/resources/lang/en/adminMenu.php b/resources/lang/en/adminMenu.php new file mode 100644 index 0000000..f948090 --- /dev/null +++ b/resources/lang/en/adminMenu.php @@ -0,0 +1,11 @@ + [ + 'options' => [ + 'options' => 'Options', + 'general' => 'General', + 'development' => 'Development', + 'platform' => 'Platform', + ], + ], +); diff --git a/resources/lang/en/adminOptionLabels.php b/resources/lang/en/adminOptionLabels.php new file mode 100644 index 0000000..0c225f9 --- /dev/null +++ b/resources/lang/en/adminOptionLabels.php @@ -0,0 +1,12 @@ + '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', +]; diff --git a/resources/lang/en/adminPageHeader.php b/resources/lang/en/adminPageHeader.php new file mode 100644 index 0000000..26b6045 --- /dev/null +++ b/resources/lang/en/adminPageHeader.php @@ -0,0 +1,9 @@ + [ + 'general' => 'General Settings', + 'development' => 'Development Settings', + 'platform' => 'Platform Settings', + ], +]; diff --git a/resources/lang/en/form.php b/resources/lang/en/form.php new file mode 100644 index 0000000..1d52aba --- /dev/null +++ b/resources/lang/en/form.php @@ -0,0 +1,6 @@ + [ + 'update' => 'Update' + ] +]; diff --git a/resources/lang/en/message.php b/resources/lang/en/message.php new file mode 100644 index 0000000..8b0d461 --- /dev/null +++ b/resources/lang/en/message.php @@ -0,0 +1,4 @@ + 'Success' +]; diff --git a/resources/views/admin/components/navItems.blade.php b/resources/views/admin/components/navItems.blade.php new file mode 100644 index 0000000..3bfb4b8 --- /dev/null +++ b/resources/views/admin/components/navItems.blade.php @@ -0,0 +1,2 @@ +@inject('adminMenuItemPresenter', App\Presenters\Admin\MainMenuItemPresenter) +{{ $adminMenuItemPresenter->render() }} diff --git a/resources/views/admin/index.blade.php b/resources/views/admin/index.blade.php index f923744..7e34fb3 100644 --- a/resources/views/admin/index.blade.php +++ b/resources/views/admin/index.blade.php @@ -1,3 +1,9 @@ @extends('admin.layouts.app') @section('title', 'Admin Area') + +@section('admin-page-content') +
+            {{ print_r(app('AdminMenu')->getMenu()) }}
+    
+@endsection diff --git a/resources/views/admin/layouts/app.blade.php b/resources/views/admin/layouts/app.blade.php index 042cc65..77255b5 100644 --- a/resources/views/admin/layouts/app.blade.php +++ b/resources/views/admin/layouts/app.blade.php @@ -51,6 +51,7 @@