<?php

namespace Zenaton\Query;

use Zenaton\Client;
use Zenaton\Exceptions\ExternalZenatonException;
use Zenaton\Interfaces\EventInterface;
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Traits\IsImplementationOfTrait;

class Builder
{
    use IsImplementationOfTrait;

    /**
     * The Zenaton client.
     *
     * @var \Zenaton\Client
     */
    protected $client;

    /**
     * The queried class.
     *
     * @var string
     */
    protected $class;

    /**
     * The instance custom id.
     *
     * @var mixed
     */
    protected $id;

    /**
     * Create a new query builder.
     *
     * @param string $class
     *
     * @return self
     */
    public function __construct($class)
    {
        $this->checkIsWorkflow($class);

        $this->class = $class;
        $this->client = Client::getInstance();
    }

    /**
     * Create a new pending job dispatch.
     *
     * @return self
     */
    public function whereId($id)
    {
        $this->id = $id;

        return $this;
    }

    /**
     * Retrieve an instance.
     *
     * @return Zenaton\Interfaces\WorkflowInterface
     */
    public function find()
    {
        return $this->client->findWorkflow($this->class, $this->id);
    }

    /**
     * Send an event to a workflow instance.
     *
     * @return self
     */
    public function send(EventInterface $event)
    {
        $this->client->sendEvent($this->class, $this->id, $event);

        return $this;
    }

    /**
     * Kill a workflow instance.
     */
    public function kill()
    {
        $this->client->killWorkflow($this->class, $this->id);

        return $this;
    }

    /**
     * Pause a workflow instance.
     */
    public function pause()
    {
        $this->client->pauseWorkflow($this->class, $this->id);

        return $this;
    }

    /**
     * Resume a workflow instance.
     */
    public function resume()
    {
        $this->client->resumeWorkflow($this->class, $this->id);

        return $this;
    }

    protected function checkIsWorkflow($class)
    {
        if (!$this->isImplementationOf($class, WorkflowInterface::class)) {
            throw new ExternalZenatonException($class.' should implements '.WorkflowInterface::class);
        }
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
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;

/**
 * This is the class that loads and manages your bundle configuration.
 */
class SonataSeoExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);
        $config = $this->fixConfiguration($config);

        $bundles = $container->getParameter('kernel.bundles');

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

        if (isset($bundles['SonataBlockBundle'], $bundles['KnpMenuBundle'])) {
            $loader->load('blocks.xml');
        }

        $loader->load('event.xml');
        $loader->load('services.xml');
        $loader->load('commands.xml');

        $this->configureSeoPage($config['page'], $container);
        $this->configureSitemap($config['sitemap'], $container);

