<?php

namespace Sofa\Eloquence\Metable;

use BadMethodCallException;

/**
 * This class provides instance scope for the closures
 * so they can be rebound later onto the acutal model.
 */
class Hooks
{
    /**
     * Register hook on getAttribute method.
     *
     * @return \Closure
     */
    public function getAttribute()
    {
        return function ($next, $value, $args) {
            $key = $args->get('key');

            if (is_null($value)) {
                $value = $this->getMeta($key);
            }

            return $next($value, $args);
        };
    }

    /**
     * Register hook on setAttribute method.
     *
     * @return \Closure
     */
    public function setAttribute()
    {
        return function ($next, $value, $args) {
            $key = $args->get('key');

            if (!$this->hasColumn($key) && $this->allowsMeta($key) && !$this->hasSetMutator($key)) {
                return $this->setMeta($key, $value);
            }

            return $next($value, $args);
        };
    }

    /**
     * Register hook on toArray method.
     *
     * @return \Closure
     */
    public function toArray()
    {
        return function ($next, $attributes) {
            unset($attributes['meta_attributes'], $attributes['metaAttributes']);

            $attributes = array_merge($attributes, $this->getMetaAttributesArray());

            return $next($attributes);
        };
    }

    /**
     * Register hook on replicate method.
     *
     * @return \Closure
     */
    public function replicate()
    {
        return function ($next, $copy, $args) {
            $metaAttributes = $args->get('original')
                                    ->getMetaAttributes()
                                    ->replicate($args->get('except'));

            $copy->setRelation('metaAttributes', $metaAttributes);

            return $next($copy, $args);
        };
    }

    /**
     * Register hook on save method.
     *
     * @return \Closure
     */
    public function save()
    {
        return function ($next, $value, $args) {
            $this->saveMeta();

            return $next($value, $args);
        };
    }

    /**
     * Register hook on isset call.
     *
     * @return \Closure
     */
    public function __issetHook()
    {
        return function ($next, $isset, $args) {
            $key = $args->get('key');

            if (!$isset) {
                $isset = (bool) $this->hasMeta($key);
            }

            return $next($isset, $args);
        };
    }

    /**
     * Register hook on unset call.
     *
     * @return \Closure
     */
    public function __unsetHook()
    {
        return function ($next, $value, $args) {
            $key = $args->get('key');

            if ($this->hasMeta($key)) {
                return $this->setMeta($key, null);
            }

            return $next($value, $args);
        };
    }

    /**
     * Register hook on queryHook method.
     *
     * @return \Closure
     */
    public function queryHook()
    {
        return function ($next, $query, $bag) {
            $method = $bag->get('method');
            $args   = $bag->get('args');
            $column = $args->get('column');

            if (!$this->hasColumn($column) && $this->allowsMeta($column) && $this->isMetaQueryable($method)) {
                return call_user_func_array([$this, 'metaQuery'], [$query, $method, $args]);
            }

            if (in_array($method, ['select', 'addSelect'])) {
                call_user_func_array([$this, 'metaSelect'], [$query, $args]);
            }

            return $next($query, $bag);
        };
    }

    public function __call($method, $params)
    {
        if (strpos($method, '__') === 0 && method_exists($this, $method.'Hook')) {
            return call_user_func_array([$this, $method.'Hook'], $params);
        }

        throw new BadMethodCallException("Method [{$method}] doesn't exist on this object.");
    }
}


<?php

namespace Sofa\Eloquence\Metable;

use InvalidArgumentException;
use Illuminate\Database\Eloquent\Collection;
use Sofa\Eloquence\Contracts\AttributeBag as AttributeBagContract;

class AttributeBag extends Collection implements AttributeBagContract
{
    /**
     * Create new AttributeBag.
     *
     * @param array $attributes
     */
    public function __construct($attributes = [])
    {
        foreach ($attributes as $attribute) {
            $this->add($attribute);
        }
    }

    /**
     * Add or update attribute.
     *
     * @param  \Sofa\Eloquence\Metable\Attribute|string $key
     * @param  mixed $value
     * @return $this
     */
    public function set($key, $value = null, $group = null)
    {
        if ($key instanceof Attribute) {
            return $this->setInstance($key);
        }

        if ($this->has($key)) {
            $this->update($key, $value, $group);
        } else {
            $this->items[$key] = $this->newAttribute($key, $value, $group);
        }

        return $this;
    }

    /**
     * Set attribute.
     *
     * @param \Sofa\Eloquence\Metable\Attribute $attribute
     */
    protected function setInstance(Attribute $attribute)
    {
        if ($this->has($attribute->getMetaKey())) {
            $this->update($attribute);
        } else {
            $this->items[$attribute->getMetaKey()] = $attribute;
        }

        return $this;
    }

    /**
     * Set attribute.
     *
     * @param \Sofa\Eloquence\Metable\Attribute $attribute
     */
    public function add($attribute)
    {
        return $this->addInstance($attribute);
    }

    /**
     * Set attribute.
     *
     * @param \Sofa\Eloquence\Metable\Attribute $attribute
     */
    protected function addInstance(Attribute $attribute)
    {
        return $this->set($attribute);
    }

    /**
     * Update existing attribute.
     *
     * @param  \Sofa\Eloquence\Metable\Attribute|string $key
     * @param  mixed  $value
     * @return $this
     */
    protected function update($key, $value = null, $group = null)
    {
        if ($key instanceof Attribute) {
            $value = $key->getValue();
            $group = $key->getMetaGroup();
            $key = $key->getMetaKey();
        }

        $this->get($key)->setValue($value);
        $this->get($key)->setMetaGroup($group);

        return $this;
    }

    /**
     * New attribute instance.
     *
     * @param  string $key
     * @param  mixed  $value
     * @return \Sofa\Eloquence\Metable\Attribute
     */
    protected function newAttribute($key, $value, $group = null)
    {
        return new Attribute($key, $value, $group);
    }

    /**
     * Get attribute value.
     *
     * @param  string $key
     * @return mixed
     */
    public function getValue($key)
    {
        if ($attribute = $this->get($key)) {
            return $attribute->getValue();
        }
    }
    /**
     * Get attribute values by group.
     *
     * @param  string $group
     * @return mixed
     */
    public function getMetaByGroup($group)
    {
        return $this->where('meta_group', $group);
    }
    /**
     * Get collection as key-value array.
     *
     * @return array
     */
    public function toArray()
    {
        return array_filter(array_map(function ($attribute) {
            return $attribute->getValue();
        }, $this->items));
    }

    /**
     * Unset attribute.
     *
     * @param  string $key
     * @return $this
     */
    public function forget($key)
    {
        if ($attribute = $this->get($key)) {
            $attribute->setValue(null);
        }

        return $this;
    }

    /**
     * Set attribute.
     *
     * @param  string $key
     * @param  mixed  $value
     * @return void
     */
    public function offsetSet($key, $value)
    {
        $this->set($key, $value);
    }

    /**
     * Set attribute to null.
     *
     * @param  string  $key
     * @return void
     */
    public function offsetUnset($key)
    {
        $this->forget($key);
    }

    /**
     * Handle dynamic properties.
     *
     * @param string $key
     * @param mixed  $value
     */
    public function __set($key, $value)
    {
        $this->set($key, $value);
    }

    /**
     * Handle dynamic properties.
     *
     * @param  string $key
     * @return mixed
     */
    public function __get($key)
    {
        return $this->getValue($key);
    }

    /**
     * Handle isset calls.
     *
     * @param  string  $key
     * @return boolean
     */
    public function __isset($key)
    {
        return (bool) $this->get($key);
    }

    /**
     * Handle unset calls.
     *
     * @param  string $key
     * @return void
     */
    public function __unset($key)
    {
        $this->forget($key);
    }

    /**
     * Create copy of the attribute bag.
     *
     * @return static
     */
    public function replicate($except = null)
    {
        $except = $except ? array_combine($except, $except) : [];

        $attributes = [];

        foreach (array_diff_key($this->items, $except) as $attribute) {
            $attributes[] = $attribute->replicate();
        }

        return new static($attributes);
    }
}


<?php

namespace Sofa\Eloquence;

use Sofa\Eloquence\Metable\Hooks;
use Sofa\Eloquence\Metable\Attribute;
use Sofa\Eloquence\Metable\AttributeBag;
use Sofa\Hookable\Contracts\ArgumentBag;

/**
 * @property array $allowedMeta
 */
trait Metable
{
    /**
     * Query methods customizable by this trait.
     *
     * @var array
     */
    protected $metaQueryable = [
        'where', 'whereBetween', 'whereIn', 'whereNull',
        'whereDate', 'whereYear', 'whereMonth', 'whereDay',
        'orderBy', 'pluck', 'value', 'aggregate',
    ];

    /**
     * Register hooks for the trait.
     *
     * @codeCoverageIgnore
     *
     * @return void
     */
    public static function bootMetable()
    {
        $hooks = new Hooks;

        foreach ([
                'setAttribute',
                'getAttribute',
                'toArray',
                'replicate',
                'save',
                '__isset',
                '__unset',
                'queryHook',
            ] as $method) {
            static::hook($method, $hooks->{$method}());
        }
    }

    /**
     * Determine wheter method called on the query is customizable by this trait.
     *
     * @param  string  $method
     * @return boolean
     */
    protected function isMetaQueryable($method)
    {
        return in_array($method, $this->metaQueryable);
    }

    /**
     * Custom query handler for querying meta attributes.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  string $method
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @return mixed
     */
    protected function metaQuery(Builder $query, $method, ArgumentBag $args)
    {
        if (in_array($method, ['pluck', 'value', 'aggregate', 'orderBy', 'lists'])) {
            return $this->metaJoinQuery($query, $method, $args);
        }

        return $this->metaHasQuery($query, $method, $args);
    }

    /**
     * Adjust meta columns for select statement.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @return void
     */
    protected function metaSelect(Builder $query, ArgumentBag $args)
    {
        $columns = $args->get('columns');

        foreach ($columns as $key => $column) {
            list($column, $alias) = $this->extractColumnAlias($column);

            if ($this->hasColumn($column)) {
                $select = "{$this->getTable()}.{$column}";

                if ($column !== $alias) {
                    $select .= " as {$alias}";
                }

                $columns[$key] = $select;
            } elseif (is_string($column) && $column != '*' && strpos($column, '.') === false) {
                $table = $this->joinMeta($query, $column);

                $columns[$key] = "{$table}.meta_value as {$alias}";
            }
        }

        $args->set('columns', $columns);
    }

