diff --git a/beike/Admin/Providers/AdminServiceProvider.php b/beike/Admin/Providers/AdminServiceProvider.php index 67970b75..18a97596 100644 --- a/beike/Admin/Providers/AdminServiceProvider.php +++ b/beike/Admin/Providers/AdminServiceProvider.php @@ -16,10 +16,10 @@ use Beike\Admin\View\Components\Filter; use Beike\Admin\View\Components\Form\Image; use Beike\Admin\View\Components\Form\Input; use Beike\Admin\View\Components\Form\InputLocale; +use Beike\Admin\View\Components\Form\Richtext; use Beike\Admin\View\Components\Form\Select; use Beike\Admin\View\Components\Form\SwitchRadio; use Beike\Admin\View\Components\Form\Textarea; -use Beike\Admin\View\Components\Form\Richtext; use Beike\Admin\View\Components\Header; use Beike\Admin\View\Components\NoData; use Beike\Admin\View\Components\Sidebar; diff --git a/beike/Helpers.php b/beike/Helpers.php index 325e71ee..afa59ff7 100644 --- a/beike/Helpers.php +++ b/beike/Helpers.php @@ -1,5 +1,6 @@ setCallback($function, $parameters); + } + + public function setCallback($function, $parameters) + { + $this->function = $function; + $this->parameters = $parameters; + } + + public function call($parameters = null) + { + if ($this->run) { + $this->run = false; + + return call_user_func_array($this->function, ($parameters ?: $this->parameters)); + } + } + + public function reset() + { + $this->run = true; + } +} diff --git a/beike/Hook/Console/HookListeners.php b/beike/Hook/Console/HookListeners.php new file mode 100644 index 00000000..3535f4e7 --- /dev/null +++ b/beike/Hook/Console/HookListeners.php @@ -0,0 +1,48 @@ + $lister) { + foreach ($lister as $key => $element) { + $array[] = [ + $key, + $hook, + $element['caller']['class'], + ]; + } + } + + $headers = ['Sort', 'Hook name', 'Listener class']; + + $this->table($headers, $array); + } +} diff --git a/beike/Hook/Facades/Hook.php b/beike/Hook/Facades/Hook.php new file mode 100644 index 00000000..1bc22cf9 --- /dev/null +++ b/beike/Hook/Facades/Hook.php @@ -0,0 +1,13 @@ +createCallbackObject($callback, $params); + + $output = $this->returnMockIfDebugModeAndMockExists($hook); + if ($output) { + return $output; + } + + $output = $this->run($hook, $params, $callbackObject, $htmlContent); + + if (! $output) { + $output = $callbackObject->call(); + } + + unset($callbackObject); + + return $output; + } + + /** + * Stop all another hook running. + * + * @param string $hook Hook name + */ + public function stop(string $hook) + { + $this->stop[$hook] = true; + } + + /** + * Subscribe to hook. + * + * @param string $hook Hook name + * @param $priority + * @param $function + */ + public function listen(string $hook, $function, $priority = null) + { + $caller = debug_backtrace(null, 3)[2]; + + if (in_array(Arr::get($caller, 'function'), ['include', 'require'])) { + $caller = debug_backtrace(null, 4)[3]; + } + + if (empty($this->watch[$hook])) { + $this->watch[$hook] = []; + } + + if (! is_numeric($priority)) { + $priority = null; + } + + $this->watch[$hook][$priority] = [ + 'function' => $function, + 'caller' => [ + //'file' => $caller['file'], + //'line' => $caller['line'], + 'class' => Arr::get($caller, 'class'), + ], + ]; + + ksort($this->watch[$hook]); + } + + /** + * Return all registered hooks. + * + * @return array + */ + public function getHooks(): array + { + $hookNames = (array_keys($this->watch)); + ksort($hookNames); + + return $hookNames; + } + + /** + * Return all listeners for hook. + * + * @param string $hook + * + * @return array + */ + public function getEvents(string $hook): array + { + $output = []; + + foreach ($this->watch[$hook] as $key => $value) { + $output[$key] = $value['caller']; + } + + return $output; + } + + /** + * For testing. + * + * @param string $name Hook name + * @param mixed $return Answer + */ + public function mock(string $name, mixed $return) + { + $this->testing = true; + $this->mock[$name] = ['return' => $return]; + } + + /** + * Return the mock value. + * + * @param string $hook Hook name + */ + protected function returnMockIfDebugModeAndMockExists(string $hook) + { + if ($this->testing) { + if (array_key_exists($hook, $this->mock)) { + $output = $this->mock[$hook]['return']; + unset($this->mock[$hook]); + + return $output; + } + } + + return ''; + } + + /** + * Return a new callback object. + * + * @param callable $callback function + * @param array $params parameters + * + * @return Callback + */ + protected function createCallbackObject(callable $callback, array $params): Callback + { + return new Callback($callback, $params); + } + + /** + * Run hook events. + * + * @param string $hook Hook name + * @param array $params Parameters + * @param Callback $callback Callback object + * @param string|null $output html wrapped by hook + * + * @return mixed + */ + protected function run(string $hook, array $params, Callback $callback, string $output = null): mixed + { + array_unshift($params, $output); + array_unshift($params, $callback); + + if (array_key_exists($hook, $this->watch)) { + if (is_array($this->watch[$hook])) { + foreach ($this->watch[$hook] as $function) { + if (! empty($this->stop[$hook])) { + unset($this->stop[$hook]); + + break; + } + + $output = call_user_func_array($function['function'], $params); + $params[1] = $output; + } + } + } + + return $output; + } + + /** + * Return the listeners. + * + * @return array + */ + public function getListeners(): array + { + return $this->watch; + } +} diff --git a/beike/Hook/HookServiceProvider.php b/beike/Hook/HookServiceProvider.php new file mode 100644 index 00000000..e2f44f6b --- /dev/null +++ b/beike/Hook/HookServiceProvider.php @@ -0,0 +1,72 @@ +bootDirectives(); + } + + public function register() + { + $this->commands([ + HookListeners::class, + ]); + + $this->app->singleton('Hook', function () { + return new Hook(); + }); + } + + protected function bootDirectives() + { + Blade::directive('hook', function ($parameter) { + $parameter = trim($parameter, '()'); + $parameters = explode(',', $parameter); + + $name = trim($parameters[0], "'"); + + // $parameters[1] => bool => is this wrapper component? + if (!isset($parameters[1])) { + return ' <'.'?php + + $__definedVars = (get_defined_vars()["__data"]); + if (empty($__definedVars)) + { + $__definedVars = []; + } + $output = \Hook::get("' . $name . '",["data"=>$__definedVars],function($data) { return null; }); + if ($output) + echo $output; + ?'.'>'; + } else { + return ' <'.'?php + $__hook_name="'. $name .'"; + ob_start(); + ?'.'>'; + } + }); + + Blade::directive('endhook', function ($parameter) { + return ' <'.'?php + $__definedVars = (get_defined_vars()["__data"]); + if (empty($__definedVars)) + { + $__definedVars = []; + } + $__hook_content = ob_get_clean(); + $output = \Hook::get("$__hook_name",["data"=>$__definedVars],function($data) { return null; },$__hook_content); + unset($__hook_name); + unset($__hook_content); + if ($output) + echo $output; + ?'.'>'; + }); + } +} diff --git a/config/app.php b/config/app.php index 01d92f94..bded880f 100644 --- a/config/app.php +++ b/config/app.php @@ -180,6 +180,7 @@ return [ Beike\Shop\Providers\ShopServiceProvider::class, Beike\Shop\Providers\PluginServiceProvider::class, Beike\Installer\Providers\InstallerServiceProvider::class, + Beike\Hook\HookServiceProvider::class, ], @@ -235,6 +236,7 @@ return [ 'URL' => Illuminate\Support\Facades\URL::class, 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, + 'Hook' => Beike\Hook\Facades\Hook::class, ], diff --git a/pint.json b/pint.json index 647a7fe1..6cea97a9 100644 --- a/pint.json +++ b/pint.json @@ -48,7 +48,8 @@ }, "exclude": [ "database", - "plugins" + "plugins", + "beike/Hook" ], "notName": [ "server.php" diff --git a/plugins/LatestProducts/Bootstrap.php b/plugins/LatestProducts/Bootstrap.php index 3f1056a2..e779ea90 100644 --- a/plugins/LatestProducts/Bootstrap.php +++ b/plugins/LatestProducts/Bootstrap.php @@ -16,6 +16,9 @@ class Bootstrap public function boot() { $this->addLatestProducts(); + + // $this->modifyHeader(); + // $this->modifyProductDetail(); } /** @@ -31,4 +34,44 @@ class Bootstrap return $data; }); } + + + /** + * 修改前台全局 header 模板 + */ + private function modifyHeader() + { + blade_hook('header.top.currency', function ($callback, $output, $data) { + return $output . '货币后'; + }); + + blade_hook('header.top.language', function ($callback, $output, $data) { + return $output . '语言后'; + }); + + blade_hook('header.top.telephone', function ($callback, $output, $data) { + return '电话前' . $output; + }); + + blade_hook('header.menu.logo', function ($callback, $output, $data) { + return $output . 'logo后'; + }); + + blade_hook('header.menu.icon', function ($callback, $output, $data) { + $view = view('LatestProducts::header_icon')->render(); + return $output . $view; + }); + } + + private function modifyProductDetail() + { + blade_hook('product.detail.brand', function ($callback, $output, $data) { + return $output . '
Brand 2:品牌 2
'; + }); + + blade_hook('product.detail.buy.after', function ($callback, $output, $data) { + $view = view('LatestProducts::product_button')->render(); + return $output . $view; + }); + } } diff --git a/plugins/LatestProducts/Views/header_icon.blade.php b/plugins/LatestProducts/Views/header_icon.blade.php new file mode 100644 index 00000000..fa2210f4 --- /dev/null +++ b/plugins/LatestProducts/Views/header_icon.blade.php @@ -0,0 +1,3 @@ + diff --git a/plugins/LatestProducts/Views/product_button.blade.php b/plugins/LatestProducts/Views/product_button.blade.php new file mode 100644 index 00000000..f95f97e2 --- /dev/null +++ b/plugins/LatestProducts/Views/product_button.blade.php @@ -0,0 +1,3 @@ + diff --git a/themes/default/layout/header.blade.php b/themes/default/layout/header.blade.php index b833589f..23176664 100644 --- a/themes/default/layout/header.blade.php +++ b/themes/default/layout/header.blade.php @@ -2,6 +2,7 @@
+ @hook('header.top.currency', true)