public function process(
        callable $callback = null,
        callable $successCallback = null,
        callable $failureCallback = null
    ) {

        if ($this->processed() === true) {
            // Do not process twice, ever.
            return null;
        }

        if ($this->targetType() === null) {
            $this->logger->error('Can not process object schedule: no object type defined.');
            return false;
        }

        if ($this->targetId() === null) {
            $this->logger->error(sprintf(
                'Can not process object schedule: no object "%s" ID defined.',
                $this->targetType()
            ));
            return false;
        }

        if (empty($this->dataDiff())) {
            $this->logger->error('Can not process object schedule: no changes (diff) defined.');
            return false;
        }

        $obj = $this->modelFactory()->create($this->targetType());
        $obj->load($this->targetId());
        if (!$obj->id()) {
            $this->logger->error(sprintf(
                'Can not load "%s" object %s',
                $this->targetType(),
                $this->targetId()
            ));
        }
        $obj->setData($this->dataDiff());
        $update = $obj->update(array_keys($this->dataDiff()));

        if ($update) {
            $this->setProcessed(true);
            $this->setProcessedDate('now');
            $this->update(['processed', 'processed_date']);

            if ($successCallback !== null) {
                $successCallback($this);
            }
        } else {
            if ($failureCallback !== null) {
                $failureCallback($this);
            }
        }

        if ($callback !== null) {
            $callback($this);
        }

        return $update;
    }

Process the item.

@param  callable $callback        An optional callback routine executed after the item is processed.
@param  callable $successCallback An optional callback routine executed when the item is resolved.
@param  callable $failureCallback An optional callback routine executed when the item is rejected.
@return boolean|null  Success / Failure, or null in case of a skipped item.


protected function preUpdate(array $properties = null)
    {
        $this->setCreationDate('now');
        $this->setLastModificationDate('now');

        return parent::preUpdate($properties);
    }

protected function preUpdate ( array $ properties = null ) { $ this -> setCreationDate ( 'now' ) ; $ this -> setLastModificationDate ( 'now' ) ; return parent :: preUpdate ( $ properties ) ; }

Event called before _updating_ the object.

@see    Charcoal\Source\StorableTrait::preUpdate() For the "update" Event.
@param  array $properties Optional. The list of properties to update.
@return boolean

Event called before _updating_ the object .


public function isSlugUnique()
    {
        $proto = $this->modelFactory()->get(self::class);
        $loader = $this->collectionLoader();
        $loader
            ->reset()
            ->setModel($proto)
            ->addFilter('active', true)
            ->addFilter('slug', $this->slug())
            ->addFilter('lang', $this->lang())
            ->addOrder('creation_date', 'desc')
            ->setPage(1)
            ->setNumPerPage(1);

        $routes = $loader->load()->objects();
        if (!$routes) {
            return true;
        }
        $obj = reset($routes);
        if (!$obj->id()) {
            return true;
        }
        if ($obj->id() === $this->id()) {
            return true;
        }
        if ($obj->routeObjId() === $this->routeObjId() &&
            $obj->routeObjType() === $this->routeObjType() &&
            $obj->lang() === $this->lang()
        ) {
            $this->setId($obj->id());

            return true;
        }

        return false;
    }

public function isSlugUnique ( ) { $ proto = $ this -> modelFactory ( ) -> get ( self :: class ) ; $ loader = $ this -> collectionLoader ( ) ; $ loader -> reset ( ) -> setModel ( $ proto ) -> addFilter ( 'active' , true ) -> addFilter ( 'slug' , $ this -> slug ( ) ) -> addFilter ( 'lang' , $ this -> lang ( ) ) -> addOrder ( 'creation_date' , 'desc' ) -> setPage ( 1 ) -> setNumPerPage ( 1 ) ; $ routes = $ loader -> load ( ) -> objects ( ) ; if ( ! $ routes ) { return true ; } $ obj = reset ( $ routes ) ; if ( ! $ obj -> id ( ) ) { return true ; } if ( $ obj -> id ( ) === $ this -> id ( ) ) { return true ; } if ( $ obj -> routeObjId ( ) === $ this -> routeObjId ( ) && $ obj -> routeObjType ( ) === $ this -> routeObjType ( ) && $ obj -> lang ( ) === $ this -> lang ( ) ) { $ this -> setId ( $ obj -> id ( ) ) ; return true ; } return false ; }

Determine if the current slug is unique.

@return boolean

Determine if the current slug is unique .


public function generateUniqueSlug()
    {
        if (!$this->isSlugUnique()) {
            if (!$this->originalSlug) {
                $this->originalSlug = $this->slug();
            }
            $this->slugInc++;
            $this->setSlug($this->originalSlug.'-'.$this->slugInc);

            return $this->generateUniqueSlug();
        }

        return $this;
    }

public function generateUniqueSlug ( ) { if ( ! $ this -> isSlugUnique ( ) ) { if ( ! $ this -> originalSlug ) { $ this -> originalSlug = $ this -> slug ( ) ; } $ this -> slugInc ++ ; $ this -> setSlug ( $ this -> originalSlug . '-' . $ this -> slugInc ) ; return $ this -> generateUniqueSlug ( ) ; } return $ this ; }

Generate a unique URL slug for routable object.

@return self

Generate a unique URL slug for routable object .


public function setSlug($slug)
    {
        if ($slug === null) {
            $this->slug = null;

            return $this;
        }
        if (!is_string($slug)) {
            throw new InvalidArgumentException(
                'Slug is not a string'
            );
        }
        $this->slug = $slug;

        return $this;
    }

public
function
setSlug
(
$
slug
)
{
if
(
$
slug
===
)
{
$
this
->
slug
=
;
return
$
this
;
}
if
(
!
is_string
(
$
slug
)
)
{
throw
new
InvalidArgumentException
(
'Slug is not a string'
)
;
}
$
this
->
slug
=
$
slug
;
return
$
this
;
}

Set the object route URI.

@param  string|null $slug The route.
@throws InvalidArgumentException If the slug argument is not a string.
@return self


public function setCreationDate($time)
    {
        if (empty($time) && !is_numeric($time)) {
            $this->creationDate = null;

            return $this;
        }

        if (is_string($time)) {
            try {
                $time = new DateTime($time);
            } catch (Exception $e) {
                throw new InvalidArgumentException(sprintf(
                    'Invalid Creation Date: %s',
                    $e->getMessage()
                ), $e->getCode(), $e);
            }
        }

        if (!$time instanceof DateTimeInterface) {
            throw new InvalidArgumentException(
                'Creation Date must be a date/time string or an instance of DateTimeInterface'
            );
        }

        $this->creationDate = $time;

        return $this;
    }

Set the route's last creation date.

@param  string|DateTimeInterface|null $time The date/time value.
@throws InvalidArgumentException If the date/time value is invalid.
@return self


public function setLastModificationDate($time)
    {
        if (empty($time) && !is_numeric($time)) {
            $this->lastModificationDate = null;

            return $this;
        }

        if (is_string($time)) {
            try {
                $time = new DateTime($time);
            } catch (Exception $e) {
                throw new InvalidArgumentException(sprintf(
                    'Invalid Updated Date: %s',
                    $e->getMessage()
                ), $e->getCode(), $e);
            }
        }

        if (!$time instanceof DateTimeInterface) {
            throw new InvalidArgumentException(
                'Updated Date must be a date/time string or an instance of DateTimeInterface'
            );
        }

        $this->lastModificationDate = $time;

        return $this;
    }

Set the route's last modification date.

@param  string|DateTimeInterface|null $time The date/time value.
@throws InvalidArgumentException If the date/time value is invalid.
@return self

Set the route s last modification date .


public function setRouteOptions($options)
    {
        if (is_string($options)) {
            $options = json_decode($options, true);
        }

        $this->routeOptions = $options;

        return $this;
    }

public function setRouteOptions ( $ options ) { if ( is_string ( $ options ) ) { $ options = json_decode ( $ options , true ) ; } $ this -> routeOptions = $ options ; return $ this ; }

Customize the template's options.

@param  mixed $options Template options.
@return self


public function setMaster($master)
    {
        $master = $this->objFromIdent($master);

        if ($master instanceof ModelInterface) {
            if ($master->id() === $this->id()) {
                throw new UnexpectedValueException(sprintf(
                    'Can not be ones own parent: %s',
                    $master->id()
                ));
            }
        }

        $this->master = $master;

        $this->resetHierarchy();

        return $this;
    }

public
function
setMaster
(
$
master
)
{
$
master
=
$
this
->
objFromIdent
(
$
master
)
;
if
(
$
master
instanceof
ModelInterface
)
{
if
(
$
master
->
id
(
)
===
$
this
->
id
(
)
)
{
throw
new
UnexpectedValueException
(
sprintf
(
'Can not be ones own parent: %s'
,
$
master
->
id
(
)
)
)
;
}
}
$
this
->
master
=
$
master
;
$
this
->
resetHierarchy
(
)
;
return
$
this
;
}

Set this object's immediate parent.

@param  mixed $master The object's parent (or master).
@throws UnexpectedValueException The current object cannot be its own parent.
@return HierarchicalInterface Chainable


public function children()
    {
        if ($this->children !== null) {
            return $this->children;
        }

        $this->children = $this->loadChildren();
        return $this->children;
    }

public function children ( ) { if ( $ this -> children !== null ) { return $ this -> children ; } $ this -> children = $ this -> loadChildren ( ) ; return $ this -> children ; }

Get the children directly under this object.
@return array

Get the children directly under this object .


public function siblings()
    {
        if ($this->siblings !== null) {
            return $this->siblings;
        }
        $master = $this->master();
        if ($master === null) {
            // Todo: return all top-level objects.
            $siblings = [];
        } else {
            // Todo: Remove "current" object from siblings
            $siblings = $master->children();
        }
        $this->siblings = $siblings;
        return $this->siblings;
    }

Get all the objects on the same level as this one.
@return array

Get all the objects on the same level as this one .


private function loadObjectFromSource($id)
    {
        $obj = $this->modelFactory()->create($this->objType());
        $obj->load($id);

        if ($obj->id()) {
            return $obj;
        } else {
            return null;
        }
    }

private function loadObjectFromSource ( $ id ) { $ obj = $ this -> modelFactory ( ) -> create ( $ this -> objType ( ) ) ; $ obj -> load ( $ id ) ; if ( $ obj -> id ( ) ) { return $ obj ; } else { return null ; } }

Retrieve an object from the storage source by its ID.

@param mixed $id The object id.
@return null|HierarchicalInterface

Retrieve an object from the storage source by its ID .


private function loadObjectFromCache($id)
    {
        $objType = $this->objType();
        if (isset(static::$objectCache[$objType][$id])) {
            return static::$objectCache[$objType][$id];
        } else {
            return null;
        }
    }

private function loadObjectFromCache ( $ id ) { $ objType = $ this -> objType ( ) ; if ( isset ( static :: $ objectCache [ $ objType ] [ $ id ] ) ) { return static :: $ objectCache [ $ objType ] [ $ id ] ; } else { return null ; } }

Retrieve an object from the cache store by its ID.

@param mixed $id The object id.
@return null|HierarchicalInterface

Retrieve an object from the cache store by its ID .


private function addObjectToCache(ModelInterface $obj)
    {
        static::$objectCache[$this->objType()][$obj->id()] = $obj;

        return $this;
    }

private function addObjectToCache ( ModelInterface $ obj ) { static :: $ objectCache [ $ this -> objType ( ) ] [ $ obj -> id ( ) ] = $ obj ; return $ this ; }

Add an object to the cache store.

@param ModelInterface $obj The object to store.
@return HierarchicalInterface Chainable


public function categoryItems()
    {
        if ($this->categoryItems === null) {
            $this->categoryItems = $this->loadCategoryItems();
        }
        return $this->categoryItems;
    }

public function categoryItems ( ) { if ( $ this -> categoryItems === null ) { $ this -> categoryItems = $ this -> loadCategoryItems ( ) ; } return $ this -> categoryItems ; }

Retrieves the category items, directly within it.

@return \Charcoal\Object\CategorizableInterface[]|array A list of `CategorizableInterface` objects

Retrieves the category items directly within it .


public function slugPattern()
    {
        if (!$this->slugPattern) {
            $metadata = $this->metadata();

            if (isset($metadata['routable']['pattern'])) {
                $this->setSlugPattern($metadata['routable']['pattern']);
            } elseif (isset($metadata['slug_pattern'])) {
                $this->setSlugPattern($metadata['slug_pattern']);
            } else {
                throw new Exception(sprintf(
                    'Undefined route pattern (slug) for %s',
                    get_called_class()
                ));
            }
        }

        return $this->slugPattern;
    }