    /**
     * Join meta attributes table in order to call provided method.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  string $method
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @return mixed
     */
    protected function metaJoinQuery(Builder $query, $method, ArgumentBag $args)
    {
        $alias = $this->joinMeta($query, $args->get('column'));

        // For aggregates we need the actual function name
        // so it can be called directly on the builder.
        $method = $args->get('function') ?: $method;

        return (in_array($method, ['orderBy', 'lists', 'pluck']))
            ? $this->{"{$method}Meta"}($query, $args, $alias)
            : $this->metaSingleResult($query, $method, $alias);
    }

    /**
     * Order query by meta attribute.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @param  string $alias
     * @return \Sofa\Eloquence\Builder
     */
    protected function orderByMeta(Builder $query, $args, $alias)
    {
        $query->with('metaAttributes')->getQuery()->orderBy("{$alias}.meta_value", $args->get('direction'));

        return $query;
    }

    /**
     * Get an array with the values of given meta attribute.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @param  string $alias
     * @return array
     */
    protected function pluckMeta(Builder $query, ArgumentBag $args, $alias)
    {
        list($column, $key) = [$args->get('column'), $args->get('key')];

        $query->select("{$alias}.meta_value as {$column}");

        if (!is_null($key)) {
            $this->metaSelectListsKey($query, $key);
        }

        return $query->callParent('pluck', $args->all());
    }

    /**
     * Add select clause for key of the list array.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  string $key
     * @return \Sofa\Eloquence\Builder
     */
    protected function metaSelectListsKey(Builder $query, $key)
    {
        if (strpos($key, '.') !== false) {
            return $query->addSelect($key);
        } elseif ($this->hasColumn($key)) {
            return $query->addSelect($this->getTable() . '.' . $key);
        }

        $alias = $this->joinMeta($query, $key);

        return $query->addSelect("{$alias}.meta_value as {$key}");
    }

    /**
     * Get single value result from the meta attribute.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  string $method
     * @param  string $alias
     * @return mixed
     */
    protected function metaSingleResult(Builder $query, $method, $alias)
    {
        return $query->getQuery()->select("{$alias}.meta_value")->{$method}("{$alias}.meta_value");
    }

    /**
     * Join meta attributes table.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  string $column
     * @return string
     */
    protected function joinMeta(Builder $query, $column)
    {
        $query->prefixColumnsForJoin();

        $alias = $this->generateMetaAlias();

        $table = (new Attribute)->getTable();

        $query->leftJoin("{$table} as {$alias}", function ($join) use ($alias, $column) {
            $join->on("{$alias}.metable_id", '=', $this->getQualifiedKeyName())
                ->where("{$alias}.metable_type", '=', $this->getMorphClass())
                ->where("{$alias}.meta_key", '=', $column);
        });

        return $alias;
    }

    /**
     * Generate unique alias for meta attributes table.
     *
     * @return string
     */
    protected function generateMetaAlias()
    {
        return md5(microtime(true)) . '_meta_alias';
    }

    /**
     * Add whereHas subquery on the meta attributes relation.
     *
     * @param  \Sofa\Eloquence\Builder $query
     * @param  string $method
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @return \Sofa\Eloquence\Builder
     */
    protected function metaHasQuery(Builder $query, $method, ArgumentBag $args)
    {
        $boolean = $this->getMetaBoolean($args);

        $operator = $this->getMetaOperator($method, $args);

        if (in_array($method, ['whereBetween', 'where'])) {
            $this->unbindNumerics($args);
        }

        return $query
            ->has('metaAttributes', $operator, 1, $boolean, $this->getMetaWhereConstraint($method, $args))
            ->with('metaAttributes');
    }

    /**
     * Get boolean called on the original method and set it to default.
     *
     * @param  \Sofa\EloquenceArgumentBag $args
     * @return string
     */
    protected function getMetaBoolean(ArgumentBag $args)
    {
        $boolean = $args->get('boolean');

        $args->set('boolean', 'and');

        return $boolean;
    }

    /**
     * Determine the operator for count relation query.
     *
     * @param  string $method
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @return string
     */
    protected function getMetaOperator($method, ArgumentBag $args)
    {
        if ($not = $args->get('not')) {
            $args->set('not', false);
        }

        return ($not ^ $this->isWhereNull($method, $args)) ? '<' : '>=';
    }

    /**
     * Integers and floats must be passed in raw form in order to avoid string
     * comparison, due to the fact that all meta values are stored as strings.
     *
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @return void
     */
    protected function unbindNumerics(ArgumentBag $args)
    {
        if (($value = $args->get('value')) && (is_int($value) || is_float($value))) {
            $args->set('value', $this->raw($value));
        } elseif ($values = $args->get('values')) {
            foreach ($values as $key => $value) {
                if (is_int($value) || is_float($value)) {
                    $values[$key] = $this->raw($value);
                }
            }

            $args->set('values', $values);
        }
    }

    /**
     * Get the relation constraint closure.
     *
     * @param  string $method
     * @param  \Sofa\Hookable\Contracts\ArgumentBag $args
     * @return \Closure
     */
    protected function getMetaWhereConstraint($method, ArgumentBag $args)
    {
        $column = $args->get('column');

        $args->set('column', 'meta_value');

        if ($method === 'whereBetween') {
            return $this->getMetaBetweenConstraint($column, $args->get('values'));
        }

        return function ($query) use ($column, $method, $args) {
            $query->where('meta_key', $column);

            if ($args->get('value') || $args->get('values')) {
                call_user_func_array([$query, $method], $args->all());
            }
        };
    }

    /**
     * Query Builder whereBetween override required to pass raw numeric values.
     *
     * @param  string $column
     * @param  array  $values
     * @return \Closure
     */
    protected function getMetaBetweenConstraint($column, array $values)
    {
        $min = $values[0];
        $max = $values[1];

        return function ($query) use ($column, $min, $max) {
            $query->where('meta_key', $column)
                ->where('meta_value', '>=', $min)
                ->where('meta_value', '<=', $max);
        };
    }

    /**
     * Save new or updated meta attributes and delete the ones that were unset.
     *
     * @return void
     */
    protected function saveMeta()
    {
        foreach ($this->getMetaAttributes() as $attribute) {
            if (is_null($attribute->getValue())) {
                $attribute->delete();
            } else {
                $this->metaAttributes()->save($attribute);
            }
        }
    }

    /**
     * Determine whether meta attribute is allowed for the model.
     *
     * @param  string $key
     * @return boolean
     */
    public function allowsMeta($key)
    {
        $allowed = $this->getAllowedMeta();

        return empty($allowed) || in_array($key, $allowed);
    }

    /**
     * Determine whether meta attribute exists on the model.
     *
     * @param  string $key
     * @return boolean
     */
    public function hasMeta($key)
    {
        return array_key_exists($key, $this->getMetaAttributesArray());
    }

    /**
     * Get meta attribute value.
     *
     * @param  string $key
     * @return mixed
     */
    public function getMeta($key)
    {
        return $this->getMetaAttributes()->getValue($key);
    }
    /**
     * Get meta attribute values by group.
     *
     * @param  string $key
     * @return mixed
     */
    public function getMetaByGroup($group)
    {
        return $this->getMetaAttributes()->getMetaByGroup($group);
    }
    /**
     * Set meta attribute.
     *
     * @param  string $key
     * @param  mixed  $value
     * @return void
     */
    public function setMeta($key, $value, $group = null)
    {
        $this->getMetaAttributes()->set($key, $value, $group);
    }

    /**
     * Meta attributes relation.
     *
     * @codeCoverageIgnore
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function metaAttributes()
    {
        return $this->morphMany('Sofa\Eloquence\Metable\Attribute', 'metable');
    }

    /**
     * Get meta attributes as collection.
     *
     * @return \Sofa\Eloquence\Metable\AttributeBag
     */
    public function getMetaAttributes()
    {
        $this->loadMetaAttributes();

        return $this->getRelation('metaAttributes');
    }

    /**
     * Accessor for metaAttributes property
     *
     * @return \Sofa\Eloquence\Metable\AttributeBag
     */
    public function getMetaAttributesAttribute()
    {
        return $this->getMetaAttributes();
    }

    /**
     * Get meta attributes as associative array.
     *
     * @return array
     */
    public function getMetaAttributesArray()
    {
        return $this->getMetaAttributes()->toArray();
    }

    /**
     * Load meta attributes relation.
     *
     * @return void
     */
    protected function loadMetaAttributes()
    {
        if (!array_key_exists('metaAttributes', $this->relations)) {
            $this->reloadMetaAttributes();
        }

        $attributes = $this->getRelation('metaAttributes');

        if (!$attributes instanceof AttributeBag) {
            $this->setRelation('metaAttributes', (new Attribute)->newBag($attributes->all()));
        }
    }

    /**
     * Reload meta attributes from db or set empty bag for newly created model.
     *
     * @return $this
     */
    protected function reloadMetaAttributes()
    {
        return ($this->exists)
            ? $this->load('metaAttributes')
            : $this->setRelation('metaAttributes', (new Attribute)->newBag());
    }

    /**
     * Get allowed meta attributes array.
     *
     * @return array
     */
    public function getAllowedMeta()
    {
        return (property_exists($this, 'allowedMeta')) ? $this->allowedMeta : [];
    }
}


<?php

namespace BlakeGardner;

/**
 * This class allows you to preform various operations with
 * Media Access Control (MAC addresses) on UNIX type systems.
 *
 * @author Blake Gardner <blakegardner[at]cox.net>
 * @copyright Copyright (c) 2012, Blake Gardner
 * @license MIT License (see License.txt)
 */
class MacAddress
{

    /**
     * Regular expression for matching and validating a MAC address
     * @var string
     */
    private static $valid_mac = "([0-9A-F]{2}[:-]){5}([0-9A-F]{2})";

    /**
     * An array of valid MAC address characters
     * @var array
     */
    private static $mac_address_vals = array(
        "0", "1", "2", "3", "4", "5", "6", "7",
        "8", "9", "A", "B", "C", "D", "E", "F"
     );

    /**
     * Change the MAC address of the network interface specified
     * @param string $interface Name of the interface e.g. eth0
     * @param string $mac The new MAC address to be set to the interface
     * @return bool Returns true on success else returns false
     */
    public static function setFakeMacAddress($interface, $mac = null)
    {

        // if a valid mac address was not passed then generate one
        if (!self::validateMacAddress($mac)) {
            $mac = self::generateMacAddress();
        }

        // bring the interface down, set the new mac, bring it back up
        self::runCommand("ifconfig {$interface} down");
        self::runCommand("ifconfig {$interface} hw ether {$mac}");
        self::runCommand("ifconfig {$interface} up");

        // TODO: figure out if there is a better method of doing this
        // run DHCP client to grab a new IP address
        self::runCommand("dhclient {$interface}");

        // run a test to see if the operation was a success
        if (self::getCurrentMacAddress($interface) == $mac) {
            return true;
        }

        // by default just return false
        return false;
    }