        $container->getDefinition('sonata.seo.twig.extension')
            ->replaceArgument(1, $config['encoding']);
    }

    /**
     * Configure the default seo page.
     *
     * @param array            $config
     * @param ContainerBuilder $container
     */
    protected function configureSeoPage(array $config, ContainerBuilder $container)
    {
        $container->setParameter('sonata.seo.config', $config);
    }

    /**
     * Configure the sitemap source manager.
     *
     * @param array            $config
     * @param ContainerBuilder $container
     */
    protected function configureSitemap(array $config, ContainerBuilder $container)
    {
        $source = $container->getDefinition('sonata.seo.sitemap.manager');

        if (method_exists($source, 'setShared')) { // Symfony 2.8+
            $source->setShared(false);
        } else {
            // For Symfony <2.8 compatibility
            $source->setScope(ContainerInterface::SCOPE_PROTOTYPE);
        }

        foreach ($config['doctrine_orm'] as $pos => $sitemap) {
            // define the connectionIterator
            $connectionIteratorId = 'sonata.seo.source.doctrine_connection_iterator_'.$pos;

            $connectionIterator = new Definition('%sonata.seo.exporter.database_source_iterator.class%', [
                new Reference($sitemap['connection']),
                $sitemap['query'],
            ]);

            $connectionIterator->setPublic(false);
            $container->setDefinition($connectionIteratorId, $connectionIterator);

            // define the sitemap proxy iterator
            $sitemapIteratorId = 'sonata.seo.source.doctrine_sitemap_iterator_'.$pos;

            $sitemapIterator = new Definition('%sonata.seo.exporter.sitemap_source_iterator.class%', [
                new Reference($connectionIteratorId),
                new Reference('router'),
                $sitemap['route'],
                $sitemap['parameters'],
            ]);

            $sitemapIterator->setPublic(false);

            $container->setDefinition($sitemapIteratorId, $sitemapIterator);

            $source->addMethodCall('addSource', [$sitemap['group'], new Reference($sitemapIteratorId), $sitemap['types']]);
        }

        foreach ($config['services'] as $service) {
            $source->addMethodCall('addSource', [$service['group'], new Reference($service['id']), $service['types']]);
        }
    }

    /**
     * Fix the sitemap configuration.
     *
     * @param array $config
     *
     * @return array
     */
    protected function fixConfiguration(array $config)
    {
        foreach ($config['sitemap']['doctrine_orm'] as $pos => $sitemap) {
            $sitemap['group'] = $sitemap['group'] ?? false;
            $sitemap['types'] = $sitemap['types'] ?? [];
            $sitemap['connection'] = $sitemap['connection'] ?? 'doctrine.dbal.default_connection';
            $sitemap['route'] = $sitemap['route'] ?? false;
            $sitemap['parameters'] = $sitemap['parameters'] ?? false;
            $sitemap['query'] = $sitemap['query'] ?? false;

            if (false === $sitemap['route']) {
                throw new \RuntimeException('Route cannot be empty, please review the sonata_seo.sitemap configuration');
            }

            if (false === $sitemap['query']) {
                throw new \RuntimeException('Query cannot be empty, please review the sonata_seo.sitemap configuration');
            }

            if (false === $sitemap['parameters']) {
                throw new \RuntimeException('Route\'s parameters cannot be empty, please review the sonata_seo.sitemap configuration');
            }

            $config['sitemap']['doctrine_orm'][$pos] = $sitemap;
        }

        foreach ($config['sitemap']['services'] as $pos => $sitemap) {
            if (!\is_array($sitemap)) {
                $sitemap = [
                    'group' => false,
                    'types' => [],
                    'id' => $sitemap,
                ];
            } else {
                $sitemap['group'] = $sitemap['group'] ?? false;
                $sitemap['types'] = $sitemap['types'] ?? [];

                if (!isset($sitemap['id'])) {
                    throw new \RuntimeException('Service id must to be defined, please review the sonata_seo.sitemap configuration');
                }
            }

            $config['sitemap']['services'][$pos] = $sitemap;
        }

        return $config;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Block\Social;

use Sonata\AdminBundle\Form\FormMapper;
use Sonata\BlockBundle\Model\BlockInterface;
use Sonata\CoreBundle\Form\Type\ImmutableArrayType;
use Sonata\CoreBundle\Model\Metadata;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Facebook send button integration.
 *
 * @see https://developers.facebook.com/docs/plugins/send-button/
 *
 * @author Sylvain Deloux <sylvain.deloux@ekino.com>
 */
class FacebookSendButtonBlockService extends BaseFacebookSocialPluginsBlockService
{
    /**
     * {@inheritdoc}
     */
    public function configureSettings(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'template' => '@SonataSeo/Block/block_facebook_send_button.html.twig',
            'url' => null,
            'width' => null,
            'height' => null,
            'colorscheme' => $this->colorschemeList['light'],
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function buildEditForm(FormMapper $formMapper, BlockInterface $block)
    {
        $formMapper->add('settings', ImmutableArrayType::class, [
            'keys' => [
                ['url', UrlType::class, [
                    'required' => false,
                    'label' => 'form.label_url',
                ]],
                ['width', IntegerType::class, [
                    'required' => false,
                    'label' => 'form.label_width',
                ]],
                ['height', IntegerType::class, [
                    'required' => false,
                    'label' => 'form.label_height',
                ]],
                ['colorscheme', ChoiceType::class, [
                    'required' => true,
                    'choices' => $this->colorschemeList,
                    'label' => 'form.label_colorscheme',
                ]],
            ],
            'translation_domain' => 'SonataSeoBundle',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockMetadata($code = null)
    {
        return new Metadata($this->getName(), (null !== $code ? $code : $this->getName()), false, 'SonataSeoBundle', [
            'class' => 'fa fa-facebook-official',
        ]);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Sitemap;

use Sonata\Exporter\Source\ChainSourceIterator;
use Sonata\Exporter\Source\SourceIteratorInterface;

/**
 * Manager several chain source iterator grouped.
 */
class SourceManager implements SourceIteratorInterface
{
    /**
     * @var \ArrayIterator
     */
    protected $sources;

    public function __construct()
    {
        $this->sources = new \ArrayIterator();
    }

    /**
     * Adding source with his group.
     *
     * @param string                  $group
     * @param SourceIteratorInterface $source
     * @param array                   $types
     */
    public function addSource($group, SourceIteratorInterface $source, array $types = [])
    {
        if (!isset($this->sources[$group])) {
            $this->sources[$group] = new \stdClass();

            $this->sources[$group]->sources = new ChainSourceIterator();
            $this->sources[$group]->types = [];
        }

        $this->sources[$group]->sources->addSource($source);

        if ($types) {
            $this->sources[$group]->types += array_diff($types, $this->sources[$group]->types);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function current()
    {
        return $this->sources->current();
    }

    /**
     * {@inheritdoc}
     */
    public function next()
    {
        $this->sources->next();
    }

    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->sources->key();
    }

    /**
     * {@inheritdoc}
     */
    public function valid()
    {
        return $this->sources->valid();
    }

    /**
     * {@inheritdoc}
     */
    public function rewind()
    {
        $this->sources->rewind();
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Block\Social;

use Sonata\AdminBundle\Form\FormMapper;
use Sonata\BlockBundle\Model\BlockInterface;
use Sonata\CoreBundle\Form\Type\ImmutableArrayType;
use Sonata\CoreBundle\Model\Metadata;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Facebook like button integration.
 *
 * @see https://developers.facebook.com/docs/plugins/like-button/
 *
 * @author Sylvain Deloux <sylvain.deloux@ekino.com>
 */
class FacebookLikeButtonBlockService extends BaseFacebookSocialPluginsBlockService
{
    /**
     * @var string[]
     */
    protected $layoutList = [
        'standard' => 'form.label_layout_standard',
        'box_count' => 'form.label_layout_box_count',
        'button_count' => 'form.label_layout_button_count',
        'button' => 'form.label_layout_button',
    ];

    /**
     * @var string[]
     */
    protected $actionTypes = [
        'like' => 'form.label_action_like',
        'recommend' => 'form.label_action_recommend',
    ];

    /**
     * {@inheritdoc}
     */
    public function configureSettings(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'template' => '@SonataSeo/Block/block_facebook_like_button.html.twig',
            'url' => null,
            'width' => null,
            'show_faces' => true,
            'share' => true,
            'layout' => $this->layoutList['standard'],
            'colorscheme' => $this->colorschemeList['light'],
            'action' => $this->actionTypes['like'],
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function buildEditForm(FormMapper $formMapper, BlockInterface $block)
    {
        $formMapper->add('settings', ImmutableArrayType::class, [
            'keys' => [
                ['url', UrlType::class, [
                    'required' => false,
                    'label' => 'form.label_url',
                ]],
                ['width', IntegerType::class, [
                    'required' => false,
                    'label' => 'form.label_width',
                ]],
                ['show_faces', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_show_faces',
                ]],
                ['share', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_share',
                ]],
                ['layout', ChoiceType::class, [
                    'required' => true,
                    'choices' => $this->layoutList,
                    'label' => 'form.label_layout',
                ]],
                ['colorscheme', ChoiceType::class, [
                    'required' => true,
                    'choices' => $this->colorschemeList,
                    'label' => 'form.label_colorscheme',
                ]],
                ['action', ChoiceType::class, [
                    'required' => true,
                    'choices' => $this->actionTypes,
                    'label' => 'form.label_action',
                ]],
            ],
            'translation_domain' => 'SonataSeoBundle',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockMetadata($code = null)
    {
        return new Metadata($this->getName(), (null !== $code ? $code : $this->getName()), false, 'SonataSeoBundle', [
            'class' => 'fa fa-facebook-official',
        ]);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Event;

use Sonata\BlockBundle\Block\BlockServiceInterface;
use Sonata\BlockBundle\Event\BlockEvent;
use Sonata\BlockBundle\Model\Block;

/**
 * BreadcrumbListener for Block Event.
 *
 * @author Sylvain Deloux <sylvain.deloux@ekino.com>
 */
class BreadcrumbListener
{
    /**
     * @var array
     */
    protected $blockServices = [];

    /**
     * Add a renderer to the status services list.
     *
     * @param string                $type
     * @param BlockServiceInterface $blockService
     */
    public function addBlockService($type, BlockServiceInterface $blockService)
    {
        $this->blockServices[$type] = $blockService;
    }

    /**
     * Add context related BlockService, if found.
     *
     * @param BlockEvent $event
     */
    public function onBlock(BlockEvent $event)
    {
        $context = $event->getSetting('context', null);

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

        foreach ($this->blockServices as $type => $blockService) {
            if ($blockService->handleContext($context)) {
                $block = new Block();
                $block->setId(uniqid());
                $block->setSettings($event->getSettings());
                $block->setType($type);

                $event->addBlock($block);

                return;
            }
        }
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Block\Breadcrumb;

use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Knp\Menu\Provider\MenuProviderInterface;
use Sonata\BlockBundle\Block\BlockContextInterface;
use Sonata\BlockBundle\Block\Service\MenuBlockService;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Abstract class for breadcrumb menu services.
 *
 * @author Sylvain Deloux <sylvain.deloux@ekino.com>
 */
abstract class BaseBreadcrumbMenuBlockService extends MenuBlockService
{
    /**
     * @var string
     */
    private $context;

    /**
     * @var FactoryInterface
     */
    private $factory;

    /**
     * @param string                $context
     * @param string                $name
     * @param EngineInterface       $templating
     * @param MenuProviderInterface $menuProvider
     * @param FactoryInterface      $factory
     */
    public function __construct($context, $name, EngineInterface $templating, MenuProviderInterface $menuProvider, FactoryInterface $factory)
    {
        parent::__construct($name, $templating, $menuProvider);

        $this->context = $context;
        $this->factory = $factory;
    }

    /**
     * Return true if current BlockService handles the given context.
     *
     * @param string $context
     *
     * @return bool
     */
    public function handleContext($context)
    {
        return $this->context === $context;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return sprintf('Breadcrumb %s', $this->context);
    }

    /**
     * {@inheritdoc}
     */
    public function configureSettings(OptionsResolver $resolver)
    {
        parent::configureSettings($resolver);

        $resolver->setDefaults([
            'menu_template' => '@SonataSeo/Block/breadcrumb.html.twig',
            'include_homepage_link' => true,
            'context' => false,
        ]);
    }

    /**
     * @return FactoryInterface
     */
    protected function getFactory()
    {
        return $this->factory;
    }

    /**
     * @return string
     */
    protected function getContext()
    {
        return $this->context;
    }

    /**
     * Initialize breadcrumb menu.
     *
     * @param BlockContextInterface $blockContext
     *
     * @return ItemInterface
     */
    protected function getRootMenu(BlockContextInterface $blockContext)
    {
        $settings = $blockContext->getSettings();
        /*
         * @todo : Use the router to get the homepage URI
         */

        $menu = $this->factory->createItem('breadcrumb');

        $menu->setChildrenAttribute('class', 'breadcrumb');

        if (method_exists($menu, 'setCurrentUri')) {
            $menu->setCurrentUri($settings['current_uri']);
        }

        if (method_exists($menu, 'setCurrent')) {
            $menu->setCurrent($settings['current_uri']);
        }

        if ($settings['include_homepage_link']) {
            $menu->addChild('sonata_seo_homepage_breadcrumb', ['uri' => '/']);
        }

        return $menu;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Seo;

/**
 * http://en.wikipedia.org/wiki/Meta_element.
 */
class SeoPage implements SeoPageInterface
{
    /**
     * @var string
     */
    protected $title;

    /**
     * @var array
     */
    protected $metas;

    /**
     * @var array
     */
    protected $htmlAttributes;

    /**
     * @var string
     */
    protected $linkCanonical;

    /**
     * @var string
     */
    protected $separator;

    /**
     * @var array
     */
    protected $headAttributes;

    /**
     * @var array
     */
    protected $langAlternates;

    /**
     * @var array
     */
    protected $oembedLinks;

    /**
     * @param string $title
     */
    public function __construct($title = '')
    {
        $this->title = $title;
        $this->metas = [
            'http-equiv' => [],
            'name' => [],
            'schema' => [],
            'charset' => [],
            'property' => [],
        ];

        $this->htmlAttributes = [];
        $this->headAttributes = [];
        $this->linkCanonical = '';
        $this->separator = ' ';
        $this->langAlternates = [];
        $this->oembedLinks = [];
    }

    /**
     * {@inheritdoc}
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function addTitle($title)
    {
        $this->title = $title.$this->separator.$this->title;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * {@inheritdoc}
     */
    public function getMetas()
    {
        return $this->metas;
    }

    /**
     * {@inheritdoc}
     */
    public function addMeta($type, $name, /* string */ $content, array $extras = [])
    {
        if (!\is_string($content)) {
            @trigger_error(sprintf(
                'Passing meta content of type %s in %s is deprecated since version 2.x and will be unsupported in version 3. Please cast the value to a string first.',
                 \gettype($content),
                  __METHOD__
              ), E_USER_DEPRECATED);
        }

        if (!isset($this->metas[$type])) {
            $this->metas[$type] = [];
        }

        $this->metas[$type][$name] = [$content, $extras];

        return $this;
    }

    /**
     * @param string $type
     * @param string $name
     *
     * @return bool
     */
    public function hasMeta($type, $name)
    {
        return isset($this->metas[$type][$name]);
    }

    /**
     * @param string $type
     * @param string $name
     *
     * @return $this
     */
    public function removeMeta($type, $name)
    {
        unset($this->metas[$type][$name]);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function setMetas(array $metadatas)
    {
        $this->metas = [];

        foreach ($metadatas as $type => $metas) {
            if (!\is_array($metas)) {
                throw new \RuntimeException('$metas must be an array');
            }

            foreach ($metas as $name => $meta) {
                list($content, $extras) = $this->normalize($meta);

                $this->addMeta($type, $name, $content, $extras);
            }
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function setHtmlAttributes(array $attributes)
    {
        $this->htmlAttributes = $attributes;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function addHtmlAttributes($name, $value)
    {
        $this->htmlAttributes[$name] = $value;

        return $this;
    }

    /**
     * @param string $name
     *
     * @return $this
     */
    public function removeHtmlAttributes($name)
    {
        unset($this->htmlAttributes[$name]);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getHtmlAttributes()
    {
        return $this->htmlAttributes;
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasHtmlAttribute($name)
    {
        return isset($this->htmlAttributes[$name]);
    }

    /**
     * @param array $attributes
     *
     * @return SeoPageInterface
     */
    public function setHeadAttributes(array $attributes)
    {
        $this->headAttributes = $attributes;

        return $this;
    }

    /**
     * @param string $name
     * @param string $value
     *
     * @return SeoPageInterface
     */
    public function addHeadAttribute($name, $value)
    {
        $this->headAttributes[$name] = $value;

        return $this;
    }

    /**
     * @param string $name
     *
     * @return $this
     */
    public function removeHeadAttribute($name)
    {
        unset($this->headAttributes[$name]);

        return $this;
    }

    /**
     * @return array
     */
    public function getHeadAttributes()
    {
        return $this->headAttributes;
    }

    /**
     * @param string $name
     *
     * @return array
     */
    public function hasHeadAttribute($name)
    {
        return isset($this->headAttributes[$name]);
    }

    /**
     * {@inheritdoc}
     */
    public function setLinkCanonical($link)
    {
        $this->linkCanonical = $link;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getLinkCanonical()
    {
        return $this->linkCanonical;
    }

    /**
     * {@inheritdoc}
     */
    public function removeLinkCanonical()
    {
        $this->linkCanonical = '';
    }

    /**
     * {@inheritdoc}
     */
    public function setSeparator($separator)
    {
        $this->separator = $separator;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function setLangAlternates(array $langAlternates)
    {
        $this->langAlternates = $langAlternates;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function addLangAlternate($href, $hrefLang)
    {
        $this->langAlternates[$href] = $hrefLang;

        return $this;
    }

    /**
     * @param string $href
     *
     * @return $this
     */
    public function removeLangAlternate($href)
    {
        unset($this->langAlternates[$href]);

        return $this;
    }

    /**
     * @param string $href
     *
     * @return $this
     */
    public function hasLangAlternate($href)
    {
        return isset($this->langAlternates[$href]);
    }

    /**
     * {@inheritdoc}
     */
    public function getLangAlternates()
    {
        return  $this->langAlternates;
    }

    /**
     * @param $title
     * @param $link
     *
     * @return SeoPageInterface
     */
    public function addOEmbedLink($title, $link)
    {
        $this->oembedLinks[$title] = $link;

        return $this;
    }

    /**
     * @return array
     */
    public function getOEmbedLinks()
    {
        return $this->oembedLinks;
    }

    /**
     * @param mixed $meta
     *
     * @return array
     */
    private function normalize($meta)
    {
        if (\is_string($meta)) {
            return [$meta, []];
        }

        return $meta;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Block\Social;

use Sonata\AdminBundle\Form\FormMapper;
use Sonata\BlockBundle\Model\BlockInterface;
use Sonata\CoreBundle\Form\Type\ImmutableArrayType;
use Sonata\CoreBundle\Model\Metadata;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Facebook like box integration.
 *
 * @see https://developers.facebook.com/docs/plugins/like-box-for-pages/
 *
 * @author Sylvain Deloux <sylvain.deloux@ekino.com>
 */
class FacebookLikeBoxBlockService extends BaseFacebookSocialPluginsBlockService
{
    /**
     * {@inheritdoc}
     */
    public function configureSettings(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'template' => '@SonataSeo/Block/block_facebook_like_box.html.twig',
            'url' => null,
            'width' => null,
            'height' => null,
            'colorscheme' => $this->colorschemeList['light'],
            'show_faces' => true,
            'show_header' => true,
            'show_posts' => false,
            'show_border' => true,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function buildEditForm(FormMapper $formMapper, BlockInterface $block)
    {
        $formMapper->add('settings', ImmutableArrayType::class, [
            'keys' => [
                ['url', UrlType::class, [
                    'required' => false,
                    'label' => 'form.label_url',
                ]],
                ['width', IntegerType::class, [
                    'required' => false,
                    'label' => 'form.label_width',
                ]],
                ['height', IntegerType::class, [
                    'required' => false,
                    'label' => 'form.label_height',
                ]],
                ['colorscheme', ChoiceType::class, [
                    'required' => true,
                    'choices' => $this->colorschemeList,
                    'label' => 'form.label_colorscheme',
                ]],
                ['show_faces', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_show_faces',
                ]],
                ['show_header', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_show_header',
                ]],
                ['show_posts', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_show_posts',
                ]],
                ['show_border', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_show_border',
                ]],
            ],
            'translation_domain' => 'SonataSeoBundle',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockMetadata($code = null)
    {
        return new Metadata($this->getName(), (null !== $code ? $code : $this->getName()), false, 'SonataSeoBundle', [
            'class' => 'fa fa-facebook-official',
        ]);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Command;

use Sonata\Exporter\Handler;
use Sonata\Exporter\Writer\SitemapWriter;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

/**
 * Create a sitemap.
 *
 * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
 */
class SitemapGeneratorCommand extends ContainerAwareCommand
{
    /**
     * {@inheritdoc}
     */
    public function configure()
    {
        $this->setName('sonata:seo:sitemap');

        $this->addArgument('folder', InputArgument::REQUIRED, 'The folder to store the sitemap.xml file');
        $this->addArgument('host', InputArgument::REQUIRED, 'Set the host');
        $this->addOption('scheme', null, InputOption::VALUE_OPTIONAL, 'Set the scheme', 'http');
        $this->addOption('baseurl', null, InputOption::VALUE_OPTIONAL, 'Set the base url', '');
        $this->addOption('sitemap_path', null, InputOption::VALUE_OPTIONAL, 'Set the sitemap relative path (if in a specific folder)', '');

        $this->setDescription('Create a sitemap');
        $this->setHelp(<<<'EOT'
The <info>sonata:seo:sitemap</info> command create new sitemap files (index + sitemap).

EOT
        );
    }

    /**
     * {@inheritdoc}
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        $this->getContainer()->get('router')->getContext()->setHost($input->getArgument('host'));
        $this->getContainer()->get('router')->getContext()->setScheme($input->getOption('scheme'));
        $this->getContainer()->get('router')->getContext()->setBaseUrl($input->getOption('baseurl'));

        $tempFolder = sys_get_temp_dir().'/sonata_sitemap_'.md5(__DIR__);

        $fs = new Filesystem();

        // step 1
        $output->writeln(sprintf('Creating temporary folder: %s', $tempFolder));

        if ($fs->exists($tempFolder)) {
            $output->writeln('<error>The temporary folder already exists</error>');
            $output->writeln('<error>If the task is not running please delete this folder</error>');

            return 1;
        }

        $fs->mkdir($tempFolder);

        // step 2.
        $manager = $this->getContainer()->get('sonata.seo.sitemap.manager');

        // step 3
        $output->writeln(sprintf('Generating sitemap - this can take a while'));
        foreach ($manager as $group => $sitemap) {
            $write = new SitemapWriter($tempFolder, $group, $sitemap->types, false);

            try {
                Handler::create($sitemap->sources, $write)->export();
            } catch (\Exception $e) {
                $fs->remove($tempFolder);

                throw $e;
            }
        }

        // generate global sitemap index
        $appendPath = $input->hasOption('sitemap_path') ? $input->getOption('sitemap_path') : $input->getOption('baseurl');
        SitemapWriter::generateSitemapIndex($tempFolder, sprintf('%s://%s%s', $input->getOption('scheme'), $input->getArgument('host'), $appendPath), 'sitemap*.xml', 'sitemap.xml');

        // step 4
        $output->writeln(sprintf('Moving temporary file to %s ...', $input->getArgument('folder')));

        $oldFiles = Finder::create()->files()->name('sitemap*.xml')->in($input->getArgument('folder'));
        foreach ($oldFiles as $file) {
            $fs->remove($file->getRealPath());
        }

        $newFiles = Finder::create()->files()->name('sitemap*.xml')->in($tempFolder);
        foreach ($newFiles as $file) {
            $fs->rename($file->getRealPath(), sprintf('%s/%s', $input->getArgument('folder'), $file->getFilename()));
        }

        $fs->remove($tempFolder);

        $output->writeln('<info>done!</info>');
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Block\Social;

use Sonata\AdminBundle\Form\FormMapper;
use Sonata\BlockBundle\Model\BlockInterface;
use Sonata\CoreBundle\Form\Type\ImmutableArrayType;
use Sonata\CoreBundle\Model\Metadata;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Twitter mention button integration.
 *
 * @see https://about.twitter.com/resources/buttons#mention
 *
 * @author Sylvain Deloux <sylvain.deloux@ekino.com>
 */
class TwitterMentionButtonBlockService extends BaseTwitterButtonBlockService
{
    /**
     * {@inheritdoc}
     */
    public function configureSettings(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'template' => '@SonataSeo/Block/block_twitter_mention_button.html.twig',
            'user' => null,
            'text' => null,
            'recommend' => null,
            'large_button' => false,
            'opt_out' => false,
            'language' => $this->languageList['en'],
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function buildEditForm(FormMapper $formMapper, BlockInterface $block)
    {
        $formMapper->add('settings', ImmutableArrayType::class, [
            'keys' => [
                ['user', TextType::class, [
                    'required' => true,
                    'label' => 'form.label_user',
                ]],
                ['text', TextType::class, [
                    'required' => false,
                    'label' => 'form.label_text',
                ]],
                ['recommend', TextType::class, [
                    'required' => false,
                    'label' => 'form.label_recommend',
                ]],
                ['large_button', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_large_button',
                ]],
                ['opt_out', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_opt_out',
                ]],
                ['language', ChoiceType::class, [
                    'required' => true,
                    'choices' => $this->languageList,
                    'label' => 'form.label_language',
                ]],
            ],
            'translation_domain' => 'SonataSeoBundle',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockMetadata($code = null)
    {
        return new Metadata($this->getName(), (null !== $code ? $code : $this->getName()), false, 'SonataSeoBundle', [
            'class' => 'fa fa-twitter',
        ]);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\Block\Social;

use Guzzle\Http\Exception\CurlException;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\BlockBundle\Block\BlockContextInterface;
use Sonata\BlockBundle\Model\BlockInterface;
use Sonata\CoreBundle\Form\Type\ImmutableArrayType;
use Sonata\CoreBundle\Model\Metadata;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * This block service allows to embed a tweet by requesting the Twitter API.
 *
 * @see https://dev.twitter.com/docs/api/1/get/statuses/oembed
 *
 * @author Hugo Briand <briand@ekino.com>
 */
class TwitterEmbedTweetBlockService extends BaseTwitterButtonBlockService
{
    public const TWITTER_OEMBED_URI = 'https://api.twitter.com/1/statuses/oembed.json';
    public const TWEET_URL_PATTERN = '%^(https://)(www.)?(twitter.com/)(.*)(/status)(es)?(/)([0-9]*)$%i';
    public const TWEET_ID_PATTERN = '%^([0-9]*)$%';

    /**
     * {@inheritdoc}
     */
    public function execute(BlockContextInterface $blockContext, Response $response = null)
    {
        $tweet = $blockContext->getSetting('tweet');

        if (($uriMatched = preg_match(self::TWEET_URL_PATTERN, $tweet))
            || preg_match(self::TWEET_ID_PATTERN, $tweet)) {
            // We matched an URL or an ID, we'll need to ask the API
            if (false === class_exists('Guzzle\Http\Client')) {
                throw new \RuntimeException('The guzzle http client library is required to call the Twitter API. Make sure to add guzzle/guzzle to your composer.json.');
            }

            // TODO cache API result
            $client = new \Guzzle\Http\Client();
            $client->setConfig(['curl.options' => [CURLOPT_CONNECTTIMEOUT_MS => 1000]]);

            try {
                $request = $client->get($this->buildUri($uriMatched, $blockContext->getSettings()));
                $apiTweet = json_decode($request->send()->getBody(true), true);

                $tweet = $apiTweet['html'];
            } catch (CurlException $e) {
                // log error
            }
        }

        return $this->renderResponse($blockContext->getTemplate(), [
            'block' => $blockContext->getBlock(),
            'tweet' => $tweet,
        ], $response);
    }

    /**
     * {@inheritdoc}
     */
    public function configureSettings(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'template' => '@SonataSeo/Block/block_twitter_embed.html.twig',
            'tweet' => '',
            'maxwidth' => null,
            'hide_media' => false,
            'hide_thread' => false,
            'omit_script' => false,
            'align' => 'none',
            'related' => null,
            'lang' => null,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function buildEditForm(FormMapper $form, BlockInterface $block)
    {
        $form->add('settings', ImmutableArrayType::class, [
            'keys' => [
                ['tweet', TextareaType::class, [
                    'required' => true,
                    'label' => 'form.label_tweet',
                    'sonata_help' => 'form.help_tweet',
                ]],
                ['maxwidth', IntegerType::class, [
                    'required' => false,
                    'label' => 'form.label_maxwidth',
                    'sonata_help' => 'form.help_maxwidth',
                ]],
                ['hide_media', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_hide_media',
                    'sonata_help' => 'form.help_hide_media',
                ]],
                ['hide_thread', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_hide_thread',
                    'sonata_help' => 'form.help_hide_thread',
                ]],
                ['omit_script', CheckboxType::class, [
                    'required' => false,
                    'label' => 'form.label_omit_script',
                    'sonata_help' => 'form.help_omit_script',
                ]],
                ['align', ChoiceType::class, [
                    'required' => false,
                    'choices' => [
                        'left' => 'form.label_align_left',
                        'right' => 'form.label_align_right',
                        'center' => 'form.label_align_center',
                        'none' => 'form.label_align_none',
                    ],
                    'label' => 'form.label_align',
                ]],
                ['related', TextType::class, [
                    'required' => false,
                    'label' => 'form.label_related',
                    'sonata_help' => 'form.help_related',
                ]],
                ['lang', ChoiceType::class, [
                    'required' => true,
                    'choices' => $this->languageList,
                    'label' => 'form.label_lang',
                ]],
            ],
            'translation_domain' => 'SonataSeoBundle',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockMetadata($code = null)
    {
        return new Metadata($this->getName(), (null !== $code ? $code : $this->getName()), false, 'SonataSeoBundle', [
            'class' => 'fa fa-twitter',
        ]);
    }

    /**
     * Returns supported API parameters from settings.
     *
     * @return array
     */
    protected function getSupportedApiParams()
    {
        return [
            'maxwidth',
            'hide_media',
            'hide_thread',
            'omit_script',
            'align',
            'related',
            'lang',
            'url',
            'id',
        ];
    }

    /**
     * Builds the API query URI based on $settings.
     *
     * @param bool  $uriMatched
     * @param array $settings
     *
     * @return string
     */
    protected function buildUri($uriMatched, array $settings)
    {
        $apiParams = $settings;
        $supportedParams = $this->getSupportedApiParams();

        if ($uriMatched) {
            // We matched the uri
            $apiParams['url'] = $settings['tweet'];
        } else {
            $apiParams['id'] = $settings['tweet'];
        }

        unset($apiParams['tweet']);

        $parameters = [];
        foreach ($apiParams as $key => $value) {
            if ($value && \in_array($key, $supportedParams, true)) {
                $parameters[] = $key.'='.$value;
            }
        }

        return sprintf('%s?%s', self::TWITTER_OEMBED_URI, implode('&', $parameters));
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle;

use Sonata\SeoBundle\DependencyInjection\Compiler\BreadcrumbBlockServicesCompilerPass;
use Sonata\SeoBundle\DependencyInjection\Compiler\ServiceCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

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

        $container->addCompilerPass(new BreadcrumbBlockServicesCompilerPass());
        $container->addCompilerPass(new ServiceCompilerPass());
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\SeoBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * This is the class that validates and merges configuration from your app/config files.
 */
class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder('sonata_seo');

        // Keep compatibility with symfony/config < 4.2
        if (!method_exists($treeBuilder, 'getRootNode')) {
            $rootNode = $treeBuilder->root('sonata_seo');
        } else {
            $rootNode = $treeBuilder->getRootNode();
        }

        $rootNode
            ->children()
                ->scalarNode('encoding')->defaultValue('UTF-8')->end()
                ->arrayNode('page')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->scalarNode('default')->defaultValue('sonata.seo.page.default')->end()
                        ->arrayNode('head')
                            ->normalizeKeys(false)
                            ->useAttributeAsKey('attribute')
                            ->prototype('scalar')->end()
                        ->end()
                        ->arrayNode('metas')
                            ->normalizeKeys(false)
                            ->useAttributeAsKey('element')
                            ->prototype('array')
                                ->normalizeKeys(false)
                                ->useAttributeAsKey('name')
                                ->prototype('scalar')->end()
                            ->end()
                        ->end()
                        ->scalarNode('separator')->defaultValue(' - ')->end()
                        // NEXT_MAJOR: Make this field required
                        ->scalarNode('title')->defaultValue('Project name')->end()
                    ->end()
                ->end()
                ->arrayNode('sitemap')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->arrayNode('doctrine_orm')
                            ->prototype('variable')->end()
                        ->end()
                        ->arrayNode('services')
                            ->prototype('variable')->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: lucasweijers
 * Date: 04-09-17
 * Time: 14:58
 *
 * This field will offer the user a choice between various icons in the selected font, and generates the needed css
 */
namespace plugins\dolphiq\iconpicker\fields;

use Craft;
use craft\base\ElementInterface;
use craft\base\Field;
use craft\base\PreviewableFieldInterface;
use craft\helpers\FileHelper;
use FontLib\Font;
use plugins\dolphiq\iconpicker\assets\sharedAsset;
use plugins\dolphiq\iconpicker\models\IconpickerModel;
use yii\db\Schema;
use yii\helpers\ArrayHelper;

class Iconpicker extends Field implements PreviewableFieldInterface
{
    // Static
    // =========================================================================

    /**
     * @var string The directory where the fonts are
     */
    const FONT_DIR = '@vendor/dolphiq/iconpicker/src/resources-shared/fonts/';

    /**
     * @var array All extentions that are allowed to be imported as a font
     */
    const FONT_EXT = ['*.woff', '*.ttf'];

    /**
     * @var string Icon class
     */
    const ICON_CLASS = 'dq-icon-';

    /**
     * @var string Pattern to format a safe file name
     */
    const SAFE_NAME_PATTERN = '/[^A-Za-z0-9_\-]/';

    /**
     * @inheritdoc
     */
    public static function displayName(): string
    {
        return Craft::t('app', 'Iconpicker');
    }

    // Properties
    // =========================================================================

    /**
     * @var string|null The input’s placeholder text
     */
    public $placeholder;

    /**
     * @var int|null The maximum number of characters allowed in the field
     */
    public $charLimit;

    /**
     * @var string The type of database column the field should have in the content table
     */
    public $columnType = Schema::TYPE_STRING;

    /**
     * @var string The current selected iconfont to use
     */
    public $iconFont = "";

    /**
     * @var array A list with the avaiable fonts
     */
    private $fonts = [];

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

    /**
     * @inheritdoc
     */
    public function getContentColumnType(): string
    {
        return $this->columnType;
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        $rules = parent::rules();
        $rules[] = [['iconFont'], 'safe'];
        return $rules;
    }

    /**
     * @inheritdoc
     */
    public function getSettingsHtml()
    {
        return Craft::$app->getView()->renderTemplate('dolphiq-iconpicker/fieldSettings', [
            'field' => $this,
            'fonts' => $this->getFontOptions()
        ]);
    }

    /**
     * @inheritdoc
     */
    public function getInputHtml($value, ElementInterface $element = null): string
    {
        // Load the assetbundle
        Craft::$app->view->registerAssetBundle(\plugins\dolphiq\iconpicker\assets\appAsset::class);

        // Display the field
        return Craft::$app->getView()->render('@vendor/dolphiq/iconpicker/src/views/main/_field', [
            'name' => $this->handle,
            'value' => $value,
            'field' => $this,
            'icons' => $this->getIcons(),
        ]);
    }

    /**
     * @inheritdoc
     */
    public function normalizeValue($value, ElementInterface $element = null)
    {
        $model = new IconpickerModel();

        /**
         * Serialised value from the DB
         */
        if (is_string($value)) {
            $value = json_decode($value, true);
        }

        /**
         * Array value from post or unserialised array
         */
        if (is_array($value) && !empty(array_filter($value))) {
            $model->load($value, '');
        }

        return $model;
    }

    /**
     * Get all the fonts that are residing in the fonts directory and have the right extention.
     * Index them by path as key and pathinfo array as value
     *
     * @return array
     */

    private function getFonts()
    {
        if (empty($this->fonts)) {
            $files = FileHelper::findFiles(Craft::getAlias(self::FONT_DIR), ['only' => self::FONT_EXT]);
            $filenames = [];
            $fonts = [];

            foreach ($files as $file) {
                $pathInfo = pathinfo($file);
                $safename = in_array($pathInfo['filename'], $filenames) ? $pathInfo['basename'] : $pathInfo['filename'];
                $safename = $this->safeName($safename);
                $fonts[$safename] = ArrayHelper::merge(
                    [
                        'path' => $file,
                        'safename' => $safename
                    ],
                    $pathInfo
                );

                $filenames[] = $pathInfo['filename'];
            }

            $this->fonts = $fonts;
        }

        return $this->fonts;
    }

    /**
     * Returns a options list for the settings dropdown when defining a field
     * @return array
     */
    private function getFontOptions()
    {
        $f = $this->getFonts();
        if (!empty($f)) {
            return ArrayHelper::map($f, 'safename', 'basename');
        }
    }

    /**
     * Load a font and get all unicode characters available in that font.
     *
     * @return array|null
     */
    private function getIcons()
    {
        if (!empty($this->iconFont)) {
            $fonts = $this->getFonts();
            if (!empty($fonts) && isset($fonts[$this->iconFont])) {
                $font = Font::load($fonts[$this->iconFont]['path']);
                $font->parse();

                if ($font !== null) {
                    return $font->getUnicodeCharMap();
                }
            }
        }

        return null;
    }

    /**
     * @param $font \FontLib\TrueType\File|null
     * @return array|null
     */
    private function getUnicodeList($font)
    {
        if (!empty($font)) {
            $unicodes = $font->getUnicodeCharMap();
            if (!empty($unicodes)) {
                return array_keys($unicodes);
            }
        }

        return null;
    }

    /**
     * Generate a css file that creates font families for each font file in the font directory
     */
    public function getFontCss()
    {
        $sharedAsset = new sharedAsset();
        $scss = "";

        foreach ($this->getFonts() as $safeName => $pathInfo) {
            $fontFile = $pathInfo['path'];
            $font = Font::load($fontFile);
            $font->parse();

            if (!empty($font)) {
                $iconFontName = $safeName;

                if (!empty($iconFontName)) {
                    $scss .= "
@font-face {
    font-family: 'dq-iconpicker-".$iconFontName."';
    src: url('../fonts/".$pathInfo['basename']."');
    font-weight: 100;
    font-style: normal;
}\n\n";

                    $scss .= '
[class*="dq-icon-'.$iconFontName.'"] {
  /* use !important to prevent issues with browser extensions that change fonts */
  font-family: dq-iconpicker-'.$iconFontName.' !important;
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-variant: normal;
  text-transform: none;
  line-height: 1;
  display: inline-block;
  vertical-align: baseline;

  /* Better Font Rendering =========== */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

}'."\n\n";
                }
            }
        }

        file_put_contents(Craft::getAlias($sharedAsset->sourcePath . '/css/fonts.css'), $scss);

        // Register the assetbundle that loads the generated css
        Craft::$app->view->registerAssetBundle(sharedAsset::className());
    }

    // Get the fontname of the currently selected font.
    public function getIconFontName()
    {
        return $this->iconFont;
    }

    private function safeName($filename)
    {
        $name = preg_replace(self::SAFE_NAME_PATTERN, '-', $filename);
        return $name;
    }
}


<?php
declare(strict_types=1);
namespace ParagonIE\PasswordLock;

use \Defuse\Crypto\Crypto;
use \Defuse\Crypto\Key;
use \ParagonIE\ConstantTime\Base64;
use \ParagonIE\ConstantTime\Binary;

class PasswordLock
{
    /**
     * 1. Hash password using bcrypt-base64-SHA256
     * 2. Encrypt-then-MAC the hash
     *
     * @param string $password
     * @param Key $aesKey
     * @return string
     * @throws \Exception
     * @throws \InvalidArgumentException
     */
    public static function hashAndEncrypt(string $password, Key $aesKey): string
    {
        /** @var string $hash */
        $hash = \password_hash(
            Base64::encode(
                \hash('sha384', $password, true)
            ),
            PASSWORD_DEFAULT
        );
        if (!\is_string($hash)) {
            throw new \Exception("Unknown hashing error.");
        }
        return Crypto::encrypt($hash, $aesKey);
    }
    /**
     * 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash
     * 2. Verify that the password matches the hash
     *
     * @param string $password
     * @param string $ciphertext
     * @param string $aesKey - must be exactly 16 bytes
     * @return bool
     * @throws \Exception
     * @throws \InvalidArgumentException
     */
    public static function decryptAndVerifyLegacy(string $password, string $ciphertext, string $aesKey): bool
    {
        if (Binary::safeStrlen($aesKey) !== 16) {
            throw new \Exception("Encryption keys must be 16 bytes long");
        }
        $hash = Crypto::legacyDecrypt(
            $ciphertext,
            $aesKey
        );
        if (!\is_string($hash)) {
            throw new \Exception("Unknown hashing error.");
        }
        return \password_verify(
            Base64::encode(
                \hash('sha256', $password, true)
            ),
            $hash
        );
    }

    /**
     * 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash
     * 2. Verify that the password matches the hash
     *
     * @param string $password
     * @param string $ciphertext
     * @param Key $aesKey
     * @return bool
     * @throws \Exception
     * @throws \InvalidArgumentException
     */
    public static function decryptAndVerify(string $password, string $ciphertext, Key $aesKey): bool
    {
        $hash = Crypto::decrypt(
            $ciphertext,
            $aesKey
        );
        if (!\is_string($hash)) {
            throw new \Exception("Unknown hashing error.");
        }
        return \password_verify(
            Base64::encode(
                \hash('sha384', $password, true)
            ),
            $hash
        );
    }

    /**
     * Key rotation method -- decrypt with your old key then re-encrypt with your new key
     *
     * @param string $ciphertext
     * @param  Key $oldKey
     * @param Key $newKey
     * @return string
     */
    public static function rotateKey(string $ciphertext, Key $oldKey, Key $newKey): string
    {
        $plaintext = Crypto::decrypt($ciphertext, $oldKey);
        return Crypto::encrypt($plaintext, $newKey);
    }

    /**
     * For migrating from an older version of the library
     *
     * @param string $password
     * @param string $ciphertext
     * @param string $oldKey
     * @param Key $newKey
     * @return string
     * @throws \Exception
     */
    public static function upgradeFromVersion1(
        string $password,
        string $ciphertext,
        string $oldKey,
        Key $newKey
    ): string {
        if (!self::decryptAndVerifyLegacy($password, $ciphertext, $oldKey)) {
            throw new \Exception(
                'The correct password is necessary for legacy migration.'
            );
        }
        $plaintext = Crypto::legacyDecrypt($ciphertext, $oldKey);
        return self::hashAndEncrypt($plaintext, $newKey);
    }
}


<?php

namespace Matthias\SymfonyConfigTest\Partial;

use Matthias\SymfonyConfigTest\Partial\Exception\ChildIsNotAnArrayNode;
use Matthias\SymfonyConfigTest\Partial\Exception\UndefinedChildNode;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;

class PartialNode
{
    /**
     * @var \ReflectionProperty
     */
    private static $nodeChildrenProperty;

    /**
     * @var \ReflectionProperty
     */
    private static $nodePrototypeProperty;

    /**
     * Provide an ArrayNode instance (e.g. the root node created by a TreeBuilder) and a path that is relevant to you,
     * e.g. "dbal.connections": this will strip every node that is not contained in the given path (e.g. the "orm" node
     * would be removed entirely.
     *
     * @param ArrayNode $node
     * @param string    $breadcrumbPath
     */
    public static function excludeEverythingNotInBreadcrumbPath(ArrayNode $node, $breadcrumbPath)
    {
        if ($breadcrumbPath === null) {
            return;
        }

        $path = explode('.', $breadcrumbPath);

        self::excludeEverythingNotInPath($node, $path);
    }

    /**
     * @param array $path
     */
    public static function excludeEverythingNotInPath(ArrayNode $node, array $path = [])
    {
        if (empty($path)) {
            return;
        }

        $nextNodeName = array_shift($path);
        $nextNode = self::childNode($node, $nextNodeName);

        $children = self::nodeChildrenProperty()->getValue($node);
        foreach ($children as $name => $child) {
            if ($name !== $nextNodeName) {
                unset($children[$name]);
            }
        }
        self::nodeChildrenProperty()->setValue($node, $children);

        if (!($nextNode instanceof ArrayNode)) {
            if (!empty($path)) {
                throw new ChildIsNotAnArrayNode($node, $nextNodeName);
            }

            return;
        }

        self::excludeEverythingNotInPath($nextNode, $path);
    }

    /**
     * @param ArrayNode $node
     * @param string    $childNodeName
     *
     * @return NodeInterface
     */
    private static function childNode(ArrayNode $node, $childNodeName)
    {
        if ($node instanceof PrototypedArrayNode && '*' === $childNodeName) {
            return self::nodePrototypeProperty()->getValue($node);
        }

        $children = self::nodeChildrenProperty()->getValue($node);

        if (!isset($children[$childNodeName])) {
            throw new UndefinedChildNode(
                $node,
                $childNodeName
            );
        }

        return $children[$childNodeName];
    }

    /**
     * @return \ReflectionProperty
     */
    private static function nodeChildrenProperty()
    {
        if (!isset(self::$nodeChildrenProperty)) {
            self::$nodeChildrenProperty = new \ReflectionProperty(
                ArrayNode::class,
                'children'
            );
            self::$nodeChildrenProperty->setAccessible(true);
        }

        return self::$nodeChildrenProperty;
    }

    /**
     * @return \ReflectionProperty
     */
    private static function nodePrototypeProperty()
    {
        if (!isset(self::$nodePrototypeProperty)) {
            self::$nodePrototypeProperty = new \ReflectionProperty(
                PrototypedArrayNode::class,
                'prototype'
            );
            self::$nodePrototypeProperty->setAccessible(true);
        }

        return self::$nodePrototypeProperty;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\CoreBundle\EventListener\Notification;

use PHPDish\Bundle\CoreBundle\Util\NotificationHelper;
use PHPDish\Bundle\PostBundle\Event\CategoryFollowedEvent;
use PHPDish\Bundle\PostBundle\Event\CommentMentionUserEvent;
use PHPDish\Bundle\PostBundle\Event\Events;
use PHPDish\Bundle\PostBundle\Event\PostCommentedEvent;
use PHPDish\Bundle\PostBundle\Event\VoteCommentEvent;
use PHPDish\Bundle\PostBundle\Event\VotePostEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class PostSubscriber implements EventSubscriberInterface
{
    /**
     * @var NotificationHelper
     */
    protected $notificationHelper;

    public function __construct(NotificationHelper $notificationHelper)
    {
        $this->notificationHelper = $notificationHelper;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            Events::POST_COMMENTED => 'onPostCommented',
            Events::POST_VOTED => 'onPostVoted',
            Events::COMMENT_VOTED => 'onCommentVoted',
            Events::USER_MENTIONED_COMMENT => 'onUserMentioned',
            Events::CATEGORY_FOLLOWED => 'onCategoryFollowed'
        ];
    }

    /**
     * 文章被评论时触发.
     *
     * @param PostCommentedEvent $event
     */
    public function onPostCommented(PostCommentedEvent $event)
    {
        if ($event->getPost()->getUser() === $event->getComment()->getUser()) {
            return;
        }

        $notification = $this->notificationHelper->createCommentPostNotification($event->getPost(), $event->getComment());

        $this->notificationHelper->sendNotification($event->getPost()->getUser(), $notification);
    }

    /**
     * 文章被点赞触发.
     *
     * @param VotePostEvent $event
     *
     */
    public function onPostVoted(VotePostEvent $event)
    {
        if ($event->getPost()->getUser() === $event->getVoter()) {
            return;
        }

        $this->notificationHelper
            ->createVotePostNotification($event->getPost(), $event->getVoter());
    }

    /**
     * 评论被点赞时触发.
     *
     * @param VoteCommentEvent $event
     */
    public function onCommentVoted(VoteCommentEvent $event)
    {
        //自己点赞自己不用通知
        if ($event->getComment()->getUser() === $event->getVoter()) {
            return;
        }
        $this->notificationHelper->createVoteCommentNotification(
            $event->getPost(),
            $event->getComment(),
            $event->getVoter()
        );
    }

    /**
     * 当用户在文章评论中被提及.
     *
     * @param CommentMentionUserEvent $event
     */
    public function onUserMentioned(CommentMentionUserEvent $event)
    {
        foreach ($event->getMentionedUsers() as $user) {
            if (
                $event->getComment()->getUser() === $user
                || $event->getComment()->getPost()->getUser() === $user
            ) {
                continue;
            }
            $this->notificationHelper->createMentionUserInPostNotification($event->getComment());
        }
    }

    /**
     * 专栏被关注时给专栏创建者发消息.
     *
     * @param CategoryFollowedEvent $event
     */
    public function onCategoryFollowed(CategoryFollowedEvent $event)
    {
        if ($event->getCategory()->getCreator() === $event->getFollower()) {
            return;
        }
        $this->notificationHelper->createFollowCategoryNotification($event->getCategory(), $event->getFollower());
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ChatBundle\Controller;

use FOS\MessageBundle\FormModel\NewThreadMessage;
use PHPDish\Bundle\ChatBundle\Form\Type\NewChatType;
use PHPDish\Bundle\ChatBundle\Message\Provider;
use PHPDish\Bundle\ResourceBundle\Controller\ResourceConfigurationInterface;
use PHPDish\Bundle\ResourceBundle\Controller\ResourceController;
use PHPDish\Bundle\UserBundle\Controller\ManagerTrait;
use PHPDish\Bundle\UserBundle\Model\UserInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatorInterface;

class ChatsController extends ResourceController
{
    use ManagerTrait;

    /**
     * @var Provider
     */
    protected $provider;

    public function __construct(ResourceConfigurationInterface $configuration, Provider $provider)
    {
        parent::__construct($configuration);
        $this->provider = $provider;
    }

    /**
     * 收件箱
     * @Route("/chats/inbox", name="chat_inbox")
     * @param Request $request
     * @return Response
     */
    public function inboxAction(Request $request)
    {
        $threads = $this->provider->getInboxThreadsPager(
            $request->query->get('page', 1)
        );
        return $this->render($this->configuration->getTemplate('Chat:inbox.html.twig'), [
            'threads' => $threads
        ]);
    }

    /**
     * 发送箱
     *
     * @Route("/chats/sent", name="chat_sent")
     *
     * @param Request $request
     * @return Response
     */
    public function sentAction(Request $request)
    {
        $threads = $this->provider->getSentThreadsPager(
            $request->query->get('page', 1)
        );
        return $this->render($this->configuration->getTemplate('Chat:sent.html.twig'), [
            'threads' => $threads
        ]);
    }

    /**
     * 添加新的聊天
     *
     * @Route("/chats/new", name="chat_add")
     * @param Request $request
     * @return Response
     */
    public function createAction(Request $request)
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        $username = $request->query->get('mail_to');
        $user = $this->getUserManager()->findUserByName($username);

        if (is_null($user)) {
            throw $this->createNotFoundException();
        }

        $form = $this->createForm(NewChatType::class, $this->createBlankThreadMessage($user));
        $formHandler = $this->get('fos_message.new_thread_form.handler');

        if ($message = $formHandler->process($form)) {
            return new RedirectResponse($this->get('router')->generate('fos_message_thread_view', [
                'threadId' => $message->getThread()->getId(),
            ]));
        }
        return $this->render($this->configuration->getTemplate('Chat:new_chat.html.twig'), array(
            'form' => $form->createView(),
            'recipient' => $user,
            'data' => $form->getData(),
        ));
    }

    protected function createBlankThreadMessage(UserInterface $user)
    {
        /**@var TranslatorInterface*/
        $translator = $this->get('translator');
        $threadMessage = new NewThreadMessage();
        $threadMessage->setRecipient($user);
        $threadMessage->setSubject($translator->trans('chat_with', [
            '%username%' => $user->getUsername()
        ]));
        return $threadMessage;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ChatBundle\DependencyInjection;

use PHPDish\Bundle\ChatBundle\Model\Message;
use PHPDish\Bundle\ChatBundle\Model\MessageMetadata;
use PHPDish\Bundle\ChatBundle\Model\Thread;
use PHPDish\Bundle\ChatBundle\Model\ThreadMetadata;
use PHPDish\Bundle\ResourceBundle\DependencyInjection\AbstractConfiguration;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * This is the class that validates and merges configuration from your app/config files.
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/configuration.html}
 */
class Configuration extends AbstractConfiguration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('phpdish_chat');

        $rootNode
            ->children()
                ->arrayNode('resources')
                    ->children()
                        ->arrayNode('message')
                            ->children()
                                ->scalarNode('interface')->defaultValue(\FOS\MessageBundle\Model\Message::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Message::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()

                        ->arrayNode('message_metadata')
                            ->children()
                                ->scalarNode('interface')->defaultValue(\FOS\MessageBundle\Model\MessageMetadata::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(MessageMetadata::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()

                        ->arrayNode('message_thread')
                            ->children()
                                ->scalarNode('interface')->defaultValue(\FOS\MessageBundle\Model\ThreadInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Thread::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()

                        ->arrayNode('message_thread_metadata')
                            ->children()
                                ->scalarNode('interface')->defaultValue(\FOS\MessageBundle\Model\ThreadMetadata::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(ThreadMetadata::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end();

        $this->addTemplatesSection($rootNode);
        return $treeBuilder;
    }
}


<?php

namespace PHPDish\Bundle\CoreBundle\Command\Proxy;

use Sylius\Bundle\ThemeBundle\Command\AssetsInstallCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class InstallThemeAssetsCommand extends Command
{
    /**
     * @var AssetsInstallCommand
     */
    protected $rawCommand;

    /**
     * {@inheritdoc}
     */
    public function configure()
    {
        $this->setName('phpdish:theme:assets:install')
            ->setDefinition([
                new InputArgument('target', InputArgument::OPTIONAL, 'The target directory', 'web'),
            ])
            ->addOption('symlink', null, InputOption::VALUE_NONE, 'Symlinks the assets instead of copying it')
            ->addOption('relative', null, InputOption::VALUE_NONE, 'Make relative symlinks')
            ->setDescription('Installs themes web assets under a public web directory');
    }

    /**
     * {@inheritdoc}
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        $this->rawCommand = $this->getApplication()->get('sylius:theme:assets:install');
        $this->setHelp($this->rawCommand->getHelp());
        $arguments = array(
            'command' => 'sylius:theme:assets:install',
            'target'  => $input->getArgument('target'),
            '--symlink'  => $input->getOption('symlink'),
            '--relative'  => $input->getOption('relative'),
        );
        $greetInput = new ArrayInput($arguments);
        $this->rawCommand->run($greetInput, $output);
    }

    /**
     * {@inheritdoc}
     */
    public function getAliases()
    {
        return ['theme:assets:install'];
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\PaymentBundle\Twig;

use PHPDish\Bundle\PaymentBundle\Model\PaymentInterface;
use PHPDish\Bundle\PaymentBundle\Model\WalletHistoryInterface;
use Symfony\Component\Translation\TranslatorInterface;

class WalletExtension extends \Twig_Extension
{
    /**
     * @var TranslatorInterface
     */
    protected $translator;

    public function __construct(TranslatorInterface $translator)
    {
        $this->translator = $translator;
    }

    /**
     * {@inheritdoc}
     */
    public function getFilters()
    {
        return [
            new \Twig_SimpleFilter('wallet_history_type', [$this, 'translateWalletHistoryType']),
            new \Twig_SimpleFilter('wallet_history_status', [$this, 'translateWalletHistoryStatus']),
        ];
    }

    /**
     * 翻译钱包历史类型
     * @param WalletHistoryInterface $history
     * @return string
     */
    public function translateWalletHistoryType($history)
    {
        switch ($history->getType()) {
            case PaymentInterface::TYPE_BUY_BOOK:
                $text = $this->translator->trans('payment.type.buy_book');
                break;
            case PaymentInterface::TYPE_FOLLOW_CATEGORY:
                $text = $this->translator->trans('payment.type.subscribe_category');
                break;
            case PaymentInterface::TYPE_CATEGORY_INCOME:
                $text = $this->translator->trans('payment.type.category_income');
                break;
            case PaymentInterface::TYPE_BOOK_INCOME:
                $text = $this->translator->trans('payment.type.book_income');
                break;
            case PaymentInterface::TYPE_WITHDRAW:
                $text = $this->translator->trans('payment.type.withdraw');
                break;
            default:
                $text =$this->translator->trans('payment.type.unknown');
        }
        return $text;
    }

    /**
     * 翻译钱包历史状态
     * @param WalletHistoryInterface $history
     * @return string
     */
    public function translateWalletHistoryStatus(WalletHistoryInterface $history)
    {
        switch ($history->getStatus()) {
            case PaymentInterface::STATUS_OK:
                if ($history->isIncome()) {
                    $text = $this->translator->trans('payment.status.have_received');
                } elseif ($history->getType() === PaymentInterface::TYPE_WITHDRAW) {
                    $text = $this->translator->trans('payment.status.have_tansfer');
                } else {
                    $text = $this->translator->trans('payment.status.paid');
                }
                break;
            case PaymentInterface::STATUS_WAITING:
                if ($history->getType() === PaymentInterface::TYPE_WITHDRAW) {
                    $text = $this->translator->trans('payment.status.processing');
                } else {
                    $text = $this->translator->trans('payment.status.unpaid');
                }
                break;
            case PaymentInterface::STATUS_CLOSED:
                $text = $this->translator->trans('payment.status.closed');
                break;
            default:
                $text = $this->translator->trans('payment.status.unknown');
        }
        return $text;
    }
}


<?php

namespace PHPDish\Bundle\CoreBundle\DataFixtures\ORM;

use Carbon\Carbon;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Bundle\FixturesBundle\Fixture;
use PHPDish\Bundle\ForumBundle\Service\TopicManagerInterface;

class TopicFixtures extends Fixture
{
    /**
     * {@inheritdoc}
     */
    public function load(ObjectManager $manager)
    {
        $title = '基于 PHP 的内网穿透工具 “Spike”';
        $body = <<<EOT
Spike [ https://github.com/slince/spike]( https://github.com/slince/spike);
之前由于要与一个同事远程协作开发一款app需要用到内网穿透服务，在网上找到了frp与ngrok；后来我在想能不能用php也写出来一个这样的服务软件？大家都知道php多进程多线程不够友好，在window上还不支持；写服务确实很吃力；不过幸运的是有[ReactPHP](https://github.com/reactphp)的存在，关于ReactPHP不做赘述有兴趣的同学可以自行百度。

基于ReactPHP的IO多路复用，使得Spike并没有比Frp性能差太多；下面是我简单做的一个benchmark，基于apache ab检验http隧道的服务性能；客户端与服务端都搭在本地，代理同事电脑上的http服务。不是特别符合应用场景，大家简单看一下。

Spike:
```
Concurrency Level:      10
Time taken for tests:   37.727 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      2569900 bytes
HTML transferred:       2514600 bytes
Requests per second:    2.65 [#/sec] (mean)
Time per request:       3772.747 [ms] (mean)
Time per request:       377.275 [ms] (mean, across all concurrent requests)
Transfer rate:          66.52 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       3
Processing:   533 3602 591.9   3714    4096
Waiting:      516 3587 592.3   3701    4076
Total:        534 3602 591.9   3715    4097

Percentage of the requests served within a certain time (ms)
  50%   3715
  66%   3791
  75%   3822
  80%   3844
  90%   3970
  95%   4015
  98%   4053
  99%   4097
 100%   4097 (longest request)
```

Frp:
```
Concurrency Level:      10
Time taken for tests:   38.230 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      2569900 bytes
HTML transferred:       2514600 bytes
Requests per second:    2.62 [#/sec] (mean)
Time per request:       3823.045 [ms] (mean)
Time per request:       382.304 [ms] (mean, across all concurrent requests)
Transfer rate:          65.65 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       1
Processing:   379 3650 644.4   3809    4140
Waiting:      360 3633 645.5   3789    4124
Total:        380 3650 644.4   3809    4140

Percentage of the requests served within a certain time (ms)
  50%   3809
  66%   3847
  75%   3909
  80%   3923
  90%   4026
  95%   4053
  98%   4129
  99%   4140
 100%   4140 (longest request)
```

从上面可以看出Spike性能要稍微好点，不过这个地方有点不公平，我在做spike的测试时只开启了服务端的日志，客户端的日志是关闭的；而FRP的两端日志都是开启的；我不知道怎么关frp的日志；

在这里简单提一点由于Spike的日志IO是同步的所以日志的读写会耗掉部分性能，提升日志等级减少日志写入可以提升不少的性能；

最后再次附上项目地址： [https://github.com/slince/spike](https://github.com/slince/spike) 欢迎star，欢迎fork
EOT;

        $this->createTopic($title, $body);

        $title = 'Composer 的源管理工具 Composer Registry Manager';
        $body = <<<EOT
惯例先附上项目地址： [https://github.com/slince/composer-registry-manager](https://github.com/slince/composer-registry-manager)
之前使用的是简称，由于crm容易与常说的crm系统混淆，所以这次改成全称并重新在packagist发布了；
如果以前安装的同学请先使用下面命令卸载
```bash
$ composer global remove slince/crm
```
使用下面命令安装
```bash
$ composer global require slince/composer-registry-manager ^1.2
```
此次在1.2版本中做了个不小的重构，API没有做任何改变，主要改善了下以前的一些写法，重写了部分单元测试；除此之外将源管理配置从库文件的位置移出：
Windows下移至 `C:\Users\用户名下\AppData\Roaming\ComposerRegistryManager\crm.json`
Linux下移至`~/.config/composer-registry-manager/crm.json`
从而修复了因版本升级导致原先做的配置丢失的问题；

上一张GIF图:
![screenshot](https://github.com/slince/composer-registry-manager/raw/master/assets/screenshot.gif)
感谢[NRM](http://cnodejs.org/topic/5326e78c434e04172c006826)给的灵感；
再次附上地址； 欢迎star，欢迎fork；[https://github.com/slince/composer-registry-manager](https://github.com/slince/composer-registry-manager)
EOT;

        $this->createTopic($title, $body);
    }

    /**
     * 创建话题.
     *
     * @param string $title
     * @param string $body
     */
    protected function createTopic($title, $body)
    {
        $manager = $this->getTopicManager();
        $topic = $manager->createTopic($this->getReference('general-user'));
        $topic->setUpdatedAt(Carbon::now())
            ->setTitle($title)
            ->setOriginalBody($body)
            ->setThreads([$this->getReference('thread-share')])
            ->setEnabled(true);
        $manager->saveTopic($topic);
    }

    /**
     * @return TopicManagerInterface
     */
    protected function getTopicManager()
    {
        return $this->container->get('phpdish.manager.topic');
    }

    /**
     * {@inheritdoc}
     */
    public function getDependencies()
    {
        return [
            UserFixtures::class,
            ThreadFixtures::class,
        ];
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\CoreBundle\EventListener;

use PHPDish\Bundle\CoreBundle\Locale\LocaleManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;

final class LocaleSubscriber implements EventSubscriberInterface
{
    /**
     * @var LocaleManager
     */
    protected $localeManager;

    /**
     * @var Session
     */
    private $session;

    public function __construct(LocaleManager $localeManager, Session $session)
    {
        $this->localeManager = $localeManager;
        $this->session = $session;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::REQUEST => 'onKernelRequest',
            SecurityEvents::INTERACTIVE_LOGIN => array(array('onInteractiveLogin', 15)),
        ];
    }

    /**
     * 用户登录之后触发，记录用户的语言
     *
     * @param InteractiveLoginEvent $event
     */
    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();

        if (null !== $user->getLocale()) {
            $this->session->set('_locale', $user->getLocale());
        }
    }

    /**
     * 处理request
     *
     * @param GetResponseEvent $event
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        $locale = null;
        if (!$request->hasPreviousSession()) {
            $locale = $this->getClientPreferredLocale($request);
        } elseif (!$request->attributes->get('_locale')) {
            $locale = $request->getSession()->get('_locale', $this->getClientPreferredLocale($request));
        }

        if ($locale !== null) {
            $request->setLocale($locale);
        }
    }

    /**
     * 获取最匹配客户端的语言
     *
     * @param Request $request
     * @return null|string
     */
    protected function getClientPreferredLocale(Request $request)
    {
        return $request->getPreferredLanguage($this->localeManager->all());
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\UserBundle\DependencyInjection;

use PHPDish\Bundle\ResourceBundle\DependencyInjection\AbstractConfiguration;
use PHPDish\Bundle\UserBundle\Model\PointHistory;
use PHPDish\Bundle\UserBundle\Model\PointHistoryInterface;
use PHPDish\Bundle\UserBundle\Model\Profile;
use PHPDish\Bundle\UserBundle\Model\ProfileInterface;
use PHPDish\Bundle\UserBundle\Model\User;
use PHPDish\Bundle\UserBundle\Model\UserInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration extends AbstractConfiguration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('phpdish_user');

        $rootNode
            ->children()
                ->scalarNode('avatar_downloader')->isRequired()->cannotBeEmpty()->end()
                ->arrayNode('resources')
                    ->children()
                        ->arrayNode('user')
                            ->children()
                                ->scalarNode('interface')->defaultValue(UserInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(User::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                        ->arrayNode('profile')
                            ->children()
                                ->scalarNode('interface')->defaultValue(ProfileInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Profile::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                        ->arrayNode('point_history')
                            ->children()
                                ->scalarNode('interface')->defaultValue(PointHistoryInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(PointHistory::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end();

        $this->addTemplatesSection($rootNode);
        return $treeBuilder;
    }
}


<?php

namespace PHPDish\Bundle\CoreBundle\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class InstallCommand extends AbstractInstallCommand
{
    /**
     * @var array
     */
    protected $commands = [
        [
            'command' => 'phpdish:install:check-requirements',
            'message' => 'Checking system requirements.',
        ],
        [
            'command' => 'phpdish:install:database',
            'message' => 'Setting up the database.',
        ],
        [
            'command' => 'phpdish:install:fixture',
            'message' => 'Install sample data.',
        ],
        [
            'command' => 'fos:user:create',
            'message' => 'Creats the super admin acount.',
            'options' => [
                '--super-admin' => true,
            ],
        ],
    ];

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName('phpdish:install')
            ->setDescription('Install PHPDish in your machine');
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $outputStyle = new SymfonyStyle($input, $output);
        $outputStyle->writeln('<info>Installing PHPDish...</info>');
        $outputStyle->writeln(static::PHPDISH_LOGO);

        $this->ensureDirectoryWritableAndExists($this->getContainer()->getParameter('kernel.cache_dir'), $output);
        $this->ensureDirectoryWritableAndExists($this->getContainer()->getParameter('app.dir.media'), $output);
        $this->ensureDirectoryWritableAndExists($this->getContainer()->getParameter('app.dir.uploads'), $output);
        $this->ensureDirectoryWritableAndExists($this->getContainer()->getParameter('app.dir.avatar'), $output);

        $this->getApplication()->setAutoExit(false);

        foreach ($this->commands as $index => $command) {
            $outputStyle->newLine();
            $outputStyle->section(sprintf(
                'Step %d of %d. <info>%s</info>',
                $index + 1,
                count($this->commands),
                $command['message']
            ));
            $this->executeCommand($command['command'], $output, isset($command['options']) ? $command['options'] : []);
        }

        $output->writeln('PHPDish has been successfully installed.');
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\AdminBundle\GridFactory;

use PHPDish\Bundle\AdminBundle\Grid\Factory;
use PHPDish\Bundle\AdminBundle\Grid\GridInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class GridFactoryAggregate
{
    /**
     * [
     *     'PHPDish\Bundle\UserBundle\UserInterface' => 'phpdish_admin.source_factory.user'
     * ]
     * @var GridFactoryInterface[]
     */
    protected $factories = [];

    /**
     * @var Factory
     */
    protected $factory;

    /**
     * @var RequestStack
     */
    protected $requestStack;

    public function __construct(
        Factory $factory,
        RequestStack $requestStack
    ){
        $this->factory = $factory;
        $this->requestStack = $requestStack;
    }

    /**
     * 获取grid source
     *
     * @param string $sourceClass
     * @return GridInterface
     * @throws \ReflectionException
     */
    public function get($sourceClass)
    {
        if (isset($this->factories[$sourceClass])) {
            $factory = $this->factory[$sourceClass];
            $grid = $factory->getGrid();
        } else {
            $reflection = new \ReflectionClass($sourceClass);
            if (!$reflection->isInstantiable()) {
                throw new \InvalidArgumentException(sprintf('The "%s" cannot be instantiated'));
            }
            $grid = $this->factory->createGrid($sourceClass);
        }
        //handle request
        $grid->handleRequest($this->requestStack->getCurrentRequest());
        return $grid;
    }

    public function addFactory($sourceClass, $factory)
    {
        $this->factories[$sourceClass] = $factory;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\CoreBundle\EventListener\Notification;

use PHPDish\Bundle\CoreBundle\Util\NotificationHelper;
use PHPDish\Bundle\UserBundle\Event\Events;
use PHPDish\Bundle\UserBundle\Event\UserFollowedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class UserSubscriber implements EventSubscriberInterface
{
    /**
     * @var NotificationHelper
     */
    protected $notificationHelper;

    public function __construct(NotificationHelper $notificationHelper)
    {
        $this->notificationHelper = $notificationHelper;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            Events::USER_FOLLOWED => 'onUserFollowed'
        ];
    }

    /**
     * 用户被关注时，生成通知.
     *
     * @param UserFollowedEvent $event
     *
     */
    public function onUserFollowed(UserFollowedEvent $event)
    {
        if ($event->getUser() === $event->getFollower()) {
            return;
        }
        $notification = $this->notificationHelper->createFollowUserNotification($event->getFollower());
        $this->notificationHelper->sendNotification($event->getUser(), $notification);
    }
}