public
function
slugPattern
(
)
{
if
(
!
$
this
->
slugPattern
)
{
$
metadata
=
$
this
->
metadata
(
)
;
if
(
isset
(
$
metadata
[
'routable'
]
[
'pattern'
]
)
)
{
$
this
->
setSlugPattern
(
$
metadata
[
'routable'
]
[
'pattern'
]
)
;
}
elseif
(
isset
(
$
metadata
[
'slug_pattern'
]
)
)
{
$
this
->
setSlugPattern
(
$
metadata
[
'slug_pattern'
]
)
;
}
else
{
throw
new
Exception
(
sprintf
(
'Undefined route pattern (slug) for %s'
,
get_called_class
(
)
)
)
;
}
}
return
$
this
->
slugPattern
;
}

Retrieve the object's URL slug pattern.

@throws Exception If a slug pattern is not defined.
@return \Charcoal\Translator\Translation|null

Retrieve the object s URL slug pattern .


public function slugPrefix()
    {
        if (!$this->slugPrefix) {
            $metadata = $this->metadata();

            if (isset($metadata['routable']['prefix'])) {
                $this->slugPrefix = $this->translator()->translation($metadata['routable']['prefix']);
            }
        }

        return $this->slugPrefix;
    }

public function slugPrefix ( ) { if ( ! $ this -> slugPrefix ) { $ metadata = $ this -> metadata ( ) ; if ( isset ( $ metadata [ 'routable' ] [ 'prefix' ] ) ) { $ this -> slugPrefix = $ this -> translator ( ) -> translation ( $ metadata [ 'routable' ] [ 'prefix' ] ) ; } } return $ this -> slugPrefix ; }

Retrieve route prefix for the object's URL slug pattern.

@return \Charcoal\Translator\Translation|null

Retrieve route prefix for the object s URL slug pattern .


public function slugSuffix()
    {
        if (!$this->slugSuffix) {
            $metadata = $this->metadata();

            if (isset($metadata['routable']['suffix'])) {
                $this->slugSuffix = $this->translator()->translation($metadata['routable']['suffix']);
            }
        }

        return $this->slugSuffix;
    }

public function slugSuffix ( ) { if ( ! $ this -> slugSuffix ) { $ metadata = $ this -> metadata ( ) ; if ( isset ( $ metadata [ 'routable' ] [ 'suffix' ] ) ) { $ this -> slugSuffix = $ this -> translator ( ) -> translation ( $ metadata [ 'routable' ] [ 'suffix' ] ) ; } } return $ this -> slugSuffix ; }

Retrieve route suffix for the object's URL slug pattern.

@return \Charcoal\Translator\Translation|null

Retrieve route suffix for the object s URL slug pattern .


public function isSlugEditable()
    {
        if ($this->isSlugEditable === null) {
            $metadata = $this->metadata();

            if (isset($metadata['routable']['editable'])) {
                $this->isSlugEditable = !!$metadata['routable']['editable'];
            } else {
                $this->isSlugEditable = false;
            }
        }

        return $this->isSlugEditable;
    }

public function isSlugEditable ( ) { if ( $ this -> isSlugEditable === null ) { $ metadata = $ this -> metadata ( ) ; if ( isset ( $ metadata [ 'routable' ] [ 'editable' ] ) ) { $ this -> isSlugEditable = ! ! $ metadata [ 'routable' ] [ 'editable' ] ; } else { $ this -> isSlugEditable = false ; } } return $ this -> isSlugEditable ; }

Determine if the slug is editable.

@return boolean


public function setSlug($slug)
    {
        $slug = $this->translator()->translation($slug);
        if ($slug !== null) {
            $this->slug = $slug;

            $values = $this->slug->data();
            foreach ($values as $lang => $val) {
                $this->slug[$lang] = $this->slugify($val);
            }
        } else {
            /** @todo Hack used for regenerating route */
            if (isset($_POST['slug'])) {
                $this->slug = [];
            } else {
                $this->slug = null;
            }
        }

        return $this;
    }

Set the object's URL slug.

@param  mixed $slug The slug.
@return RoutableInterface Chainable


public function generateSlug()
    {
        $languages = $this->translator()->availableLocales();
        $patterns  = $this->slugPattern();
        $curSlug   = $this->slug();
        $newSlug   = [];

        $origLang = $this->translator()->getLocale();
        foreach ($languages as $lang) {
            $pattern = $patterns[$lang];

            $this->translator()->setLocale($lang);
            if ($this->isSlugEditable() && isset($curSlug[$lang]) && strlen($curSlug[$lang])) {
                $newSlug[$lang] = $curSlug[$lang];
            } else {
                $newSlug[$lang] = $this->generateRoutePattern($pattern);
                if (!strlen($newSlug[$lang])) {
                    throw new UnexpectedValueException(sprintf(
                        'The slug is empty. The pattern is "%s"',
                        $pattern
                    ));
                }
            }
            $newSlug[$lang] = $this->finalizeSlug($newSlug[$lang]);

            $newRoute = $this->createRouteObject();
            $newRoute->setData([
                'lang'           => $lang,
                'slug'           => $newSlug[$lang],
                'route_obj_type' => $this->objType(),
                'route_obj_id'   => $this->id(),
            ]);

            if (!$newRoute->isSlugUnique()) {
                $newRoute->generateUniqueSlug();
                $newSlug[$lang] = $newRoute->slug();
            }
        }
        $this->translator()->setLocale($origLang);

        return $this->translator()->translation($newSlug);
    }

Generate a URL slug from the object's URL slug pattern.

@throws UnexpectedValueException If the slug is empty.
@return \Charcoal\Translator\Translation

Generate a URL slug from the object s URL slug pattern .


protected function generateRoutePattern($pattern)
    {
        if ($this instanceof ViewableInterface && $this->view() !== null) {
            $route = $this->view()->render($pattern, $this->viewController());
        } else {
            $route = preg_replace_callback('~\{\{\s*(.*?)\s*\}\}~i', [ $this, 'parseRouteToken' ], $pattern);
        }

        return $this->slugify($route);
    }

protected function generateRoutePattern ( $ pattern ) { if ( $ this instanceof ViewableInterface && $ this -> view ( ) !== null ) { $ route = $ this -> view ( ) -> render ( $ pattern , $ this -> viewController ( ) ) ; } else { $ route = preg_replace_callback ( '~\{\{\s*(.*?)\s*\}\}~i' , [ $ this , 'parseRouteToken' ] , $ pattern ) ; } return $ this -> slugify ( $ route ) ; }

Generate a route from the given pattern.

@uses   self::parseRouteToken() If a view renderer is unavailable.
@param  string $pattern The slug pattern.
@return string Returns the generated route.

Generate a route from the given pattern .


protected function filterRouteToken($value, $token = null)
    {
        unset($token);

        if ($value instanceof \Closure) {
            $value = $value();
        }

        if ($value instanceof \DateTime) {
            $value = $value->format('Y-m-d-H:i');
        }

        if (method_exists($value, '__toString')) {
            $value = strval($value);
        }

        return $value;
    }

protected function filterRouteToken ( $ value , $ token = null ) { unset ( $ token ) ; if ( $ value instanceof \ Closure ) { $ value = $ value ( ) ; } if ( $ value instanceof \ DateTime ) { $ value = $ value -> format ( 'Y-m-d-H:i' ) ; } if ( method_exists ( $ value , '__toString' ) ) { $ value = strval ( $ value ) ; } return $ value ; }

Filter the given value for a URI.

@used-by self::parseRouteToken() To resolve the token's value.
@param   mixed  $value A value to filter.
@param   string $token The parsed token.
@return  string The filtered $value.


protected function generateObjectRoute($slug = null, array $data = [])
    {
        if (!$slug) {
            $slug = $this->generateSlug();
        }

        if ($slug instanceof Translation) {
            $slugs = $slug->data();
        } else {
            throw new InvalidArgumentException(sprintf(
                '[%s] slug parameter must be an instance of %s, received %s',
                get_called_class().'::'.__FUNCTION__,
                Translation::class,
                is_object($slug) ? get_class($slug) : gettype($slug)
            ));
        }

        if (!is_array($data)) {
            $data = [];
        }

        $origLang = $this->translator()->getLocale();
        foreach ($slugs as $lang => $slug) {
            if (!in_array($lang, $this->translator()->availableLocales())) {
                continue;
            }
            $this->translator()->setLocale($lang);

            $newRoute = $this->createRouteObject();
            $oldRoute = $this->getLatestObjectRoute();

            $defaultData = [
                // Not used, might be too much.
                'route_template'      => $this->templateIdent(),
                'route_options'       => $this->routeOptions(),
                'route_options_ident' => $this->routeOptionsIdent(),
            ];

            $immutableData = [
                'lang'                => $lang,
                'slug'                => $slug,
                'route_obj_type'      => $this->objType(),
                'route_obj_id'        => $this->id(),
                'active'              => true,
            ];

            $newData = array_merge($defaultData, $data, $immutableData);

            // Unchanged but sync extra properties
            if ($slug === $oldRoute->slug()) {
                $oldRoute->setData([
                    'route_template'      => $newData['route_template'],
                    'route_options'       => $newData['route_options'],
                    'route_options_ident' => $newData['route_options_ident'],
                ]);
                $oldRoute->update([ 'route_template', 'route_options' ]);

                continue;
            }

            $newRoute->setData($newData);

            if (!$newRoute->isSlugUnique()) {
                $newRoute->generateUniqueSlug();
            }

            if ($newRoute->id()) {
                $newRoute->update();
            } else {
                $newRoute->save();
            }
        }

        $this->translator()->setLocale($origLang);
    }

Route generation.

Saves all routes to {@see \Charcoal\Object\ObjectRoute}.

@param  mixed $slug Slug by langs.
@param  array $data Object route custom data.
@throws InvalidArgumentException If the slug is invalid.
@return void


public function url($lang = null)
    {
        $slug = $this->slug();

        if ($slug instanceof Translation && $lang) {
            return $slug[$lang];
        }

        if ($slug) {
            return $slug;
        }

        $url = (string)$this->getLatestObjectRoute($lang)->slug();
        return $url;
    }

public function url ( $ lang = null ) { $ slug = $ this -> slug ( ) ; if ( $ slug instanceof Translation && $ lang ) { return $ slug [ $ lang ] ; } if ( $ slug ) { return $ slug ; } $ url = ( string ) $ this -> getLatestObjectRoute ( $ lang ) -> slug ( ) ; return $ url ; }

Retrieve the object's URI.

@param  string|null $lang If object is multilingual, return the object route for the specified locale.
@return string


public function slugify($str)
    {
        static $sluggedArray;

        if (isset($sluggedArray[$str])) {
            return $sluggedArray[$str];
        }

        $metadata    = $this->metadata();
        $separator   = isset($metadata['routable']['separator']) ? $metadata['routable']['separator'] : '-';
        $delimiters  = '-_|';
        $pregDelim   = preg_quote($delimiters);
        $directories = '\\/';
        $pregDir     = preg_quote($directories);

        // Do NOT remove forward slashes.
        $slug = preg_replace('![^(\p{L}|\p{N})(\s|\/)]!u', $separator, $str);

        if (!isset($metadata['routable']['lowercase']) || $metadata['routable']['lowercase'] === false) {
            $slug = mb_strtolower($slug, 'UTF-8');
        }

        // Strip HTML
        $slug = strip_tags($slug);

        // Remove diacritics
        $slug = htmlentities($slug, ENT_COMPAT, 'UTF-8');
        $slug = preg_replace('!&([a-zA-Z])(uml|acute|grave|circ|tilde|cedil|ring);!', '$1', $slug);

        // Simplify ligatures
        $slug = preg_replace('!&([a-zA-Z]{2})(lig);!', '$1', $slug);

        // Remove unescaped HTML characters
        $unescaped = '!&(raquo|laquo|rsaquo|lsaquo|rdquo|ldquo|rsquo|lsquo|hellip|amp|nbsp|quot|ordf|ordm);!';
        $slug = preg_replace($unescaped, '', $slug);

        // Unify all dashes/underscores as one separator character
        $flip = ($separator === '-') ? '_' : '-';
        $slug = preg_replace('!['.preg_quote($flip).']+!u', $separator, $slug);

        // Remove all whitespace and normalize delimiters
        $slug = preg_replace('![_\|\s|\(\)]+!', $separator, $slug);

        // Squeeze multiple delimiters and whitespace with a single separator
        $slug = preg_replace('!['.$pregDelim.'\s]{2,}!', $separator, $slug);

        // Squeeze multiple URI path delimiters
        $slug = preg_replace('!['.$pregDir.']{2,}!', $separator, $slug);

        // Remove delimiters surrouding URI path delimiters
        $slug = preg_replace('!(?<=['.$pregDir.'])['.$pregDelim.']|['.$pregDelim.'](?=['.$pregDir.'])!', '', $slug);

        // Strip leading and trailing dashes or underscores
        $slug = trim($slug, $delimiters);

        // Cache the slugified string
        $sluggedArray[$str] = $slug;

        return $slug;
    }