    /**
     * @return string generated MAC address
     */
    public static function generateMacAddress()
    {
        $vals = self::$mac_address_vals;
        if (count($vals) >= 1) {
            $mac = array("00"); // set first two digits manually
            while (count($mac) < 6) {
                shuffle($vals);
                $mac[] = $vals[0] . $vals[1];
            }
            $mac = implode(":", $mac);
        }
        return $mac;
    }

    /**
     * Make sure the provided MAC address is in the correct format
     * @param string $mac
     * @return bool true if valid; otherwise false
     */
    public static function validateMacAddress($mac)
    {
        return (bool) preg_match("/^" . self::$valid_mac . "$/i", $mac);
    }

    /**
     * Run the specified command and return it's output
     * @param string $command
     * @return string Output from command that was ran
     */
    protected static function runCommand($command)
    {
        return shell_exec($command);
    }

    /**
     * Get the system's current MAC address
     * @param string $interface The name of the interface e.g. eth0
     * @return string|bool Systems current MAC address; otherwise false on error
     */
    public static function getCurrentMacAddress($interface)
    {
        $ifconfig = self::runCommand("ifconfig {$interface}");
        preg_match("/" . self::$valid_mac . "/i", $ifconfig, $ifconfig);
        if (isset($ifconfig[0])) {
            return trim(strtoupper($ifconfig[0]));
        }
        return false;
    }
}


<?php
/**
 * sitemap plugin for Craft CMS 3.x
 *
 * Craft 3 plugin that provides an easy way to enable and manage a xml sitemap for search engines like Google
 *
 * @link      https://github.com/Dolphiq/craft3-plugin-sitemap
 * @copyright Copyright (c) 2017 Johan Zandstra
 */

namespace dolphiq\sitemap;

use dolphiq\sitemap\models\Settings;

use Craft;
use craft\base\Plugin;
use craft\services\Plugins;
use craft\events\PluginEvent;
use craft\web\UrlManager;
use craft\events\RegisterUrlRulesEvent;

use yii\base\Event;

/**
 * Craft plugins are very much like little applications in and of themselves. We’ve made
 * it as simple as we can, but the training wheels are off. A little prior knowledge is
 * going to be required to write a plugin.
 *
 * For the purposes of the plugin docs, we’re going to assume that you know PHP and SQL,
 * as well as some semi-advanced concepts like object-oriented programming and PHP namespaces.
 *
 * https://craftcms.com/docs/plugins/introduction
 *
 * @author    Johan Zandstra
 * @package   Sitemap
 * @since     1.0.0
 *
 * @property  SitemapServiceService $sitemapService
 * @property  Settings $settings
 * @method    Settings getSettings()
 */
class Sitemap extends Plugin
{
    // Static Properties
    // =========================================================================

    /**
     * Static property that is an instance of this plugin class so that it can be accessed via
     * Sitemap::$plugin
     *
     * @var Sitemap
     */
    public static $plugin;

    // Public Methods
    // =========================================================================

    public $hasCpSection = true;
    public $hasCpSettings = true;

    // table schema version
    public $schemaVersion = '1.0.2';

    /**
     * Return the settings response (if some one clicks on the settings/plugin icon)
     *
     */

    public function getSettingsResponse()
    {
        $url = \craft\helpers\UrlHelper::cpUrl('settings/sitemap');
        return \Craft::$app->controller->redirect($url);
    }

    /**
     * Register CP URL rules
     *
     * @param RegisterUrlRulesEvent $event
     */

    public function registerCpUrlRules(RegisterUrlRulesEvent $event)
    {
        // only register CP URLs if the user is logged in
        if (!\Craft::$app->user->identity)
            return;

        $rules = [

            // register routes for the settings tab
            'settings/sitemap' => [
                'route'=>'sitemap/settings',
                'params'=>['source' => 'CpSettings']],
            'settings/sitemap/save-sitemap' => [
                'route'=>'sitemap/settings/save-sitemap',
                'params'=>['source' => 'CpSettings']],
        ];
        $event->rules = array_merge($event->rules, $rules);
    }
    /**
     * Set our $plugin static property to this class so that it can be accessed via
     * Sitemap::$plugin
     *
     * Called after the plugin class is instantiated; do any one-time initialization
     * here such as hooks and events.
     *
     * If you have a '/vendor/autoload.php' file, it will be loaded for you automatically;
     * you do not need to load it in your init() method.
     *
     */
    public function init()
    {
        parent::init();
        self::$plugin = $this;

        // Register our CP routes
        Event::on(
            UrlManager::class,
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
            [$this, 'registerCpUrlRules']
        );

        // Register our site routes
        Event::on(
            UrlManager::class,
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
            function (RegisterUrlRulesEvent $event) {
                $event->rules['sitemap.xml'] = 'sitemap/sitemap/index';
            }
        );

        // Do something after we're installed
        Event::on(
            Plugins::class,
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
            function (PluginEvent $event) {
                if ($event->plugin === $this) {
                    // We were just installed
                }
            }
        );

/**
 * Logging in Craft involves using one of the following methods:
 *
 * Craft::trace(): record a message to trace how a piece of code runs. This is mainly for development use.
 * Craft::info(): record a message that conveys some useful information.
 * Craft::warning(): record a warning message that indicates something unexpected has happened.
 * Craft::error(): record a fatal error that should be investigated as soon as possible.
 *
 * Unless `devMode` is on, only Craft::warning() & Craft::error() will log to `craft/storage/logs/web.log`
 *
 * It's recommended that you pass in the magic constant `__METHOD__` as the second parameter, which sets
 * the category to the method (prefixed with the fully qualified class name) where the constant appears.
 *
 * To enable the Yii debug toolbar, go to your user account in the AdminCP and check the
 * [] Show the debug toolbar on the front end & [] Show the debug toolbar on the Control Panel
 *
 * http://www.yiiframework.com/doc-2.0/guide-runtime-logging.html
 */
        Craft::info(
            Craft::t(
                'sitemap',
                '{name} plugin loaded',
                ['name' => $this->name]
            ),
            __METHOD__
        );
    }

    // Protected Methods
    // =========================================================================

    /**
     * Creates and returns the model used to store the plugin’s settings.
     *
     * @return \craft\base\Model|null
     */
    protected function createSettingsModel()
    {
        return new Settings();
    }

    /**
     * Returns the rendered settings HTML, which will be inserted into the content
     * block on the settings page.
     *
     * @return string The rendered settings HTML
     */
    protected function settingsHtml(): string
    {
        return Craft::$app->view->renderTemplate(
            'sitemap/settings',
            [
                'settings' => $this->getSettings()
            ]
        );
    }
}


<?php
/**
 * sitemap plugin for Craft CMS 3.x
 *
 * Craft 3 plugin that provides an easy way to enable and manage a xml sitemap for search engines like Google
 *
 * @link      https://github.com/Dolphiq/craft3-plugin-sitemap
 * @copyright Copyright (c) 2017 Johan Zandstra
 */

namespace dolphiq\sitemap\controllers;

use dolphiq\sitemap\models\SitemapEntryModel;
use dolphiq\sitemap\records\SitemapEntry;
use dolphiq\sitemap\records\SitemapCrawlerVisit;
use dolphiq\sitemap\Sitemap;

use Craft;
use craft\db\Query;
use craft\web\Controller;
use craft\helpers\UrlHelper;

use Jaybizzle\CrawlerDetect\CrawlerDetect;

/**
 * Default Controller
 *
 * Generally speaking, controllers are the middlemen between the front end of
 * the CP/website and your plugin’s services. They contain action methods which
 * handle individual tasks.
 *
 * A common pattern used throughout Craft involves a controller action gathering
 * post data, saving it on a model, passing the model off to a service, and then
 * responding to the request appropriately depending on the service method’s response.
 *
 * Action methods begin with the prefix “action”, followed by a description of what
 * the method does (for example, actionSaveIngredient()).
 *
 * https://craftcms.com/docs/plugins/controllers
 *
 * @author    Johan Zandstra
 * @package   Sitemap
 * @since     1.0.0
 */
class SitemapController extends Controller
{
    private $_sourceRouteParams = [];
    protected $allowAnonymous = ['index'];
    // Public Methods
// =========================================================================

    /**
     * @inheritdoc
     */
    private function getUrl($uri, $siteId)
    {
        if ($uri !== null) {
            $path = ($uri === '__home__') ? '' : $uri;
            return UrlHelper::siteUrl($path, null, null, $siteId);
        }

        return null;
    }

    /**
     * Handle a request going to our plugin's index action URL,
     * e.g.: actions/sitemap/default
     *
     * @return mixed
     */
    public function actionIndex()
    {

        try {
            // try to register the searchengine visit
            $CrawlerDetect = new CrawlerDetect;

            // Check the user agent of the current 'visitor'
            if($CrawlerDetect->isCrawler()) {
                // insert into table!
                $visit = new SitemapCrawlerVisit();
                $visit->name = $CrawlerDetect->getMatches();
                $visit->save();
            }
        } catch(\Exception $err) {

        }
        Craft::$app->response->format = \yii\web\Response::FORMAT_RAW;
        $headers = Craft::$app->response->headers;
        $headers->add('Content-Type', 'text/xml');

        $dom = new \DOMDocument('1.0','UTF-8');
        $dom->formatOutput = true;

        $urlset = $dom->createElementNS('http://www.sitemaps.org/schemas/sitemap/0.9', 'urlset');
        $urlset->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:xhtml',
            'http://www.w3.org/1999/xhtml'
        );
        $dom->appendChild($urlset);

        foreach($this->_createEntrySectionQuery()->all() as $item) {
            $loc = $this->getUrl($item['uri'], $item['siteId']);
            if($loc === null) continue;

            $url = $dom->createElement('url');
            $urlset->appendChild($url);
            $url->appendChild($dom->createElement('loc', $loc));
            $url->appendChild($dom->createElement('priority', $item['priority']));
            $url->appendChild($dom->createElement('changefreq', $item['changefreq']));
            $dateUpdated = strtotime($item['dateUpdated']);
            $url->appendChild($dom->createElement('lastmod', date('Y-m-d\TH:i:sP', $dateUpdated)));
            if ($item['alternateLinkCount'] > 1) {
                $alternateLinks = $this->_createAlternateSectionQuery($item['elementId'])->all();
                if (count($alternateLinks) > 0) {
                    foreach ($alternateLinks as $alternateItem) {
                        $alternateLoc = $this->getUrl($alternateItem['uri'], $alternateItem['siteId']);
                        if ($alternateLoc === null) continue;

                        $alternateLink = $dom->createElementNS('http://www.w3.org/1999/xhtml', 'xhtml:link');
                        $alternateLink->setAttribute('rel', 'alternate');
                        $alternateLink->setAttribute('hreflang', strtolower($alternateItem['siteLanguate']));
                        $alternateLink->setAttribute('href', $alternateLoc);
                        $url->appendChild($alternateLink);
                    }
                }
            }
        }

