<?php namespace Illuminate\View; use Closure; use Illuminate\Support\Arr; use Illuminate\Support\Str; use InvalidArgumentException; use Illuminate\Contracts\Support\Arrayable; use Illuminate\View\Engines\EngineResolver; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\View\Factory as FactoryContract; class Factory implements FactoryContract { /** * The engine implementation. * * @var \Illuminate\View\Engines\EngineResolver */ protected $engines; /** * The view finder implementation. * * @var \Illuminate\View\ViewFinderInterface */ protected $finder; /** * The event dispatcher instance. * * @var \Illuminate\Contracts\Events\Dispatcher */ protected $events; /** * The IoC container instance. * * @var \Illuminate\Contracts\Container\Container */ protected $container; /** * Data that should be available to all templates. * * @var array */ protected $shared = []; /** * Array of registered view name aliases. * * @var array */ protected $aliases = []; /** * All of the registered view names. * * @var array */ protected $names = []; /** * The extension to engine bindings. * * @var array */ protected $extensions = ['blade.php' => 'blade', 'php' => 'php']; /** * The view composer events. * * @var array */ protected $composers = []; /** * All of the finished, captured sections. * * @var array */ protected $sections = []; /** * The stack of in-progress sections. * * @var array */ protected $sectionStack = []; /** * All of the finished, captured push sections. * * @var array */ protected $pushes = []; /** * The stack of in-progress push sections. * * @var array */ protected $pushStack = []; /** * The number of active rendering operations. * * @var int */ protected $renderCount = 0; /** * Create a new view factory instance. * * @param \Illuminate\View\Engines\EngineResolver $engines * @param \Illuminate\View\ViewFinderInterface $finder * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void */ public function __construct(EngineResolver $engines, ViewFinderInterface $finder, Dispatcher $events) { $this->finder = $finder; $this->events = $events; $this->engines = $engines; $this->share('__env', $this); } /** * Get the evaluated view contents for the given view. * * @param string $path * @param array $data * @param array $mergeData * @return \Illuminate\Contracts\View\View */ public function file($path, $data = [], $mergeData = []) { $data = array_merge($mergeData, $this->parseData($data)); $this->callCreator($view = new View($this, $this->getEngineFromPath($path), $path, $path, $data)); return $view; } /** * Get the evaluated view contents for the given view. * * @param string $view * @param array $data * @param array $mergeData * @return \Illuminate\Contracts\View\View */ public function make($view, $data = [], $mergeData = []) { if (isset($this->aliases[$view])) { $view = $this->aliases[$view]; } $view = $this->normalizeName($view); $path = $this->finder->find($view); $data = array_merge($mergeData, $this->parseData($data)); $this->callCreator($view = new View($this, $this->getEngineFromPath($path), $view, $path, $data)); return $view; } /** * Normalize a view name. * * @param string $name * @return string */ protected function normalizeName($name) { $delimiter = ViewFinderInterface::HINT_PATH_DELIMITER; if (strpos($name, $delimiter) === false) { return str_replace('/', '.', $name); } list($namespace, $name) = explode($delimiter, $name); return $namespace.$delimiter.str_replace('/', '.', $name); } /** * Parse the given data into a raw array. * * @param mixed $data * @return array */ protected function parseData($data) { return $data instanceof Arrayable ? $data->toArray() : $data; } /** * Get the evaluated view contents for a named view. * * @param string $view * @param mixed $data * @return \Illuminate\Contracts\View\View */ public function of($view, $data = []) { return $this->make($this->names[$view], $data); } /** * Register a named view. * * @param string $view * @param string $name * @return void */ public function name($view, $name) { $this->names[$name] = $view; } /** * Add an alias for a view. * * @param string $view * @param string $alias * @return void */ public function alias($view, $alias) { $this->aliases[$alias] = $view; } /** * Determine if a given view exists. * * @param string $view * @return bool */ public function exists($view) { try { $this->finder->find($view); } catch (InvalidArgumentException $e) { return false; } return true; } /** * Get the rendered contents of a partial from a loop. * * @param string $view * @param array $data * @param string $iterator * @param string $empty * @return string */ public function renderEach($view, $data, $iterator, $empty = 'raw|') { $result = ''; // If is actually data in the array, we will loop through the data and append // an instance of the partial view to the final result HTML passing in the // iterated value of this data array, allowing the views to access them. if (count($data) > 0) { foreach ($data as $key => $value) { $data = ['key' => $key, $iterator => $value]; $result .= $this->make($view, $data)->render(); } } // If there is no data in the array, we will render the contents of the empty // view. Alternatively, the "empty view" could be a raw string that begins // with "raw|" for convenience and to let this know that it is a string. else { if (Str::startsWith($empty, 'raw|')) { $result = substr($empty, 4); } else { $result = $this->make($empty)->render(); } } return $result; } /** * Get the appropriate view engine for the given path. * * @param string $path * @return \Illuminate\View\Engines\EngineInterface * * @throws \InvalidArgumentException */ public function getEngineFromPath($path) { if (! $extension = $this->getExtension($path)) { throw new InvalidArgumentException("Unrecognized extension in file: $path"); } $engine = $this->extensions[$extension]; return $this->engines->resolve($engine); } /** * Get the extension used by the view file. * * @param string $path * @return string */ protected function getExtension($path) { $extensions = array_keys($this->extensions); return Arr::first($extensions, function ($key, $value) use ($path) { return Str::endsWith($path, '.'.$value); }); } /** * Add a piece of shared data to the environment. * * @param array|string $key * @param mixed $value * @return mixed */ public function share($key, $value = null) { if (! is_array($key)) { return $this->shared[$key] = $value; } foreach ($key as $innerKey => $innerValue) { $this->share($innerKey, $innerValue); } } /** * Register a view creator event. * * @param array|string $views * @param \Closure|string $callback * @return array */ public function creator($views, $callback) { $creators = []; foreach ((array) $views as $view) { $creators[] = $this->addViewEvent($view, $callback, 'creating: '); } return $creators; } /** * Register multiple view composers via an array. * * @param array $composers * @return array */ public function composers(array $composers) { $registered = []; foreach ($composers as $callback => $views) { $registered = array_merge($registered, $this->composer($views, $callback)); } return $registered; } /** * Register a view composer event. * * @param array|string $views * @param \Closure|string $callback * @param int|null $priority * @return array */ public function composer($views, $callback, $priority = null) { $composers = []; foreach ((array) $views as $view) { $composers[] = $this->addViewEvent($view, $callback, 'composing: ', $priority); } return $composers; } /** * Add an event for a given view. * * @param string $view * @param \Closure|string $callback * @param string $prefix * @param int|null $priority * @return \Closure|null */ protected function addViewEvent($view, $callback, $prefix = 'composing: ', $priority = null) { $view = $this->normalizeName($view); if ($callback instanceof Closure) { $this->addEventListener($prefix.$view, $callback, $priority); return $callback; } elseif (is_string($callback)) { return $this->addClassEvent($view, $callback, $prefix, $priority); } } /** * Register a class based view composer. * * @param string $view * @param string $class * @param string $prefix * @param int|null $priority * @return \Closure */ protected function addClassEvent($view, $class, $prefix, $priority = null) { $name = $prefix.$view; // When registering a class based view "composer", we will simply resolve the // classes from the application IoC container then call the compose method // on the instance. This allows for convenient, testable view composers. $callback = $this->buildClassEventCallback($class, $prefix); $this->addEventListener($name, $callback, $priority); return $callback; } /** * Add a listener to the event dispatcher. * * @param string $name * @param \Closure $callback * @param int|null $priority * @return void */ protected function addEventListener($name, $callback, $priority = null) { if (is_null($priority)) { $this->events->listen($name, $callback); } else { $this->events->listen($name, $callback, $priority); } } /** * Build a class based container callback Closure. * * @param string $class * @param string $prefix * @return \Closure */ protected function buildClassEventCallback($class, $prefix) { list($class, $method) = $this->parseClassEvent($class, $prefix); // Once we have the class and method name, we can build the Closure to resolve // the instance out of the IoC container and call the method on it with the // given arguments that are passed to the Closure as the composer's data. return function () use ($class, $method) { $callable = [$this->container->make($class), $method]; return call_user_func_array($callable, func_get_args()); }; } /** * Parse a class based composer name. * * @param string $class * @param string $prefix * @return array */ protected function parseClassEvent($class, $prefix) { if (Str::contains($class, '@')) { return explode('@', $class); } $method = Str::contains($prefix, 'composing') ? 'compose' : 'create'; return [$class, $method]; } /** * Call the composer for a given view. * * @param \Illuminate\Contracts\View\View $view * @return void */ public function callComposer(View $view) { $this->events->fire('composing: '.$view->getName(), [$view]); } /** * Call the creator for a given view. * * @param \Illuminate\Contracts\View\View $view * @return void */ public function callCreator(View $view) { $this->events->fire('creating: '.$view->getName(), [$view]); } /** * Start injecting content into a section. * * @param string $section * @param string $content * @return void */ public function startSection($section, $content = '') { if ($content === '') { if (ob_start()) { $this->sectionStack[] = $section; } } else { $this->extendSection($section, $content); } } /** * Inject inline content into a section. * * @param string $section * @param string $content * @return void */ public function inject($section, $content) { return $this->startSection($section, $content); } /** * Stop injecting content into a section and return its contents. * * @return string */ public function yieldSection() { if (empty($this->sectionStack)) { return ''; } return $this->yieldContent($this->stopSection()); } /** * Stop injecting content into a section. * * @param bool $overwrite * @return string * @throws \InvalidArgumentException */ public function stopSection($overwrite = false) { if (empty($this->sectionStack)) { throw new InvalidArgumentException('Cannot end a section without first starting one.'); } $last = array_pop($this->sectionStack); if ($overwrite) { $this->sections[$last] = ob_get_clean(); } else { $this->extendSection($last, ob_get_clean()); } return $last; } /** * Stop injecting content into a section and append it. * * @return string * @throws \InvalidArgumentException */ public function appendSection() { if (empty($this->sectionStack)) { throw new InvalidArgumentException('Cannot end a section without first starting one.'); } $last = array_pop($this->sectionStack); if (isset($this->sections[$last])) { $this->sections[$last] .= ob_get_clean(); } else { $this->sections[$last] = ob_get_clean(); } return $last; } /** * Append content to a given section. * * @param string $section * @param string $content * @return void */ protected function extendSection($section, $content) { if (isset($this->sections[$section])) { $content = str_replace('@parent', $content, $this->sections[$section]); } $this->sections[$section] = $content; } /** * Get the string contents of a section. * * @param string $section * @param string $default * @return string */ public function yieldContent($section, $default = '') { $sectionContent = $default; if (isset($this->sections[$section])) { $sectionContent = $this->sections[$section]; } $sectionContent = str_replace('@@parent', '--parent--holder--', $sectionContent); return str_replace( '--parent--holder--', '@parent', str_replace('@parent', '', $sectionContent) ); } /** * Start injecting content into a push section. * * @param string $section * @param string $content * @return void */ public function startPush($section, $content = '') { if ($content === '') { if (ob_start()) { $this->pushStack[] = $section; } } else { $this->extendPush($section, $content); } } /** * Stop injecting content into a push section. * * @return string * @throws \InvalidArgumentException */ public function stopPush() { if (empty($this->pushStack)) { throw new InvalidArgumentException('Cannot end a section without first starting one.'); } $last = array_pop($this->pushStack); $this->extendPush($last, ob_get_clean()); return $last; } /** * Append content to a given push section. * * @param string $section * @param string $content * @return void */ protected function extendPush($section, $content) { if (! isset($this->pushes[$section])) { $this->pushes[$section] = []; } if (! isset($this->pushes[$section][$this->renderCount])) { $this->pushes[$section][$this->renderCount] = $content; } else { $this->pushes[$section][$this->renderCount] .= $content; } } /** * Get the string contents of a push section. * * @param string $section * @param string $default * @return string */ public function yieldPushContent($section, $default = '') { if (! isset($this->pushes[$section])) { return $default; } return implode(array_reverse($this->pushes[$section])); } /** * Flush all of the section contents. * * @return void */ public function flushSections() { $this->renderCount = 0; $this->sections = []; $this->sectionStack = []; $this->pushes = []; $this->pushStack = []; } /** * Flush all of the section contents if done rendering. * * @return void */ public function flushSectionsIfDoneRendering() { if ($this->doneRendering()) { $this->flushSections(); } } /** * Increment the rendering counter. * * @return void */ public function incrementRender() { $this->renderCount++; } /** * Decrement the rendering counter. * * @return void */ public function decrementRender() { $this->renderCount--; } /** * Check if there are no active render operations. * * @return bool */ public function doneRendering() { return $this->renderCount == 0; } /** * Add a location to the array of view locations. * * @param string $location * @return void */ public function addLocation($location) { $this->finder->addLocation($location); } /** * Add a new namespace to the loader. * * @param string $namespace * @param string|array $hints * @return void */ public function addNamespace($namespace, $hints) { $this->finder->addNamespace($namespace, $hints); } /** * Prepend a new namespace to the loader. * * @param string $namespace * @param string|array $hints * @return void */ public function prependNamespace($namespace, $hints) { $this->finder->prependNamespace($namespace, $hints); } /** * Register a valid view extension and its engine. * * @param string $extension * @param string $engine * @param \Closure $resolver * @return void */ public function addExtension($extension, $engine, $resolver = null) { $this->finder->addExtension($extension); if (isset($resolver)) { $this->engines->register($engine, $resolver); } unset($this->extensions[$extension]); $this->extensions = array_merge([$extension => $engine], $this->extensions); } /** * Get the extension to engine bindings. * * @return array */ public function getExtensions() { return $this->extensions; } /** * Get the engine resolver instance. * * @return \Illuminate\View\Engines\EngineResolver */ public function getEngineResolver() { return $this->engines; } /** * Get the view finder instance. * * @return \Illuminate\View\ViewFinderInterface */ public function getFinder() { return $this->finder; } /** * Set the view finder instance. * * @param \Illuminate\View\ViewFinderInterface $finder * @return void */ public function setFinder(ViewFinderInterface $finder) { $this->finder = $finder; } /** * Get the event dispatcher instance. * * @return \Illuminate\Contracts\Events\Dispatcher */ public function getDispatcher() { return $this->events; } /** * Set the event dispatcher instance. * * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void */ public function setDispatcher(Dispatcher $events) { $this->events = $events; } /** * Get the IoC container instance. * * @return \Illuminate\Contracts\Container\Container */ public function getContainer() { return $this->container; } /** * Set the IoC container instance. * * @param \Illuminate\Contracts\Container\Container $container * @return void */ public function setContainer(Container $container) { $this->container = $container; } /** * Get an item from the shared data. * * @param string $key * @param mixed $default * @return mixed */ public function shared($key, $default = null) { return Arr::get($this->shared, $key, $default); } /** * Get all of the shared data for the environment. * * @return array */ public function getShared() { return $this->shared; } /** * Check if section exists. * * @param string $name * @return bool */ public function hasSection($name) { return array_key_exists($name, $this->sections); } /** * Get the entire array of sections. * * @return array */ public function getSections() { return $this->sections; } /** * Get all of the registered named views in environment. * * @return array */ public function getNames() { return $this->names; } }