Convert a string into a slug.

@param  string $str The string to slugify.
@return string The slugified string.


protected function finalizeSlug($slug)
    {
        $prefix = $this->slugPrefix();
        if ($prefix) {
            $prefix = $this->generateRoutePattern((string)$prefix);
            if ($slug === $prefix) {
                throw new UnexpectedValueException('The slug is the same as the prefix.');
            }
            $slug = $prefix.preg_replace('!^'.preg_quote($prefix).'\b!', '', $slug);
        }

        $suffix = $this->slugSuffix();
        if ($suffix) {
            $suffix = $this->generateRoutePattern((string)$suffix);
            if ($slug === $suffix) {
                throw new UnexpectedValueException('The slug is the same as the suffix.');
            }
            $slug = preg_replace('!\b'.preg_quote($suffix).'$!', '', $slug).$suffix;
        }

        $slug = rtrim($slug, '/');

        return $slug;
    }

protected
function
finalizeSlug
(
$
slug
)
{
$
prefix
=
$
this
->
slugPrefix
(
)
;
if
(
$
prefix
)
{
$
prefix
=
$
this
->
generateRoutePattern
(
(
string
)
$
prefix
)
;
if
(
$
slug
===
$
prefix
)
{
throw
new
UnexpectedValueException
(
'The slug is the same as the prefix.'
)
;
}
$
slug
=
$
prefix
.
preg_replace
(
'!^'
.
preg_quote
(
$
prefix
)
.
'\b!'
,
''
,
$
slug
)
;
}
$
suffix
=
$
this
->
slugSuffix
(
)
;
if
(
$
suffix
)
{
$
suffix
=
$
this
->
generateRoutePattern
(
(
string
)
$
suffix
)
;
if
(
$
slug
===
$
suffix
)
{
throw
new
UnexpectedValueException
(
'The slug is the same as the suffix.'
)
;
}
$
slug
=
preg_replace
(
'!\b'
.
preg_quote
(
$
suffix
)
.
'$!'
,
''
,
$
slug
)
.
$
suffix
;
}
$
slug
=
rtrim
(
$
slug
,
'/'
)
;
return
$
slug
;
}

Finalize slug.

Adds any prefix and suffix defined in the routable configuration set.

@param  string $slug A slug.
@throws UnexpectedValueException If the slug affixes are invalid.
@return string


protected function deleteObjectRoutes()
    {
        if (!$this->objType()) {
            return false;
        }

        if (!$this->id()) {
            return false;
        }

        $loader = $this->createRouteObjectCollectionLoader();
        $loader
            ->addFilters([
                [
                    'property' => 'route_obj_type',
                    'value'    => $this->objType(),
                ],
                [
                    'property' => 'route_obj_id',
                    'value'    => $this->id(),
                ],
            ]);

        $collection = $loader->load();
        foreach ($collection as $route) {
            $route->delete();
        }

        return true;
    }

protected function deleteObjectRoutes ( ) { if ( ! $ this -> objType ( ) ) { return false ; } if ( ! $ this -> id ( ) ) { return false ; } $ loader = $ this -> createRouteObjectCollectionLoader ( ) ; $ loader -> addFilters ( [ [ 'property' => 'route_obj_type' , 'value' => $ this -> objType ( ) , ] , [ 'property' => 'route_obj_id' , 'value' => $ this -> id ( ) , ] , ] ) ; $ collection = $ loader -> load ( ) ; foreach ( $ collection as $ route ) { $ route -> delete ( ) ; } return true ; }

Delete all object routes.

Should be called on object deletion {@see \Charcoal\Model\AbstractModel::preDelete()}.

@return boolean Success or failure.


public function createRouteObjectCollectionLoader()
    {
        $loader = new CollectionLoader([
            'logger'  => $this->logger,
            'factory' => $this->modelFactory(),
            'model'   => $this->getRouteObjectPrototype(),
        ]);

        return $loader;
    }

public function createRouteObjectCollectionLoader ( ) { $ loader = new CollectionLoader ( [ 'logger' => $ this -> logger , 'factory' => $ this -> modelFactory ( ) , 'model' => $ this -> getRouteObjectPrototype ( ) , ] ) ; return $ loader ; }

Create a route collection loader.

@return CollectionLoader


public function allRevisions(callable $callback = null)
    {
        $loader = new CollectionLoader([
            'logger'    => $this->logger,
            'factory'   => $this->modelFactory()
        ]);
        $loader->setModel($this->createRevisionObject());
        $loader->addFilter('target_type', $this->objType());
        $loader->addFilter('target_id', $this->id());
        $loader->addOrder('rev_ts', 'desc');
        if ($callback !== null) {
            $loader->setCallback($callback);
        }
        $revisions = $loader->load();

        return $revisions->objects();
    }

public function allRevisions ( callable $ callback = null ) { $ loader = new CollectionLoader ( [ 'logger' => $ this -> logger , 'factory' => $ this -> modelFactory ( ) ] ) ; $ loader -> setModel ( $ this -> createRevisionObject ( ) ) ; $ loader -> addFilter ( 'target_type' , $ this -> objType ( ) ) ; $ loader -> addFilter ( 'target_id' , $ this -> id ( ) ) ; $ loader -> addOrder ( 'rev_ts' , 'desc' ) ; if ( $ callback !== null ) { $ loader -> setCallback ( $ callback ) ; } $ revisions = $ loader -> load ( ) ; return $ revisions -> objects ( ) ; }

Retrieves all revisions for the current objet

@param callable $callback Optional object callback.
@return array


public function setPublishDate($time)
    {
        if ($time === null || $time === '') {
            $this->publishDate = null;
            return $this;
        }

        if (is_string($time)) {
            try {
                $time = new DateTime($time);
            } catch (Exception $e) {
                throw new UnexpectedValueException(sprintf(
                    'Invalid Publication Date: %s',
                    $e->getMessage()
                ), $e->getCode(), $e);
            }
        }

        if (!$time instanceof DateTimeInterface) {
            throw new InvalidArgumentException(
                'Publication Date must be a date/time string or an instance of DateTimeInterface'
            );
        }

        $this->publishDate = $time;

        return $this;
    }

Set the object's publication date.

@param  string|DateTimeInterface|null $time The date/time value.
@throws UnexpectedValueException If the date/time value is invalid.
@throws InvalidArgumentException If the value is not a date/time instance.
@return PublishableInterface Chainable


public function setExpiryDate($time)
    {
        if ($time === null || $time === '') {
            $this->expiryDate = null;
            return $this;
        }

        if (is_string($time)) {
            try {
                $time = new DateTime($time);
            } catch (Exception $e) {
                throw new UnexpectedValueException(sprintf(
                    'Invalid Expiration Date: %s',
                    $e->getMessage()
                ), $e->getCode(), $e);
            }
        }

        if (!$time instanceof DateTimeInterface) {
            throw new InvalidArgumentException(
                'Expiration Date must be a date/time string or an instance of DateTimeInterface'
            );
        }

        $this->expiryDate = $time;

        return $this;
    }

Set the object's expiration date.

@param  string|DateTimeInterface|null $time The date/time value.
@throws UnexpectedValueException If the date/time value is invalid.
@throws InvalidArgumentException If the value is not a date/time instance.
@return PublishableInterface Chainable


public function setPublishStatus($status)
    {
        if ($status === null || $status === '') {
            $this->publishStatus = null;
            return $this;
        }

        $specialStatus = [
            static::STATUS_EXPIRED  => static::STATUS_PUBLISHED,
            static::STATUS_UPCOMING => static::STATUS_PUBLISHED
        ];

        /** Resolve any special statuses */
        if (isset($specialStatus[$status])) {
            $status = $specialStatus[$status];
        }

        $validStatus = [
            static::STATUS_DRAFT,
            static::STATUS_PENDING,
            static::STATUS_PUBLISHED
        ];

        if (!in_array($status, $validStatus)) {
            throw new InvalidArgumentException(sprintf(
                'Status "%s" is not a valid publish status.',
                $status
            ));
        }

        $this->publishStatus = $status;

        return $this;
    }

Set the object's publication status.

@param  string $status A publication status.
@throws InvalidArgumentException If the status is invalid.
@return PublishableInterface Chainable


public function publishDateStatus()
    {
        $now = new DateTime();
        $publish = $this->publishDate();
        $expiry  = $this->expiryDate();
        $status  = $this->publishStatus();

        if ($status !== static::STATUS_PUBLISHED) {
            return $status;
        }

        if (!$publish) {
            if (!$expiry || $now < $expiry) {
                return static::STATUS_PUBLISHED;
            } else {
                return static::STATUS_EXPIRED;
            }
        } else {
            if ($now < $publish) {
                return static::STATUS_UPCOMING;
            } else {
                if (!$expiry || $now < $expiry) {
                    return static::STATUS_PUBLISHED;
                } else {
                    return static::STATUS_EXPIRED;
                }
            }
        }
    }

public function publishDateStatus ( ) { $ now = new DateTime ( ) ; $ publish = $ this -> publishDate ( ) ; $ expiry = $ this -> expiryDate ( ) ; $ status = $ this -> publishStatus ( ) ; if ( $ status !== static :: STATUS_PUBLISHED ) { return $ status ; } if ( ! $ publish ) { if ( ! $ expiry || $ now < $ expiry ) { return static :: STATUS_PUBLISHED ; } else { return static :: STATUS_EXPIRED ; } } else { if ( $ now < $ publish ) { return static :: STATUS_UPCOMING ; } else { if ( ! $ expiry || $ now < $ expiry ) { return static :: STATUS_PUBLISHED ; } else { return static :: STATUS_EXPIRED ; } } } }

Retrieve the object's publication status based on publication and expiration dates.

- If the publication status is not "published", that status is returned (e.g., "draft", "pending", NULL).
- If no publication date is set, then it's assumed to be always published (or "expired").
- If no expiration date is set, then it's assumed to never expire.
- If a publication date is set to a future date, then it's assumed to be scheduled to be published ("upcoming").

@return string|null

Retrieve the object s publication status based on publication and expiration dates .


public function index()
    {
        if (!userHasPermission('admin:admin:help:view')) {
            unauthorised();
        }

        // --------------------------------------------------------------------------

        //  Page Title
        $this->data['page']->title = 'Help Videos';

        // --------------------------------------------------------------------------

        //  Get data
        $oInput      = Factory::service('Input');
        $oHelpModel  = Factory::model('Help', 'nails/module-admin');
        $sTableAlias = $oHelpModel->getTableAlias();

        //  Get pagination and search/sort variables
        $iPage      = (int) $oInput->get('page') ?: 0;
        $iPerPage   = (int) $oInput->get('perPage') ?: 50;
        $sSortOn    = $oInput->get('sortOn') ?: $sTableAlias . '.label';
        $sSortOrder = $oInput->get('sortOrder') ?: 'asc';
        $sKeywords  = $oInput->get('keywords') ?: '';

        // --------------------------------------------------------------------------

        //  Define the sortable columns
        $aSortColumns = [
            $sTableAlias . '.label'    => 'Label',
            $sTableAlias . '.duration' => 'Duration',
            $sTableAlias . '.created'  => 'Added',
            $sTableAlias . '.modified' => 'Modified',
        ];

        // --------------------------------------------------------------------------

        //  Define the $aData variable for the queries
        $aData = [
            'sort'     => [
                [$sSortOn, $sSortOrder],
            ],
            'keywords' => $sKeywords,
        ];

        //  Get the items for the page
        $iTotalRows           = $oHelpModel->countAll($aData);
        $this->data['videos'] = $oHelpModel->getAll($iPage, $iPerPage, $aData);

        //  Set Search and Pagination objects for the view
        $this->data['search']     = Helper::searchObject(true, $aSortColumns, $sSortOn, $sSortOrder, $iPerPage, $sKeywords);
        $this->data['pagination'] = Helper::paginationObject($iPage, $iPerPage, $iTotalRows);

        // --------------------------------------------------------------------------

        $oAsset = Factory::service('Asset');
        $oAsset->inline('$(\'a.video-button\').fancybox({ type : \'iframe\' });', 'JS');

        // --------------------------------------------------------------------------

        //  Load views
        Helper::loadView('index');
    }