        foreach($this->_createEntryCategoryQuery()->all() as $item) {
            $loc = $this->getUrl($item['uri'], $item['siteId']);
            if($loc === null) continue;

            $url = $dom->createElement('url');
            $urlset->appendChild($url);
            $url->appendChild($dom->createElement('loc', $loc));
            $url->appendChild($dom->createElement('priority', $item['priority']));
            $url->appendChild($dom->createElement('changefreq', $item['changefreq']));
            $dateUpdated = strtotime($item['dateUpdated']);
            $url->appendChild($dom->createElement('lastmod', date('Y-m-d\TH:i:sP', $dateUpdated)));

        }
        return $dom->saveXML();
    }

    private function _createEntrySectionQuery(): Query
    {

        $subQuery = (new Query())
            ->select('COUNT(DISTINCT other_elements_sites.id)')
            ->from('{{%elements_sites}} other_elements_sites')
            ->where('[[other_elements_sites.elementId]] = [[elements_sites.elementId]] AND [[other_elements_sites.enabled]] = 1');
        return (new Query())
            ->select([
                'elements_sites.uri uri',
                'elements_sites.dateUpdated dateUpdated',
                'elements_sites.siteId',
                'sitemap_entries.changefreq changefreq',
                'sitemap_entries.priority priority',
                'sites.language siteLanguage',
                'elements.id elementId',
                'alternateLinkCount' => $subQuery,

            ])
            ->from(['{{%sections}} sections'])
            ->innerJoin('{{%dolphiq_sitemap_entries}} sitemap_entries', '[[sections.id]] = [[sitemap_entries.linkId]] AND [[sitemap_entries.type]] = "section"')
            ->leftJoin('{{%structures}} structures', '[[structures.id]] = [[sections.structureId]]')
            ->innerJoin('{{%sections_sites}} sections_sites', '[[sections_sites.sectionId]] = [[sections.id]] AND [[sections_sites.hasUrls]] = 1')
            ->innerJoin('{{%entries}} entries', '[[sections.id]] = [[entries.sectionId]]')
            ->innerJoin('{{%elements}} elements', '[[entries.id]] = [[elements.id]] AND [[elements.enabled]] = 1')
            ->innerJoin('{{%elements_sites}} elements_sites', '[[elements_sites.elementId]] = [[elements.id]] AND [[elements_sites.enabled]] = 1')
            ->innerJoin('{{%sites}} sites', '[[elements_sites.siteId]] = [[sites.id]]')

            ->groupBy(['elements_sites.id']);
    }

    private function _createAlternateSectionQuery($elementId): Query
    {
        return (new Query())
            ->select([
                'elements_sites.uri uri',
                'elements_sites.dateUpdated dateUpdated',
                'elements_sites.siteId',
                'sites.language siteLanguate',
            ])
            ->from('{{%elements}} elements')
            ->innerJoin('{{%elements_sites}} elements_sites', '[[elements_sites.elementId]] = [[elements.id]] AND [[elements_sites.enabled]] = 1')
            ->innerJoin('{{%sites}} sites', '[[elements_sites.siteId]] = [[sites.id]]')
            ->where(['=', '[[elements_sites.elementId]]', $elementId])
            ->groupBy(['elements_sites.id']);
    }

    private function _createEntryCategoryQuery(): Query
    {
        return (new Query())
            ->select([

                'elements_sites.uri uri',
                'elements_sites.dateUpdated dateUpdated',
                'elements_sites.siteId',
                'sitemap_entries.changefreq changefreq',
                'sitemap_entries.priority priority',
            ])
            ->from(['{{%categories}} categories'])
            ->innerJoin('{{%dolphiq_sitemap_entries}} sitemap_entries', '[[categories.groupId]] = [[sitemap_entries.linkId]] AND [[sitemap_entries.type]] = "category"')
            ->innerJoin('{{%categorygroups_sites}} categorygroups_sites', '[[categorygroups_sites.groupId]] = [[categories.groupId]] AND [[categorygroups_sites.hasUrls]] = 1')
            ->innerJoin('{{%elements}} elements', '[[elements.id]] = [[categories.id]] AND [[elements.enabled]] = 1')
            ->innerJoin('{{%elements_sites}} elements_sites', '[[elements_sites.elementId]] = [[elements.id]] AND [[elements_sites.enabled]] = 1')
            ->innerJoin('{{%sites}} sites', '[[elements_sites.siteId]] = [[sites.id]]')
            ->groupBy(['elements_sites.id']);
    }

}

?>


<?php
/**
 * sitemap plugin for Craft CMS 3.x
 *
 * Craft 3 plugin that provides an easy way to enable and manage a xml sitemap for search engines like Google
 *
 * @link      https://github.com/Dolphiq/craft3-plugin-sitemap
 * @copyright Copyright (c) 2017 Johan Zandstra
 */

namespace dolphiq\sitemap\migrations;

use dolphiq\sitemap\Sitemap;

use Craft;
use craft\config\DbConfig;
use craft\db\Migration;

/**
 * sitemap Install Migration
 *
 * If your plugin needs to create any custom database tables when it gets installed,
 * create a migrations/ folder within your plugin folder, and save an Install.php file
 * within it using the following template:
 *
 * If you need to perform any additional actions on install/uninstall, override the
 * safeUp() and safeDown() methods.
 *
 * @author    Johan Zandstra
 * @package   Sitemap
 * @since     1.0.0
 */
class Install extends Migration
{
    // Public Properties
    // =========================================================================

    /**
     * @var string The database driver to use
     */
    public $driver;

    // Public Methods
    // =========================================================================

    /**
     * This method contains the logic to be executed when applying this migration.
     * This method differs from [[up()]] in that the DB logic implemented here will
     * be enclosed within a DB transaction.
     * Child classes may implement this method instead of [[up()]] if the DB logic
     * needs to be within a transaction.
     *
     * @return boolean return a false value to indicate the migration fails
     * and should not proceed further. All other return values mean the migration succeeds.
     */
    public function safeUp()
    {
        $this->driver = Craft::$app->getConfig()->getDb()->driver;
        if ($this->createTables()) {
            $this->createIndexes();
            $this->addForeignKeys();
            // Refresh the db schema caches
            Craft::$app->db->schema->refresh();
            $this->insertDefaultData();
        }

        return true;
    }

    /**
     * This method contains the logic to be executed when removing this migration.
     * This method differs from [[down()]] in that the DB logic implemented here will
     * be enclosed within a DB transaction.
     * Child classes may implement this method instead of [[down()]] if the DB logic
     * needs to be within a transaction.
     *
     * @return boolean return a false value to indicate the migration fails
     * and should not proceed further. All other return values mean the migration succeeds.
     */
    public function safeDown()
    {
        $this->driver = Craft::$app->getConfig()->getDb()->driver;
        $this->removeTables();

        return true;
    }

    // Protected Methods
    // =========================================================================

    /**
     * Creates the tables needed for the Records used by the plugin
     *
     * @return bool
     */
    protected function createTables()
    {
        $tablesCreated = false;

    // sitemap_sitemaprecord table
        $tableSchema = Craft::$app->db->schema->getTableSchema('{{%dolphiq_sitemap_entries}}');
        if ($tableSchema === null) {
            $tablesCreated = true;
            $this->createTable(
                '{{%dolphiq_sitemap_entries}}',
                [
                    'id' => $this->primaryKey(),
                    'dateCreated' => $this->dateTime()->notNull(),
                    'dateUpdated' => $this->dateTime()->notNull(),
                    'uid' => $this->uid(),
                // Custom columns in the table
                    'linkId' => $this->integer()->notNull(),
                    'type' => $this->string(30)->notNull()->defaultValue(''),
                    'priority' => $this->double(2)->notNull()->defaultValue(0.5),
                    'changefreq' => $this->string(30)->notNull()->defaultValue(''),
                ]
            );
        }

        return $tablesCreated;
    }

    /**
     * Creates the indexes needed for the Records used by the plugin
     *
     * @return void
     */
    protected function createIndexes()
    {
    // sitemap_sitemaprecord table
        $this->createIndex(
            $this->db->getIndexName(
                '{{%dolphiq_sitemap_entries}}',
                ['type', 'linkId'],
                true
            ),
            '{{%dolphiq_sitemap_entries}}',
            ['type', 'linkId'],
            true
        );
        // Additional commands depending on the db driver
        switch ($this->driver) {
            case DbConfig::DRIVER_MYSQL:
                break;
            case DbConfig::DRIVER_PGSQL:
                break;
        }
    }

    /**
     * Creates the foreign keys needed for the Records used by the plugin
     *
     * @return void
     */
    protected function addForeignKeys()
    {

    }

    /**
     * Populates the DB with the default data.
     *
     * @return void
     */
    protected function insertDefaultData()
    {
    }

    /**
     * Removes the tables needed for the Records used by the plugin
     *
     * @return void
     */
    protected function removeTables()
    {
    // sitemap_sitemaprecord table
        $this->dropTableIfExists('{{%dolphiq_sitemap_entries}}');
    }
}


<?php
/**
 * sitemap plugin for Craft CMS 3.x
 *
 * Craft 3 plugin that provides an easy way to enable and manage a xml sitemap for search engines like Google
 *
 * @link      https://github.com/Dolphiq/craft3-plugin-sitemap
 * @copyright Copyright (c) 2017 Johan Zandstra
 */

namespace dolphiq\sitemap\controllers;

use dolphiq\sitemap\models\SitemapEntryModel;
use dolphiq\sitemap\records\SitemapEntry;
use dolphiq\sitemap\Sitemap;

use Craft;
use craft\db\Query;
use craft\web\Controller;

/**
 * Default Controller
 *
 * Generally speaking, controllers are the middlemen between the front end of
 * the CP/website and your plugin’s services. They contain action methods which
 * handle individual tasks.
 *
 * A common pattern used throughout Craft involves a controller action gathering
 * post data, saving it on a model, passing the model off to a service, and then
 * responding to the request appropriately depending on the service method’s response.
 *
 * Action methods begin with the prefix “action”, followed by a description of what
 * the method does (for example, actionSaveIngredient()).
 *
 * https://craftcms.com/docs/plugins/controllers
 *
 * @author    Johan Zandstra
 * @package   Sitemap
 * @since     1.0.0
 */
class SettingsController extends Controller
{