Renders the admin help pagge
@return void


public function rewrite_routes()
    {
        if (!userHasPermission('admin:admin:utilities:rewriteRoutes')) {
            unauthorised();
        }

        // --------------------------------------------------------------------------

        $oInput = Factory::service('Input');
        if ($oInput->post('go')) {
            $oRoutesService = Factory::service('Routes');
            if ($oRoutesService->update()) {
                $this->data['success'] = 'Routes rewritten successfully.';
            } else {
                $this->data['error'] = 'There was a problem writing the routes. ';
                $this->data['error'] .= $oRoutesService->lastError();
            }
        }

        // --------------------------------------------------------------------------

        //  Load views
        Helper::loadView('rewriteRoutes');
    }


public function reset()
    {
        if (!empty($this->aData)) {
            reset($this->aData);
        } elseif ($this->oSource instanceof \PDOStatement) {
            //  unsupported
        } elseif ($this->oSource instanceof \CI_DB_mysqli_result) {
            $this->oSource->data_seek(0);
        }
    }

public
function
reset
(
)
{
if
(
!
empty
(
$
this
->
aData
)
)
{
reset
(
$
this
->
aData
)
;
}
elseif
(
$
this
->
oSource
instanceof
\
PDOStatement
)
{
//  unsupported
}
elseif
(
$
this
->
oSource
instanceof
\
CI_DB_mysqli_result
)
{
$
this
->
oSource
->
data_seek
(
0
)
;
}
}

Resets the pointer in the data set to the beginning


public function getNextItem()
    {
        $oRow = null;

        if (!empty($this->aData)) {
            $oRow = current($this->aData);
            next($this->aData);
        } elseif ($this->oSource instanceof \PDOStatement) {
            $oRow = $this->oSource->fetch(\PDO::FETCH_ASSOC);
        } elseif ($this->oSource instanceof \CI_DB_mysqli_result) {
            $oRow = $this->oSource->unbuffered_row();
        }

        return is_callable($this->cFormatter) ? call_user_func($this->cFormatter, $oRow) : $oRow;
    }

public function getNextItem ( ) { $ oRow = null ; if ( ! empty ( $ this -> aData ) ) { $ oRow = current ( $ this -> aData ) ; next ( $ this -> aData ) ; } elseif ( $ this -> oSource instanceof \ PDOStatement ) { $ oRow = $ this -> oSource -> fetch ( \ PDO :: FETCH_ASSOC ) ; } elseif ( $ this -> oSource instanceof \ CI_DB_mysqli_result ) { $ oRow = $ this -> oSource -> unbuffered_row ( ) ; } return is_callable ( $ this -> cFormatter ) ? call_user_func ( $ this -> cFormatter , $ oRow ) : $ oRow ; }

Returns the next item in the data set, passing it to the formatter, if defined

@return \stdClass

Returns the next item in the data set passing it to the formatter if defined


public function site()
    {
        if (!userHasPermission('admin:admin:logs:site:browse')) {
            unauthorised();
        }

        // --------------------------------------------------------------------------

        Factory::helper('string');
        $oUri    = Factory::service('Uri');
        $sMethod = $oUri->segment(5) ? $oUri->segment(5) : 'index';
        $sMethod = 'site' . underscoreToCamelcase(strtolower($sMethod), false);

        if (method_exists($this, $sMethod)) {
            $this->{$sMethod}();
        } else {
            show404('', true);
        }
    }

Route site log pages
@throws \Nails\Common\Exception\FactoryException


protected function siteIndex()
    {
        if (!userHasPermission('admin:admin:logs:site:browse')) {
            unauthorised();
        }

        // --------------------------------------------------------------------------

        $this->data['page']->title = 'Browse Logs';

        $oAsset = Factory::service('Asset');
        $oAsset->library('MUSTACHE');
        $oAsset->load('nails.admin.logs.site.min.js', 'NAILS');
        $oAsset->inline('logsSite = new NAILS_Admin_Logs_Site();', 'JS');

        Helper::loadView('site/index');
    }


protected function siteView()
    {
        if (!userHasPermission('admin:admin:logs:site:browse')) {
            unauthorised();
        }

        // --------------------------------------------------------------------------

        $oUri                      = Factory::service('Uri');
        $sFile                     = $oUri->segment(6);
        $this->data['page']->title = 'Browse Logs › ' . $sFile;

        $oSiteLogModel      = Factory::model('SiteLog', 'nails/module-admin');
        $this->data['logs'] = $oSiteLogModel->readLog($sFile);

        if (!$this->data['logs']) {
            show404();
        }

        Helper::loadView('site/view');
    }


public function event()
    {
        if (!userHasPermission('admin:admin:logs:event:browse')) {
            unauthorised();
        }

        // --------------------------------------------------------------------------

        //  Set method info
        $this->data['page']->title = 'Browse Events';

        // --------------------------------------------------------------------------

        $sTableAlias = $this->event->getTableAlias();

        // --------------------------------------------------------------------------

        //  Get pagination and search/sort variables
        $oInput     = Factory::service('Input');
        $iPage      = (int) $oInput->get('page') ?: 0;
        $iPerPage   = (int) $oInput->get('perPage') ?: 50;
        $sSortOn    = $oInput->get('sortOn') ?: $sTableAlias . '.created';
        $sSortOrder = $oInput->get('sortOrder') ?: 'desc';
        $sKeywords  = $oInput->get('keywords') ?: '';

        // --------------------------------------------------------------------------

        //  Define the sortable columns
        $aSortColumns = [
            $sTableAlias . '.created' => 'Created',
            $sTableAlias . '.type'    => 'Type',
        ];

        // --------------------------------------------------------------------------

        //  Define the $aData variable for the queries
        $aData = [
            'sort'     => [
                [$sSortOn, $sSortOrder],
            ],
            'keywords' => $sKeywords,
        ];

        //  Are we downloading? Or viewing?
        if ($oInput->get('dl') && userHasPermission('admin:admin:logs:event:download')) {

            //  Get all items for the search, the view will iterate over the resultset
            $oEvents = $this->event->getAllRawQuery(null, null, $aData);

            Helper::loadCsv($oEvents, 'export-events-' . toUserDatetime(null, 'Y-m-d_h-i-s') . '.csv');

        } else {

            //  Get the items for the page
            $iTotalRows           = $this->event->countAll($aData);
            $this->data['events'] = $this->event->getAll($iPage, $iPerPage, $aData);

            //  Set Search and Pagination objects for the view
            $this->data['search']     = Helper::searchObject(true, $aSortColumns, $sSortOn, $sSortOrder, $iPerPage, $sKeywords);
            $this->data['pagination'] = Helper::paginationObject($iPage, $iPerPage, $iTotalRows);

            //  Add the header button for downloading
            if (userHasPermission('admin:admin:logs:event:download')) {

                //  Build the query string, so that the same search is applies
                $oInput               = Factory::service('Input');
                $aParams              = [];
                $aParams['dl']        = true;
                $aParams['sortOn']    = $oInput->get('sortOn');
                $aParams['sortOrder'] = $oInput->get('sortOrder');
                $aParams['keywords']  = $oInput->get('keywords');

                $aParams = array_filter($aParams);
                $aParams = http_build_query($aParams);

                Helper::addHeaderButton('admin/admin/logs/event?' . $aParams, 'Download As CSV');
            }

            Helper::loadView('event/index');
        }
    }


public static function permissions(): array
    {
        $aPermissions = parent::permissions();

        if (!empty(static::CONFIG_PERMISSION)) {
            $aPermissions['browse']  = 'Can browse items';
            $aPermissions['create']  = 'Can create items';
            $aPermissions['edit']    = 'Can edit items';
            $aPermissions['delete']  = 'Can delete items';
            $aPermissions['restore'] = 'Can restore items';
        }

        return $aPermissions;
    }

public
static
function
permissions
(
)
:
array
{
$
aPermissions
=
parent
::
permissions
(
)
;
if
(
!
empty
(
static
::
CONFIG_PERMISSION
)
)
{
$
aPermissions
[
'browse'
]
=
'Can browse items'
;
$
aPermissions
[
'create'
]
=
'Can create items'
;
$
aPermissions
[
'edit'
]
=
'Can edit items'
;
$
aPermissions
[
'delete'
]
=
'Can delete items'
;
$
aPermissions
[
'restore'
]
=
'Can restore items'
;
}
return
$
aPermissions
;
}

Returns an array of extra permissions for this controller

@return array


public function index(): void
    {
        if (!static::userCan('browse')) {
            unauthorised();
        }

        $oInput  = Factory::service('Input');
        $oModel  = $this->getModel();
        $aConfig = $this->getConfig();

        $sAlias      = $oModel->getTableAlias();
        $aSortConfig = $aConfig['SORT_OPTIONS'];

        if (classUses($oModel, Nestable::class)) {
            $aSortConfig = array_merge(['Hierarchy' => 'order'], $aSortConfig);
        }

        //  Get the first key (i.e the default sort)
        $sFirstKey = reset($aSortConfig);

        //  Prepare the sort options so they have the appropriate table alias
        $aSortCol = [];
        foreach ($aSortConfig as $sLabel => $sColumn) {
            if (strpos($sColumn, '.') === false) {
                $aSortCol[$sAlias . '.' . $sColumn] = $sLabel;
            } else {
                $aSortCol[$sColumn] = $sLabel;
            }
        }

        //  Other parameters
        $iPage      = $oInput->get('page') ? $oInput->get('page') : 0;
        $iPerPage   = $oInput->get('perPage') ? $oInput->get('perPage') : 50;
        $sSortOn    = $oInput->get('sortOn') ? $oInput->get('sortOn') : $sAlias . '.' . $sFirstKey;
        $sSortOrder = $oInput->get('sortOrder') ? $oInput->get('sortOrder') : $aConfig['SORT_DIRECTION'];
        $sKeywords  = $oInput->get('keywords');
        $aCbFilters = $this->indexCheckboxFilters();
        $aDdFilters = $this->indexDropdownFilters();

        $aData = [
                'cbFilters' => $aCbFilters,
                'ddFilters' => $aDdFilters,
                'keywords'  => $sKeywords,
                'sort'      => [
                    [$sSortOn, $sSortOrder],
                ],
            ] + $aConfig['INDEX_DATA'];

        // --------------------------------------------------------------------------

        if (classUses($oModel, Localised::class)) {
            $aData['NO_LOCALISE_FILTER'] = true;
        }

        // --------------------------------------------------------------------------

        $iTotalRows               = $oModel->countAll($aData);
        $this->data['items']      = $oModel->getAll($iPage, $iPerPage, $aData);
        $this->data['pagination'] = Helper::paginationObject($iPage, $iPerPage, $iTotalRows);
        $this->data['search']     = Helper::searchObject(
            true,
            $aSortCol,
            $sSortOn,
            $sSortOrder,
            $iPerPage,
            $sKeywords,
            $aCbFilters,
            $aDdFilters
        );

        // --------------------------------------------------------------------------

        static::addHeaderButtons($aConfig['INDEX_HEADER_BUTTONS']);

        // --------------------------------------------------------------------------

        $this->data['page']->title = $aConfig['TITLE_PLURAL'] . ' › Manage';
        Helper::loadView('index');
    }


public function delete(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_DELETE']) {
            show404();
        } elseif (!static::userCan('delete')) {
            unauthorised();
        }

        $oDb    = Factory::service('Database');
        $oModel = $this->getModel();
        $oItem  = $this->getItem();

        if (empty($oItem)) {
            show404();
        }

        try {

            $oDb->trans_begin();
            $this->beforeDelete($oItem);

            if (classUses($oModel, Localised::class)) {
                $oModel->delete($oItem->id, $oItem->locale);
            } elseif (!$oModel->delete($oItem->id)) {
                throw new NailsException(static::DELETE_ERROR_MESSAGE . ' ' . $oModel->lastError());
            }

            $this->afterDelete($oItem);
            $oDb->trans_commit();

            if ($aConfig['CAN_RESTORE'] && static::userCan('restore')) {
                if (classUses($oModel, Localised::class)) {
                    $sRestoreLink = anchor(
                        $aConfig['BASE_URL'] . '/restore/' . $oItem->id . '/' . $oItem->locale,
                        'Restore?'
                    );
                } else {
                    $sRestoreLink = anchor(
                        $aConfig['BASE_URL'] . '/restore/' . $oItem->id,
                        'Restore?'
                    );
                }
            } else {
                $sRestoreLink = '';
            }

            $oSession = Factory::service('Session', 'nails/module-auth');
            $oSession->setFlashData('success', static::DELETE_SUCCESS_MESSAGE . ' ' . $sRestoreLink);
            $this->returnToIndex();

        } catch (\Exception $e) {
            $oDb->trans_rollback();
            $oSession = Factory::service('Session', 'nails/module-auth');
            $oSession->setFlashData('error', static::DELETE_ERROR_MESSAGE . ' ' . $e->getMessage());
            $this->returnToIndex();
        }
    }

public
function
delete
(
)
:
void
{
$
aConfig
=
$
this
->
getConfig
(
)
;
if
(
!
$
aConfig
[
'CAN_DELETE'
]
)
{
show404
(
)
;
}
elseif
(
!
static
::
userCan
(
'delete'
)
)
{
unauthorised
(
)
;
}
$
oDb
=
Factory
::
service
(
'Database'
)
;
$
oModel
=
$
this
->
getModel
(
)
;
$
oItem
=
$
this
->
getItem
(
)
;
if
(
empty
(
$
oItem
)
)
{
show404
(
)
;
}
try
{
$
oDb
->
trans_begin
(
)
;
$
this
->
beforeDelete
(
$
oItem
)
;
if
(
classUses
(
$
oModel
,
Localised
::
class
)
)
{
$
oModel
->
delete
(
$
oItem
->
id
,
$
oItem
->
locale
)
;
}
elseif
(
!
$
oModel
->
delete
(
$
oItem
->
id
)
)
{
throw
new
NailsException
(
static
::
DELETE_ERROR_MESSAGE
.
' '
.
$
oModel
->
lastError
(
)
)
;
}
$
this
->
afterDelete
(
$
oItem
)
;
$
oDb
->
trans_commit
(
)
;
if
(
$
aConfig
[
'CAN_RESTORE'
]
&&
static
::
userCan
(
'restore'
)
)
{
if
(
classUses
(
$
oModel
,
Localised
::
class
)
)
{
$
sRestoreLink
=
anchor
(
$
aConfig
[
'BASE_URL'
]
.
'/restore/'
.
$
oItem
->
id
.
'/'
.
$
oItem
->
locale
,
'Restore?'
)
;
}
else
{
$
sRestoreLink
=
anchor
(
$
aConfig
[
'BASE_URL'
]
.
'/restore/'
.
$
oItem
->
id
,
'Restore?'
)
;
}
}
else
{
$
sRestoreLink
=
''
;
}
$
oSession
=
Factory
::
service
(
'Session'
,
'nails/module-auth'
)
;
$
oSession
->
setFlashData
(
'success'
,
static
::
DELETE_SUCCESS_MESSAGE
.
' '
.
$
sRestoreLink
)
;
$
this
->
returnToIndex
(
)
;
}
catch
(
\
Exception
$
e
)
{
$
oDb
->
trans_rollback
(
)
;
$
oSession
=
Factory
::
service
(
'Session'
,
'nails/module-auth'
)
;
$
oSession
->
setFlashData
(
'error'
,
static
::
DELETE_ERROR_MESSAGE
.
' '
.
$
e
->
getMessage
(
)
)
;
$
this
->
returnToIndex
(
)
;
}
}


public function restore(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_RESTORE']) {
            show404();
        } elseif (!static::userCan('restore')) {
            unauthorised();
        }

        $oUri     = Factory::service('Uri');
        $oDb      = Factory::service('Database');
        $oSession = Factory::service('Session', 'nails/module-auth');
        $oModel   = $this->getModel();
        $oItem    = $this->getItem([], null, true);

        try {

            $oDb->trans_begin();
            if (classUses($oModel, Localised::class)) {
                $bResult = $oModel->restore($oItem->id, $oItem->locale);
            } else {
                $bResult = $oModel->restore($oItem->id);
            }

            if (!$bResult) {
                throw new NailsException(static::RESTORE_ERROR_MESSAGE . ' ' . $oModel->lastError());
            }

            $oDb->trans_commit();
            $oSession->setFlashData('success', static::RESTORE_SUCCESS_MESSAGE);
            $this->returnToIndex();

        } catch (\Exception $e) {
            $oDb->trans_rollback();
            $oSession->setFlashData('error', static::RESTORE_ERROR_MESSAGE . ' ' . $e->getMessage());
            $this->returnToIndex();
        }
    }

public
function
restore
(
)
:
void
{
$
aConfig
=
$
this
->
getConfig
(
)
;
if
(
!
$
aConfig
[
'CAN_RESTORE'
]
)
{
show404
(
)
;
}
elseif
(
!
static
::
userCan
(
'restore'
)
)
{
unauthorised
(
)
;
}
$
oUri
=
Factory
::
service
(
'Uri'
)
;
$
oDb
=
Factory
::
service
(
'Database'
)
;
$
oSession
=
Factory
::
service
(
'Session'
,
'nails/module-auth'
)
;
$
oModel
=
$
this
->
getModel
(
)
;
$
oItem
=
$
this
->
getItem
(
[
]
,
,
true
)
;
try
{
$
oDb
->
trans_begin
(
)
;
if
(
classUses
(
$
oModel
,
Localised
::
class
)
)
{
$
bResult
=
$
oModel
->
restore
(
$
oItem
->
id
,
$
oItem
->
locale
)
;
}
else
{
$
bResult
=
$
oModel
->
restore
(
$
oItem
->
id
)
;
}
if
(
!
$
bResult
)
{
throw
new
NailsException
(
static
::
RESTORE_ERROR_MESSAGE
.
' '
.
$
oModel
->
lastError
(
)
)
;
}
$
oDb
->
trans_commit
(
)
;
$
oSession
->
setFlashData
(
'success'
,
static
::
RESTORE_SUCCESS_MESSAGE
)
;
$
this
->
returnToIndex
(
)
;
}
catch
(
\
Exception
$
e
)
{
$
oDb
->
trans_rollback
(
)
;
$
oSession
->
setFlashData
(
'error'
,
static
::
RESTORE_ERROR_MESSAGE
.
' '
.
$
e
->
getMessage
(
)
)
;
$
this
->
returnToIndex
(
)
;
}
}


public function sort(): void
    {
        $aConfig = $this->getConfig();
        if (!$aConfig['CAN_EDIT']) {
            show404();
        } elseif (!static::userCan('edit')) {
            unauthorised();
        }

        $oModel = $this->getModel();
        $oInput = Factory::service('Input');
        $oDb    = Factory::service('Database');

        if ($oInput->post()) {
            try {

                $oDb->trans_begin();
                $aItems = array_values((array) $oInput->post('order'));
                foreach ($aItems as $iOrder => $iId) {

                    if (classUses($oModel, Localised::class)) {

                        $aItems = $oModel->getAll([
                            'NO_LOCALISE_FILTER' => true,
                            'where'              => [
                                ['id', $iId],
                            ],
                        ]);

                        foreach ($aItems as $oItem) {
                            if (!$oModel->update($iId, ['order' => $iOrder], $oItem->locale)) {
                                throw new NailsException(
                                    static::ORDER_ERROR_MESSAGE . ' ' . $oModel->lastError()
                                );
                            }
                        }

                    } elseif (!$oModel->update($iId, ['order' => $iOrder])) {
                        throw new NailsException(
                            static::ORDER_ERROR_MESSAGE . ' ' . $oModel->lastError()
                        );
                    }
                }

                $oDb->trans_commit();

                $oSession = Factory::service('Session', 'nails/module-auth');
                $oSession->setFlashData('success', static::ORDER_SUCCESS_MESSAGE);

                redirect($aConfig['BASE_URL'] . '/sort');

            } catch (\Exception $e) {
                $oDb->trans_rollback();
                $this->data['error'] = $e->getMessage();
            }
        }

        $aItems                    = $oModel->getAll($aConfig['SORT_DATA']);
        $this->data['items']       = $aItems;
        $this->data['page']->title = $aConfig['TITLE_PLURAL'] . ' › Sort';
        Helper::loadView('order');
    }

public
function
sort
(
)
:
void
{
$
aConfig
=
$
this
->
getConfig
(
)
;
if
(
!
$
aConfig
[
'CAN_EDIT'
]
)
{
show404
(
)
;
}
elseif
(
!
static
::
userCan
(
'edit'
)
)
{
unauthorised
(
)
;
}
$
oModel
=
$
this
->
getModel
(
)
;
$
oInput
=
Factory
::
service
(
'Input'
)
;
$
oDb
=
Factory
::
service
(
'Database'
)
;
if
(
$
oInput
->
post
(
)
)
{
try
{
$
oDb
->
trans_begin
(
)
;
$
aItems
=
array_values
(
(
array
)
$
oInput
->
post
(
'order'
)
)
;
foreach
(
$
aItems
as
$
iOrder
=>
$
iId
)
{
if
(
classUses
(
$
oModel
,
Localised
::
class
)
)
{
$
aItems
=
$
oModel
->
getAll
(
[
'NO_LOCALISE_FILTER'
=>
true
,
'where'
=>
[
[
'id'
,
$
iId
]
,
]
,
]
)
;
foreach
(
$
aItems
as
$
oItem
)
{
if
(
!
$
oModel
->
update
(
$
iId
,
[
'order'
=>
$
iOrder
]
,
$
oItem
->
locale
)
)
{
throw
new
NailsException
(
static
::
ORDER_ERROR_MESSAGE
.
' '
.
$
oModel
->
lastError
(
)
)
;
}
}
}
elseif
(
!
$
oModel
->
update
(
$
iId
,
[
'order'
=>
$
iOrder
]
)
)
{
throw
new
NailsException
(
static
::
ORDER_ERROR_MESSAGE
.
' '
.
$
oModel
->
lastError
(
)
)
;
}
}
$
oDb
->
trans_commit
(
)
;
$
oSession
=
Factory
::
service
(
'Session'
,
'nails/module-auth'
)
;
$
oSession
->
setFlashData
(
'success'
,
static
::
ORDER_SUCCESS_MESSAGE
)
;
redirect
(
$
aConfig
[
'BASE_URL'
]
.
'/sort'
)
;
}
catch
(
\
Exception
$
e
)
{
$
oDb
->
trans_rollback
(
)
;
$
this
->
data
[
'error'
]
=
$
e
->
getMessage
(
)
;
}
}
$
aItems
=
$
oModel
->
getAll
(
$
aConfig
[
'SORT_DATA'
]
)
;
$
this
->
data
[
'items'
]
=
$
aItems
;
$
this
->
data
[
'page'
]
->
title
=
$
aConfig
[
'TITLE_PLURAL'
]
.
' › Sort'
;
Helper
::
loadView
(
'order'
)
;
}


protected static function localisedItemCanBeDeleted(Resource $oItem)
    {
        /** @var Locale $oLocale */
        $oLocale        = Factory::service('Locale');
        $sDefaultLocale = (string) $oLocale->getDefautLocale();
        $sItemLocale    = (string) $oItem->locale;
        if ($sDefaultLocale !== $sItemLocale) {
            return true;
        } elseif ($sDefaultLocale === $sItemLocale && count($oItem->available_locales) === 1) {
            return true;
        } else {
            return false;
        }
    }

protected
static
function
localisedItemCanBeDeleted
(
Resource
$
oItem
)
{
/** @var Locale $oLocale */
$
oLocale
=
Factory
::
service
(
'Locale'
)
;
$
sDefaultLocale
=
(
string
)
$
oLocale
->
getDefautLocale
(
)
;
$
sItemLocale
=
(
string
)
$
oItem
->
locale
;
if
(
$
sDefaultLocale
!==
$
sItemLocale
)
{
return
true
;
}
elseif
(
$
sDefaultLocale
===
$
sItemLocale
&&
count
(
$
oItem
->
available_locales
)
===
1
)
{
return
true
;
}
else
{
return
false
;
}
}

Determines whether a localised item can be delted

@param Resource $oItem The item to test

@return bool
@throws FactoryException