    // Protected Properties
    // =========================================================================

    /**
     * @var    bool|array Allows anonymous access to this controller's actions.
     *         The actions must be in 'kebab-case'
     * @access protected
     */
    protected $allowAnonymous = false;

    private function _createEntrySectionQuery(): Query
    {
        return (new Query())
        ->select([
            'sections.id',
            'sections.structureId',
            'sections.name',
            'sections.handle',
            'sections.type',
            'count(DISTINCT entries.id) entryCount',
            'count(DISTINCT elements.id) elementCount',
            'sitemap_entries.id sitemapEntryId',
            'sitemap_entries.changefreq changefreq',
            'sitemap_entries.priority priority',
        ])->from(['{{%sections}} sections'])
            ->leftJoin('{{%structures}} structures', '[[structures.id]] = [[sections.structureId]]')
            ->innerJoin('{{%sections_sites}} sections_sites', '[[sections_sites.sectionId]] = [[sections.id]] AND [[sections_sites.hasUrls]] = 1')
            ->leftJoin('{{%entries}} entries', '[[sections.id]] = [[entries.sectionId]]')
            ->leftJoin('{{%elements}} elements', '[[entries.id]] = [[elements.id]] AND [[elements.enabled]] = 1')
            ->leftJoin('{{%dolphiq_sitemap_entries}} sitemap_entries', '[[sections.id]] = [[sitemap_entries.linkId]] AND [[sitemap_entries.type]] = "section"')

            ->groupBy(['sections.id'])
        ->orderBy(['type' => SORT_ASC],['name' => SORT_ASC]);
    }

    private function _createCategoryQuery(): Query
    {
        return (new Query())
            ->select([
                'categorygroups.id',
                'categorygroups.name',
                'count(DISTINCT entries.id) entryCount',
                'count(DISTINCT elements.id) elementCount',
                'sitemap_entries.id sitemapEntryId',
                'sitemap_entries.changefreq changefreq',
                'sitemap_entries.priority priority',
            ])
            ->from(['{{%categories}} categories'])
            ->innerJoin('{{%categorygroups}} categorygroups', '[[categories.groupId]] = [[categorygroups.id]]')
            ->innerJoin('{{%categorygroups_sites}} categorygroups_sites', '[[categorygroups_sites.groupId]] = [[categorygroups.id]] AND [[categorygroups_sites.hasUrls]] = 1')
            ->leftJoin('{{%entries}} entries', '[[categories.id]] = [[entries.sectionId]]')
            ->leftJoin('{{%elements}} elements', '[[entries.id]] = [[elements.id]] AND [[elements.enabled]] = 1')
            ->leftJoin('{{%dolphiq_sitemap_entries}} sitemap_entries', '[[categorygroups.id]] = [[sitemap_entries.linkId]] AND [[sitemap_entries.type]] = "category"')
            ->groupBy(['categorygroups.id'])
            ->orderBy(['name' => SORT_ASC]);
    }

// Public Methods
// =========================================================================

/**
* Handle a request going to our plugin's index action URL,
* e.g.: actions/sitemap/default
*
* @return mixed
*/
    public function actionIndex(): craft\web\Response
    {
        $this->requireLogin();

        $routeParameters = Craft::$app->getUrlManager()->getRouteParams();

        $source = (isset($routeParameters['source'])?$routeParameters['source']:'CpSection');

        // $allSections = Craft::$app->getSections()->getAllSections();
        $allSections = $this->_createEntrySectionQuery()->all();
        $allStructures = [];

        if (is_array($allSections)) {
            foreach ($allSections as $section) {
                $allStructures[] = [
                    'id' => $section['id'],
                    'type' => $section['type'],
                    'heading' => $section['name'],
                    'enabled' => ($section['sitemapEntryId'] > 0 ? true : false),
                    'elementCount' => $section['elementCount'],
                    'changefreq' => ($section['sitemapEntryId'] > 0 ? $section['changefreq'] : 'weekly'),
                    'priority' => ($section['sitemapEntryId'] > 0 ? $section['priority'] : 0.5),
                ];
            }
        }

        $allCategories = $this->_createCategoryQuery()->all();
        $allCategoryStructures = [];
        if (is_array($allCategories)) {
            // print_r($allSections);
            foreach ($allCategories as $category) {
                $allCategoryStructures[] = [
                    'id' => $category['id'],
                    'type' => 'category',
                    'heading' => $category['name'],
                    'enabled' => ($category['sitemapEntryId'] > 0 ? true : false),
                    'elementCount' => $category['elementCount'],
                    'changefreq' => ($category['sitemapEntryId'] > 0 ? $category['changefreq'] : 'weekly'),
                    'priority' => ($category['sitemapEntryId'] > 0 ? $category['priority'] : 0.5),
                ];
            }
        }
        $variables = [
            'settings' => Sitemap::$plugin->getSettings(),
            'source' => $source,
            'pathPrefix' => ($source == 'CpSettings' ? 'settings/': ''),
            'allStructures' => $allStructures,
            'allCategories' => $allCategoryStructures
            // 'allRedirects' => $allRedirects
        ];

        return $this->renderTemplate('sitemap/settings', $variables);
    }

    /**
     * Called when saving the settings.
     *
     * @return Response
     */
    public function actionSaveSitemap(): craft\web\Response
    {
        $this->requirePostRequest();
        $this->requireAdmin();
        $request = Craft::$app->getRequest();
        // @TODO: check the input and save the sections
        $sitemapSections = $request->getBodyParam('sitemapSections');
        // filter the enabled sections
        $allSectionIds = [];
        if (is_array($sitemapSections)) {
            foreach ($sitemapSections as $key => $entry) {
                if ($entry['enabled']) {
                    // filter section id from key

                    $id = (int)str_replace('id:', '', $key);
                    if ($id > 0) {
                        // find the entry, else add one
                        $sitemapEntry = SitemapEntry::find()->where(['linkId' => $id, 'type' => 'section'])->one();
                        if (!$sitemapEntry) {
                            // insert / update this section
                            $sitemapEntry = new SitemapEntry();
                        }
                        $sitemapEntry->linkId = $id;
                        $sitemapEntry->type = 'section';
                        $sitemapEntry->priority = $entry['priority'];
                        $sitemapEntry->changefreq = $entry['changefreq'];
                        $sitemapEntry->save();
                        array_push($allSectionIds, $id);
                    }
                }
            }
        }
        // remove all sitemaps not in the id list
        if(count($allSectionIds) == 0) {
            SitemapEntry::deleteAll(['type' => 'section']);
        } else {
            foreach (SitemapEntry::find()->where(['type' => 'section'])->andWhere(['NOT IN','linkId',$allSectionIds])->all() as $entry) {
                $entry->delete();
            }
        }

        // now save the sitemapCategories
        $sitemapCategories = $request->getBodyParam('sitemapCategories');
        // filter the enabled sections
        $allCategoryIds = [];
        if (is_array($sitemapCategories)) {
            foreach ($sitemapCategories as $key => $entry) {
                if ($entry['enabled']) {
                    // filter section id from key

                    $id = (int)str_replace('id:', '', $key);
                    if ($id > 0) {
                        // find the entry, else add one
                        $sitemapEntry = SitemapEntry::find()->where(['linkId' => $id, 'type' => 'category'])->one();
                        if (!$sitemapEntry) {
                            // insert / update this section
                            $sitemapEntry = new SitemapEntry();
                        }
                        $sitemapEntry->linkId = $id;
                        $sitemapEntry->type = 'category';
                        $sitemapEntry->priority = $entry['priority'];
                        $sitemapEntry->changefreq = $entry['changefreq'];
                        $sitemapEntry->save();
                        array_push($allCategoryIds, $id);
                    }
                }
            }
        }
        // remove all sitemaps not in the id list
        if(count($allCategoryIds) == 0) {
            SitemapEntry::deleteAll(['type' => 'category']);
        } else {
            foreach (SitemapEntry::find()->where(['type' => 'category'])->andWhere(['NOT IN','linkId',$allCategoryIds])->all() as $entry) {
                $entry->delete();
            }
        }
        return $this->actionIndex();

    }

}


<?php

namespace Timiki\Bundle\RpcServerBundle\Serializer;

use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

class BaseSerializer implements SerializerInterface
{
    /**
     * @var null|Serializer
     */
    protected $serializer;

    /**
     * BaseSerializer constructor.
     *
     * @param null|Serializer $serializer
     */
    public function __construct(Serializer $serializer = null)
    {
        $this->serializer = $serializer;

        if (null === $this->serializer) {
            $this->serializer = new Serializer([new ObjectNormalizer()], [new JsonEncode()]);
        }
    }