protected static function getTitleSingle(): string
    {
        if (!empty(static::CONFIG_TITLE_SINGLE)) {
            return static::CONFIG_TITLE_SINGLE;
        }

        $sTitle = preg_replace('/([a-z])([A-Z])/', '$1 $2', static::CONFIG_MODEL_NAME);
        $sTitle = strtolower($sTitle);
        $sTitle = ucwords($sTitle);

        return $sTitle;
    }

protected
static
function
getTitleSingle
(
)
:
string
{
if
(
!
empty
(
static
::
CONFIG_TITLE_SINGLE
)
)
{
return
static
::
CONFIG_TITLE_SINGLE
;
}
$
sTitle
=
preg_replace
(
'/([a-z])([A-Z])/'
,
'$1 $2'
,
static
::
CONFIG_MODEL_NAME
)
;
$
sTitle
=
strtolower
(
$
sTitle
)
;
$
sTitle
=
ucwords
(
$
sTitle
)
;
return
$
sTitle
;
}

Returns the string to use as the "single" title

@return string

Returns the string to use as the single title


protected function indexDropdownFilters(): array
    {
        $aFilters = [];
        if (classUses(static::getModel(), Localised::class)) {

            /** @var Locale $oLocale */
            $oLocale = Factory::service('Locale');

            $aOptions   = [];
            $aOptions[] = Factory::factory('IndexFilterOption', 'nails/module-admin')
                ->setLabel('All Locales');

            foreach ($oLocale->getSupportedLocales() as $oSupportedLocale) {
                $aOptions[] = Factory::factory('IndexFilterOption', 'nails/module-admin')
                    ->setLabel($oSupportedLocale->getDisplayLanguage())
                    ->setValue($oSupportedLocale->getLanguage() . '_' . $oSupportedLocale->getRegion());
            }

            $aFilters[] = Factory::factory('IndexFilter', 'nails/module-admin')
                ->setLabel('Locale')
                ->setColumn('CONCAT(`language`, \'_\', `region`)')
                ->addOptions($aOptions);
        }

        return $aFilters;
    }

protected
function
indexDropdownFilters
(
)
:
array
{
$
aFilters
=
[
]
;
if
(
classUses
(
static
::
getModel
(
)
,
Localised
::
class
)
)
{
/** @var Locale $oLocale */
$
oLocale
=
Factory
::
service
(
'Locale'
)
;
$
aOptions
=
[
]
;
$
aOptions
[
]
=
Factory
::
factory
(
'IndexFilterOption'
,
'nails/module-admin'
)
->
setLabel
(
'All Locales'
)
;
foreach
(
$
oLocale
->
getSupportedLocales
(
)
as
$
oSupportedLocale
)
{
$
aOptions
[
]
=
Factory
::
factory
(
'IndexFilterOption'
,
'nails/module-admin'
)
->
setLabel
(
$
oSupportedLocale
->
getDisplayLanguage
(
)
)
->
setValue
(
$
oSupportedLocale
->
getLanguage
(
)
.
'_'
.
$
oSupportedLocale
->
getRegion
(
)
)
;
}
$
aFilters
[
]
=
Factory
::
factory
(
'IndexFilter'
,
'nails/module-admin'
)
->
setLabel
(
'Locale'
)
->
setColumn
(
'CONCAT(`language`, \'_\', `region`)'
)
->
addOptions
(
$
aOptions
)
;
}
return
$
aFilters
;
}

Any dropdown style filters to include on the index page

@return array


protected function getPostObject(): array
    {
        $aConfig = $this->getConfig();
        $oModel  = static::getModel();
        $oInput  = Factory::service('Input');
        $oUri    = Factory::service('Uri');
        $aOut    = [];

        foreach ($aConfig['FIELDS'] as $oField) {
            if (in_array($oField->key, $aConfig['EDIT_IGNORE_FIELDS'])) {
                continue;
            }

            $aOut[$oField->key] = $oInput->post($oField->key);

            if ($oField->allow_null && empty($aOut[$oField->key])) {
                $aOut[$oField->key] = null;
            }

            //  Type casting
            switch ($oField->type) {
                case 'boolean':
                    $aOut[$oField->key] = (bool) $aOut[$oField->key];
                    break;
            }
        }

        if (classUses($oModel, Localised::class)) {
            $iExistingId = $oUri->segment(5);
            if ($oUri->segment(5)) {
                $aOut['id'] = $oUri->segment(5);
            }
        }

        return $aOut;
    }

protected
function
getPostObject
(
)
:
array
{
$
aConfig
=
$
this
->
getConfig
(
)
;
$
oModel
=
static
::
getModel
(
)
;
$
oInput
=
Factory
::
service
(
'Input'
)
;
$
oUri
=
Factory
::
service
(
'Uri'
)
;
$
aOut
=
[
]
;
foreach
(
$
aConfig
[
'FIELDS'
]
as
$
oField
)
{
if
(
in_array
(
$
oField
->
key
,
$
aConfig
[
'EDIT_IGNORE_FIELDS'
]
)
)
{
continue
;
}
$
aOut
[
$
oField
->
key
]
=
$
oInput
->
post
(
$
oField
->
key
)
;
if
(
$
oField
->
allow_null
&&
empty
(
$
aOut
[
$
oField
->
key
]
)
)
{
$
aOut
[
$
oField
->
key
]
=
;
}
//  Type casting
switch
(
$
oField
->
type
)
{
case
'boolean'
:
$
aOut
[
$
oField
->
key
]
=
(
bool
)
$
aOut
[
$
oField
->
key
]
;
break
;
}
}
if
(
classUses
(
$
oModel
,
Localised
::
class
)
)
{
$
iExistingId
=
$
oUri
->
segment
(
5
)
;
if
(
$
oUri
->
segment
(
5
)
)
{
$
aOut
[
'id'
]
=
$
oUri
->
segment
(
5
)
;
}
}
return
$
aOut
;
}

Extract data from post variable

@return array


protected function getItem(array $aData = [], int $iSegment = null, bool $bIncludeDeleted = false, bool $b404 = true)
    {
        $iSegment = $iSegment ?? 5;
        $oUri     = Factory::service('Uri');
        $oModel   = $this->getModel();
        $iItemId  = (int) $oUri->segment($iSegment);

        if (classUses($oModel, Localised::class) && $oUri->segment($iSegment + 1)) {
            $aData['USE_LOCALE'] = $oUri->segment($iSegment + 1);
        }

        if (!array_key_exists('where', $aData)) {
            $aData['where'] = [];
        }

        $aData['where'][] = ['id', $iItemId];

        $aItems = $oModel->getAll(
            null,
            null,
            $aData,
            $bIncludeDeleted
        );

        $oItem = reset($aItems);

        if ($b404 && empty($oItem)) {
            show404();
        }

        return $oItem;
    }

protected function getItem ( array $ aData = [ ] , int $ iSegment = null , bool $ bIncludeDeleted = false , bool $ b404 = true ) { $ iSegment = $ iSegment ?? 5 ; $ oUri = Factory :: service ( 'Uri' ) ; $ oModel = $ this -> getModel ( ) ; $ iItemId = ( int ) $ oUri -> segment ( $ iSegment ) ; if ( classUses ( $ oModel , Localised :: class ) && $ oUri -> segment ( $ iSegment + 1 ) ) { $ aData [ 'USE_LOCALE' ] = $ oUri -> segment ( $ iSegment + 1 ) ; } if ( ! array_key_exists ( 'where' , $ aData ) ) { $ aData [ 'where' ] = [ ] ; } $ aData [ 'where' ] [ ] = [ 'id' , $ iItemId ] ; $ aItems = $ oModel -> getAll ( null , null , $ aData , $ bIncludeDeleted ) ; $ oItem = reset ( $ aItems ) ; if ( $ b404 && empty ( $ oItem ) ) { show404 ( ) ; } return $ oItem ; }

Returns the item being requested, thorwing a 404 if it's not found

@param array   $aData    Data to pass to the getById method
@param integer $iSegment The URL segment contianing the ID

@return mixed
@throws FactoryException

Returns the item being requested thorwing a 404 if it s not found


protected function returnToIndex(): void
    {
        $oInput    = Factory::service('Input');
        $sReferrer = $oInput->server('HTTP_REFERER');

        if (!empty($sReferrer)) {
            redirect($sReferrer);
        } else {
            redirect($this->getConfig()['BASE_URL']);
        }
    }

protected function returnToIndex ( ) : void { $ oInput = Factory :: service ( 'Input' ) ; $ sReferrer = $ oInput -> server ( 'HTTP_REFERER' ) ; if ( ! empty ( $ sReferrer ) ) { redirect ( $ sReferrer ) ; } else { redirect ( $ this -> getConfig ( ) [ 'BASE_URL' ] ) ; } }

Returns the user to the index pagel if a referrer is available then go there instead
This is useful for returning the user to a filtered view

@throws FactoryException


public function addOption($sLabel, $mValue = null, $bIsSelected = false, $bIsQuery = null)
    {
        if ($sLabel instanceof Option) {
            $this->aOptions[] = $sLabel;
        } else {
            $this->aOptions[] = Factory::factory('IndexFilterOption', 'nails/module-admin')
                ->setLabel($sLabel)
                ->setValue($mValue)
                ->setIsSelected($bIsSelected)
                ->setIsQuery($bIsQuery);
        }

        return $this;
    }

public function addOption ( $ sLabel , $ mValue = null , $ bIsSelected = false , $ bIsQuery = null ) { if ( $ sLabel instanceof Option ) { $ this -> aOptions [ ] = $ sLabel ; } else { $ this -> aOptions [ ] = Factory :: factory ( 'IndexFilterOption' , 'nails/module-admin' ) -> setLabel ( $ sLabel ) -> setValue ( $ mValue ) -> setIsSelected ( $ bIsSelected ) -> setIsQuery ( $ bIsQuery ) ; } return $ this ; }

Adds a new option

@param string|Option $sLabel      The label to give the option, or an IndexFilterOption object
@param mixed         $mValue      The value to give the option
@param bool          $bIsSelected Whether the item is selected
@param bool          $bIsQuery    If true, treat the value as the entire query

@return $this


public function addOptions($aOptions)
    {
        foreach ($aOptions as $aOption) {
            if ($aOption instanceof Option) {
                $this->aOptions[] = $aOption;
            } else {
                $sLabel      = getFromArray('label', $aOption, getFromArray(0, $aOption));
                $mValue      = getFromArray('value', $aOption, getFromArray(1, $aOption));
                $bIsSelected = (bool) getFromArray('selected', $aOption, getFromArray(2, $aOption));
                $bIsQuery    = (bool) getFromArray('query', $aOption, getFromArray(3, $aOption));
                $this->addOption($sLabel, $mValue, $bIsSelected, $bIsQuery);
            }
        }

        return $this;
    }

public function addOptions ( $ aOptions ) { foreach ( $ aOptions as $ aOption ) { if ( $ aOption instanceof Option ) { $ this -> aOptions [ ] = $ aOption ; } else { $ sLabel = getFromArray ( 'label' , $ aOption , getFromArray ( 0 , $ aOption ) ) ; $ mValue = getFromArray ( 'value' , $ aOption , getFromArray ( 1 , $ aOption ) ) ; $ bIsSelected = ( bool ) getFromArray ( 'selected' , $ aOption , getFromArray ( 2 , $ aOption ) ) ; $ bIsQuery = ( bool ) getFromArray ( 'query' , $ aOption , getFromArray ( 3 , $ aOption ) ) ; $ this -> addOption ( $ sLabel , $ mValue , $ bIsSelected , $ bIsQuery ) ; } } return $ this ; }

Add multiple options

@param array $aOptions An array of options to add

@return $this


public function getOption($iOptionIndex)
    {
        return array_key_exists($iOptionIndex, $this->aOptions) ? $this->aOptions[$iOptionIndex] : null;
    }

public function getOption ( $ iOptionIndex ) { return array_key_exists ( $ iOptionIndex , $ this -> aOptions ) ? $ this -> aOptions [ $ iOptionIndex ] : null ; }

Returns a specific option

@param integer $iOptionIndex the option index to return

@return mixed|null


private function handleMethodCall($aMethods, $sMethod, $mValue)
    {
        if (substr($sMethod, 0, 3) !== 'set') {
            return $this->{$aMethods[$sMethod]};
        }

        $this->{$aMethods[$sMethod]} = $mValue;
        return $this;
    }

private function handleMethodCall ( $ aMethods , $ sMethod , $ mValue ) { if ( substr ( $ sMethod , 0 , 3 ) !== 'set' ) { return $ this -> { $ aMethods [ $ sMethod ] } ; } $ this -> { $ aMethods [ $ sMethod ] } = $ mValue ; return $ this ; }

Executes the appropriate "method" behaviour

@param array  $aMethods The array of methods to check against
@param string $sMethod  The method being called
@param mixed  $mValue   The value to assign when setting

@return $this|mixed

Executes the appropriate method behaviour


public function setBatchStatus(array $aIds, $sStatus, $sError = '')
    {
        if (is_object(reset($aIds))) {
            $aIds = arrayExtractProperty($aIds, 'id');
        }
        if (!empty($aIds)) {
            $oDb = Factory::service('Database');
            $oDb->set('status', $sStatus);
            $oDb->set('modified', 'NOW()', false);
            if ($sError) {
                $oDb->set('error', $sError);
            }
            $oDb->where_in('id', $aIds);
            $oDb->update($this->getTableName());
        }
    }

public function setBatchStatus ( array $ aIds , $ sStatus , $ sError = '' ) { if ( is_object ( reset ( $ aIds ) ) ) { $ aIds = arrayExtractProperty ( $ aIds , 'id' ) ; } if ( ! empty ( $ aIds ) ) { $ oDb = Factory :: service ( 'Database' ) ; $ oDb -> set ( 'status' , $ sStatus ) ; $ oDb -> set ( 'modified' , 'NOW()' , false ) ; if ( $ sError ) { $ oDb -> set ( 'error' , $ sError ) ; } $ oDb -> where_in ( 'id' , $ aIds ) ; $ oDb -> update ( $ this -> getTableName ( ) ) ; } }

Updates the status of multiple request items in a batch

@param array  $aIds    The requests to update
@param string $sStatus The status to set
@param string $sError  Any error message to set


public function setBatchDownloadId(array $aIds, $iDownloadId)
    {
        if (is_object(reset($aIds))) {
            $aIds = arrayExtractProperty($aIds, 'id');
        }
        if (!empty($aIds)) {
            $oDb = Factory::service('Database');
            $oDb->set('download_id', $iDownloadId);
            $oDb->set('modified', 'NOW()', false);
            $oDb->where_in('id', $aIds);
            $oDb->update($this->getTableName());
        }
    }

public function setBatchDownloadId ( array $ aIds , $ iDownloadId ) { if ( is_object ( reset ( $ aIds ) ) ) { $ aIds = arrayExtractProperty ( $ aIds , 'id' ) ; } if ( ! empty ( $ aIds ) ) { $ oDb = Factory :: service ( 'Database' ) ; $ oDb -> set ( 'download_id' , $ iDownloadId ) ; $ oDb -> set ( 'modified' , 'NOW()' , false ) ; $ oDb -> where_in ( 'id' , $ aIds ) ; $ oDb -> update ( $ this -> getTableName ( ) ) ; } }

Updates the download ID of multiple requests

@param array   $aIds        The requests to update
@param integer $iDownloadId The download ID


public function index()
    {
        //  When executing on the CLI we don't need to perform a few bit's of sense checking
        $oInput = Factory::service('Input');
        if (!$oInput::isCli()) {

            //  Is there an AdminIP whitelist?
            $whitelistIp = (array) appSetting('whitelist', 'admin');

            if ($whitelistIp) {
                if (!isIpInRange($oInput->ipAddress(), $whitelistIp)) {
                    show404();
                }
            }

            //  Before we do anything, is the user an admin?
            if (!isAdmin()) {
                unauthorised();
            }
        }

        //  Determine which admin controllers are available to the system
        $this->findAdminControllers();

        // --------------------------------------------------------------------------

        /**
         * Sort the controllers into a view friendly array, taking into
         * consideration the user's order and state preferences.
         */

        $this->prepAdminControllersNav();

        //  Save adminControllers to controller data so everyone can use it
        $this->data['adminControllers']    = $this->adminControllers;
        $this->data['adminControllersNav'] = $this->adminControllersNav;

        // --------------------------------------------------------------------------

        //  Route the user's request
        $this->routeRequest();
    }

Initial touch point for admin, all requests are routed through here.

@return void

Initial touch point for admin all requests are routed through here .


protected function findAdminControllers()
    {
        //  Look in the admin module
        $this->loadAdminControllers(
            'admin',
            NAILS_PATH . 'module-admin/admin/controllers/',
            NAILS_APP_PATH . 'application/modules/admin/controllers/',
            ['adminRouter.php']
        );

        //  Look in all enabled modules
        foreach (Components::modules() as $module) {

            /**
             * Skip the admin module. We use the moduleName rather than the component name
             * so that we don't inadvertently load up the admin module (or any module identifying
             * itself as admin) and listing all the files contained therein; we only want
             * admin/controllers.
             */
            if ($module->moduleName == 'admin') {
                continue;
            }

            $this->loadAdminControllers(
                $module->moduleName,
                $module->path . 'admin/controllers/',
                NAILS_APP_PATH . 'application/modules/' . $module->moduleName . '/admin/controllers/'
            );
        }

        //  Finally, look for app admin controllers
        $this->loadAppAdminControllers();
    }

Searches modules and the app for valid admin controllers which the active
user has permission to access.

@return void

Searches modules and the app for valid admin controllers which the active user has permission to access .


protected function loadAdminControllers($moduleName, $controllerPath, $appPath, $ignore = [])
    {
        //  Does a path exist? Don't pollute the array with empty modules
        if (is_dir($controllerPath)) {
            //  Look for controllers
            $files = directory_map($controllerPath, 1);

            foreach ($files as $file) {
                if (in_array($file, $ignore)) {
                    continue;
                }

                $this->loadAdminController($file, $moduleName, $controllerPath, $appPath);
            }
        }
    }

Looks for admin controllers in specific locations

@param  string $moduleName     The name of the module currently being searched
@param  string $controllerPath The full path to the controllers
@param  string $appPath        If the app can extend these controllers, this is where it will place the file
@param  array  $ignore         An array of filenames to ignore

@return void


protected function loadAdminController($file, $moduleName, $controllerPath, $appPath)
    {
        $fileName = substr($file, 0, strpos($file, '.php'));

        //  PHP file, no leading underscore
        if (!$this->isValidAdminFile($file)) {
            return;
        }

        //  Valid file, load it up and define the full class path and name
        require_once $controllerPath . $file;
        $classPath = $controllerPath . $file;
        $className = 'Nails\Admin\\' . ucfirst($moduleName) . '\\' . ucfirst($fileName);

        //  If there's an app version of this controller than we'll use that one instead.
        if (is_file($appPath . $file)) {

            require_once $appPath . $file;
            $classPath = $appPath . $file;
            $className = 'App\Admin\\' . ucfirst($moduleName) . '\\' . ucfirst($fileName);

            //  Does the expected class exist? If it doesn't fall back to the previous one
            if (!class_exists($className)) {
                $classPath = $controllerPath . $file;
                $className = 'Nails\Admin\\' . ucfirst($moduleName) . '\\' . ucfirst($fileName);
            }
        }

        //  Load and process the class
        $this->loadAdminClass($fileName, $className, $classPath, $moduleName);
    }

Loads a specific admin controller

@param  string $file           The file to load
@param  string $moduleName     The name of the module currently being searched
@param  string $controllerPath The full path to the controller
@param  string $appPath        If the app can extend this controllers, this is where it will place the file

@return void


protected function loadAdminClass($fileName, $className, $classPath, $moduleName)
    {
        //  Does the expected class exist?
        if (!class_exists($className)) {
            return false;
        }

        //  Does it have an announce method?
        if (!method_exists($className, 'announce')) {
            return false;
        }

        //  Cool! We have a controller which is valid, Add it to the stack!
        if (!isset($this->adminControllers[$moduleName])) {
            $this->adminControllers[$moduleName]              = new \stdClass();
            $this->adminControllers[$moduleName]->controllers = [];
        }

        $aNavGroupings = $className::announce();

        if (!empty($aNavGroupings) && !is_array($aNavGroupings) && !($aNavGroupings instanceof \Nails\Admin\Factory\Nav)) {

            /**
             * @todo Use an admin specific exception class, and autoload it.
             */

            throw new RouterException('Admin Nav groupings returned by ' . $className . '::announce() were invalid', 1);

        } elseif (!is_array($aNavGroupings)) {
            $aNavGroupings = array_filter([$aNavGroupings]);
        }

        $this->adminControllers[$moduleName]->controllers[$fileName] = [
            'className' => (string) $className,
            'path'      => (string) $classPath,
            'groupings' => $aNavGroupings,
        ];

        return true;
    }

Attempts to load an AdminClass

@param  string $fileName   The filename of the class being loaded
@param  string $className  The name of the class being loaded
@param  string $classPath  The path of the class being loaded
@param  string $moduleName The name of the module to which this class belongs

@throws RouterException
@return boolean


protected function routeRequest()
    {
        //  What are we trying to access?
        $oUri        = Factory::service('Uri');
        $sModule     = $oUri->rsegment(3) ? $oUri->rsegment(3) : '';
        $sController = ucfirst($oUri->rsegment(4) ? $oUri->rsegment(4) : $sModule);
        $sMethod     = $oUri->rsegment(5) ? $oUri->rsegment(5) : 'index';

        if (empty($sModule)) {

            $oSession = Factory::service('Session', 'nails/module-auth');
            $oSession->keepFlashData();
            redirect('admin/admin/dashboard');

        } elseif (isset($this->adminControllers[$sModule]->controllers[$sController])) {

            $aRequestController           = $this->adminControllers[$sModule]->controllers[$sController];
            $this->data['currentRequest'] = $aRequestController;
            $sControllerName              = $aRequestController['className'];

            if (is_callable([$sControllerName, $sMethod])) {
                $oController = new $sControllerName();
                $oController->$sMethod();
            } else {
                show404();
            }

        } else {
            show404();
        }
    }

protected
function
routeRequest
(
)
{
//  What are we trying to access?
$
oUri
=
Factory
::
service
(
'Uri'
)
;
$
sModule
=
$
oUri
->
rsegment
(
3
)
$
oUri
->
rsegment
(
3
)
:
''
;
$
sController
=
ucfirst
(
$
oUri
->
rsegment
(
4
)
$
oUri
->
rsegment
(
4
)
:
$
sModule
)
;
$
sMethod
=
$
oUri
->
rsegment
(
5
)
$
oUri
->
rsegment
(
5
)
:
'index'
;
if
(
empty
(
$
sModule
)
)
{
$
oSession
=
Factory
::
service
(
'Session'
,
'nails/module-auth'
)
;
$
oSession
->
keepFlashData
(
)
;
redirect
(
'admin/admin/dashboard'
)
;
}
elseif
(
isset
(
$
this
->
adminControllers
[
$
sModule
]
->
controllers
[
$
sController
]
)
)
{
$
aRequestController
=
$
this
->
adminControllers
[
$
sModule
]
->
controllers
[
$
sController
]
;
$
this
->
data
[
'currentRequest'
]
=
$
aRequestController
;
$
sControllerName
=
$
aRequestController
[
'className'
]
;
if
(
is_callable
(
[
$
sControllerName
,
$
sMethod
]
)
)
{
$
oController
=
new
$
sControllerName
(
)
;
$
oController
->
$
sMethod
(
)
;
}
else
{
show404
(
)
;
}
}
else
{
show404
(
)
;
}
}

Routes the request to the appropriate controller

@return void


public static function loadView($sViewFile, $bLoadStructure = true, $bReturnView = false)
    {
        $aData  =& getControllerData();
        $oInput = Factory::service('Input');
        $oView  = Factory::service('View');

        //  Are we in a modal?
        if ($oInput->get('isModal')) {

            if (!isset($aData['headerOverride']) && !isset($aData['isModal'])) {
                $aData['isModal'] = true;
            }

            if (empty($aData['headerOverride'])) {
                $aData['headerOverride'] = '_components/structure/headerBlank';
            }

            if (empty($aData['footerOverride'])) {
                $aData['footerOverride'] = '_components/structure/footerBlank';
            }
        }

        $aHeaderViews = array_filter([
            $bLoadStructure && !empty($aData['headerOverride']) ? $aData['headerOverride'] : '',
            $bLoadStructure && empty($aData['headerOverride']) ? '_components/structure/header' : '',
        ]);
        $sHeaderView  = reset($aHeaderViews);

        $aFooterViews = array_filter([
            $bLoadStructure && !empty($aData['footerOverride']) ? $aData['footerOverride'] : '',
            $bLoadStructure && empty($aData['footerOverride']) ? '_components/structure/footer' : '',
        ]);
        $sFooterView  = reset($aFooterViews);

        if ($bReturnView) {
            $sOut = $oView->load($sHeaderView, $aData, true);
            $sOut .= static::loadInlineView($sViewFile, $aData, true);
            $sOut .= $oView->load($sFooterView, $aData, true);
            return $sOut;
        } else {
            $oView->load($sHeaderView, $aData);
            static::loadInlineView($sViewFile, $aData);
            $oView->load($sFooterView, $aData);
        }
    }