    /**
     * Serialize data.
     *
     * @param mixed $data
     *
     * @return array
     */
    public function serialize($data)
    {
        if ($this->serializer) {
            return \json_decode($this->serializer->serialize($data, 'json'), true);
        }

        return $data;
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle\Handler;

use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
use Timiki\Bundle\RpcServerBundle\Event;
use Timiki\Bundle\RpcServerBundle\Exceptions;
use Timiki\Bundle\RpcServerBundle\Traits\EventDispatcherTrait;
use Timiki\Bundle\RpcServerBundle\Traits\ProfilerTrait;
use Timiki\RpcCommon\JsonRequest;
use Timiki\RpcCommon\JsonResponse;

class HttpHandler
{
    use EventDispatcherTrait;
    use ProfilerTrait;

    /**
     * Json handler.
     *
     * @var null|JsonHandler
     */
    private $jsonHandler;

    /**
     * Response error code.
     *
     * @var int
     */
    private $errorCode;

    /**
     * HttpHandler constructor.
     *
     * @param JsonHandler $jsonHandler
     * @param int         $errorCode
     */
    public function __construct(JsonHandler $jsonHandler, $errorCode = 200)
    {
        $this->jsonHandler = $jsonHandler;
        $this->errorCode = $errorCode;
    }

    /**
     * Parser HttpRequest to JsonRequest.
     *
     * @param HttpRequest $httpRequest
     *
     * @throws Exceptions\ParseException
     *
     * @return JsonRequest|JsonRequest[]
     */
    public function parserHttpRequest(HttpRequest $httpRequest)
    {
        $json = \json_decode($httpRequest->getContent(), true);

        if (JSON_ERROR_NONE !== \json_last_error()) {
            throw new Exceptions\ParseException();
        }

        /**
         * Create new JsonRequest.
         *
         * @param array $json
         *
         * @return JsonRequest
         */
        $createJsonRequest = function ($json) use ($httpRequest) {
            $id = null;
            $method = null;
            $params = [];

            if (\is_array($json)) {
                $id = \array_key_exists('id', $json) ? $json['id'] : null;
                $method = \array_key_exists('method', $json) ? $json['method'] : null;
                $params = \array_key_exists('params', $json) ? $json['params'] : [];
            }

            $request = new JsonRequest($method, $params, $id);
            $request->headers()->add($httpRequest->headers->all());

            return $request;
        };

        // If batch request
        if (\array_keys($json) === \range(0, \count($json) - 1)) {
            $requests = [];
            foreach ($json as $part) {
                $requests[] = $createJsonRequest($part);
            }
        } else {
            $requests = $createJsonRequest($json);
        }

        return $requests;
    }

    /**
     * Handle http request.
     *
     * @param HttpRequest $httpRequest
     *
     * @return HttpResponse
     */
    public function handleHttpRequest(HttpRequest $httpRequest)
    {
        // @var Event\HttpRequestEvent $event
        $event = $this->dispatch(Event\HttpRequestEvent::EVENT, new Event\HttpRequestEvent($httpRequest));
        $httpRequest = $event->getHttpRequest();

        try {
            $jsonRequests = $this->parserHttpRequest($httpRequest);
        } catch (Exceptions\ParseException  $e) {
            return $this->createHttpResponseFromException($e);
        }

        $jsonResponses = $this->jsonHandler->handleJsonRequest($jsonRequests);
        $httpResponse = HttpResponse::create();

        if ($this->profiler) {
            /**
             * @param JsonResponse|JsonResponse[] $jsonResponse
             */
            $collect = function ($jsonResponse) use (&$collect, $httpRequest, $httpResponse) {
                if (\is_array($jsonResponse)) {
                    foreach ($jsonResponse as $value) {
                        $collect($value);
                    }
                } else {
                    if ($jsonResponse->isError()) {
                        $this->collectException(
                            $httpRequest,
                            $httpResponse,
                            new Exceptions\ErrorException($jsonResponse->getErrorMessage(), $jsonResponse->getErrorCode(), $jsonResponse->getErrorData(), $jsonResponse->getId())
                        );
                    }
                }
            };

            $collect($jsonResponses);
        }

        // Set httpResponse content.

        if (\is_array($jsonResponses)) {
            $results = [];

            foreach ($jsonResponses as $jsonResponse) {
                if ($jsonResponse->isError() || $jsonResponse->getId()) {
                    $results[] = $jsonResponse;
                }

                if ($jsonResponse->isError()) {
                    $httpResponse->setStatusCode($this->errorCode);
                }
            }

            $httpResponse->setContent(\json_encode($results));
        } else {
            if ($jsonResponses->isError() || $jsonResponses->getId()) {
                $httpResponse->setContent(\json_encode($jsonResponses));
            }

            if ($jsonResponses->isError()) {
                $httpResponse->setStatusCode($this->errorCode);
            }
        }

        // Set httpResponse headers
        if (\is_array($jsonResponses)) {
            foreach ($jsonResponses as $jsonResponse) {
                if ($jsonResponse->isError() || $jsonResponse->getId()) {
                    $httpResponse->headers->add($jsonResponse->headers()->all());
                }
            }
        } else {
            $httpResponse->headers->add($jsonResponses->headers()->all());
        }

        $httpResponse->headers->set('Content-Type', 'application/json');

        $this->dispatch(
            Event\HttpResponseEvent::EVENT,
            new Event\HttpResponseEvent($httpResponse)
        );

        return $httpResponse;
    }

    /**
     * Create new HttpResponse from exception.
     *
     * @param \Exception $exception
     *
     * @return HttpResponse
     */
    public function createHttpResponseFromException(\Exception $exception)
    {
        $httpResponse = HttpResponse::create();
        $json = [];
        $json['jsonrpc'] = '2.0';
        $json['error'] = [];

        if ($exception instanceof Exceptions\ErrorException) {
            $json['error']['code'] = $exception->getCode();
            $json['error']['message'] = $exception->getMessage();

            if ($exception->getData()) {
                $json['error']['data'] = $exception->getData();
            }

            $json['id'] = $exception->getId();
        } else {
            $json['error']['code'] = -32603;
            $json['error']['message'] = 'Internal error';
            $json['id'] = null;
        }

        $httpResponse->headers->set('Content-Type', 'application/json');
        $httpResponse->setContent(\json_encode($json));
        $httpResponse->setStatusCode($this->errorCode);

        $this->dispatch(
            Event\HttpResponseEvent::EVENT,
            new Event\HttpResponseEvent($httpResponse)
        );

        return $httpResponse;
    }

    /**
     * Collect exception.
     *
     * @param HttpRequest  $httpRequest
     * @param HttpResponse $httpResponse
     * @param \Exception   $exception
     */
    private function collectException($httpRequest, $httpResponse, $exception)
    {
        if ($this->profiler) {
            $collector = new ExceptionDataCollector();
            $collector->collect($httpRequest, $httpResponse, $exception);
            $this->profiler->add($collector);
        }
    }

    /**
     * Get Json handler.
     *
     * @return null|\Timiki\Bundle\RpcServerBundle\Handler\JsonHandler
     */
    public function getJsonHandler()
    {
        return $this->jsonHandler;
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle\DependencyInjection\Compiler;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\DocParser;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Timiki\Bundle\RpcServerBundle\Exceptions\InvalidMappingException;
use Timiki\Bundle\RpcServerBundle\Mapper\MapperInterface;
use Timiki\Bundle\RpcServerBundle\Mapper\MethodInterface;
use Timiki\Bundle\RpcServerBundle\Mapping;

/**
 * Class RpcMethodPass add methods to mappers.
 */
class RpcMethodPass implements CompilerPassInterface
{
    private $reader;

    private $methodMeta = [];

    /**
     * RpcMethodPass constructor.
     *
     * @throws \Doctrine\Common\Annotations\AnnotationException
     */
    public function __construct()
    {
        $this->reader = new AnnotationReader(new DocParser());
        AnnotationRegistry::registerLoader('class_exists');
    }

    /**
     * @param ContainerBuilder $container
     *
     * @throws InvalidMappingException
     * @throws \ReflectionException
     */
    public function process(ContainerBuilder $container)
    {
        $taggedMethods = $container->findTaggedServiceIds(MethodInterface::class);

        // Collect meta data from methods tags
        foreach ($taggedMethods as $methodId => $attrs) {
            foreach ($attrs as $attr) {
                $cacheForMapperId = $attr['mapperId'];

                if (true === isset($this->methodMeta[$cacheForMapperId][$methodId])) {
                    continue;
                }

                $this->methodMeta[$cacheForMapperId][$methodId] = $this->loadClassMetadata(
                    $container->getReflectionClass($methodId)
                );
            }
        }

        $taggedMappers = $container->findTaggedServiceIds(MapperInterface::class);

        $methodsMetaData = [];
        // Inject method meta data to mapper
        foreach ($taggedMappers as $mapperId => $mapperAttr) {
            if (false === isset($this->methodMeta[$mapperId])) {
                continue; // Method not exists
            }

            $methodsMeta = $this->methodMeta[$mapperId];

            foreach ($methodsMeta as $methodId => $meta) {
                if (null === $meta) {
                    continue;
                }

                $methodsMetaData[$mapperId][$meta['method']] = [
                    $methodId,
                    $meta['executeMethod'],
                    $meta['params'],
                    $meta['cache'],
                    $meta['roles'],
                    $meta['cache'],
                ];
            }
        }

        // Inject method meta data to mapper
        foreach ($methodsMetaData as $mapperId => $methods) {
            $definition = $container->getDefinition($mapperId);
            $definition->addMethodCall('addMethods', [$methods]);
        }
    }

    /**
     * @param \ReflectionClass $reflectionClass
     *
     * @throws InvalidMappingException
     *
     * @return null|array
     */
    public function loadClassMetadata(\ReflectionClass $reflectionClass): ?array
    {
        $method = $this->reader->getClassAnnotation($reflectionClass, Mapping\Method::class);

        if (null === $method) {
            return null;
        }

        $meta = [];

        if (empty($method->value)) {
            throw new InvalidMappingException(
                \sprintf(
                    '@Method annotation must have name in class "%s", @Method("method name")',
                    $reflectionClass->getName()
                )
            );
        }

        $meta['method'] = $method->value;
        $meta['class'] = $reflectionClass->getName();
        $meta['file'] = $reflectionClass->getFileName();

        // Cache
        $meta['cache'] = null;
        $cache = $this->reader->getClassAnnotation($reflectionClass, Mapping\Cache::class);
        /** @var Mapping\Cache $cache */
        if (null !== $cache) {
            $meta['cache'] = (int) $cache->lifetime;
        }

        // Roles
        $meta['roles'] = null;
        $roles = $this->reader->getClassAnnotation($reflectionClass, Mapping\Roles::class);
        /** @var Mapping\Roles $roles */
        if (null !== $roles) {
            $meta['roles'] = (array) $roles->value;
        }

        // Method execute. On in class
        $meta['executeMethod'] = null;

        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
            if ($paramMeta = $this->reader->getMethodAnnotation($reflectionMethod, Mapping\Execute::class)) {
                $meta['executeMethod'] = $reflectionMethod->name;
            }
        }

        if (empty($meta['executeMethod'])) {
            throw new InvalidMappingException(
                \sprintf(
                    'Method need have @Execute annotation in class "%s", @Execute()',
                    $reflectionClass->getName()
                )
            );
        }

        // Params
        $meta['params'] = [];

        foreach ($reflectionClass->getProperties() as $reflectionProperty) {
            if ($paramMeta = $this->reader->getPropertyAnnotation($reflectionProperty, Mapping\Param::class)) {
                $meta['params'][$reflectionProperty->name] = true;
            }
        }

        return $meta;
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Timiki\Bundle\RpcServerBundle\Registry\HttpHandlerRegistry;

class RpcController extends AbstractController
{
    /**
     * @var \Timiki\Bundle\RpcServerBundle\Registry\HttpHandlerRegistry
     */
    private $handlerRegistry;

    /**
     * RpcController constructor.
     *
     * @param \Timiki\Bundle\RpcServerBundle\Registry\HttpHandlerRegistry $handlerRegistry
     */
    public function __construct(HttpHandlerRegistry $handlerRegistry)
    {
        $this->handlerRegistry = $handlerRegistry;
    }

    /**
     * @param Request $request
     * @param string  $version
     *
     * @throws \Exception
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function handlerAction(Request $request, $version = 'default')
    {
        return $this
            ->handlerRegistry
            ->get($version)
            ->handleHttpRequest($request);
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle\Handler;

use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Timiki\Bundle\RpcServerBundle\Event;
use Timiki\Bundle\RpcServerBundle\Exceptions;
use Timiki\Bundle\RpcServerBundle\Mapper\Mapper;
use Timiki\Bundle\RpcServerBundle\Mapper\MethodMetaData;
use Timiki\Bundle\RpcServerBundle\Serializer\SerializerInterface;
use Timiki\Bundle\RpcServerBundle\Traits\CacheTrait;
use Timiki\Bundle\RpcServerBundle\Traits\EventDispatcherTrait;
use Timiki\Bundle\RpcServerBundle\Traits\StopwatchTrait;
use Timiki\RpcCommon\JsonRequest;
use Timiki\RpcCommon\JsonResponse;

class JsonHandler implements ContainerAwareInterface
{
    use ContainerAwareTrait;
    use CacheTrait;
    use StopwatchTrait;
    use EventDispatcherTrait;

    /**
     * Rpc mapper.
     *
     * @var null|Mapper
     */
    private $mapper;

    /**
     * Serializer.
     *
     * @var null|SerializerInterface
     */
    private $serializer;

    /**
     * JsonHandler constructor.
     *
     * @param Mapper                   $mapper
     * @param null|SerializerInterface $serializer
     */
    public function __construct(Mapper $mapper, SerializerInterface $serializer = null)
    {
        $this->mapper = $mapper;
        $this->serializer = $serializer;
    }

    /**
     * Is debug.
     *
     * @return bool
     */
    public function isDebug()
    {
        if (null !== $this->container && $this->container->has('kernel')) {
            return $this->container->get('kernel')->isDebug();
        }

        return true;
    }

    /**
     * Get serializer.
     *
     * @return null|SerializerInterface
     */
    public function getSerializer()
    {
        return $this->serializer;
    }

    /**
     * Serialize data.
     *
     * @param mixed $data
     *
     * @return null|array|int|\JsonSerializable|string
     */
    public function serialize($data)
    {
        if (!$this->serializer || \is_numeric($data) || \is_string($data) || empty($data) || $data instanceof \JsonSerializable) {
            return $data;
        }

        return $this->getSerializer()->serialize($data);
    }

    /**
     * Get mapper.
     *
     * @return null|Mapper
     */
    public function getMapper()
    {
        return $this->mapper;
    }

    /**
     * Create new JsonResponse from exception.
     *
     * @param \Exception       $exception
     * @param null|JsonRequest $jsonRequest
     *
     * @return JsonResponse
     */
    public function createJsonResponseFromException(\Exception $exception, JsonRequest $jsonRequest = null)
    {
        $jsonResponse = new JsonResponse();

        if ($exception instanceof Exceptions\ErrorException) {
            $jsonResponse->setErrorCode(0 !== $exception->getCode() ? $exception->getCode() : -32603);
            $jsonResponse->setErrorMessage(!empty($exception->getMessage()) ? $exception->getMessage() : 'Internal error');
            $jsonResponse->setErrorData($exception->getData());
        } else {
            $jsonResponse->setErrorCode(0 !== $exception->getCode() ? $exception->getCode() : -32603);
            $jsonResponse->setErrorMessage(!empty($exception->getMessage()) ? $exception->getMessage() : 'Internal error');
        }

        if ($jsonRequest) {
            $jsonResponse->setId($jsonRequest->getId());
        }

        return $jsonResponse;
    }

    /**
     * Handle json request.
     *
     * @param JsonRequest|JsonRequest[] $jsonRequest
     *
     * @return JsonResponse|JsonResponse[]
     */
    public function handleJsonRequest($jsonRequest)
    {
        // Batch requests
        if (\is_array($jsonRequest)) {
            $jsonResponse = [];
            foreach ($jsonRequest as $request) {
                $jsonResponse[] = $this->handleJsonRequest($request);
            }

            return $jsonResponse;
        }

        if ($this->stopwatch) {
            $this->stopwatch->start('rpc.execute');
        }

        try {
            $this->dispatch(Event\JsonRequestEvent::EVENT, new Event\JsonRequestEvent($jsonRequest));

            $metadata = $this->getMethod($jsonRequest);
            $isCache = $this->isCacheSupport($jsonRequest);
            $cacheId = $jsonRequest->getHash();

            $jsonResponse = new JsonResponse($jsonRequest);
            // Cache
            if (true === $isCache && true === $this->getCache()->contains($cacheId)) {
                $jsonResponse->setResult($this->getCache()->fetch($cacheId));
                $isCache = false; // we don't want warm check without left ttl
            }

            $result = $jsonResponse->getResult();
            if (null === $result) { // if not cache
                $result = $this->executeJsonRequest($metadata, $jsonRequest);
            }

            if ($result instanceof JsonResponse) {
                $jsonResponse = $result;
                $jsonResponse->setRequest($jsonRequest);
            } else {
                $jsonResponse->setResult($this->serialize($result));
            }

            // Save cache
            $isCache && $this->cache->save($cacheId, $jsonResponse->getResult(), $metadata->getCache());

            $this->dispatch(Event\JsonResponseEvent::EVENT, new Event\JsonResponseEvent($jsonResponse));
        } catch (\Exception $exception) {
            $jsonResponse = $this->createJsonResponseFromException($exception, $jsonRequest);
        }

        if ($this->stopwatch) {
            $this->stopwatch->stop('rpc.execute');
        }

        return $jsonResponse;
    }

    /**
     * Check is cache support for JsonRequest.
     *
     * @param JsonRequest $jsonRequest
     *
     * @return bool
     */
    private function isCacheSupport(JsonRequest $jsonRequest)
    {
        try {
            return $jsonRequest->getId()
                && null !== $this->getMethod($jsonRequest)->getCache()
                && !$this->isDebug()
                && $this->getCache();
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * @param JsonRequest $jsonRequest
     *
     * @return MethodMetaData
     */
    private function getMethod($jsonRequest): MethodMetaData
    {
        $method = $jsonRequest->getMethod();

        if (false === $this->mapper->hasMethod($method)) {
            throw new Exceptions\MethodNotFoundException($jsonRequest->getMethod());
        }

        return $this->mapper->getMethod($method);
    }

    /**
     * @param MethodMetaData $methodMetaData
     * @param JsonRequest    $jsonRequest
     *
     * @return mixed
     */
    private function executeJsonRequest(MethodMetaData $methodMetaData, JsonRequest $jsonRequest)
    {
        $method = clone $this->container->get($methodMetaData->getMethod());

        // Inject container
        if ($method instanceof ContainerAwareInterface && null !== $this->container) {
            $method->setContainer($this->container);
        }

        // Dispatch execute json
        $this->dispatch(
            Event\JsonExecuteEvent::EVENT,
            new Event\JsonExecuteEvent($method, $methodMetaData, $jsonRequest)
        );

        return $method->{$methodMetaData->getExecute()}();
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Timiki\Bundle\RpcServerBundle\DependencyInjection\Compiler\RpcMethodPass;

class RpcServerBundle extends Bundle
{
    /**
     * {@inheritdoc}
     */
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(
            new RegisterListenersPass(
                'rpc.server.event_dispatcher',
                'rpc.server.event_listener',
                'rpc.server.event_subscriber'
            )
        );
        // Compile prc methods
        $container->addCompilerPass(new RpcMethodPass());
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Timiki\Bundle\RpcServerBundle\Handler\HttpHandler;
use Timiki\Bundle\RpcServerBundle\Handler\JsonHandler;
use Timiki\Bundle\RpcServerBundle\Mapper\Mapper;
use Timiki\Bundle\RpcServerBundle\Mapper\MapperInterface;
use Timiki\Bundle\RpcServerBundle\Mapper\MethodInterface;
use Timiki\Bundle\RpcServerBundle\Registry\HttpHandlerRegistry;
use Timiki\Bundle\RpcServerBundle\Serializer\BaseSerializer;

/**
 * This is the class that loads and manages your bundle configuration.
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class RpcServerExtension extends Extension
{
    private $loadedMethodPath = [];

    /**
     * {@inheritdoc}
     *
     * @throws \Exception
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);
        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');

        if (false === isset($config['mapping'])) {
            throw new \RuntimeException(__CLASS__.': mapping can\'t be empty');
        }

        $errorCode = empty($config['error_code']) ? 200 : $config['error_code'];

        /**
         * Cache.
         */
        $cacheId = empty($config['cache']) ? 'rpc.server.cache' : $config['cache'];

        if (!$container->hasDefinition($cacheId)) {
            $cacheDefinition = new Definition(
                'Doctrine\Common\Cache\FilesystemCache',
                ['%kernel.cache_dir%/rpc', '']
            );

            $cacheDefinition->setPublic(true);
            $container->setDefinition($cacheId, $cacheDefinition);
        }

        /**
         * Serializer.
         */
        $serializerId = empty($config['serializer']) ? 'rpc.server.serializer' : $config['serializer'];

        if (!$container->hasDefinition($serializerId)) {
            $serializerDefinition = new Definition(
                BaseSerializer::class,
                [new Reference('serializer', ContainerInterface::NULL_ON_INVALID_REFERENCE)]
            );

            $serializerDefinition->setPublic(true);
            $container->setDefinition($serializerId, $serializerDefinition);
        }

        /**
         * Registry.
         */
        $registry = new Definition(HttpHandlerRegistry::class);

        /**
         * Mapping.
         *
         * @param string       $name
         * @param array|string $paths
         */
        $createMapping = function ($name, $paths) use ($cacheId, $serializerId, $container, $errorCode, $registry) {
            $name = empty($name) ? 'default' : $name;
            $mapperId = 'rpc.server.mapper.'.$name;

            $this->prepareMethods($mapperId, $paths, $container);

            $mapper = new Definition(Mapper::class);

            $mapper
                ->setPublic(true)
                ->addTag(MapperInterface::class); // Tag it for access from another place

            // Json Handler
            $jsonHandlerId = empty($name) ? 'rpc.server.json_handler' : 'rpc.server.json_handler.'.$name;
            $jsonHandler = new Definition(JsonHandler::class, [new Reference($mapperId), new Reference($serializerId)]);

            $jsonHandler->setPublic(true);
            $jsonHandler->addMethodCall(
                'setEventDispatcher',
                [new Reference('rpc.server.event_dispatcher', ContainerInterface::NULL_ON_INVALID_REFERENCE)]
            );

            $jsonHandler->addMethodCall(
                'setCache',
                [new Reference($cacheId, ContainerInterface::NULL_ON_INVALID_REFERENCE)]
            );

            $jsonHandler->addMethodCall(
                'setContainer',
                [new Reference('service_container')]
            );

            // Http handler
            $httpHandlerId = empty($name) ? 'rpc.server.http_handler' : 'rpc.server.http_handler.'.$name;
            $httpHandler = new Definition(HttpHandler::class, [new Reference($jsonHandlerId), $errorCode]);

            $httpHandler->setPublic(true);
            $httpHandler->addMethodCall(
                'setEventDispatcher',
                [new Reference('rpc.server.event_dispatcher', ContainerInterface::NULL_ON_INVALID_REFERENCE)]
            );

            $httpHandler->addMethodCall(
                'setProfiler',
                [new Reference('profiler', ContainerInterface::NULL_ON_INVALID_REFERENCE)]
            );

            // Set definitions
            $container->setDefinition($mapperId, $mapper);
            $container->setDefinition($jsonHandlerId, $jsonHandler);
            $container->setDefinition($httpHandlerId, $httpHandler);

            // Add http handler to registry
            $registry->addMethodCall('add', [$name, new Reference($httpHandlerId)]);
        };

        $mapping = $config['mapping'];

        foreach ((array) $mapping as $key => $value) {
            if (\is_numeric($key)) {
                $createMapping(null, $value);
            } elseif (\is_string($key) && !empty($value)) {
                $createMapping($key, (array) $value);
            }
        }

        $container->setDefinition(HttpHandlerRegistry::class, $registry);
    }

    private function prepareMethods($mapperId, $paths, ContainerBuilder $container)
    {
        if (true === \is_string($paths)) {
            $paths = [$paths];
        }

        foreach ($paths as $path) {
            $path = \realpath($path);

            if (false === \is_dir($path)) {
                continue;
            }

            // Add method path to resource
            $container->addResource(new GlobResource($path, '/*', true));

            $this->loadMethods($path, $container);

            if (false === isset($this->loadedMethodPath[$path]['classes'])) {
                continue;
            }

            foreach ($this->loadedMethodPath[$path]['classes'] as $class) {
                if (true === $container->hasDefinition($class)) {
                    $container->getDefinition($class)
                        ->addTag(
                            MethodInterface::class,
                            [
                                'mapperId' => $mapperId,
                            ]
                        );
                    continue;
                }

                $container->register($class, $class)
                    ->setAutowired(true)
                    ->setAutoconfigured(true)
                    ->setPublic(true)
                    ->addTag(
                        MethodInterface::class,
                        [
                            'mapperId' => $mapperId,
                        ]
                    );
            }
        }
    }

    private function loadMethods($path, ContainerBuilder $container)
    {
        if (true === isset($this->loadedMethodPath[$path])) {
            return; // Already loaded
        }

        $dir = new \DirectoryIterator($path);
        $loaderPhp = new Loader\PhpFileLoader($container, new FileLocator($path));

        foreach ($dir as $file) {
            if ($file->isFile()) {
                $classesBefore = \get_declared_classes();

                $loaderPhp->load($file->getFilename());

                $classesAfter = \get_declared_classes();

                $diff = \array_diff($classesAfter, $classesBefore);

                // Mark as loaded.
                $classes = $this->loadedMethodPath[$path]['classes'] ?? [];
                // merge all classes to parent
                $this->loadedMethodPath[$path]['classes'] = \array_merge($classes, $diff ?? []);
            }

            if ($file->isDir() && !$file->isDot()) {
                $classesBefore = \get_declared_classes();

                $this->loadMethods($file->getPathname(), $container);

                $classesAfter = \get_declared_classes();

                $diff = \array_diff($classesAfter, $classesBefore);

                $classes = $this->loadedMethodPath[$path]['classes'] ?? [];
                // merge all classes to parent
                $this->loadedMethodPath[$path]['classes'] = \array_merge($classes, $diff ?? []);
            }
        }
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle\Registry;

use Timiki\Bundle\RpcServerBundle\Handler\HttpHandler;

class HttpHandlerRegistry
{
    /**
     * Http handlers.
     *
     * @var HttpHandler[]
     */
    private $httpHandlers = [];

    /**
     * @param string      $name
     * @param HttpHandler $httpHandler
     */
    public function add($name, HttpHandler $httpHandler)
    {
        $this->httpHandlers[$name] = $httpHandler;
    }

    /**
     * Ger http handler by name.
     *
     * @param string $name
     *
     * @throws \Exception
     *
     * @return HttpHandler
     */
    public function get($name)
    {
        if (!isset($this->httpHandlers[$name])) {
            throw new \Exception("HttpHandler {$name} not found");
        }

        return $this->httpHandlers[$name];
    }

    /**
     * Is http handler exist.
     *
     * @param string $name
     *
     * @return bool
     */
    public function has($name)
    {
        return isset($this->httpHandlers[$name]);
    }
}


<?php

namespace Timiki\Bundle\RpcServerBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class GenerateMethodCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('generate:method')
            ->setDescription('Generates a new RPC method')
            ->addArgument('name', InputArgument::OPTIONAL, 'The method name')
            ->setHelp(
                <<<EOT
The <info>generate:method</info> command helps you generate new RPC method
inside bundles. Provide the bundle name as the first argument and the method
name as the second argument:

<info>php bin/console generate:method</info>
EOT
            );
    }

    public function interact(InputInterface $input, OutputInterface $output)
    {
        $io = new SymfonyStyle($input, $output);
        $bundle = $input->getArgument('bundle');
        $name = $input->getArgument('name');
        $container = $this->getContainer();

        if (null !== $bundle && null !== $name) {
            return;
        }

        $io->title('Generate new RPC method');

        // Method name

        $name = $io->ask(
            'Method name',
            null,
            function ($answer) use ($container, $bundle) {
                if (empty($answer)) {
                    throw new \RuntimeException('Method name can`t be empty.');
                }

                $answer = \str_replace(' ', ':', $answer);

                if ($this->isMethodExist($container->get('kernel')->getBundle($bundle))) {
                    throw new \RuntimeException(
                        \sprintf(
                            'Method "%s" already exist.',
                            $answer
                        )
                    );
                }

                return $answer;
            }
        );

        $input->setArgument('name', $name);
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $io = new SymfonyStyle($input, $output);
        $name = \str_replace(' ', ':', $input->getArgument('name'));

        if ($this->isMethodExist($name)) {
            throw new \RuntimeException(
                \sprintf(
                    'Method "%s" already exist.',
                    $name
                )
            );
        }

        $methodClassName = $this->classify($name);
        $methodFile = $this->getMethodPath($name);

        if (\file_exists($methodFile)) {
            throw new \RuntimeException(
                \sprintf(
                    'Method "%s" already exists',
                    $name
                )
            );
        }

        $parameters = [
            'class' => $methodClassName,
            'name' => $name,
        ];

        $this->renderFile('Method.php.twig', $methodFile, $parameters);

        $io->success(
            \sprintf(
                'Method "%s" was generate in file "%s".',
                $name,
                $methodFile
            )
        );
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    protected function isMethodExist($name)
    {
        return \file_exists($this->getMethodPath($name));
    }

    /**
     * @param string $name
     *
     * @return string
     */
    protected function getMethodPath($name)
    {
        $methodDir = $this->getContainer()->getParameter('kernel.root_dir').'/Method';

        return $methodDir.'/'.$this->classify($name).'.php';
    }

    /**
     * @param string $string
     *
     * @return string
     */
    protected function classify($string)
    {
        return \str_replace(' ', '', \ucwords(\strtr($string, '_-:', '   '))).'Method';
    }

    /**
     * @param string $template
     * @param array  $parameters
     *
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     *
     * @return string
     */
    protected function render($template, $parameters)
    {
        $twig = $this->getTwigEnvironment();

        return $twig->render($template, $parameters);
    }

    /**
     * @return \Twig_Environment
     */
    protected function getTwigEnvironment()
    {
        return new \Twig_Environment(
            new \Twig_Loader_Filesystem(
                [
                    \dirname(__DIR__).'/Resources/skeleton',
                ]
            ),
            [
                'debug' => true,
                'cache' => false,
                'strict_variables' => true,
                'autoescape' => false,
            ]
        );
    }

    /**
     * @param string $template
     * @param string $target
     * @param array  $parameters
     *
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     *
     * @return int
     */
    protected function renderFile($template, $target, $parameters)
    {
        if (!\is_dir(\dirname($target))) {
            \mkdir(\dirname($target), 0777, true);
        }

        return \file_put_contents($target, $this->render($template, $parameters));
    }
}


<?php

namespace Spatie\MigrateFresh\TableDroppers;

use Illuminate\Support\Facades\DB;

class Pgsql implements TableDropper
{
    public function dropAllTables()
    {
        $tableNames = $this->getTableNames();

        if ($tableNames->isEmpty()) {
            return;
        }

        DB::statement("DROP TABLE {$tableNames->implode(',')} CASCADE");
    }

    /**
     * Get a list of all tables in the schema.
     *
     * @return \Illuminate\Support\Collection
     */
    protected function getTableNames()
    {
        $schemas = DB::getConfig('used_schemas') ?: [DB::getConfig('schema')];

        $schemaCount = count($schemas);

        $binds = implode(',', array_fill(0, $schemaCount, '?'));

        return collect(
            DB::select("SELECT schemaname || '.' || tablename AS table FROM pg_catalog.pg_tables WHERE schemaname IN (".$binds.')', $schemas)
        )->pluck('table')->reject(function ($value, $key) {
            $tableName = explode('.', $value)[1];

            return $tableName === 'spatial_ref_sys';
        });
    }
}


<?php

namespace Spatie\MigrateFresh\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\ConfirmableTrait;
use Spatie\MigrateFresh\TableDropperFactory;
use Spatie\MigrateFresh\Events\DroppedTables;
use Spatie\MigrateFresh\Events\DroppingTables;
use Spatie\MigrateFresh\TableDroppers\TableDropper;

class MigrateFresh extends Command
{
    use ConfirmableTrait;

    /**
     * The console command name.
     *
     * @var string
     */
    protected $signature = 'migrate:fresh {--database= : The database connection to use.}
                {--force : Force the operation to run when in production.}
                {--path= : The path of migrations files to be executed.}
                {--seed : Indicates if the seed task should be re-run.}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Drop all database tables and re-run all migrations';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $database = $this->input->getOption('database');

        $path = $this->input->getOption('path');

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

        $this->info('Dropping all tables...');

        event(new DroppingTables());
        if ($database !== null) {
            DB::setDefaultConnection($database);
        }
        $this->getTableDropper()->dropAllTables();
        event(new DroppedTables());

        $this->info('Running migrations...');
        $this->call('migrate', [
            '--database' => $database,
            '--path' => $path,
            '--force' => true,
        ]);

        if ($this->option('seed')) {
            $this->info('Running seeders...');
            $this->call('db:seed', ['--force' => true]);
        }

        $this->comment('All done!');
    }

    public function getTableDropper(): TableDropper
    {
        return TableDropperFactory::create(DB::getDriverName());
    }
}