public
static
function
loadView
(
$
sViewFile
,
$
bLoadStructure
=
true
,
$
bReturnView
=
false
)
{
$
aData
=
&
getControllerData
(
)
;
$
oInput
=
Factory
::
service
(
'Input'
)
;
$
oView
=
Factory
::
service
(
'View'
)
;
//  Are we in a modal?
if
(
$
oInput
->
get
(
'isModal'
)
)
{
if
(
!
isset
(
$
aData
[
'headerOverride'
]
)
&&
!
isset
(
$
aData
[
'isModal'
]
)
)
{
$
aData
[
'isModal'
]
=
true
;
}
if
(
empty
(
$
aData
[
'headerOverride'
]
)
)
{
$
aData
[
'headerOverride'
]
=
'_components/structure/headerBlank'
;
}
if
(
empty
(
$
aData
[
'footerOverride'
]
)
)
{
$
aData
[
'footerOverride'
]
=
'_components/structure/footerBlank'
;
}
}
$
aHeaderViews
=
array_filter
(
[
$
bLoadStructure
&&
!
empty
(
$
aData
[
'headerOverride'
]
)
$
aData
[
'headerOverride'
]
:
''
,
$
bLoadStructure
&&
empty
(
$
aData
[
'headerOverride'
]
)
'_components/structure/header'
:
''
,
]
)
;
$
sHeaderView
=
reset
(
$
aHeaderViews
)
;
$
aFooterViews
=
array_filter
(
[
$
bLoadStructure
&&
!
empty
(
$
aData
[
'footerOverride'
]
)
$
aData
[
'footerOverride'
]
:
''
,
$
bLoadStructure
&&
empty
(
$
aData
[
'footerOverride'
]
)
'_components/structure/footer'
:
''
,
]
)
;
$
sFooterView
=
reset
(
$
aFooterViews
)
;
if
(
$
bReturnView
)
{
$
sOut
=
$
oView
->
load
(
$
sHeaderView
,
$
aData
,
true
)
;
$
sOut
.=
static
::
loadInlineView
(
$
sViewFile
,
$
aData
,
true
)
;
$
sOut
.=
$
oView
->
load
(
$
sFooterView
,
$
aData
,
true
)
;
return
$
sOut
;
}
else
{
$
oView
->
load
(
$
sHeaderView
,
$
aData
)
;
static
::
loadInlineView
(
$
sViewFile
,
$
aData
)
;
$
oView
->
load
(
$
sFooterView
,
$
aData
)
;
}
}

Loads a view in admin taking into account the module being accessed. Passes controller
data and optionally loads the header and footer views.

@param  string  $sViewFile      The view to load
@param  boolean $bLoadStructure Whether or not to include the header and footers in the output
@param  boolean $bReturnView    Whether to return the view or send it to the Output class

@return mixed                  String when $bReturnView is true, void otherwise
@throws \Nails\Common\Exception\FactoryException

Loads a view in admin taking into account the module being accessed . Passes controller data and optionally loads the header and footer views .


public static function loadCsv($mData, $sFilename = '', $bHeaderRow = true)
    {
        //  Determine what type of data has been supplied
        if (is_array($mData) || get_class($mData) == 'CI_DB_mysqli_result') {

            //  If filename has been specified then set some additional headers
            if (!empty($sFilename)) {

                $oInput  = Factory::service('Input');
                $oOutput = Factory::service('Output');

                //  Common headers
                $oOutput->set_content_type('text/csv');
                $oOutput->set_header('Content-Disposition: attachment; filename="' . $sFilename . '"');
                $oOutput->set_header('Expires: 0');
                $oOutput->set_header("Content-Transfer-Encoding: binary");

                //  Handle IE, classic.
                $userAgent = $oInput->server('HTTP_USER_AGENT');

                if (strpos($userAgent, "MSIE") !== false) {

                    $oOutput->set_header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
                    $oOutput->set_header('Pragma: public');

                } else {

                    $oOutput->set_header('Pragma: no-cache');
                }
            }

            //  Not using self::loadInlineView() as this may be called from many contexts
            $oView = Factory::service('View');
            if (is_array($mData)) {
                $oView->load('admin/_components/csv/array', ['data' => $mData, 'header' => $bHeaderRow]);
            } elseif (get_class($mData) == 'CI_DB_mysqli_result') {
                $oView->load('admin/_components/csv/dbResult', ['data' => $mData, 'header' => $bHeaderRow]);
            }

        } else {
            throw new NailsException('Unsupported object type passed to ' . get_class() . '::loadCSV');
        }
    }

Generates a CSV and sends to the browser, if a filename is given then it's
sent as a download

@param  mixed   $mData      The data to render, either an array or a DB query object
@param  string  $sFilename  The filename to give the file if downloading
@param  boolean $bHeaderRow The first element in the $mData resultset is a header row

@return void
@throws \Nails\Common\Exception\FactoryException

Generates a CSV and sends to the browser if a filename is given then it s sent as a download


public static function loadInlineView($sViewFile, $aViewData = [], $bReturnView = false)
    {
        $aCtrlData   =& getControllerData();
        $sCtrlPath   = !empty($aCtrlData['currentRequest']['path']) ? $aCtrlData['currentRequest']['path'] : '';
        $sCtrlName   = basename($sCtrlPath, '.php');
        $aCtrlPath   = explode(DIRECTORY_SEPARATOR, $sCtrlPath);
        $aCtrlPath   = array_splice($aCtrlPath, 0, count($aCtrlPath) - 2);
        $aCtrlPath[] = 'views';
        $aCtrlPath[] = $sCtrlName;
        $aCtrlPath[] = $sViewFile;
        $sViewPath   = implode(DIRECTORY_SEPARATOR, $aCtrlPath) . '.php';

        $oView = Factory::service('View');

        //  Load the view
        try {
            return $oView->load($sViewPath, $aViewData, $bReturnView);
        } catch (ViewNotFoundException $e) {
            //  If it fails, and the controller is a default admin controller then load up that view
            $sClassName = $aCtrlData['currentRequest']['className'];
            if (!classExtends($sClassName, 'Nails\\Admin\\Controller\\DefaultController')) {
                throw new ViewNotFoundException(
                    $e->getMessage(),
                    $e->getCode()
                );
            }

            //  Step through the class hierarchy and look there
            $aParents = class_parents($sClassName);
            foreach ($aParents as $sParent) {
                try {

                    if ($sParent !== 'Nails\\Admin\\Controller\\DefaultController') {

                        $oReflection = new \ReflectionClass('\\' . $sParent);
                        $sViewPath   = realpath(dirname($oReflection->getFileName()) . '/../views') . '/';
                        $aClassBits  = explode('\\', $oReflection->getName());
                        $sViewPath   .= end($aClassBits) . '/';

                    } else {
                        $sViewPath     = 'admin/DefaultController/';
                        $bTriedDefault = true;
                    };

                    return $oView->load(
                        $sViewPath . $sViewFile,
                        $aViewData,
                        $bReturnView
                    );
                } catch (ViewNotFoundException $e) {
                    //  Allow the loop to continue, unless we've already tried the default views
                    if (!empty($bTriedDefault)) {
                        throw $e;
                    }
                }
            }
        }
    }

Load a single view taking into account the module being accessed.

@param  string  $sViewFile   The view to load
@param  array   $aViewData   The data to pass to the view
@param  boolean $bReturnView Whether to return the view or send it to the Output class

@throws \Exception

@return mixed               String when $bReturnView is true, void otherwise

Load a single view taking into account the module being accessed .


public static function loadSearch($oSearchObj, $bReturnView = true)
    {
        $aData = [
            'searchable'     => isset($oSearchObj->searchable) ? $oSearchObj->searchable : true,
            'sortColumns'    => isset($oSearchObj->sortColumns) ? $oSearchObj->sortColumns : [],
            'sortOn'         => isset($oSearchObj->sortOn) ? $oSearchObj->sortOn : null,
            'sortOrder'      => isset($oSearchObj->sortOrder) ? $oSearchObj->sortOrder : null,
            'perPage'        => isset($oSearchObj->perPage) ? $oSearchObj->perPage : 50,
            'keywords'       => isset($oSearchObj->keywords) ? $oSearchObj->keywords : '',
            'checkboxFilter' => isset($oSearchObj->checkboxFilter) ? $oSearchObj->checkboxFilter : [],
            'dropdownFilter' => isset($oSearchObj->dropdownFilter) ? $oSearchObj->dropdownFilter : [],
        ];

        //  Not using self::loadInlineView() as this may be called from many contexts
        $oView = Factory::service('View');
        return $oView->load('admin/_components/search', $aData, $bReturnView);
    }

Loads the admin "search" component

@param  \stdClass $oSearchObj  An object as created by self::searchObject();
@param  boolean   $bReturnView Whether to return the view to the caller, or output to the browser

@return mixed                  String when $bReturnView is true, void otherwise
@throws \Nails\Common\Exception\FactoryException


public static function searchFilterGetValueAtKey($oFilterObj, $iKey)
    {
        return isset($oFilterObj->options[$iKey]->value) ? $oFilterObj->options[$iKey]->value : null;
    }

public static function searchFilterGetValueAtKey ( $ oFilterObj , $ iKey ) { return isset ( $ oFilterObj -> options [ $ iKey ] -> value ) ? $ oFilterObj -> options [ $ iKey ] -> value : null ; }

Returns a value from a filter object at a specific key

@param  \stdClass $oFilterObj The filter object to search
@param  integer   $iKey       The key to inspect

@return mixed                  Mixed on success, null on failure


public static function loadPagination($oPaginationObject, $bReturnView = true)
    {
        $aData = [
            'page'      => isset($oPaginationObject->page) ? $oPaginationObject->page : null,
            'perPage'   => isset($oPaginationObject->perPage) ? $oPaginationObject->perPage : null,
            'totalRows' => isset($oPaginationObject->totalRows) ? $oPaginationObject->totalRows : null,
        ];

        //  Not using self::loadInlineView() as this may be called from many contexts
        $oView = Factory::service('View');
        return $oView->load('admin/_components/pagination', $aData, $bReturnView);
    }

Loads the admin "pagination" component

@param  \stdClass $oPaginationObject An object as created by self::paginationObject();
@param  boolean   $bReturnView       Whether to return the view to the caller, or output to the browser

@return mixed                      String when $bReturnView is true, void otherwise
@throws \Nails\Common\Exception\FactoryException


public static function loadCellAuto($mValue, $sCellClass = '', $sCellAdditional = '')
    {
        //  @todo - handle more field types
        if (is_bool($mValue)) {
            return Helper::loadBoolCell($mValue);
        } elseif (preg_match('/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $mValue)) {
            return Helper::loadDateTimeCell($mValue);
        } elseif (preg_match('/\d\d\d\d-\d\d-\d\d/', $mValue)) {
            return Helper::loadDateCell($mValue);
        } else {
            return '' . $mValue . $sCellAdditional . '	';
        }
    }

public
static
function
loadCellAuto
(
$
mValue
,
$
sCellClass
=
''
,
$
sCellAdditional
=
''
)
{
//  @todo - handle more field types
if
(
is_bool
(
$
mValue
)
)
{
return
Helper
::
loadBoolCell
(
$
mValue
)
;
}
elseif
(
preg_match
(
'/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/'
,
$
mValue
)
)
{
return
Helper
::
loadDateTimeCell
(
$
mValue
)
;
}
elseif
(
preg_match
(
'/\d\d\d\d-\d\d-\d\d/'
,
$
mValue
)
)
{
return
Helper
::
loadDateCell
(
$
mValue
)
;
}
else
{
return
''
.
$
mValue
.
$
sCellAdditional
.
'	'
;
}
}

Automatically decides what type of cell to load

@param mixed  $mValue          The value of the cell
@param string $sCellClass      Any classes to add to the cell
@param string $sCellAdditional Any additional HTML to add to the cell (after the value)

@return string
@throws \Nails\Common\Exception\FactoryException