diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..6b1dcf4432 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,34 @@ +name: Deploy + +on: + push: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + + - name: Install dependencies + run: | + composer install + - name: Build page + run: | + ./vendor/bin/sculpin generate --env=prod --url="https://developers.shopware.com" + ./contrib/package_plugins.sh + git clone https://github.com/shopware5/devdocs.git -b styletile output_prod/styletile + rm -rf output_prod/styletile/.git + echo "developers.shopware.com" > output_prod/CNAME + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./output_prod diff --git a/.gitignore b/.gitignore index 29410bc849..e9e85bc4d1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /source/assets/css/basic.css /deploy_key /deploy_key.pub +/node_modules diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2225ff411f..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: php - -php: - - 7.0 - -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -before_script: - - if [[ $TRAVIS_PULL_REQUEST == 'false' && $TRAVIS_BRANCH == 'master' ]]; then export SYMFONY__ALGOLIA_ENABLED="1"; fi; - - composer self-update - - composer install - -script: - - ./vendor/bin/sculpin generate --env=prod --url="https://developers.shopware.com" - - ./contrib/package_plugins.sh - -after_success: - - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && ./deploy.sh diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000000..842cb5dc0c --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,31 @@ +module.exports = (grunt) => { + grunt.initConfig({ + less: { + development: { + options: { + compress: true, + sourceMapFileInline: true, + rootpath: 'assets/' + }, + files: { + 'output_dev/assets/css/basic.css': 'source/assets/css/basic.less' + } + } + }, + + watch: { + styles: { + files: ['source/assets/css/*/**.less', 'source/assets/css/*.less'], + tasks: ['less:development'], + options: { + livereload: true + } + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-less'); + + grunt.registerTask('default', [ 'less:development' ]); +}; diff --git a/README.md b/README.md index d0b347c28e..578b60fc7f 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,28 @@ # Shopware Development Documentation ## Install + This site is generated with [Sculpin][2], a PHP based static site generator. -First you have to [install Sculpin][3] and run the install command in the project directory. +First you have to [install Sculpin][3] and run the installation command in the project directory. This can be done via the `init.sh` shell script in the project root. -``` +```bash ./init.sh ``` -This will download sculping and install the required dependencies. +This will download Sculpin and install the required dependencies. ## Running the website locally -``` +```bash ./watch.sh ``` This will start a local webserver at . You can use a different port like so: -``` + +```bash ./watch.sh 8001 ``` @@ -31,30 +33,42 @@ It is important to put the plugin into the proper Frontend/Backend/Core subdirec ### Example -`exampleplugins/Frontend/SwagSloganOfTheDay/` will result in `exampleplugins/SwagSloganOfTheDay.zip` in the generated output directory. Please not that the subdirectory is not part of the resulting directory/filename. +`exampleplugins/Frontend/SwagSloganOfTheDay/` will result in `exampleplugins/SwagSloganOfTheDay.zip` in the generated output directory. Please note that the subdirectory is not part of the resulting directory/filename. [2]: https://sculpin.io/ [3]: https://sculpin.io/download -## Table of contents in your markdown documents +## Table of contents in your Markdown documents + You have the ability to generate a table of content list for your document. Simply place the following code into your document and a table of contents will appear there magically: -``` +```html
``` -If you want, you can customize the behavior of the toc list as well. You can define the text of the headline using the attribute `data-headline` and you can define the max depth of headlines you wanna include into the list with the attribute `data-depth`: +If you want, you can customize the behavior of the toc list as well. You can define the text of the headline using the attribute `data-headline` and you can define the max depth of headlines you want to include into the list with the attribute `data-depth`: -``` +```html
``` +## Hiding blog posts from search engines + +To add a `` to your blog post, +you have to add the following frontmatter entry to your blog post: + +```yaml +robots: + hide: true +``` + ## Version History + To create a version history table, you simply have to add a `history` array to your metadata. Example: -``` +```yaml --- layout: default indexed: true @@ -64,7 +78,6 @@ history: 2015-11-23: added frontend documentation 2016-01-01: documented millenium bug --- - ``` ## Algolia Search Configuration @@ -80,21 +93,22 @@ sculpin_algolia: ``` The API Key should be provided via the environment variable `SYMFONY__ALGOLIA_API_KEY`. -To enable the also the environment variable `SYMFONY__ALGOLIA_ENABLED` must exist. +To enable the search, the environment variable `SYMFONY__ALGOLIA_ENABLED` also must exist. ```bash SYMFONY__ALGOLIA_ENABLED=1 SYMFONY__ALGOLIA_API_KEY=MYAPIKEY ./vendor/bin/sculpin generate ``` -These variables are automatically exported during the Travis-CI build for every merge/commit on the `master` branch. +These variables are automatically exported during the build for every merge/commit on the `main` branch. ## CSS helper classes + We added a bunch of CSS helper classes which should help you to build simple layouts. ### Warning box -``` +```html
This is a warning
@@ -102,26 +116,27 @@ This is a warning ### Centering of content -``` +```html
``` Alternative: -``` + +```html
``` ### Floating images -``` +```html
Sample image
``` -``` +```html
Sample image
diff --git a/app/SculpinKernel.php b/app/SculpinKernel.php index 217a319cf3..38b94af174 100644 --- a/app/SculpinKernel.php +++ b/app/SculpinKernel.php @@ -4,16 +4,17 @@ use Janbuecker\Sculpin\Bundle\MetaNavigationBundle\SculpinMetaNavigationBundle; use Mavimo\Sculpin\Bundle\RedirectBundle\SculpinRedirectBundle; use Sculpin\Bundle\SculpinBundle\HttpKernel\AbstractKernel; +use Shopware\Devdocs\GitHistoryBundle\SculpinGitHistoryBundle; class SculpinKernel extends AbstractKernel { - protected function getAdditionalSculpinBundles() + protected function getAdditionalSculpinBundles(): array { return [ - SculpinRedirectBundle::class, SculpinLessBundle::class, SculpinCommonMarkBundle::class, - SculpinMetaNavigationBundle::class + SculpinMetaNavigationBundle::class, + SculpinGitHistoryBundle::class ]; } } diff --git a/app/config/sculpin_services.yml b/app/config/sculpin_services.yml index 7708a22b29..b55c834295 100644 --- a/app/config/sculpin_services.yml +++ b/app/config/sculpin_services.yml @@ -4,15 +4,14 @@ services: tags: - { name: kernel.event_subscriber } - webuni.commonmark.tablextension: - class: Webuni\CommonMark\TableExtension\TableExtension + league.commonmark.tablextension: + class: League\CommonMark\Extension\Table\TableExtension tags: - { name: sculpin_commonmark.extension } shopware_devdocs.twig_extension: class: Shopware\Devdocs\TwigExtension arguments: - - @sculpin_theme.theme_twig_extension - - %kernel.environment% + - '%kernel.environment%' tags: - { name: twig.extension } diff --git a/app/config/sculpin_site.yml b/app/config/sculpin_site.yml index e557196368..05969ca335 100644 --- a/app/config/sculpin_site.yml +++ b/app/config/sculpin_site.yml @@ -1,15 +1,21 @@ title: Shopware Developers -githuburl: https://github.com/shopware/devdocs/blob/master/source/ +githuburl: https://github.com/shopware5/devdocs/blob/main/source/ github_enabled: true default_date_format: "F jS, Y" authors: + dk: + name: Dominic Klein + title: Developer + bio: I work at shopware and do on-site as well as online trainings for developers. Besides that I look after our enterprise customers with focus on technical subjects. + github: https://github.com/elkmod nd: name: Niklas Dzösch title: Developer / Developer Evangelist twitter: http://twitter.com/ndzoesch github: https://github.com/ndzoesch bio: I am the shopware developer evangelist as of 2017 and a core developer at shopware. + bc: name: Benjamin Cremer title: Developer && Developer Advocate @@ -41,6 +47,7 @@ authors: github: https://github.com/Phil23 url: https://www.facebook.com/phil.schuch bio: I am a web-developer in the frontend department of shopware. I test new technologies to create forward thinking user experience. + sf: name: Sven Finke title: Developer Connect @@ -85,4 +92,40 @@ authors: title: Product Manager Core twitter: https://twitter.com/MarcelSchmaeing github: https://github.com/MarcelSchmaeing - bio: I am a product manager working at the main product shopware. I'm intreseted in every process, workflow or tool which makes the development process faster and better to create the best value for the product. + bio: I am a product manager working at the main product shopware. I'm interested in every process, workflow or tool which makes the development process faster and better to create the best value for the product. + + ps: + name: Patrick Stahl + title: Developer + twitter: https://twitter.com/PaddStah + github: https://github.com/PaddyS + + tn: + name: Thomas Eiling + title: Developer + twitter: https://twitter.com/teiling88 + github: https://github.com/teiling88 + url: http://thomas-eiling.de + bio: I am a Developer in the Enterprise team. I'm interested in code quality, testing and to automatize everything which is possible. + + lh: + name: Linus Holtstiege + title: Enterprise Developer + twitter: https://twitter.com/linushstge + github: https://github.com/linushstge + url: https://hstge.me + bio: I am a Developer in the Enterprise team. I'm interested in frontend development, continous delivery and build processing. + + cr: + name: Christian Rades + title: Developer + github: https://github.com/Christian-Rades + bio: I am a Developer in the Enterprise team. I'm interested in distributed applications, continous delivery and functional programming. + + rc: + name: Raphael Emmerich + title: Enterprise Developer + twitter: https://twitter.com/emmer_91 + github: https://github.com/emmer91 + url: http://raphael-emmerich.de + bio: I am a Developer in the Enterprise team. I'm interested in code quality, testing and functional programming. diff --git a/app/config/sculpin_site_prod.yml b/app/config/sculpin_site_prod.yml index 4d9e0f8924..bc3c568773 100644 --- a/app/config/sculpin_site_prod.yml +++ b/app/config/sculpin_site_prod.yml @@ -1,4 +1,2 @@ imports: - sculpin_site.yml - -google_tag_manager_id: GTM-5ZGXBG diff --git a/app/src/AlgoliaBundle/AlgoliaIndexListener.php b/app/src/AlgoliaBundle/AlgoliaIndexListener.php index 07ac2c551a..f79841c368 100644 --- a/app/src/AlgoliaBundle/AlgoliaIndexListener.php +++ b/app/src/AlgoliaBundle/AlgoliaIndexListener.php @@ -20,9 +20,9 @@ class AlgoliaIndexListener implements EventSubscriberInterface */ public static function getSubscribedEvents() { - return array( + return [ Sculpin::EVENT_AFTER_RUN => 'afterRun', - ); + ]; } /** @@ -32,18 +32,18 @@ public static function getSubscribedEvents() public function __construct(Client $client, $indexName) { $this->index = $client->initIndex($indexName); - $this->index->setSettings(array( - "attributesToIndex" => array("title", "tags", "unordered(body)"), - 'attributesForFaceting' => array('tags') - )); + $this->index->setSettings([ + 'attributesToIndex' => ['title', 'tags', 'unordered(body)'], + 'attributesForFaceting' => ['tags'] + ]); } /** - * @param \Sculpin\Core\Event\SourceSetEvent $event + * @param SourceSetEvent $event */ public function afterRun(SourceSetEvent $event) { - $documents = array(); + $documents = []; /** @var AbstractSource $item */ foreach ($event->allSources() as $item) { if ($item->data()->get('indexed')) { @@ -65,15 +65,15 @@ public function afterRun(SourceSetEvent $event) */ private function parseSource(AbstractSource $source) { - $document = array( + $document = [ 'objectID' => sha1($source->sourceId()), 'title' => $source->data()->get('title'), 'body' => strip_tags($source->content()), 'url' => rtrim($source->permalink()->relativeUrlPath(), '/').'/', 'date' => $source->data()->get('calculated_date'), - ); + ]; - $tags = (is_array($source->data()->get('tags'))) ? $source->data()->get('tags') : array(); + $tags = is_array($source->data()->get('tags')) ? $source->data()->get('tags') : []; if ($tags) { $document['tags'] = $tags; } diff --git a/app/src/AlgoliaBundle/Resources/config/services.xml b/app/src/AlgoliaBundle/Resources/config/services.xml index a3322eb27b..c913ac9245 100644 --- a/app/src/AlgoliaBundle/Resources/config/services.xml +++ b/app/src/AlgoliaBundle/Resources/config/services.xml @@ -10,9 +10,9 @@ - + %algolia.index_name% - + diff --git a/app/src/AnchorListener.php b/app/src/AnchorListener.php index 683fd3d82d..d4e8ca55c6 100644 --- a/app/src/AnchorListener.php +++ b/app/src/AnchorListener.php @@ -1,6 +1,7 @@ 'afterFormat', - ); + ]; } public function afterFormat(SourceSetEvent $event) @@ -63,7 +64,7 @@ private function formatSource(SourceInterface $source) return; } - $dom = new \IvoPetkov\HTML5DOMDocument(); + $dom = new HTML5DOMDocument(); $dom->loadHTML($content); if (!$dom) { return; diff --git a/app/src/GitHistoryBundle/DependencyInjection/SculpinGitHistoryExtension.php b/app/src/GitHistoryBundle/DependencyInjection/SculpinGitHistoryExtension.php new file mode 100644 index 0000000000..70850e7273 --- /dev/null +++ b/app/src/GitHistoryBundle/DependencyInjection/SculpinGitHistoryExtension.php @@ -0,0 +1,19 @@ +load('services.xml'); + } +} diff --git a/app/src/GitHistoryBundle/HistoryListener.php b/app/src/GitHistoryBundle/HistoryListener.php new file mode 100644 index 0000000000..dee17cd2d8 --- /dev/null +++ b/app/src/GitHistoryBundle/HistoryListener.php @@ -0,0 +1,190 @@ + '', + '/source/labs/index.html' => 'source/labs/' + ]; + + /** + * @param string $projectDir + * @param SourcePermalinkFactoryInterface $permalinkFactory + */ + public function __construct($projectDir, SourcePermalinkFactoryInterface $permalinkFactory) + { + $this->projectDir = $projectDir; + $this->permalinkFactory = $permalinkFactory; + } + + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() + { + return [ + Sculpin::EVENT_BEFORE_RUN => 'dumpGitHistory', + ]; + } + + /** + * @param SourceSetEvent $event + */ + public function dumpGitHistory(SourceSetEvent $event) + { + /** @var SourceInterface $source */ + foreach ($event->allSources() as $source) { + foreach ($this->historyRoots as $page => $prefix) { + if (!preg_match('#'.$page.'$#i', $source->file()->getPathname())) { + continue; + } + + if ($source->data()->get('docHistory')) { + continue; + } + + $this->addHistoryToSource($source, $event->allSources(), $prefix); + } + } + } + + /** + * @param SourceInterface $source + * @param array $sources + * @param string $prefix + */ + private function addHistoryToSource(SourceInterface $source, array $sources, $prefix = '') + { + $data = $this->fetchGitHistory(); + $history = []; + + foreach ($data as $key => $item) { + foreach ($item['articles'] as $articleKey => $article) { + if ($prefix && strpos($article, $prefix) !== 0) { + continue; + } + + $resource = $this->findArticle($sources, $article); + + if (!$resource) { + continue; + } + + $permalink = $this->permalinkFactory->create($resource); + $url = rtrim($permalink->relativeUrlPath(), '/') . '/'; + + $title = $resource->data()->get('title'); + + $history[$item['date']][$title] = $url; + } + } + + foreach ($history as $date => $items) { + ksort($items); + $history[$date] = $items; + } + + $source->data()->set('docHistory', $history); + } + + /** + * @param array $sources + * @param string $article + * + * @return null|SourceInterface + */ + private function findArticle(array $sources, $article) + { + /** @var SourceInterface $source */ + foreach ($sources as $source) { + if (preg_match('#' . $article . '$#i', $source->file()->getPathname())) { + return $source; + } + } + + return null; + } + + /** + * @param int $numOfCommits + * + * @return array + */ + private function fetchGitHistory($numOfCommits = 50) + { + $process = new Process(sprintf("git log --oneline --merges -%d | cut -d' ' -f1", $numOfCommits), $this->projectDir); + $process->run(); + + $commits = array_filter(explode(PHP_EOL, $process->getOutput())); + + if (empty($commits)) { + return []; + } + + $history = []; + $latestHash = 'HEAD^'; + + foreach ($commits as $commit) { + $getFilesProcess = new Process(sprintf('git diff --name-only %s %s', $commit, $latestHash), $this->projectDir); + $getFilesProcess->run(); + + $changedFiles = array_filter( + explode(PHP_EOL, $getFilesProcess->getOutput()), + function ($file) { + if (preg_match('#source/.*\.(md|html)#i', $file) && !in_array($file, self::$blacklist, true)) { + return true; + } + } + ); + + if (empty($changedFiles)) { + continue; + } + + $commitDateProcess = new Process(sprintf('git show --pretty=%%ct %s', $commit), $this->projectDir); + $commitDateProcess->run(); + + $commitDate = (int) trim($commitDateProcess->getOutput()); + + $latestHash = $commit; + + $history[] = [ + 'date' => date('Y-m-d', $commitDate), + 'articles' => $changedFiles + ]; + } + + return $history; + } +} diff --git a/app/src/GitHistoryBundle/Resources/config/services.xml b/app/src/GitHistoryBundle/Resources/config/services.xml new file mode 100644 index 0000000000..a4ecf1cb82 --- /dev/null +++ b/app/src/GitHistoryBundle/Resources/config/services.xml @@ -0,0 +1,14 @@ + + + + + + %sculpin.project_dir% + + + + + + diff --git a/app/src/GitHistoryBundle/SculpinGitHistoryBundle.php b/app/src/GitHistoryBundle/SculpinGitHistoryBundle.php new file mode 100644 index 0000000000..c586b4e997 --- /dev/null +++ b/app/src/GitHistoryBundle/SculpinGitHistoryBundle.php @@ -0,0 +1,8 @@ + 'afterRun', - ); + ]; } /** @@ -32,11 +32,11 @@ public function __construct($outputDir) } /** - * @param \Sculpin\Core\Event\SourceSetEvent $event + * @param SourceSetEvent $event */ public function afterRun(SourceSetEvent $event) { - $documents = array(); + $documents = []; /** @var AbstractSource $item */ foreach ($event->allSources() as $item) { if ($item->data()->get('indexed')) { @@ -60,14 +60,14 @@ public function afterRun(SourceSetEvent $event) */ private function parseSource(AbstractSource $source) { - $tags = (is_array($source->data()->get('tags'))) ? $source->data()->get('tags') : array(); + $tags = is_array($source->data()->get('tags')) ? $source->data()->get('tags') : []; - $document = array( + $document = [ 'title' => $source->data()->get('title'), 'body' => strip_tags($source->content()), 'tags' => implode(', ', $tags), 'url' => rtrim($source->permalink()->relativeUrlPath(), '/').'/', - ); + ]; return $document; } diff --git a/app/src/TwigExtension.php b/app/src/TwigExtension.php index bb269db13b..47fb04ede7 100644 --- a/app/src/TwigExtension.php +++ b/app/src/TwigExtension.php @@ -2,8 +2,6 @@ namespace Shopware\Devdocs; -use Sculpin\Bundle\ThemeBundle\ThemeTwigExtension; - class TwigExtension extends \Twig_Extension { /** @@ -11,32 +9,16 @@ class TwigExtension extends \Twig_Extension */ private $environment; - /** - * @var ThemeTwigExtension - */ - private $twigThemeExtension; - - public function __construct(ThemeTwigExtension $twigThemeExtension, $environment) - { - $this->twigThemeExtension = $twigThemeExtension; - $this->environment = $environment; - } - - /** - * Returns the name of the extension. - * - * @return string The extension name - */ - public function getName() + public function __construct($environment) { - return __CLASS__; + $this->environment = $environment; } public function getFunctions() { - return array( - 'versioned' => new \Twig_Function_Method($this, 'versioned'), - ); + return [ + new \Twig_SimpleFunction('versioned', [$this, 'versioned']), + ]; } /** @@ -45,14 +27,14 @@ public function getFunctions() */ public function versioned($resource) { - $parts = pathinfo($resource); - $version = ($this->environment === 'prod') ? '.'.time() : ''; + $parts = pathinfo($resource); + $version = ($this->environment === 'prod') ? time() : ''; return $parts['dirname'] - .'/' - .$parts['filename'] - .$version - .'.' - .$parts['extension']; + . '/' + . $parts['filename'] + . '.' + . $parts['extension'] + . '?v=' . $version; } } diff --git a/composer.json b/composer.json index 17f8450ba9..03337a6894 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,28 @@ { - "require": { - "sculpin/sculpin": "^2.1@dev", - "dflydev/embedded-composer": "^1.0@dev", - "bcremer/sculpin-less-bundle": "~0.1", - "bcremer/sculpin-commonmark-bundle": "^0.4.0", - "mavimo/sculpin-redirect-bundle": "@dev", - "webuni/commonmark-table-extension": "^0.6.0", - "janbuecker/sculpin-meta-navigation-bundle": "^0.4", - "ivopetkov/html5-dom-document-php": "^0.4.1" - }, - "autoload": { - "psr-4": { - "Shopware\\Devdocs\\": "app/src/" + "require": { + "bcremer/sculpin-commonmark-bundle": "^0.5.0", + "bcremer/sculpin-less-bundle": "0.3.0", + "dflydev/embedded-composer": "^1.0@dev", + "ivopetkov/html5-dom-document-php": "^1.1.0", + "janbuecker/sculpin-meta-navigation-bundle": "^0.9", + "league/commonmark": "^1.3.0", + "sculpin/sculpin": "3.2.0", + "symfony/config": "^4.4.13", + "symfony/dependency-injection": "^4.4.13", + "symfony/event-dispatcher": "^4.4.13", + "symfony/http-kernel": "^4.4.13", + "symfony/process": "^4.4.13", + "twig/twig": "^2.5" + }, + "autoload": { + "psr-4": { + "Shopware\\Devdocs\\": "app/src/" + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "sculpin/sculpin-theme-composer-plugin": true + } } - } } diff --git a/composer.lock b/composer.lock index 64db90e1c9..da72058cc0 100644 --- a/composer.lock +++ b/composer.lock @@ -1,28 +1,28 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "66b19e4b6e2cadaf3d30d555bb49effc", + "content-hash": "04d07f5cb69bc384002a81d469061702", "packages": [ { "name": "bcremer/sculpin-commonmark-bundle", - "version": "0.4.0", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/bcremer/sculpin-commonmark-bundle.git", - "reference": "fc6de53b4806725d4863eb6cf7d576522804b87d" + "reference": "d2d561800613cd9edd7b3fa9e74d654e4858ca20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bcremer/sculpin-commonmark-bundle/zipball/fc6de53b4806725d4863eb6cf7d576522804b87d", - "reference": "fc6de53b4806725d4863eb6cf7d576522804b87d", + "url": "https://api.github.com/repos/bcremer/sculpin-commonmark-bundle/zipball/d2d561800613cd9edd7b3fa9e74d654e4858ca20", + "reference": "d2d561800613cd9edd7b3fa9e74d654e4858ca20", "shasum": "" }, "require": { - "league/commonmark": "~0.13", - "php": ">=5.4.0" + "league/commonmark": "^1.0", + "php": ">=7.3" }, "type": "library", "autoload": { @@ -47,25 +47,29 @@ "markdown", "sculpin" ], - "time": "2016-01-16T11:27:38+00:00" + "support": { + "issues": "https://github.com/bcremer/sculpin-commonmark-bundle/issues", + "source": "https://github.com/bcremer/sculpin-commonmark-bundle/tree/0.5.0" + }, + "time": "2021-11-08T15:51:59+00:00" }, { "name": "bcremer/sculpin-less-bundle", - "version": "0.1.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/bcremer/sculpin-less-bundle.git", - "reference": "139cae83dd97201df0991ab61f4f8b09aa93f35c" + "reference": "6965f392b40620726750b09298c516903b750a31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bcremer/sculpin-less-bundle/zipball/139cae83dd97201df0991ab61f4f8b09aa93f35c", - "reference": "139cae83dd97201df0991ab61f4f8b09aa93f35c", + "url": "https://api.github.com/repos/bcremer/sculpin-less-bundle/zipball/6965f392b40620726750b09298c516903b750a31", + "reference": "6965f392b40620726750b09298c516903b750a31", "shasum": "" }, "require": { - "oyejorge/less.php": "~1.7.0", - "php": ">=5.4.0" + "php": ">=7.4.0", + "wikimedia/less.php": "^3.0.0" }, "type": "library", "autoload": { @@ -90,38 +94,41 @@ "lessphp", "sculpin" ], - "time": "2015-06-08T07:43:05+00:00" + "support": { + "issues": "https://github.com/bcremer/sculpin-less-bundle/issues", + "source": "https://github.com/bcremer/sculpin-less-bundle/tree/0.3.0" + }, + "time": "2023-01-09T13:12:39+00:00" }, { "name": "composer/ca-bundle", - "version": "1.0.6", + "version": "1.5.5", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "a795611394b3c05164fd0eb291b492b39339cba4" + "reference": "08c50d5ec4c6ced7d0271d2862dec8c1033283e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/a795611394b3c05164fd0eb291b492b39339cba4", - "reference": "a795611394b3c05164fd0eb291b492b39339cba4", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/08c50d5ec4c6ced7d0271d2862dec8c1033283e6", + "reference": "08c50d5ec4c6ced7d0271d2862dec8c1033283e6", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "psr/log": "^1.0", - "symfony/process": "^2.5 || ^3.0" - }, - "suggest": { - "symfony/process": "This is necessary to reliably check whether openssl_x509_parse is vulnerable on older php versions, but can be ignored on PHP 5.5.6+" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -148,40 +155,62 @@ "ssl", "tls" ], - "time": "2016-11-02T18:11:27+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-01-08T16:17:16+00:00" }, { "name": "composer/composer", - "version": "1.2.3", + "version": "1.10.27", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "e7f19286a7e7f940950c9069a0f549d483c30ba7" + "reference": "f8f49191eec76f039b466aa1f161406fe43aff50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/e7f19286a7e7f940950c9069a0f549d483c30ba7", - "reference": "e7f19286a7e7f940950c9069a0f549d483c30ba7", + "url": "https://api.github.com/repos/composer/composer/zipball/f8f49191eec76f039b466aa1f161406fe43aff50", + "reference": "f8f49191eec76f039b466aa1f161406fe43aff50", "shasum": "" }, "require": { "composer/ca-bundle": "^1.0", "composer/semver": "^1.0", - "composer/spdx-licenses": "^1.0", - "justinrainbow/json-schema": "^1.6 || ^2.0", - "php": "^5.3.2 || ^7.0", + "composer/spdx-licenses": "^1.2", + "composer/xdebug-handler": "^1.1", + "justinrainbow/json-schema": "^5.2.10", + "php": "^5.3.2 || ^7.0 || ^8.0", "psr/log": "^1.0", - "seld/cli-prompt": "^1.0", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.0", - "symfony/console": "^2.5 || ^3.0", - "symfony/filesystem": "^2.5 || ^3.0", - "symfony/finder": "^2.2 || ^3.0", - "symfony/process": "^2.1 || ^3.0" + "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0" + }, + "conflict": { + "symfony/console": "2.8.38" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5", - "phpunit/phpunit-mock-objects": "^2.3 || ^3.0" + "phpspec/prophecy": "^1.10", + "symfony/phpunit-bridge": "^4.2" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -194,7 +223,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.10-dev" } }, "autoload": { @@ -218,35 +247,53 @@ "homepage": "http://seld.be" } ], - "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", "homepage": "https://getcomposer.org/", "keywords": [ "autoload", "dependency", "package" ], - "time": "2016-12-01T13:33:53+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/composer/issues", + "source": "https://github.com/composer/composer/tree/1.10.27" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-09-29T08:50:23+00:00" }, { "name": "composer/semver", - "version": "1.4.2", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + "reference": "647490bbcaf7fc4891c58f47b825eb99d19c377a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "url": "https://api.github.com/repos/composer/semver/zipball/647490bbcaf7fc4891c58f47b825eb99d19c377a", + "reference": "647490bbcaf7fc4891c58f47b825eb99d19c377a", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + "phpunit/phpunit": "^4.5 || ^5.0.5" }, "type": "library", "extra": { @@ -287,33 +334,52 @@ "validation", "versioning" ], - "time": "2016-08-30T16:08:34+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/1.7.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-12-03T15:47:16+00:00" }, { "name": "composer/spdx-licenses", - "version": "1.1.5", + "version": "1.5.8", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "96c6a07b05b716e89a44529d060bc7f5c263cb13" + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/96c6a07b05b716e89a44529d060bc7f5c263cb13", - "reference": "96c6a07b05b716e89a44529d060bc7f5c263cb13", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -348,25 +414,111 @@ "spdx", "validator" ], - "time": "2016-09-28T07:17:45+00:00" + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.8" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-11-20T07:44:33+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.4.6", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-03-25T17:01:18+00:00" }, { "name": "dflydev/ant-path-matcher", - "version": "v1.0.3", + "version": "v1.0.4", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-util-antPathMatcher.git", - "reference": "66e0ed7cd07e1d989b170472d000b99ab8c9e33e" + "reference": "c8406d2d85a844b0dbb4ee76d9db9def7ca67518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-util-antPathMatcher/zipball/66e0ed7cd07e1d989b170472d000b99ab8c9e33e", - "reference": "66e0ed7cd07e1d989b170472d000b99ab8c9e33e", + "url": "https://api.github.com/repos/dflydev/dflydev-util-antPathMatcher/zipball/c8406d2d85a844b0dbb4ee76d9db9def7ca67518", + "reference": "c8406d2d85a844b0dbb4ee76d9db9def7ca67518", "shasum": "" }, "require": { "php": ">=5.3" }, + "require-dev": { + "phpunit/phpunit": "^4|^5|^6|^7|^8|^9" + }, "type": "library", "autoload": { "psr-0": { @@ -397,7 +549,11 @@ "path", "pattern" ], - "time": "2012-12-03T05:03:00+00:00" + "support": { + "issues": "https://github.com/dflydev/dflydev-util-antPathMatcher/issues", + "source": "https://github.com/dflydev/dflydev-util-antPathMatcher/tree/v1.0.4" + }, + "time": "2023-01-23T23:02:42+00:00" }, { "name": "dflydev/apache-mime-types", @@ -452,6 +608,10 @@ "mime", "mimetypes" ], + "support": { + "issues": "https://github.com/dflydev/dflydev-apache-mime-types/issues", + "source": "https://github.com/dflydev/dflydev-apache-mime-types/tree/v1.0.1" + }, "time": "2013-05-14T02:02:01+00:00" }, { @@ -507,20 +667,24 @@ "mime", "type" ], + "support": { + "issues": "https://github.com/dflydev/dflydev-canal/issues", + "source": "https://github.com/dflydev/dflydev-canal/tree/master" + }, "time": "2013-05-14T05:22:25+00:00" }, { "name": "dflydev/dot-access-configuration", - "version": "v1.0.1", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-configuration.git", - "reference": "9b65c83159c9003e00284ea1144ad96b69d9c8b9" + "reference": "2e6eb0c8b8830b26bb23defcfc38d4276508fc49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-configuration/zipball/9b65c83159c9003e00284ea1144ad96b69d9c8b9", - "reference": "9b65c83159c9003e00284ea1144ad96b69d9c8b9", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-configuration/zipball/2e6eb0c8b8830b26bb23defcfc38d4276508fc49", + "reference": "2e6eb0c8b8830b26bb23defcfc38d4276508fc49", "shasum": "" }, "require": { @@ -567,20 +731,24 @@ "config", "configuration" ], - "time": "2014-11-14T03:26:12+00:00" + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-configuration/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-configuration/tree/master" + }, + "time": "2018-09-08T23:00:17+00:00" }, { "name": "dflydev/dot-access-data", - "version": "v1.0.1", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "7a0960d088119818ce7687d200c363b01d183cbe" + "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/7a0960d088119818ce7687d200c363b01d183cbe", - "reference": "7a0960d088119818ce7687d200c363b01d183cbe", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/3fbd874921ab2c041e899d044585a2ab9795df8a", + "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a", "shasum": "" }, "require": { @@ -611,6 +779,11 @@ "name": "Beau Simensen", "email": "beau@dflydev.com", "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" } ], "description": "Given a deep data structure, access data by dot notation.", @@ -621,7 +794,11 @@ "dot", "notation" ], - "time": "2015-08-13T03:51:18+00:00" + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/master" + }, + "time": "2017-01-20T21:14:22+00:00" }, { "name": "dflydev/embedded-composer", @@ -629,12 +806,12 @@ "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-embedded-composer.git", - "reference": "c9ca20fd3ccfbfb7bfcc3c65c33191f458c8a3a7" + "reference": "65b9d65826a2d27eaf87275c012e24d51b08d661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-embedded-composer/zipball/c9ca20fd3ccfbfb7bfcc3c65c33191f458c8a3a7", - "reference": "c9ca20fd3ccfbfb7bfcc3c65c33191f458c8a3a7", + "url": "https://api.github.com/repos/dflydev/dflydev-embedded-composer/zipball/65b9d65826a2d27eaf87275c012e24d51b08d661", + "reference": "65b9d65826a2d27eaf87275c012e24d51b08d661", "shasum": "" }, "require": { @@ -655,6 +832,7 @@ "symfony/console": "~2.3", "symfony/http-kernel": "~2.1" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -688,20 +866,24 @@ "embedded", "extensibility" ], - "time": "2016-05-21 00:49:42" + "support": { + "issues": "https://github.com/dflydev/dflydev-embedded-composer/issues", + "source": "https://github.com/dflydev/dflydev-embedded-composer/tree/master" + }, + "time": "2018-04-18T14:50:04+00:00" }, { "name": "dflydev/placeholder-resolver", - "version": "v1.0.2", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-placeholder-resolver.git", - "reference": "c498d0cae91b1bb36cc7d60906dab8e62bb7c356" + "reference": "d0161b4be1e15838327b01b21d0149f382d69906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-placeholder-resolver/zipball/c498d0cae91b1bb36cc7d60906dab8e62bb7c356", - "reference": "c498d0cae91b1bb36cc7d60906dab8e62bb7c356", + "url": "https://api.github.com/repos/dflydev/dflydev-placeholder-resolver/zipball/d0161b4be1e15838327b01b21d0149f382d69906", + "reference": "d0161b4be1e15838327b01b21d0149f382d69906", "shasum": "" }, "require": { @@ -740,90 +922,46 @@ "placeholder", "resolver" ], - "time": "2012-10-28T21:08:28+00:00" - }, - { - "name": "dflydev/symfony-finder-factory", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/dflydev/dflydev-symfony-finder-factory.git", - "reference": "101b2decf308bfac9c9bbc52be1738e1fa863a8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-symfony-finder-factory/zipball/101b2decf308bfac9c9bbc52be1738e1fa863a8a", - "reference": "101b2decf308bfac9c9bbc52be1738e1fa863a8a", - "shasum": "" - }, - "require": { - "php": ">=5.3.2", - "symfony/finder": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } + "support": { + "issues": "https://github.com/dflydev/dflydev-placeholder-resolver/issues", + "source": "https://github.com/dflydev/dflydev-placeholder-resolver/tree/v1.0.3" }, - "autoload": { - "psr-0": { - "Dflydev\\Symfony\\FinderFactory": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dragonfly Development Inc.", - "email": "info@dflydev.com", - "homepage": "http://dflydev.com" - }, - { - "name": "Beau Simensen", - "email": "beau@dflydev.com", - "homepage": "http://beausimensen.com" - } - ], - "description": "Symfony Finder Factory", - "keywords": [ - "factory", - "finder", - "syfony" - ], - "time": "2012-11-09T16:45:28+00:00" + "time": "2021-12-03T16:48:58+00:00" }, { "name": "doctrine/inflector", - "version": "v1.0.1", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "0bcb2e79d8571787f18b7eb036ed3d004908e604" + "reference": "4bd5c1cdfcd00e9e2d8c484f79150f67e5d355d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/0bcb2e79d8571787f18b7eb036ed3d004908e604", - "reference": "0bcb2e79d8571787f18b7eb036ed3d004908e604", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/4bd5c1cdfcd00e9e2d8c484f79150f67e5d355d9", + "reference": "4bd5c1cdfcd00e9e2d8c484f79150f67e5d355d9", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.1 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "4.*" + "doctrine/coding-standard": "^8.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Inflector\\": "lib/" + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector", + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" } }, "notification-url": "https://packagist.org/downloads/", @@ -831,6 +969,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -839,10 +981,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -852,37 +990,64 @@ "email": "schmittjoh@gmail.com" } ], - "description": "Common String Manipulations with regard to casing and singular/plural rules.", - "homepage": "http://www.doctrine-project.org", + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", "keywords": [ "inflection", - "pluralize", - "singularize", - "string" + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/1.4.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } ], - "time": "2014-12-20T21:24:13+00:00" + "time": "2021-04-16T17:34:40+00:00" }, { "name": "evenement/evenement", - "version": "v1.0.0", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/fa966683e7df3e5dd5929d984a44abfbd6bafe8d", - "reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -892,78 +1057,49 @@ "authors": [ { "name": "Igor Wiedler", - "email": "igor@wiedler.ch", - "homepage": "http://wiedler.ch/igor/" + "email": "igor@wiedler.ch" } ], - "description": "Événement is a very simple event dispatching library for PHP 5.3", + "description": "Événement is a very simple event dispatching library for PHP", "keywords": [ - "event-dispatcher" + "event-dispatcher", + "event-emitter" ], - "time": "2012-05-30T15:01:08+00:00" + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" }, { - "name": "guzzle/guzzle", - "version": "v3.0.7", + "name": "fig/http-message-util", + "version": "1.1.5", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "f31f35d1669382936861533bd0217fcf830dc9a9" + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f31f35d1669382936861533bd0217fcf830dc9a9", - "reference": "f31f35d1669382936861533bd0217fcf830dc9a9", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=5.3.2", - "symfony/event-dispatcher": ">=2.1" - }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" + "php": "^5.3 || ^7.0 || ^8.0" }, - "require-dev": { - "doctrine/common": "*", - "monolog/monolog": "1.*", - "phpunit/phpunit": "3.7.*", - "symfony/class-loader": "*", - "zend/zend-cache1": "1.12", - "zend/zend-log1": "1.12", - "zendframework/zend-cache": "2.0.*", - "zendframework/zend-log": "2.0.*" + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { - "psr-0": { - "Guzzle\\Tests": "tests/", - "Guzzle": "src/" + "psr-4": { + "Fig\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -972,54 +1108,52 @@ ], "authors": [ { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", - "homepage": "http://guzzlephp.org/", + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", "keywords": [ - "client", - "curl", - "framework", "http", - "http client", - "rest", - "web service" + "http-message", + "psr", + "psr-7", + "request", + "response" ], - "abandoned": "guzzlehttp/guzzle", - "time": "2012-12-19T23:06:35+00:00" + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" }, { "name": "ivopetkov/html5-dom-document-php", - "version": "v0.4.1", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/ivopetkov/html5-dom-document-php.git", - "reference": "ddda8553d18d04b0ac9cc5011fce0954d7db02ae" + "reference": "2ce4a0a531e2851e9ef8d7ca76d2758f5269a595" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ivopetkov/html5-dom-document-php/zipball/ddda8553d18d04b0ac9cc5011fce0954d7db02ae", - "reference": "ddda8553d18d04b0ac9cc5011fce0954d7db02ae", + "url": "https://api.github.com/repos/ivopetkov/html5-dom-document-php/zipball/2ce4a0a531e2851e9ef8d7ca76d2758f5269a595", + "reference": "2ce4a0a531e2851e9ef8d7ca76d2758f5269a595", "shasum": "" }, "require": { - "php": ">=5.5.0" + "ext-dom": "*", + "php": ">=7.0.0" }, "require-dev": { - "phpunit/phpunit": "^4.8" + "ivopetkov/docs-generator": "0.1.*", + "phpunit/phpunit": "6.*" }, "type": "library", "autoload": { - "psr-4": { - "IvoPetkov\\": "src/" - } + "files": [ + "autoload.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1033,20 +1167,24 @@ } ], "description": "HTML5 DOMDocument PHP library (extends DOMDocument)", - "time": "2016-10-07T13:24:38+00:00" + "support": { + "issues": "https://github.com/ivopetkov/html5-dom-document-php/issues", + "source": "https://github.com/ivopetkov/html5-dom-document-php/tree/master" + }, + "time": "2018-10-31T07:14:39+00:00" }, { "name": "janbuecker/sculpin-meta-navigation-bundle", - "version": "v0.4", + "version": "v0.9", "source": { "type": "git", "url": "https://github.com/janbuecker/sculpin-meta-navigation-bundle.git", - "reference": "ee38e2d090b3379891cefabcadc323b5b7cb069c" + "reference": "9a0898dc4506c1ad2167512c0426fd2aae344df3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/janbuecker/sculpin-meta-navigation-bundle/zipball/ee38e2d090b3379891cefabcadc323b5b7cb069c", - "reference": "ee38e2d090b3379891cefabcadc323b5b7cb069c", + "url": "https://api.github.com/repos/janbuecker/sculpin-meta-navigation-bundle/zipball/9a0898dc4506c1ad2167512c0426fd2aae344df3", + "reference": "9a0898dc4506c1ad2167512c0426fd2aae344df3", "shasum": "" }, "require": { @@ -1079,39 +1217,38 @@ "static", "tree" ], - "time": "2016-09-28T08:03:06+00:00" + "support": { + "issues": "https://github.com/janbuecker/sculpin-meta-navigation-bundle/issues", + "source": "https://github.com/janbuecker/sculpin-meta-navigation-bundle/tree/v0.9" + }, + "time": "2022-08-26T12:04:31+00:00" }, { "name": "justinrainbow/json-schema", - "version": "2.0.5", + "version": "5.3.0", "source": { "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "6b2a33e6a768f96bdc2ead5600af0822eed17d67" + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/6b2a33e6a768f96bdc2ead5600af0822eed17d67", - "reference": "6b2a33e6a768f96bdc2ead5600af0822eed17d67", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", "json-schema/json-schema-test-suite": "1.2.0", - "phpdocumentor/phpdocumentor": "~2", - "phpunit/phpunit": "^4.8.22" + "phpunit/phpunit": "^4.8.35" }, "bin": [ "bin/validate-json" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -1145,54 +1282,53 @@ "json", "schema" ], - "time": "2016-06-02T10:59:52+00:00" + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + }, + "time": "2024-07-06T21:00:26+00:00" }, { "name": "league/commonmark", - "version": "0.15.2", + "version": "1.6.7", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "c3b08b911e7344e45b87529eabc8b559d48093d4" + "reference": "2b8185c13bc9578367a5bf901881d1c1b5bbd09b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c3b08b911e7344e45b87529eabc8b559d48093d4", - "reference": "c3b08b911e7344e45b87529eabc8b559d48093d4", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/2b8185c13bc9578367a5bf901881d1c1b5bbd09b", + "reference": "2b8185c13bc9578367a5bf901881d1c1b5bbd09b", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=5.4.8" + "php": "^7.1 || ^8.0" }, - "replace": { - "colinodell/commonmark-php": "*" + "conflict": { + "scrutinizer/ocular": "1.7.*" }, "require-dev": { "cebe/markdown": "~1.0", + "commonmark/commonmark.js": "0.29.2", "erusev/parsedown": "~1.0", - "jgm/commonmark": "0.27", + "ext-json": "*", + "github/gfm": "0.29.0", "michelf/php-markdown": "~1.4", - "mikehaertl/php-shellcommand": "~1.2.0", - "phpunit/phpunit": "~4.3|~5.0", - "scrutinizer/ocular": "~1.1", - "symfony/finder": "~2.3|~3.0" - }, - "suggest": { - "league/commonmark-extras": "Library of useful extensions including smart punctuation" + "mikehaertl/php-shellcommand": "^1.4", + "phpstan/phpstan": "^0.12.90", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2", + "scrutinizer/ocular": "^1.5", + "symfony/finder": "^4.2" }, "bin": [ "bin/commonmark" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.16-dev" - } - }, "autoload": { "psr-4": { - "League\\CommonMark\\": "src/" + "League\\CommonMark\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1207,87 +1343,68 @@ "role": "Lead Developer" } ], - "description": "Markdown parser for PHP based on the CommonMark spec", - "homepage": "https://github.com/thephpleague/commonmark", + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and Github-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", "keywords": [ "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", "markdown", + "md", "parser" ], - "time": "2016-11-22T17:30:29+00:00" - }, - { - "name": "mavimo/sculpin-redirect-bundle", - "version": "dev-master", - "target-dir": "Mavimo/Sculpin/Bundle/RedirectBundle", - "source": { - "type": "git", - "url": "https://github.com/mavimo/sculpin-redirect-bundle.git", - "reference": "5c42c401942008de04aa1e1d64161ca105568e2a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mavimo/sculpin-redirect-bundle/zipball/5c42c401942008de04aa1e1d64161ca105568e2a", - "reference": "5c42c401942008de04aa1e1d64161ca105568e2a", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "autoload": { - "psr-0": { - "Mavimo\\Sculpin\\Bundle\\RedirectBundle": "" - } + "support": { + "docs": "https://commonmark.thephpleague.com/", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, { - "name": "Marco Vito Moscaritolo", - "email": "marco@mavimo.org", - "homepage": "http://www.mavimo.org" + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" } ], - "description": "Sculpin Redirect Bundle", - "homepage": "https://sculpin.io", - "keywords": [ - "blog", - "blogging", - "redirect", - "site", - "static" - ], - "time": "2014-01-04 14:40:02" + "time": "2022-01-13T17:18:13+00:00" }, { "name": "michelf/php-markdown", - "version": "1.5.1", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/michelf/php-markdown.git", - "reference": "155287e4222d2dd69b6a21221617b50668d5892e" + "reference": "5024d623c1a057dcd2d076d25b7d270a1d0d55f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/michelf/php-markdown/zipball/155287e4222d2dd69b6a21221617b50668d5892e", - "reference": "155287e4222d2dd69b6a21221617b50668d5892e", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/5024d623c1a057dcd2d076d25b7d270a1d0d55f3", + "reference": "5024d623c1a057dcd2d076d25b7d270a1d0d55f3", "shasum": "" }, "require": { "php": ">=5.3.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-lib": "1.4.x-dev" - } + "require-dev": { + "phpunit/phpunit": ">=4.3 <5.8" }, + "type": "library", "autoload": { - "psr-0": { - "Michelf": "" + "psr-4": { + "Michelf\\": "Michelf/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1311,40 +1428,45 @@ "keywords": [ "markdown" ], - "time": "2015-12-22T18:18:12+00:00" + "support": { + "issues": "https://github.com/michelf/php-markdown/issues", + "source": "https://github.com/michelf/php-markdown/tree/1.9.1" + }, + "time": "2021-11-24T02:52:38+00:00" }, { "name": "netcarver/textile", - "version": "v3.5.5", + "version": "v3.8.0", "source": { "type": "git", "url": "https://github.com/textile/php-textile.git", - "reference": "1b95af533775316d09bd36a38bee2c0b804add12" + "reference": "02ed0cbe6832c2100342dabb6d01d7ba558cb8e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/textile/php-textile/zipball/1b95af533775316d09bd36a38bee2c0b804add12", - "reference": "1b95af533775316d09bd36a38bee2c0b804add12", + "url": "https://api.github.com/repos/textile/php-textile/zipball/02ed0cbe6832c2100342dabb6d01d7ba558cb8e7", + "reference": "02ed0cbe6832c2100342dabb6d01d7ba558cb8e7", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "3.7.*", - "satooshi/php-coveralls": "0.6.*", - "squizlabs/php_codesniffer": "1.5.*", - "symfony/yaml": "2.3.*" + "phpstan/phpstan": "1.6.3", + "phpunit/phpunit": "^9.5.20", + "psy/psysh": "^0.11.2", + "squizlabs/php_codesniffer": "3.*", + "symfony/yaml": "^4.4.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.5-dev" + "dev-master": "3.8-dev" } }, "autoload": { - "psr-0": { - "Netcarver\\Textile": "src/" + "psr-4": { + "Netcarver\\Textile\\": "src/Netcarver/Textile/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1364,96 +1486,87 @@ "plaintext", "textile" ], - "time": "2014-01-02T09:39:06+00:00" + "support": { + "issues": "https://github.com/textile/php-textile/issues", + "source": "https://github.com/textile/php-textile", + "wiki": "https://github.com/textile/php-textile/wiki" + }, + "time": "2022-12-03T18:19:42+00:00" }, { - "name": "oyejorge/less.php", - "version": "v1.7.0.10", + "name": "psr/container", + "version": "1.1.2", "source": { "type": "git", - "url": "https://github.com/oyejorge/less.php.git", - "reference": "a1e2d3c20794b37ac4d0baeb24613e579584033b" + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/oyejorge/less.php/zipball/a1e2d3c20794b37ac4d0baeb24613e579584033b", - "reference": "a1e2d3c20794b37ac4d0baeb24613e579584033b", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=5.3" + "php": ">=7.4.0" }, - "require-dev": { - "phpunit/phpunit": "~4.8.18" - }, - "bin": [ - "bin/lessc" - ], "type": "library", "autoload": { - "psr-0": { - "Less": "lib/" - }, - "classmap": [ - "lessc.inc.php" - ] + "psr-4": { + "Psr\\Container\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ { - "name": "Matt Agar", - "homepage": "https://github.com/agar" - }, - { - "name": "Martin Jantošovič", - "homepage": "https://github.com/Mordred" - }, - { - "name": "Josh Schmidt", - "homepage": "https://github.com/oyejorge" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "PHP port of the Javascript version of LESS http://lesscss.org", - "homepage": "http://lessphp.gpeasy.com", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "css", - "less", - "less.js", - "lesscss", - "php", - "stylesheet" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], - "time": "2015-12-30T05:47:36+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" }, { - "name": "psr/log", - "version": "1.0.2", + "name": "psr/http-message", + "version": "1.1", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1466,259 +1579,328 @@ "homepage": "http://www.php-fig.org/" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ - "log", + "http", + "http-message", "psr", - "psr-3" + "psr-7", + "request", + "response" ], - "time": "2016-10-10T12:19:37+00:00" + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" }, { - "name": "react/event-loop", - "version": "v0.2.7", - "target-dir": "React/EventLoop", + "name": "psr/log", + "version": "1.1.4", "source": { "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "134b94dc555d320bd31253a215c8a65ef151a79a" + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/134b94dc555d320bd31253a215c8a65ef151a79a", - "reference": "134b94dc555d320bd31253a215c8a65ef151a79a", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-libev": "*", - "ext-libevent": ">=0.0.5" + "php": ">=5.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.2-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { - "psr-0": { - "React\\EventLoop": "" + "psr-4": { + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Event loop abstraction layer that libraries can use for evented I/O.", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ - "event-loop" + "log", + "psr", + "psr-3" ], - "time": "2013-01-05T11:41:26+00:00" + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" }, { - "name": "react/http", - "version": "v0.2.6", - "target-dir": "React/Http", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/reactphp/http.git", - "reference": "5e920f734f4065de1582c125b4bf35128279972f" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/http/zipball/5e920f734f4065de1582c125b4bf35128279972f", - "reference": "5e920f734f4065de1582c125b4bf35128279972f", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "guzzle/http": "3.0.*", - "guzzle/parser": "3.0.*", - "php": ">=5.3.3", - "react/socket": "0.2.*" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.2-dev" - } + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, + "type": "library", "autoload": { - "psr-0": { - "React\\Http": "" + "psr-4": { + "React\\Cache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Library for building an evented http server.", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", "keywords": [ - "http" + "cache", + "caching", + "promise", + "reactphp" ], - "time": "2012-12-26T16:33:04+00:00" + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" }, { - "name": "react/socket", - "version": "v0.2.6", - "target-dir": "React/Socket", + "name": "react/dns", + "version": "v1.13.0", "source": { "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "21e3fe670b2f18e3c6b2cb73f14e1c58fe7bca84" + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/21e3fe670b2f18e3c6b2cb73f14e1c58fe7bca84", - "reference": "21e3fe670b2f18e3c6b2cb73f14e1c58fe7bca84", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", "shasum": "" }, "require": { - "evenement/evenement": "1.0.*", - "php": ">=5.3.3", - "react/event-loop": "0.2.*", - "react/stream": "0.2.*" + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.2-dev" - } + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, + "type": "library", "autoload": { - "psr-0": { - "React\\Socket": "" + "psr-4": { + "React\\Dns\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Library for building an evented socket server.", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", "keywords": [ - "Socket" + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } ], - "time": "2012-12-14T00:58:14+00:00" + "time": "2024-06-13T14:18:03+00:00" }, { - "name": "react/stream", - "version": "v0.2.6", - "target-dir": "React/Stream", + "name": "react/event-loop", + "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "34c1059cffb44873440135e9a86fe9ae9caf5acd" + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/34c1059cffb44873440135e9a86fe9ae9caf5acd", - "reference": "34c1059cffb44873440135e9a86fe9ae9caf5acd", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { - "evenement/evenement": "1.0.*", - "php": ">=5.3.3" + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "react/event-loop": "0.2.*", - "react/promise": "1.0.*" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.2-dev" - } - }, "autoload": { - "psr-0": { - "React\\Stream": "" + "psr-4": { + "React\\EventLoop\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Basic readable and writable stream interfaces that support piping.", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ - "pipe", - "stream" + "asynchronous", + "event-loop" ], - "time": "2012-12-14T00:58:14+00:00" + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" }, { - "name": "sculpin/sculpin", - "version": "dev-master", + "name": "react/http", + "version": "v1.11.0", "source": { "type": "git", - "url": "https://github.com/sculpin/sculpin.git", - "reference": "3edffeac053b3e57c02538396628559d7cd870e4" + "url": "https://github.com/reactphp/http.git", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sculpin/sculpin/zipball/3edffeac053b3e57c02538396628559d7cd870e4", - "reference": "3edffeac053b3e57c02538396628559d7cd870e4", + "url": "https://api.github.com/repos/reactphp/http/zipball/8db02de41dcca82037367f67a2d4be365b1c4db9", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9", "shasum": "" }, "require": { - "dflydev/ant-path-matcher": "1.*", - "dflydev/apache-mime-types": "~1.0,>=1.0.1", - "dflydev/canal": "1.*", - "dflydev/dot-access-configuration": "1.*", - "dflydev/embedded-composer": "^1.0@dev", - "dflydev/symfony-finder-factory": "1.*", - "doctrine/inflector": "1.0.*", - "guzzle/guzzle": "~3.0", - "michelf/php-markdown": "~1.5.0", - "netcarver/textile": "3.5.*", - "php": "^5.4|^7.0", - "react/http": "0.2.*", - "sculpin/sculpin-theme-composer-plugin": "1.0.*@dev", - "seld/jsonlint": "^1.4", - "symfony/class-loader": "~2.3", - "symfony/config": "~2.3", - "symfony/console": "~2.3", - "symfony/dependency-injection": "~2.3", - "symfony/event-dispatcher": "~2.3", - "symfony/filesystem": "~2.3", - "symfony/finder": "~2.3", - "symfony/http-kernel": "~2.3", - "symfony/process": "~2.3", - "symfony/yaml": "~2.3", - "twig/extensions": "~1.0", - "twig/twig": "~1.9" - }, - "replace": { - "sculpin/core": "self.version", - "sculpin/markdown-bundle": "self.version", - "sculpin/markdown-twig-compat-bundle": "self.version", - "sculpin/posts-bundle": "self.version", - "sculpin/proxy-source-collection-contrib": "self.version", - "sculpin/sculpin-bundle": "self.version", - "sculpin/standalone-bundle": "self.version", - "sculpin/taxonomy-contrib": "self.version", - "sculpin/textile-bundle": "self.version", - "sculpin/twig-bundle": "self.version" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", + "php": ">=5.3.0", + "psr/http-message": "^1.0", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "3.7.*", - "symfony/css-selector": "~2.6", - "symfony/dom-crawler": "~2.6" + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.2 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, - "bin": [ - "bin/sculpin", - "bin/sculpin.php" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1.x-dev" - } - }, "autoload": { "psr-4": { - "Sculpin\\": "src/Sculpin/" + "React\\Http\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1727,86 +1909,157 @@ ], "authors": [ { - "name": "Dragonfly Development Inc.", - "email": "info@dflydev.com", - "homepage": "http://dflydev.com" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Beau Simensen", - "email": "beau@dflydev.com", - "homepage": "http://beausimensen.com" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Static Site Generator", - "homepage": "https://sculpin.io", + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", "keywords": [ - "generator", - "site", - "static" + "async", + "client", + "event-driven", + "http", + "http client", + "http server", + "https", + "psr-7", + "reactphp", + "server", + "streaming" + ], + "support": { + "issues": "https://github.com/reactphp/http/issues", + "source": "https://github.com/reactphp/http/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } ], - "time": "2016-11-30 14:03:55" + "time": "2024-11-20T15:24:08+00:00" }, { - "name": "sculpin/sculpin-theme-composer-plugin", - "version": "v1.0.1", + "name": "react/promise", + "version": "v3.2.0", "source": { "type": "git", - "url": "https://github.com/sculpin/sculpin-theme-composer-plugin.git", - "reference": "8b5bb152bd7d4516edb3493a9cf3204ed99e9ef5" + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sculpin/sculpin-theme-composer-plugin/zipball/8b5bb152bd7d4516edb3493a9cf3204ed99e9ef5", - "reference": "8b5bb152bd7d4516edb3493a9cf3204ed99e9ef5", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0" + "php": ">=7.1.0" }, - "type": "composer-plugin", - "extra": { - "class": "Sculpin\\Composer\\SculpinThemePlugin\\SculpinThemePlugin", - "branch-alias": { - "dev-master": "1.0.x-dev" - } + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, + "type": "library", "autoload": { - "psr-0": { - "Sculpin\\Composer\\SculpinThemePlugin\\": "src" + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "time": "2016-05-24T19:51:53+00:00" + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" }, { - "name": "seld/cli-prompt", - "version": "1.0.2", + "name": "react/socket", + "version": "v1.16.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/cli-prompt.git", - "reference": "8cbe10923cae5bcd7c5a713f6703fc4727c8c1b4" + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/8cbe10923cae5bcd7c5a713f6703fc4727c8c1b4", - "reference": "8cbe10923cae5bcd7c5a713f6703fc4727c8c1b4", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", "shasum": "" }, "require": { - "php": ">=5.3" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, + "type": "library", "autoload": { "psr-4": { - "Seld\\CliPrompt\\": "src/" + "React\\Socket\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1815,44 +2068,73 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", "keywords": [ - "cli", - "console", - "hidden", - "input", - "prompt" + "Connection", + "Socket", + "async", + "reactphp", + "stream" ], - "time": "2016-04-18T09:31:41+00:00" + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" }, { - "name": "seld/jsonlint", - "version": "1.5.0", + "name": "react/stream", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "19495c181d6d53a0a13414154e52817e3b504189" + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/19495c181d6d53a0a13414154e52817e3b504189", - "reference": "19495c181d6d53a0a13414154e52817e3b504189", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, - "bin": [ - "bin/jsonlint" - ], "type": "library", "autoload": { "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" + "React\\Stream\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1861,9 +2143,237 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sculpin/sculpin", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sculpin/sculpin.git", + "reference": "5f705d845b2dc980ed91b79c49ccaa5f64cbdda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sculpin/sculpin/zipball/5f705d845b2dc980ed91b79c49ccaa5f64cbdda0", + "reference": "5f705d845b2dc980ed91b79c49ccaa5f64cbdda0", + "shasum": "" + }, + "require": { + "dflydev/ant-path-matcher": "^1.0.3", + "dflydev/apache-mime-types": "^1.0.1", + "dflydev/canal": "^1.0", + "dflydev/dot-access-configuration": "^1.0.3", + "doctrine/inflector": "^1.3", + "ext-mbstring": "*", + "michelf/php-markdown": "^1.9", + "netcarver/textile": "^3.6", + "php": "^8.0 || ^7.3", + "react/http": "^1.0", + "sculpin/sculpin-theme-composer-plugin": "^1.0", + "symfony/config": "^4.4.13", + "symfony/console": "^4.4.13", + "symfony/dependency-injection": "^4.4.13", + "symfony/event-dispatcher": "^4.4.13", + "symfony/filesystem": "^4.4.13", + "symfony/finder": "^4.4.13", + "symfony/http-kernel": "^4.4.13", + "symfony/yaml": "^4.4.13", + "twig/twig": "^2.5", + "webignition/internet-media-type": "^0.4.8" + }, + "replace": { + "sculpin/core": "self.version", + "sculpin/markdown-bundle": "self.version", + "sculpin/markdown-twig-compat-bundle": "self.version", + "sculpin/posts-bundle": "self.version", + "sculpin/proxy-source-collection-contrib": "self.version", + "sculpin/sculpin-bundle": "self.version", + "sculpin/standalone-bundle": "self.version", + "sculpin/taxonomy-contrib": "self.version", + "sculpin/textile-bundle": "self.version", + "sculpin/twig-bundle": "self.version" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^9.4", + "squizlabs/php_codesniffer": "^3.3", + "symfony/css-selector": "^4.1", + "symfony/dom-crawler": "^4.1", + "symfony/process": "^4.1" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/sculpin", + "bin/sculpin.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sculpin\\": "src/Sculpin/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + } + ], + "description": "Static Site Generator", + "homepage": "https://sculpin.io", + "keywords": [ + "generator", + "site", + "static" + ], + "support": { + "issues": "https://github.com/sculpin/sculpin/issues", + "source": "https://github.com/sculpin/sculpin/tree/3.2.0" + }, + "time": "2022-10-31T19:34:13+00:00" + }, + { + "name": "sculpin/sculpin-theme-composer-plugin", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sculpin/sculpin-theme-composer-plugin.git", + "reference": "e3f4e1d6a10368709d07933f8391ef7e534c5db4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sculpin/sculpin-theme-composer-plugin/zipball/e3f4e1d6a10368709d07933f8391ef7e534c5db4", + "reference": "e3f4e1d6a10368709d07933f8391ef7e534c5db4", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1|^2.0" + }, + "require-dev": { + "composer/composer": "*" + }, + "type": "composer-plugin", + "extra": { + "class": "Sculpin\\Composer\\SculpinThemePlugin\\SculpinThemePlugin", + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Sculpin\\Composer\\SculpinThemePlugin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Plugin for theming sculpin sites", + "support": { + "issues": "https://github.com/sculpin/sculpin-theme-composer-plugin/issues", + "source": "https://github.com/sculpin/sculpin-theme-composer-plugin/tree/1.0.3" + }, + "time": "2020-10-29T13:20:43+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" } ], "description": "JSON Linter", @@ -1873,20 +2383,34 @@ "parser", "validator" ], - "time": "2016-11-14T17:59:58+00:00" + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2024-07-11T14:55:45+00:00" }, { "name": "seld/phar-utils", - "version": "1.0.1", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a" + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a", - "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", "shasum": "" }, "require": { @@ -1915,40 +2439,142 @@ ], "description": "PHAR file format utilities, for when PHP phars you up", "keywords": [ - "phra" + "phar" ], - "time": "2015-10-13T18:44:15+00:00" + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" }, { - "name": "symfony/class-loader", - "version": "v2.8.14", + "name": "symfony/config", + "version": "v4.4.44", "source": { "type": "git", - "url": "https://github.com/symfony/class-loader.git", - "reference": "db9c33f62d3cdfb19e3c8de477c8457eab0d3176" + "url": "https://github.com/symfony/config.git", + "reference": "ed42f8f9da528d2c6cae36fe1f380b0c1d8f0658" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/db9c33f62d3cdfb19e3c8de477c8457eab0d3176", - "reference": "db9c33f62d3cdfb19e3c8de477c8457eab0d3176", + "url": "https://api.github.com/repos/symfony/config/zipball/ed42f8f9da528d2c6cae36fe1f380b0c1d8f0658", + "reference": "ed42f8f9da528d2c6cae36fe1f380b0c1d8f0658", "shasum": "" }, "require": { - "php": ">=5.3.9", - "symfony/polyfill-apcu": "~1.1" + "php": ">=7.1.3", + "symfony/filesystem": "^3.4|^4.0|^5.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "conflict": { + "symfony/finder": "<3.4" }, "require-dev": { - "symfony/finder": "~2.0,>=2.0.5|~3.0.0" + "symfony/event-dispatcher": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/messenger": "^4.1|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } + ], + "time": "2022-07-20T09:59:04+00:00" + }, + { + "name": "symfony/console", + "version": "v4.4.49", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "33fa45ffc81fdcc1ca368d4946da859c8cdb58d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/33fa45ffc81fdcc1ca368d4946da859c8cdb58d9", + "reference": "33fa45ffc81fdcc1ca368d4946da859c8cdb58d9", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<3.4", + "symfony/event-dispatcher": "<4.3|>=5", + "symfony/lock": "<4.4", + "symfony/process": "<3.3" }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^4.3", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/var-dumper": "^4.3|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\ClassLoader\\": "" + "Symfony\\Component\\Console\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -1968,40 +2594,503 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony ClassLoader Component", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", - "time": "2016-11-15T12:06:39+00:00" + "support": { + "source": "https://github.com/symfony/console/tree/v4.4.49" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-05T17:10:16+00:00" }, { - "name": "symfony/config", - "version": "v2.8.14", + "name": "symfony/debug", + "version": "v4.4.44", "source": { "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "1361bc4e66f97b6202ae83f4190e962c624b5e61" + "url": "https://github.com/symfony/debug.git", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "abandoned": "symfony/error-handler", + "time": "2022-07-28T16:29:46+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v4.4.49", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "9065fe97dbd38a897e95ea254eb5ddfe1310f734" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9065fe97dbd38a897e95ea254eb5ddfe1310f734", + "reference": "9065fe97dbd38a897e95ea254eb5ddfe1310f734", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/container": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1.6|^2" + }, + "conflict": { + "symfony/config": "<4.3|>=5.0", + "symfony/finder": "<3.4", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<4.4.26" + }, + "provide": { + "psr/container-implementation": "1.0", + "symfony/service-implementation": "1.0|2.0" + }, + "require-dev": { + "symfony/config": "^4.3", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/yaml": "^4.4.26|^5.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v4.4.49" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-16T16:18:09+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v4.4.44", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "be731658121ef2d8be88f3a1ec938148a9237291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/be731658121ef2d8be88f3a1ec938148a9237291", + "reference": "be731658121ef2d8be88f3a1ec938148a9237291", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3", + "symfony/debug": "^4.4.5", + "symfony/var-dumper": "^4.4|^5.0" + }, + "require-dev": { + "symfony/http-kernel": "^4.4|^5.0", + "symfony/serializer": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-28T16:29:46+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v4.4.44", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1e866e9e5c1b22168e0ce5f0b467f19bba61266a", + "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/event-dispatcher-contracts": "^1.1", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "1.1" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "~3.4|~4.4", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/stopwatch": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T09:59:04+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "761c8b8387cfe5f8026594a75fdf0a4e83ba6974" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/761c8b8387cfe5f8026594a75fdf0a4e83ba6974", + "reference": "761c8b8387cfe5f8026594a75fdf0a4e83ba6974", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "suggest": { + "psr/event-dispatcher": "", + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-20T09:59:04+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v4.4.42", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "815412ee8971209bd4c1eecd5f4f481eacd44bf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/1361bc4e66f97b6202ae83f4190e962c624b5e61", - "reference": "1361bc4e66f97b6202ae83f4190e962c624b5e61", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/815412ee8971209bd4c1eecd5f4f481eacd44bf5", + "reference": "815412ee8971209bd4c1eecd5f4f481eacd44bf5", "shasum": "" }, "require": { - "php": ">=5.3.9", - "symfony/filesystem": "~2.3|~3.0.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\Config\\": "" + "Symfony\\Component\\Filesystem\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2021,48 +3110,49 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Config Component", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "time": "2016-11-03T07:52:58+00:00" + "support": { + "source": "https://github.com/symfony/filesystem/tree/v4.4.42" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-20T08:49:14+00:00" }, { - "name": "symfony/console", - "version": "v2.8.14", + "name": "symfony/finder", + "version": "v4.4.44", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "a871ba00e0f604dceac64c56c27f99fbeaf4854e" + "url": "https://github.com/symfony/finder.git", + "reference": "66bd787edb5e42ff59d3523f623895af05043e4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a871ba00e0f604dceac64c56c27f99fbeaf4854e", - "reference": "a871ba00e0f604dceac64c56c27f99fbeaf4854e", + "url": "https://api.github.com/repos/symfony/finder/zipball/66bd787edb5e42ff59d3523f623895af05043e4f", + "reference": "66bd787edb5e42ff59d3523f623895af05043e4f", "shasum": "" }, "require": { - "php": ">=5.3.9", - "symfony/debug": "~2.7,>=2.7.2|~3.0.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1|~3.0.0", - "symfony/process": "~2.1|~3.0.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/process": "" + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" + "Symfony\\Component\\Finder\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2082,48 +3172,61 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "time": "2016-11-15T23:02:12+00:00" + "support": { + "source": "https://github.com/symfony/finder/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-07-29T07:35:46+00:00" }, { - "name": "symfony/debug", - "version": "v2.8.14", + "name": "symfony/http-client-contracts", + "version": "v2.5.5", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "62a68f640456f6761d752c62d81631428ef0d8a1" + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "48ef1d0a082885877b664332b9427662065a360c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/62a68f640456f6761d752c62d81631428ef0d8a1", - "reference": "62a68f640456f6761d752c62d81631428ef0d8a1", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/48ef1d0a082885877b664332b9427662065a360c", + "reference": "48ef1d0a082885877b664332b9427662065a360c", "shasum": "" }, "require": { - "php": ">=5.3.9", - "psr/log": "~1.0" - }, - "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + "php": ">=7.2.5" }, - "require-dev": { - "symfony/class-loader": "~2.2|~3.0.0", - "symfony/http-kernel": "~2.3.24|~2.5.9|~2.6,>=2.6.2|~3.0.0" + "suggest": { + "symfony/http-client-implementation": "" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-master": "2.8-dev" + "dev-main": "2.5-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Contracts\\HttpClient\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2131,58 +3234,79 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Debug Component", + "description": "Generic abstractions related to HTTP clients", "homepage": "https://symfony.com", - "time": "2016-11-15T12:53:17+00:00" + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v2.5.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-28T08:37:04+00:00" }, { - "name": "symfony/dependency-injection", - "version": "v2.8.14", + "name": "symfony/http-foundation", + "version": "v5.4.48", "source": { "type": "git", - "url": "https://github.com/symfony/dependency-injection.git", - "reference": "9d2c5033ca70ceade8d7584f997a9d3943f0fe5f" + "url": "https://github.com/symfony/http-foundation.git", + "reference": "3f38b8af283b830e1363acd79e5bc3412d055341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9d2c5033ca70ceade8d7584f997a9d3943f0fe5f", - "reference": "9d2c5033ca70ceade8d7584f997a9d3943f0fe5f", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3f38b8af283b830e1363acd79e5bc3412d055341", + "reference": "3f38b8af283b830e1363acd79e5bc3412d055341", "shasum": "" }, "require": { - "php": ">=5.3.9" - }, - "conflict": { - "symfony/expression-language": "<2.6" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" }, "require-dev": { - "symfony/config": "~2.2|~3.0.0", - "symfony/expression-language": "~2.6|~3.0.0", - "symfony/yaml": "~2.3.42|~2.7.14|~2.8.7|~3.0.7" + "predis/predis": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" }, "suggest": { - "symfony/config": "", - "symfony/expression-language": "For using expressions in service container configuration", - "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", - "symfony/yaml": "" + "symfony/mime": "To use the file extension guesser" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\DependencyInjection\\": "" + "Symfony\\Component\\HttpFoundation\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2202,47 +3326,91 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony DependencyInjection Component", + "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", - "time": "2016-11-18T21:10:01+00:00" + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v5.4.48" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T18:58:02+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v2.8.14", + "name": "symfony/http-kernel", + "version": "v4.4.51", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934" + "url": "https://github.com/symfony/http-kernel.git", + "reference": "ad8ab192cb619ff7285c95d28c69b36d718416c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", - "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ad8ab192cb619ff7285c95d28c69b36d718416c7", + "reference": "ad8ab192cb619ff7285c95d28c69b36d718416c7", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=7.1.3", + "psr/log": "^1|^2", + "symfony/error-handler": "^4.4", + "symfony/event-dispatcher": "^4.4", + "symfony/http-client-contracts": "^1.1|^2", + "symfony/http-foundation": "^4.4.30|^5.3.7", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/browser-kit": "<4.3", + "symfony/config": "<3.4", + "symfony/console": ">=5", + "symfony/dependency-injection": "<4.3", + "symfony/translation": "<4.2", + "twig/twig": "<1.43|<2.13,>=2" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~2.0,>=2.0.5|~3.0.0", - "symfony/dependency-injection": "~2.6|~3.0.0", - "symfony/expression-language": "~2.6|~3.0.0", - "symfony/stopwatch": "~2.3|~3.0.0" + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^4.3|^5.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0", + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^4.3|^5.0", + "symfony/dom-crawler": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/templating": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "twig/twig": "^1.43|^2.13|^3.0.4" }, "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" + "Symfony\\Component\\HttpKernel\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2262,40 +3430,64 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony EventDispatcher Component", + "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", - "time": "2016-10-13T01:43:15+00:00" + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v4.4.51" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-10T13:31:29+00:00" }, { - "name": "symfony/filesystem", - "version": "v2.8.14", + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "a3784111af9f95f102b6411548376e1ae7c93898" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/a3784111af9f95f102b6411548376e1ae7c93898", - "reference": "a3784111af9f95f102b6411548376e1ae7c93898", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.8-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2303,48 +3495,78 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Filesystem Component", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", - "time": "2016-10-18T04:28:30+00:00" + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/finder", - "version": "v2.8.14", + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "0023b024363dfc0cd21262e556f25a291fe8d7fd" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0023b024363dfc0cd21262e556f25a291fe8d7fd", - "reference": "0023b024363dfc0cd21262e556f25a291fe8d7fd", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.8-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2352,133 +3574,140 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Finder Component", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", - "time": "2016-11-03T07:52:58+00:00" + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/http-foundation", - "version": "v3.1.7", + "name": "symfony/polyfill-php72", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "5a4c8099a1547fe451256e056180ad4624177017" + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5a4c8099a1547fe451256e056180ad4624177017", - "reference": "5a4c8099a1547fe451256e056180ad4624177017", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce", + "reference": "fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce", "shasum": "" }, "require": { - "php": ">=5.5.9", - "symfony/polyfill-mbstring": "~1.1" + "php": ">=7.2" }, - "require-dev": { - "symfony/expression-language": "~2.8|~3.0" - }, - "type": "library", + "type": "metapackage", "extra": { - "branch-alias": { - "dev-master": "3.1-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony HttpFoundation Component", + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", "homepage": "https://symfony.com", - "time": "2016-11-16T22:17:09+00:00" + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/http-kernel", - "version": "v2.8.14", + "name": "symfony/polyfill-php73", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "29d9bb59c6d895e65d8fea3859c96c3b6e9368ba" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/29d9bb59c6d895e65d8fea3859c96c3b6e9368ba", - "reference": "29d9bb59c6d895e65d8fea3859c96c3b6e9368ba", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=5.3.9", - "psr/log": "~1.0", - "symfony/debug": "~2.6,>=2.6.2", - "symfony/event-dispatcher": "~2.6,>=2.6.7|~3.0.0", - "symfony/http-foundation": "~2.7.20|~2.8.13|~3.1.6" - }, - "conflict": { - "symfony/config": "<2.7" - }, - "require-dev": { - "symfony/browser-kit": "~2.3|~3.0.0", - "symfony/class-loader": "~2.1|~3.0.0", - "symfony/config": "~2.8", - "symfony/console": "~2.3|~3.0.0", - "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0", - "symfony/dependency-injection": "~2.8|~3.0.0", - "symfony/dom-crawler": "~2.0,>=2.0.5|~3.0.0", - "symfony/expression-language": "~2.4|~3.0.0", - "symfony/finder": "~2.0,>=2.0.5|~3.0.0", - "symfony/process": "~2.0,>=2.0.5|~3.0.0", - "symfony/routing": "~2.8|~3.0.0", - "symfony/stopwatch": "~2.3|~3.0.0", - "symfony/templating": "~2.2|~3.0.0", - "symfony/translation": "~2.0,>=2.0.5|~3.0.0", - "symfony/var-dumper": "~2.6|~3.0.0" - }, - "suggest": { - "symfony/browser-kit": "", - "symfony/class-loader": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "", - "symfony/finder": "", - "symfony/var-dumper": "" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.8-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" + "Symfony\\Polyfill\\Php73\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2487,44 +3716,74 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Symfony HttpKernel Component", - "homepage": "https://symfony.com", - "time": "2016-11-21T02:24:42+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-apcu", - "version": "v1.3.0", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-apcu.git", - "reference": "5d4474f447403c3348e37b70acc2b95475b7befa" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-apcu/zipball/5d4474f447403c3348e37b70acc2b95475b7befa", - "reference": "5d4474f447403c3348e37b70acc2b95475b7befa", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.3-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { "files": [ "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2532,6 +3791,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -2541,49 +3804,66 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting apcu_* functions to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "apcu", "compatibility", "polyfill", "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.3.0", + "name": "symfony/polyfill-php81", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.3-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2600,40 +3880,52 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", "polyfill", "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v2.8.14", + "version": "v4.4.44", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f" + "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f", - "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f", + "url": "https://api.github.com/repos/symfony/process/zipball/5cee9cdc4f7805e2699d9fd66991a0e6df8252a2", + "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -2656,36 +3948,155 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "time": "2016-09-29T14:03:54+00:00" + "support": { + "source": "https://github.com/symfony/process/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-06-27T13:16:42+00:00" }, { - "name": "symfony/yaml", - "version": "v2.8.14", + "name": "symfony/service-contracts", + "version": "v2.5.4", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "befb26a3713c97af90d25dd12e75621ef14d91ff" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/befb26a3713c97af90d25dd12e75621ef14d91ff", - "reference": "befb26a3713c97af90d25dd12e75621ef14d91ff", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-master": "2.8-dev" + "dev-main": "2.5-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Yaml\\": "" + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v5.4.48", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "42f18f170aa86d612c3559cfb3bd11a375df32c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/42f18f170aa86d612c3559cfb3bd11a375df32c8", + "reference": "42f18f170aa86d612c3559cfb3bd11a375df32c8", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/console": "<4.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/uid": "^5.1|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2697,51 +4108,74 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Yaml Component", + "description": "Provides mechanisms for walking through any arbitrary PHP variable", "homepage": "https://symfony.com", - "time": "2016-11-14T16:15:57+00:00" + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v5.4.48" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-08T15:21:10+00:00" }, { - "name": "twig/extensions", - "version": "v1.4.1", + "name": "symfony/yaml", + "version": "v4.4.45", "source": { "type": "git", - "url": "https://github.com/twigphp/Twig-extensions.git", - "reference": "f0bb8431c8691f5a39f1017d9a5967a082bf01ff" + "url": "https://github.com/symfony/yaml.git", + "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig-extensions/zipball/f0bb8431c8691f5a39f1017d9a5967a082bf01ff", - "reference": "f0bb8431c8691f5a39f1017d9a5967a082bf01ff", + "url": "https://api.github.com/repos/symfony/yaml/zipball/aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d", + "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d", "shasum": "" }, "require": { - "twig/twig": "~1.20|~2.0" + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" }, "require-dev": { - "symfony/translation": "~2.3" + "symfony/console": "^3.4|^4.0|^5.0" }, "suggest": { - "symfony/translation": "Allow the time_diff output to be translated" + "symfony/console": "For validating YAML files using the lint command" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, "autoload": { - "psr-0": { - "Twig_Extensions_": "lib/" - } + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2751,46 +4185,69 @@ { "name": "Fabien Potencier", "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Common additional features for Twig that do not directly belong in core", - "homepage": "http://twig.sensiolabs.org/doc/extensions/index.html", - "keywords": [ - "i18n", - "text" + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v4.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2016-10-25T17:34:14+00:00" + "time": "2022-08-02T15:47:23+00:00" }, { "name": "twig/twig", - "version": "v1.28.2", + "version": "v2.16.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "b22ce0eb070e41f7cba65d78fe216de29726459c" + "reference": "19185947ec75d433a3ac650af32fc05649b95ee1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/b22ce0eb070e41f7cba65d78fe216de29726459c", - "reference": "b22ce0eb070e41f7cba65d78fe216de29726459c", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/19185947ec75d433a3ac650af32fc05649b95ee1", + "reference": "19185947ec75d433a3ac650af32fc05649b95ee1", "shasum": "" }, "require": { - "php": ">=5.2.7" + "php": ">=7.1.3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php72": "^1.8" }, "require-dev": { - "symfony/debug": "~2.7", - "symfony/phpunit-bridge": "~3.2@dev" + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^5.4.9|^6.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.28-dev" + "dev-master": "2.16-dev" } }, "autoload": { "psr-0": { "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2804,51 +4261,115 @@ "homepage": "http://fabien.potencier.org", "role": "Lead Developer" }, + { + "name": "Twig Team", + "role": "Contributors" + }, { "name": "Armin Ronacher", "email": "armin.ronacher@active-4.com", "role": "Project Founder" - }, - { - "name": "Twig Team", - "homepage": "http://twig.sensiolabs.org/contributors", - "role": "Contributors" } ], "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "http://twig.sensiolabs.org", + "homepage": "https://twig.symfony.com", "keywords": [ "templating" ], - "time": "2016-11-23T18:41:40+00:00" + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v2.16.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-09-09T17:53:56+00:00" + }, + { + "name": "webignition/disallowed-character-terminated-string", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/webignition/disallowed-character-terminated-string.git", + "reference": "1c35b8bacbb2e76837c0aa8538dc2468a1f10e6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webignition/disallowed-character-terminated-string/zipball/1c35b8bacbb2e76837c0aa8538dc2468a1f10e6e", + "reference": "1c35b8bacbb2e76837c0aa8538dc2468a1f10e6e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.3", + "phpunit/phpunit": "~8.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "webignition\\DisallowedCharacterTerminatedString\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jon Cram", + "email": "webignition@gmail.com" + } + ], + "description": "A string terminated by one or more disallowed characters", + "homepage": "https://github.com/webignition/disallowed-character-terminated-string", + "keywords": [ + "string", + "terminated" + ], + "support": { + "issues": "https://github.com/webignition/disallowed-character-terminated-string/issues", + "source": "https://github.com/webignition/disallowed-character-terminated-string/tree/master" + }, + "time": "2019-12-20T15:52:44+00:00" }, { "name": "webignition/internet-media-type", - "version": "0.4.7", + "version": "0.4.8", "source": { "type": "git", "url": "https://github.com/webignition/internet-media-type.git", - "reference": "968b95796bc682c7f554c2ec671b6a97a9d5a5b0" + "reference": "1a5bbe38033b00b23acd5e1dd10489bb07eed77c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webignition/internet-media-type/zipball/968b95796bc682c7f554c2ec671b6a97a9d5a5b0", - "reference": "968b95796bc682c7f554c2ec671b6a97a9d5a5b0", + "url": "https://api.github.com/repos/webignition/internet-media-type/zipball/1a5bbe38033b00b23acd5e1dd10489bb07eed77c", + "reference": "1a5bbe38033b00b23acd5e1dd10489bb07eed77c", "shasum": "" }, "require": { - "php": ">=5.3.0", - "webignition/quoted-string": ">=0.1,<1.0", - "webignition/string-parser": ">=0.2.2,<1.0" + "php": ">=5.6.0", + "webignition/quoted-string": ">=0.2.1,<1.0", + "webignition/string-parser": ">=0.2.3,<1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~5.0", + "squizlabs/php_codesniffer": "3.*" }, "type": "library", "autoload": { - "psr-0": { - "webignition\\Tests": "tests/", - "": "src/" + "psr-4": { + "webignition\\InternetMediaType\\": "src/", + "webignition\\Tests\\InternetMediaType\\": "tests/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2870,29 +4391,39 @@ "media type", "media-type" ], - "time": "2015-02-20T16:52:30+00:00" + "support": { + "issues": "https://github.com/webignition/internet-media-type/issues", + "source": "https://github.com/webignition/internet-media-type/tree/master" + }, + "time": "2018-03-12T14:54:00+00:00" }, { "name": "webignition/quoted-string", - "version": "0.1", + "version": "0.2.1", "source": { "type": "git", "url": "https://github.com/webignition/quoted-string.git", - "reference": "43fa25f8c81eaa5aef4c2376703fe90d1449fdf8" + "reference": "88b36b7be067796683ab3668e175322842dd5313" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webignition/quoted-string/zipball/43fa25f8c81eaa5aef4c2376703fe90d1449fdf8", - "reference": "43fa25f8c81eaa5aef4c2376703fe90d1449fdf8", + "url": "https://api.github.com/repos/webignition/quoted-string/zipball/88b36b7be067796683ab3668e175322842dd5313", + "reference": "88b36b7be067796683ab3668e175322842dd5313", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=5.5.0", + "webignition/string-parser": ">=0.2.3,<1" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "3.*" }, "type": "library", "autoload": { - "psr-0": { - "": "src/" + "psr-4": { + "webignition\\QuotedString\\": "src/", + "webignition\\Tests\\QuotedString\\": "tests/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2911,29 +4442,39 @@ "parser", "quoted-string" ], - "time": "2012-08-15T16:52:06+00:00" + "support": { + "issues": "https://github.com/webignition/quoted-string/issues", + "source": "https://github.com/webignition/quoted-string/tree/master" + }, + "time": "2017-05-11T11:41:31+00:00" }, { "name": "webignition/string-parser", - "version": "0.2.2", + "version": "0.2.3", "source": { "type": "git", "url": "https://github.com/webignition/string-parser.git", - "reference": "eaa5a393ef3585783f6ef28558ef5183af00dcf7" + "reference": "8591e28c05bd250bcc67b8001f3588995b9ef74b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webignition/string-parser/zipball/eaa5a393ef3585783f6ef28558ef5183af00dcf7", - "reference": "eaa5a393ef3585783f6ef28558ef5183af00dcf7", + "url": "https://api.github.com/repos/webignition/string-parser/zipball/8591e28c05bd250bcc67b8001f3588995b9ef74b", + "reference": "8591e28c05bd250bcc67b8001f3588995b9ef74b", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=5.3.0", + "webignition/disallowed-character-terminated-string": ">=1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "3.*" }, "type": "library", "autoload": { - "psr-0": { - "": "src/" + "psr-4": { + "webignition\\StringParser\\": "src/", + "webignition\\Tests\\StringParser\\": "tests/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2952,74 +4493,97 @@ "parser", "string" ], - "time": "2014-04-23T09:31:03+00:00" + "support": { + "issues": "https://github.com/webignition/string-parser/issues", + "source": "https://github.com/webignition/string-parser/tree/master" + }, + "time": "2017-05-11T10:04:12+00:00" }, { - "name": "webuni/commonmark-table-extension", - "version": "0.6.0", + "name": "wikimedia/less.php", + "version": "v3.2.1", "source": { "type": "git", - "url": "https://github.com/webuni/commonmark-table-extension.git", - "reference": "cd4e3753a2693235f67493c0c2a2b8e4434dc6ee" + "url": "https://github.com/wikimedia/less.php.git", + "reference": "0d5b30ba792bdbf8991a646fc9c30561b38a5559" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webuni/commonmark-table-extension/zipball/cd4e3753a2693235f67493c0c2a2b8e4434dc6ee", - "reference": "cd4e3753a2693235f67493c0c2a2b8e4434dc6ee", + "url": "https://api.github.com/repos/wikimedia/less.php/zipball/0d5b30ba792bdbf8991a646fc9c30561b38a5559", + "reference": "0d5b30ba792bdbf8991a646fc9c30561b38a5559", "shasum": "" }, "require": { - "league/commonmark": "^0.14|^0.15", - "php": "^5.5|^7.0" + "php": ">=7.2.9" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^1.9", - "phpunit/phpunit": "^4.3|^5.0", - "sllh/php-cs-fixer-styleci-bridge": "^2.0", - "symfony/var-dumper": "^2.7|^3.0" + "mediawiki/mediawiki-codesniffer": "40.0.1", + "mediawiki/mediawiki-phan-config": "0.12.0", + "mediawiki/minus-x": "1.1.1", + "php-parallel-lint/php-console-highlighter": "1.0.0", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpunit/phpunit": "^8.5" }, + "bin": [ + "bin/lessc" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.7-dev" - } - }, "autoload": { - "psr-4": { - "Webuni\\CommonMark\\TableExtension\\": "src" - } + "psr-0": { + "Less": "lib/" + }, + "classmap": [ + "lessc.inc.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Martin Hasoň", - "email": "martin.hason@gmail.com", - "role": "Lead Developer" + "name": "Timo Tijhof", + "homepage": "https://timotijhof.net" + }, + { + "name": "Josh Schmidt", + "homepage": "https://github.com/oyejorge" + }, + { + "name": "Matt Agar", + "homepage": "https://github.com/agar" + }, + { + "name": "Martin Jantošovič", + "homepage": "https://github.com/Mordred" } ], - "description": "The table extension for CommonMark PHP implementation", - "homepage": "https://github.com/webuni/commonmark-table-extension", + "description": "PHP port of the LESS processor", + "homepage": "https://gerrit.wikimedia.org/g/mediawiki/libs/less.php", "keywords": [ - "commonmark", - "markdown", - "table" + "css", + "less", + "less.js", + "lesscss", + "php", + "stylesheet" ], - "time": "2016-09-26T07:40:08+00:00" + "support": { + "issues": "https://github.com/wikimedia/less.php/issues", + "source": "https://github.com/wikimedia/less.php/tree/v3.2.1" + }, + "time": "2023-02-03T06:43:41+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": { - "sculpin/sculpin": 20, - "dflydev/embedded-composer": 20, - "mavimo/sculpin-redirect-bundle": 20 + "dflydev/embedded-composer": 20 }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/contrib/package_plugins.sh b/contrib/package_plugins.sh index 3f39eab8b1..a18b1c9da9 100755 --- a/contrib/package_plugins.sh +++ b/contrib/package_plugins.sh @@ -42,4 +42,35 @@ for plugindir in $(find "${__DIR__}/../exampleplugins/5.2/" -mindepth 1 -maxdept cd "${tmpdir}" zip -rq "${outputdir}/${pluginname}.zip" . -done \ No newline at end of file +done + + +for plugindir in $(find "${__DIR__}/../exampleplugins/b2b/" -mindepth 1 -maxdepth 1 -type d); do + rm -rf "${tmpdir}" + + pluginname="$(basename "$plugindir")" + parentdir="$(dirname "$plugindir")" + + echo "Plugin: $pluginname " + + mkdir -p "${tmpdir}/"; + cp -r "${plugindir}" "${tmpdir}/" + + cd "${tmpdir}" + zip -rq "${outputdir}/${pluginname}.zip" . +done + +for plugindir in $(find "${__DIR__}/../exampleplugins/search/" -mindepth 1 -maxdepth 1 -type d); do + rm -rf "${tmpdir}" + + pluginname="$(basename "$plugindir")" + parentdir="$(dirname "$plugindir")" + + echo "Plugin: $pluginname " + + mkdir -p "${tmpdir}/"; + cp -r "${plugindir}" "${tmpdir}/" + + cd "${tmpdir}" + zip -rq "${outputdir}/${pluginname}.zip" . +done diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 75d31943bc..0000000000 --- a/deploy.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -o nounset -set -o errexit -set -o pipefail - -openssl aes-256-cbc -K $encrypted_8d2e99642ba8_key -iv $encrypted_8d2e99642ba8_iv -in deploy_key.enc -out deploy_key -d -chmod 600 deploy_key -echo -e "Host developers.shopware.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config -eval `ssh-agent -s` -ssh-add deploy_key - -git clone https://github.com/shopware/devdocs.git -b styletile output_prod/styletile -rsync -avze ssh --rsync-path="sudo rsync" --exclude=".git" --delete --exclude styletile/ output_prod/ shopware@developers.shopware.com:/home/shopware/www/developers.shopware.com diff --git a/deploy_key.enc b/deploy_key.enc deleted file mode 100644 index 8a5bcb0ce3..0000000000 Binary files a/deploy_key.enc and /dev/null differ diff --git a/exampleplugins/5.2/SeoExample/Controllers/Backend/Glossary.php b/exampleplugins/5.2/SeoExample/Controllers/Backend/Glossary.php new file mode 100644 index 0000000000..0d1255677b --- /dev/null +++ b/exampleplugins/5.2/SeoExample/Controllers/Backend/Glossary.php @@ -0,0 +1,38 @@ +Request()->getParam('shopId'); + + $offset = $this->Request()->getParam('offset'); + $limit = $this->Request()->getParam('limit', 50); + + /** @var Shopware_Components_SeoIndex $seoIndex */ + $seoIndex = $this->container->get('seoindex'); + $seoIndex->registerShop($shopId); + + /** @var sRewriteTable $rewriteTableModule */ + $rewriteTableModule = $this->container->get('modules')->RewriteTable(); + $rewriteTableModule->baseSetup(); + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + $words = $dbalQueryBuilder->select('glossary.id, glossary.word') + ->from('s_glossary', 'glossary') + ->setMaxResults($limit) + ->setFirstResult($offset) + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + foreach ($words as $wordId => $word) { + $rewriteTableModule->sInsertUrl('sViewport=glossary&sAction=detail&wordId=' . $wordId, 'glossary/' . $word); + } + + $this->View()->assign(['success' => true]); + } +} diff --git a/exampleplugins/5.2/SeoExample/Controllers/Frontend/Glossary.php b/exampleplugins/5.2/SeoExample/Controllers/Frontend/Glossary.php new file mode 100644 index 0000000000..8a17005226 --- /dev/null +++ b/exampleplugins/5.2/SeoExample/Controllers/Frontend/Glossary.php @@ -0,0 +1,40 @@ +container->getParameter('seo_example.plugin_dir'); + $this->View()->addTemplateDir($pluginBasePath . '/Resources/views'); + } + + public function indexAction() + { + /** @var \Doctrine\DBAL\Query\QueryBuilder $queryBuilder */ + $queryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + + $wordData = $queryBuilder->select('glossary.word, glossary.description') + ->from('s_glossary', 'glossary') + ->execute() + ->fetchAll(); + + $this->View()->assign('words', $wordData); + } + + public function detailAction() + { + $wordId = $this->Request()->getParam('wordId'); + + /** @var \Doctrine\DBAL\Query\QueryBuilder $queryBuilder */ + $queryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + + $wordData = $queryBuilder->select('glossary.word, glossary.description') + ->from('s_glossary', 'glossary') + ->where('glossary.id = :id') + ->setParameter(':id', $wordId) + ->execute() + ->fetch(); + + $this->View()->assign($wordData); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample/Resources/views/backend/performance/view/glossary.js b/exampleplugins/5.2/SeoExample/Resources/views/backend/performance/view/glossary.js new file mode 100644 index 0000000000..b0e1a9eaa3 --- /dev/null +++ b/exampleplugins/5.2/SeoExample/Resources/views/backend/performance/view/glossary.js @@ -0,0 +1,20 @@ + +//{block name="backend/performance/view/main/multi_request_tasks" append} +Ext.define('Shopware.apps.Performance.view.main.Glossary', { + override: 'Shopware.apps.Performance.view.main.MultiRequestTasks', + + initComponent: function() { + this.addProgressBar( + { + initialText: 'Glossary URLs', + progressText: '[0] of [1] glossary URLs', + requestUrl: '{url controller=glossary action=generateSeoUrl}' + }, + 'glossary', + 'seo' + ); + + this.callParent(arguments); + } +}); +//{/block} diff --git a/exampleplugins/5.2/SeoExample/Resources/views/frontend/glossary/detail.tpl b/exampleplugins/5.2/SeoExample/Resources/views/frontend/glossary/detail.tpl new file mode 100644 index 0000000000..b81330b9a2 --- /dev/null +++ b/exampleplugins/5.2/SeoExample/Resources/views/frontend/glossary/detail.tpl @@ -0,0 +1,7 @@ +{extends file="parent:frontend/glossary/index.tpl"} + +{block name="frontend_index_content"} +

{$word|ucfirst}

+ +

{$description}

+{/block} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample/Resources/views/frontend/glossary/index.tpl b/exampleplugins/5.2/SeoExample/Resources/views/frontend/glossary/index.tpl new file mode 100644 index 0000000000..383f993a64 --- /dev/null +++ b/exampleplugins/5.2/SeoExample/Resources/views/frontend/glossary/index.tpl @@ -0,0 +1,12 @@ +{extends file="parent:frontend/index/index.tpl"} + +{block name="frontend_index_content_left"}{/block} + +{block name="frontend_index_content"} + {foreach $words as $wordData} +
+ {$wordData['word']} +
{$wordData['description']}
+
+ {/foreach} +{/block} diff --git a/exampleplugins/5.2/SeoExample/SeoExample.php b/exampleplugins/5.2/SeoExample/SeoExample.php new file mode 100644 index 0000000000..167dcc7d1c --- /dev/null +++ b/exampleplugins/5.2/SeoExample/SeoExample.php @@ -0,0 +1,113 @@ + 'createGlossaryRewriteTable', + 'sRewriteTable::sCreateRewriteTable::after' => 'createGlossaryRewriteTable', + 'Enlight_Controller_Action_PostDispatch_Backend_Performance' => 'loadPerformanceExtension', + 'Shopware_Controllers_Seo_filterCounts' => 'addGlossaryCount', + 'Shopware_Components_RewriteGenerator_FilterQuery' => 'filterParameterQuery' + ]; + } + + public function install(InstallContext $context) + { + /** @var Connection $dbalConnection */ + $dbalConnection = $this->container->get('dbal_connection'); + $dbalConnection->exec( + 'CREATE TABLE IF NOT EXISTS`s_glossary` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `word` varchar(50) COLLATE utf8_unicode_ci NOT NULL, + `description` longtext COLLATE utf8_unicode_ci NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci' + ); + + $dbalConnection->exec('INSERT IGNORE INTO `s_glossary` (`word`, `description`) VALUES + (\'Foobar\', \'The terms foobar (/ˈfuːbɑːr/), or foo and others are used as placeholder names (also referred to as metasyntactic variables) in computer programming or computer-related documentation. They have been used to name entities such as variables, functions, and commands whose exact identity is unimportant and serve only to demonstrate a concept.\'), + (\'Recursion\', \'Recursion occurs when a thing is defined in terms of itself or of its type. Recursion is used in a variety of disciplines ranging from linguistics to logic. The most common application of recursion is in mathematics and computer science, where a function being defined is applied within its own definition. While this apparently defines an infinite number of instances (function values), it is often done in such a way that no loop or infinite chain of references can occur.\');'); + + parent::install($context); + } + + public function uninstall(UninstallContext $context) + { + if (!$context->keepUserData()) { + /** @var Connection $dbalConnection */ + $dbalConnection = $this->container->get('dbal_connection'); + $dbalConnection->exec('DROP TABLE s_glossary'); + } + + parent::uninstall($context); + } + + public function createGlossaryRewriteTable() + { + /** @var \sRewriteTable $rewriteTableModule */ + $rewriteTableModule = Shopware()->Container()->get('modules')->sRewriteTable(); + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + + $words = $dbalQueryBuilder->select('glossary.id, glossary.word') + ->from('s_glossary', 'glossary') + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + foreach ($words as $wordId => $word) { + $rewriteTableModule->sInsertUrl('sViewport=glossary&sAction=detail&wordId=' . $wordId, 'glossary/' . $word); + } + } + + public function loadPerformanceExtension(\Enlight_Controller_ActionEventArgs $args) + { + $subject = $args->getSubject(); + $request = $subject->Request(); + + if ($request->getActionName() !== 'load') { + return; + } + + $subject->View()->addTemplateDir($this->getPath() . '/Resources/views/'); + $subject->View()->extendsTemplate('backend/performance/view/glossary.js'); + } + + public function addGlossaryCount(\Enlight_Event_EventArgs $args) + { + $counts = $args->getReturn(); + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + $wordsCount = $dbalQueryBuilder->select('COUNT(glossary.id)') + ->from('s_glossary', 'glossary') + ->execute() + ->fetchAll(\PDO::FETCH_COLUMN); + + $counts['glossary'] = $wordsCount; + + return $counts; + } + + public function filterParameterQuery(\Enlight_Event_EventArgs $args) + { + $orgQuery = $args->getReturn(); + $query = $args->getQuery(); + + if ($query['controller'] === 'glossary' && isset($query['wordId'])) { + $orgQuery['wordId'] = $query['wordId']; + } + + return $orgQuery; + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample/plugin.xml b/exampleplugins/5.2/SeoExample/plugin.xml new file mode 100644 index 0000000000..f3bc5a212b --- /dev/null +++ b/exampleplugins/5.2/SeoExample/plugin.xml @@ -0,0 +1,13 @@ + + + + + + + 1.0.0 + (c) shopware AG + shopware AG + + + diff --git a/exampleplugins/5.2/SeoExample52/Controllers/Backend/Glossary.php b/exampleplugins/5.2/SeoExample52/Controllers/Backend/Glossary.php new file mode 100644 index 0000000000..0d1255677b --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/Controllers/Backend/Glossary.php @@ -0,0 +1,38 @@ +Request()->getParam('shopId'); + + $offset = $this->Request()->getParam('offset'); + $limit = $this->Request()->getParam('limit', 50); + + /** @var Shopware_Components_SeoIndex $seoIndex */ + $seoIndex = $this->container->get('seoindex'); + $seoIndex->registerShop($shopId); + + /** @var sRewriteTable $rewriteTableModule */ + $rewriteTableModule = $this->container->get('modules')->RewriteTable(); + $rewriteTableModule->baseSetup(); + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + $words = $dbalQueryBuilder->select('glossary.id, glossary.word') + ->from('s_glossary', 'glossary') + ->setMaxResults($limit) + ->setFirstResult($offset) + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + foreach ($words as $wordId => $word) { + $rewriteTableModule->sInsertUrl('sViewport=glossary&sAction=detail&wordId=' . $wordId, 'glossary/' . $word); + } + + $this->View()->assign(['success' => true]); + } +} diff --git a/exampleplugins/5.2/SeoExample52/Controllers/Frontend/Glossary.php b/exampleplugins/5.2/SeoExample52/Controllers/Frontend/Glossary.php new file mode 100644 index 0000000000..569a450c13 --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/Controllers/Frontend/Glossary.php @@ -0,0 +1,40 @@ +container->getParameter('seo_example52.plugin_dir'); + $this->View()->addTemplateDir($pluginBasePath . '/Resources/views'); + } + + public function indexAction() + { + /** @var \Doctrine\DBAL\Query\QueryBuilder $queryBuilder */ + $queryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + + $wordData = $queryBuilder->select('glossary.word, glossary.description') + ->from('s_glossary', 'glossary') + ->execute() + ->fetchAll(); + + $this->View()->assign('words', $wordData); + } + + public function detailAction() + { + $wordId = $this->Request()->getParam('wordId'); + + /** @var \Doctrine\DBAL\Query\QueryBuilder $queryBuilder */ + $queryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + + $wordData = $queryBuilder->select('glossary.word, glossary.description') + ->from('s_glossary', 'glossary') + ->where('glossary.id = :id') + ->setParameter(':id', $wordId) + ->execute() + ->fetch(); + + $this->View()->assign($wordData); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample52/Resources/views/backend/performance/controller/glossary.js b/exampleplugins/5.2/SeoExample52/Resources/views/backend/performance/controller/glossary.js new file mode 100644 index 0000000000..936b49c4c3 --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/Resources/views/backend/performance/controller/glossary.js @@ -0,0 +1,53 @@ + +//{block name="backend/performance/controller/multi_request" append} +Ext.define('Shopware.apps.Performance.controller.Glossary', { + override: 'Shopware.apps.Performance.controller.MultiRequest', + + init: function () { + var me = this; + + me.requestConfig.seo.requestUrls.glossary = '{url controller="glossary" action="generateSeoUrl"}'; + + me.callParent(arguments); + }, + + updateProgressBars: function (window) { + var me = this, + taskConfig = window.taskConfig; + + me.window = window; + + me.callParent(arguments); + + if (!Ext.isEmpty(taskConfig.totalCounts.glossary)) { + window.glossaryProgress.updateProgress( + 0, Ext.String.format(window.snippets[taskConfig.snippetResource].glossary, 0, taskConfig.totalCounts.glossary) + ); + } + }, + + onStartSeoIndex: function (window) { + var me = this, configs = []; + + me.updateProgressBars(window); + + configs.push(me.getSeoInitRequestConfig(window, me.requestConfig.seo)); + + configs.push(me.getRequestConfig(window, 'articleProgress', 'seo', 'article')); + configs.push(me.getRequestConfig(window, 'categoryProgress', 'seo', 'category')); + configs.push(me.getRequestConfig(window, 'emotionProgress', 'seo', 'emotion')); + configs.push(me.getRequestConfig(window, 'blogProgress', 'seo', 'blog')); + configs.push(me.getRequestConfig(window, 'staticProgress', 'seo', 'static')); + configs.push(me.getRequestConfig(window, 'contentProgress', 'seo', 'content')); + configs.push(me.getRequestConfig(window, 'supplierProgress', 'seo', 'supplier')); + configs.push(me.getRequestConfig(window, 'glossaryProgress', 'seo', 'glossary')); + + window.startButton.hide(); + window.cancelButton.show(); + window.cancelButton.enable(); + me.cancelOperation = false; + + me.runRequest(0, window, null, configs); + } +}); +//{/block} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample52/Resources/views/backend/performance/view/glossary.js b/exampleplugins/5.2/SeoExample52/Resources/views/backend/performance/view/glossary.js new file mode 100644 index 0000000000..15f5a9894d --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/Resources/views/backend/performance/view/glossary.js @@ -0,0 +1,28 @@ + +//{block name="backend/performance/view/main/multi_request_tasks" append} +Ext.define('Shopware.apps.Performance.view.main.Glossary', { + override: 'Shopware.apps.Performance.view.main.MultiRequestTasks', + + height: 475, + + initComponent: function() { + var me = this; + + me.callParent(arguments); + + me.snippets.seo.glossary = '[0] of [1] glossary urls'; + }, + + createSeoItems: function() { + var me = this, + items = me.callParent(arguments), + progressCt = items[1]; + + me.glossaryProgress = me.createProgressBar('glossary', 'Glossary URLs'); + + progressCt.items.push(me.glossaryProgress); + + return items; + } +}); +//{/block} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample52/Resources/views/frontend/glossary/detail.tpl b/exampleplugins/5.2/SeoExample52/Resources/views/frontend/glossary/detail.tpl new file mode 100644 index 0000000000..b81330b9a2 --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/Resources/views/frontend/glossary/detail.tpl @@ -0,0 +1,7 @@ +{extends file="parent:frontend/glossary/index.tpl"} + +{block name="frontend_index_content"} +

{$word|ucfirst}

+ +

{$description}

+{/block} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample52/Resources/views/frontend/glossary/index.tpl b/exampleplugins/5.2/SeoExample52/Resources/views/frontend/glossary/index.tpl new file mode 100644 index 0000000000..383f993a64 --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/Resources/views/frontend/glossary/index.tpl @@ -0,0 +1,12 @@ +{extends file="parent:frontend/index/index.tpl"} + +{block name="frontend_index_content_left"}{/block} + +{block name="frontend_index_content"} + {foreach $words as $wordData} +
+ {$wordData['word']} +
{$wordData['description']}
+
+ {/foreach} +{/block} diff --git a/exampleplugins/5.2/SeoExample52/SeoExample52.php b/exampleplugins/5.2/SeoExample52/SeoExample52.php new file mode 100644 index 0000000000..c8cebd008e --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/SeoExample52.php @@ -0,0 +1,122 @@ + 'createGlossaryRewriteTable', + 'Enlight_Controller_Action_PostDispatch_Backend_Performance' => 'loadPerformanceExtension', + 'Shopware_Components_RewriteGenerator_FilterQuery' => 'filterParameterQuery', + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Seo' => 'addGlossaryCount' + ]; + } + + public function install(InstallContext $context) + { + /** @var Connection $dbalConnection */ + $dbalConnection = $this->container->get('dbal_connection'); + $dbalConnection->exec( + 'CREATE TABLE IF NOT EXISTS`s_glossary` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `word` varchar(50) COLLATE utf8_unicode_ci NOT NULL, + `description` longtext COLLATE utf8_unicode_ci NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci' + ); + + $dbalConnection->exec('INSERT IGNORE INTO `s_glossary` (`word`, `description`) VALUES + (\'Foobar\', \'The terms foobar (/ˈfuːbɑːr/), or foo and others are used as placeholder names (also referred to as metasyntactic variables) in computer programming or computer-related documentation. They have been used to name entities such as variables, functions, and commands whose exact identity is unimportant and serve only to demonstrate a concept.\'), + (\'Recursion\', \'Recursion occurs when a thing is defined in terms of itself or of its type. Recursion is used in a variety of disciplines ranging from linguistics to logic. The most common application of recursion is in mathematics and computer science, where a function being defined is applied within its own definition. While this apparently defines an infinite number of instances (function values), it is often done in such a way that no loop or infinite chain of references can occur.\');'); + + parent::install($context); + } + + public function uninstall(UninstallContext $context) + { + if (!$context->keepUserData()) { + /** @var Connection $dbalConnection */ + $dbalConnection = $this->container->get('dbal_connection'); + $dbalConnection->exec('DROP TABLE s_glossary'); + } + + parent::uninstall($context); + } + + public function createGlossaryRewriteTable() + { + /** @var \sRewriteTable $rewriteTableModule */ + $rewriteTableModule = Shopware()->Container()->get('modules')->sRewriteTable(); + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + + $words = $dbalQueryBuilder->select('glossary.id, glossary.word') + ->from('s_glossary', 'glossary') + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + foreach ($words as $wordId => $word) { + $rewriteTableModule->sInsertUrl('sViewport=glossary&sAction=detail&wordId=' . $wordId, 'glossary/' . $word); + } + } + + public function loadPerformanceExtension(\Enlight_Controller_ActionEventArgs $args) + { + $subject = $args->getSubject(); + $request = $subject->Request(); + + if ($request->getActionName() !== 'load') { + return; + } + + $subject->View()->addTemplateDir($this->getPath() . '/Resources/views/'); + $subject->View()->extendsTemplate('backend/performance/view/glossary.js'); + $subject->View()->extendsTemplate('backend/performance/controller/glossary.js'); + } + + public function addGlossaryCount(\Enlight_Controller_ActionEventArgs $args) + { + $controller = $args->getSubject(); + $request = $controller->Request(); + $view = $controller->View(); + + if ($request->getActionName() !== 'getCount') { + return; + } + + $data = $view->getAssign('data'); + $counts = &$data['counts']; + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + $wordsCount = $dbalQueryBuilder->select('COUNT(glossary.id)') + ->from('s_glossary', 'glossary') + ->execute() + ->fetch(\PDO::FETCH_COLUMN); + + $counts['glossary'] = (int) $wordsCount; + + $view->assign('data', $data); + } + + public function filterParameterQuery(\Enlight_Event_EventArgs $args) + { + $orgQuery = $args->getReturn(); + $query = $args->getQuery(); + + if ($query['controller'] === 'glossary' && isset($query['wordId'])) { + $orgQuery['wordId'] = $query['wordId']; + } + + return $orgQuery; + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SeoExample52/plugin.xml b/exampleplugins/5.2/SeoExample52/plugin.xml new file mode 100644 index 0000000000..28e12e9d97 --- /dev/null +++ b/exampleplugins/5.2/SeoExample52/plugin.xml @@ -0,0 +1,13 @@ + + + + + + + 1.0.0 + (c) shopware AG + shopware AG + + + diff --git a/exampleplugins/legacy/Frontend/SwagAttribute/Models/SwagAttribute.php b/exampleplugins/5.2/SwagAttribute/Models/SwagAttribute.php similarity index 100% rename from exampleplugins/legacy/Frontend/SwagAttribute/Models/SwagAttribute.php rename to exampleplugins/5.2/SwagAttribute/Models/SwagAttribute.php diff --git a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/Shopware.attribute.Form.js b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/Shopware.attribute.Form.js similarity index 98% rename from exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/Shopware.attribute.Form.js rename to exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/Shopware.attribute.Form.js index d759abc70f..62272d0b92 100644 --- a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/Shopware.attribute.Form.js +++ b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/Shopware.attribute.Form.js @@ -31,7 +31,4 @@ Ext.define('Shopware.attribute.Form-SwagAttribute', { } }); -//{/block} - - - +//{/block} \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/Shopware.form.field.SwagAttributeGrid.js b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/Shopware.form.field.SwagAttributeGrid.js similarity index 98% rename from exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/Shopware.form.field.SwagAttributeGrid.js rename to exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/Shopware.form.field.SwagAttributeGrid.js index f998d8ab0d..83f0efa340 100644 --- a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/Shopware.form.field.SwagAttributeGrid.js +++ b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/Shopware.form.field.SwagAttributeGrid.js @@ -1,6 +1,4 @@ - - - +// Ext.define('Shopware.form.field.SwagAttributeGrid', { extend: 'Shopware.form.field.Grid', alias: 'widget.shopware-form-field-swag-attribute-grid', @@ -17,7 +15,4 @@ Ext.define('Shopware.form.field.SwagAttributeGrid', { createSearchField: function() { return Ext.create('Shopware.form.field.SingleSelection', this.getComboConfig()); } -}); - - - +}); \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.FieldHandler.js b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.FieldHandler.js similarity index 94% rename from exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.FieldHandler.js rename to exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.FieldHandler.js index ed463aa578..8a372447a5 100644 --- a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.FieldHandler.js +++ b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.FieldHandler.js @@ -1,7 +1,4 @@ - - - - +// Ext.define('SwagAttribute.FieldHandler', { extend: 'Shopware.attribute.FieldHandlerInterface', @@ -17,7 +14,7 @@ Ext.define('SwagAttribute.FieldHandler', { return false; } - return (name == 'my_own_validation' || name == 'my_own_type'); + return (name === 'my_own_validation' || name === 'my_own_type'); }, /** @@ -55,7 +52,4 @@ Ext.define('SwagAttribute.FieldHandler', { } }); } -}); - - - +}); \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.MultiSelectionHandler.js b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.MultiSelectionHandler.js similarity index 96% rename from exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.MultiSelectionHandler.js rename to exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.MultiSelectionHandler.js index 93a20f760e..f76c118b76 100644 --- a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.MultiSelectionHandler.js +++ b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.MultiSelectionHandler.js @@ -1,13 +1,7 @@ - - - - +// Ext.define('SwagAttribute.MultiSelectionHandler', { extend: 'Shopware.attribute.AbstractEntityFieldHandler', entity: "SwagAttribute\\Models\\SwagAttribute", singleSelectionClass: 'Shopware.form.field.SingleSelection', multiSelectionClass: 'Shopware.form.field.SwagAttributeGrid' -}); - - - +}); \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.form.field.OwnType.js b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.form.field.OwnType.js similarity index 99% rename from exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.form.field.OwnType.js rename to exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.form.field.OwnType.js index 06ad87725a..9044da1f4b 100644 --- a/exampleplugins/legacy/Frontend/SwagAttribute/Views/backend/swag_attribute/SwagAttribute.form.field.OwnType.js +++ b/exampleplugins/5.2/SwagAttribute/Resources/views/backend/swag_attribute/SwagAttribute.form.field.OwnType.js @@ -1,6 +1,4 @@ - - - +// Ext.define('SwagAttribute.form.field.OwnType', { alias: 'widget.swag-attribute-type', extend: 'Ext.form.FieldContainer', @@ -79,5 +77,3 @@ Ext.define('SwagAttribute.form.field.OwnType', { return value; } }); - - diff --git a/exampleplugins/legacy/Frontend/SwagAttribute/SwagAttribute.php b/exampleplugins/5.2/SwagAttribute/SwagAttribute.php similarity index 97% rename from exampleplugins/legacy/Frontend/SwagAttribute/SwagAttribute.php rename to exampleplugins/5.2/SwagAttribute/SwagAttribute.php index 87d5c188aa..18c8b3f217 100644 --- a/exampleplugins/legacy/Frontend/SwagAttribute/SwagAttribute.php +++ b/exampleplugins/5.2/SwagAttribute/SwagAttribute.php @@ -48,7 +48,6 @@ public function install(InstallContext $context) ], ]); - $service->update( 's_articles_attributes', 'my_own_validation', @@ -67,7 +66,6 @@ public function install(InstallContext $context) true ); - $em = $this->container->get('models'); $schemaTool = new SchemaTool($em); $schemaTool->updateSchema( @@ -93,7 +91,7 @@ public function extendExtJS(\Enlight_Event_EventArgs $arguments) { /** @var \Enlight_View_Default $view */ $view = $arguments->getSubject()->View(); - $view->addTemplateDir($this->getPath() . '/Views/'); + $view->addTemplateDir($this->getPath() . '/Resources/views/'); $view->extendsTemplate('backend/swag_attribute/Shopware.attribute.Form.js'); } } diff --git a/exampleplugins/legacy/Frontend/SwagAttributeFilter/CriteriaRequestHandler.php b/exampleplugins/5.2/SwagAttributeFilter/Components/CriteriaRequestHandler.php similarity index 96% rename from exampleplugins/legacy/Frontend/SwagAttributeFilter/CriteriaRequestHandler.php rename to exampleplugins/5.2/SwagAttributeFilter/Components/CriteriaRequestHandler.php index f7ded66893..d45f40aa6d 100644 --- a/exampleplugins/legacy/Frontend/SwagAttributeFilter/CriteriaRequestHandler.php +++ b/exampleplugins/5.2/SwagAttributeFilter/Components/CriteriaRequestHandler.php @@ -1,6 +1,6 @@ + + + + + + + diff --git a/exampleplugins/5.2/SwagAttributeFilter/SwagAttributeFilter.php b/exampleplugins/5.2/SwagAttributeFilter/SwagAttributeFilter.php new file mode 100644 index 0000000000..d501ff475f --- /dev/null +++ b/exampleplugins/5.2/SwagAttributeFilter/SwagAttributeFilter.php @@ -0,0 +1,10 @@ + +

Recommended variants for you

+ {action module=widgets controller=listing action=products numbers=$data.recommendedvariants type="slider"} + + +
+

Recommended stream

+ {action module=widgets controller=listing action=stream streamId=$data.recommendedstream type="slider"} +
+ +
+

Recommended variants for you - As listing

+ {action module=widgets controller=listing action=products numbers=$data.recommendedvariants productBoxLayout='image'} +
+ +
+

Recommended stream - As listing

+ {action module=widgets controller=listing action=stream streamId=$data.recommendedstream productBoxLayout='list' sPerPage=2} +
+{/block} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagAttributeSlider/SwagAttributeSlider.php b/exampleplugins/5.2/SwagAttributeSlider/SwagAttributeSlider.php new file mode 100644 index 0000000000..4d60d69f34 --- /dev/null +++ b/exampleplugins/5.2/SwagAttributeSlider/SwagAttributeSlider.php @@ -0,0 +1,47 @@ +container->get('shopware_attribute.crud_service'); + + $crud->update('s_user_attributes', 'recommendedVariants', 'multi_selection', [ + 'displayInBackend' => true, + 'label' => 'Recommended variants', + 'entity' => 'Shopware\Models\Article\Detail', + ]); + $crud->update('s_user_attributes', 'recommendedStream', 'single_selection', [ + 'displayInBackend' => true, + 'label' => 'Recommended stream', + 'entity' => 'Shopware\Models\ProductStream\ProductStream', + ]); + } + + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatch_Frontend_Account' => 'onPostDispatchAccount', + ]; + } + + /** + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchAccount(\Enlight_Event_EventArgs $args) + { + /** @var $controller \Shopware_Controllers_Frontend_Account */ + $controller = $args->get('subject'); + $controller->View()->addTemplateDir($this->getPath() . '/Resources/views'); + } +} diff --git a/exampleplugins/5.2/SwagAttributeSlider/plugin.xml b/exampleplugins/5.2/SwagAttributeSlider/plugin.xml new file mode 100644 index 0000000000..2e58d7836f --- /dev/null +++ b/exampleplugins/5.2/SwagAttributeSlider/plugin.xml @@ -0,0 +1,13 @@ + + + + + + + 1.0.0 + (c) Shopware AG + Shopware AG + + + diff --git a/exampleplugins/5.2/SwagBannerApi/Components/Api/Resource/Example.php b/exampleplugins/5.2/SwagBannerApi/Components/Api/Resource/Example.php new file mode 100644 index 0000000000..4aa8096e42 --- /dev/null +++ b/exampleplugins/5.2/SwagBannerApi/Components/Api/Resource/Example.php @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/exampleplugins/5.2/SwagBannerApi/plugin.xml b/exampleplugins/5.2/SwagBannerApi/plugin.xml index 4a03a6deae..1607e5cc1b 100644 --- a/exampleplugins/5.2/SwagBannerApi/plugin.xml +++ b/exampleplugins/5.2/SwagBannerApi/plugin.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> 1.0.0 diff --git a/exampleplugins/legacy/Frontend/SwagController/Controllers/Frontend/SwagControllerTest.php b/exampleplugins/5.2/SwagController/Controllers/Frontend/SwagControllerTest.php similarity index 100% rename from exampleplugins/legacy/Frontend/SwagController/Controllers/Frontend/SwagControllerTest.php rename to exampleplugins/5.2/SwagController/Controllers/Frontend/SwagControllerTest.php diff --git a/exampleplugins/legacy/Frontend/SwagController/Readme.md b/exampleplugins/5.2/SwagController/Readme.md similarity index 66% rename from exampleplugins/legacy/Frontend/SwagController/Readme.md rename to exampleplugins/5.2/SwagController/Readme.md index 9d1b95b952..f383de5753 100644 --- a/exampleplugins/legacy/Frontend/SwagController/Readme.md +++ b/exampleplugins/5.2/SwagController/Readme.md @@ -3,4 +3,5 @@ Simple example of how to create a controller from a plugin # How to use * Install and activate the plugin in the plugin manager -* call http://your-shop.com/SwagControllerTest \ No newline at end of file +* call http://your-shop.com/SwagControllerTest +* Surf to product listing \ No newline at end of file diff --git a/exampleplugins/5.2/SwagController/Resources/services.xml b/exampleplugins/5.2/SwagController/Resources/services.xml new file mode 100644 index 0000000000..538c57a720 --- /dev/null +++ b/exampleplugins/5.2/SwagController/Resources/services.xml @@ -0,0 +1,17 @@ + + + + + %swag_controller.plugin_dir% + + + + + + + + + + diff --git a/exampleplugins/legacy/Frontend/SwagController/Views/frontend/swag_controller_test/index.tpl b/exampleplugins/5.2/SwagController/Resources/views/frontend/swag_controller_test/index.tpl similarity index 100% rename from exampleplugins/legacy/Frontend/SwagController/Views/frontend/swag_controller_test/index.tpl rename to exampleplugins/5.2/SwagController/Resources/views/frontend/swag_controller_test/index.tpl diff --git a/exampleplugins/5.2/SwagController/Subscriber/ListingSubscriber.php b/exampleplugins/5.2/SwagController/Subscriber/ListingSubscriber.php new file mode 100644 index 0000000000..c2a9c352bb --- /dev/null +++ b/exampleplugins/5.2/SwagController/Subscriber/ListingSubscriber.php @@ -0,0 +1,44 @@ + 'onListingIndex' + ]; + } + + /** + * Event callback for the event registered above + */ + public function onListingIndex(\Enlight_Event_EventArgs $args) + { + /** @var $controller \Enlight_Controller_Action */ + $controller = $args->getSubject(); + $request = $controller->Request(); + + if($request->getActionName() === 'index') { + $view = $controller->View(); + $response = $controller->Response(); + $parameter = $request->getParams(); + + // DO SOMETHING TO EXTEND OR MODIFY + + echo '
';
+            echo 'TODO: Extend or modify listing';
+            echo '
'; + echo '
'; + var_export($parameter); + die(); + } + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagController/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagController/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..85b8e0dfca --- /dev/null +++ b/exampleplugins/5.2/SwagController/Subscriber/TemplateRegistration.php @@ -0,0 +1,42 @@ +pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); + } +} diff --git a/exampleplugins/5.2/SwagController/SwagController.php b/exampleplugins/5.2/SwagController/SwagController.php new file mode 100644 index 0000000000..126d69f919 --- /dev/null +++ b/exampleplugins/5.2/SwagController/SwagController.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + https://developers.shopware.com + shopware AG + + + Dieses Theme stellt ein zusätzliches Detailseitentemplate bereit welches für einzelne Produkte genutzt werden kann. Wenn das Plugin aktiv ist, solltest du das Theme im Theme Manager sehen können. + This theme creates an additional detail page template which can be used on specific products. When you have activated the plugin you should see the new theme inside your Theme Manager. + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/Theme.php b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/Theme.php similarity index 76% rename from exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/Theme.php rename to exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/Theme.php index ed67ca2cbe..44c0e4e24a 100644 --- a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/Theme.php +++ b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/Theme.php @@ -2,8 +2,6 @@ namespace Shopware\Themes\CustomListing; -use Shopware\Components\Form as Form; - class Theme extends \Shopware\Components\Theme { // Meta data @@ -12,8 +10,4 @@ class Theme extends \Shopware\Components\Theme protected $description = 'This theme creates an additional listing page template which can be used on specific categories.'; protected $author = 'shopware AG'; protected $license = 'MIT'; - - public function createConfig(Form\Container\TabContainer $container) - { - } } diff --git a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/_public/src/less/all.less b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/_public/src/less/all.less similarity index 100% rename from exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/_public/src/less/all.less rename to exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/_public/src/less/all.less diff --git a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/_public/src/less/custom-listing.less b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/_public/src/less/custom-listing.less similarity index 100% rename from exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/_public/src/less/custom-listing.less rename to exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/_public/src/less/custom-listing.less diff --git a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/listing/custom_listing.tpl b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/listing/custom_listing.tpl similarity index 100% rename from exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/listing/custom_listing.tpl rename to exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/listing/custom_listing.tpl diff --git a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/listing/product-box/box-custom.tpl b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/listing/product-box/box-custom.tpl similarity index 95% rename from exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/listing/product-box/box-custom.tpl rename to exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/listing/product-box/box-custom.tpl index e1e3363e63..79fb8802d3 100644 --- a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/frontend/listing/product-box/box-custom.tpl +++ b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/frontend/listing/product-box/box-custom.tpl @@ -3,7 +3,7 @@ {block name='frontend_listing_box_article_content'}
- +
{$sArticle.articleName|escape} diff --git a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/preview.png b/exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/preview.png similarity index 100% rename from exampleplugins/legacy/Frontend/SwagCustomListingTheme/Themes/Frontend/CustomListing/preview.png rename to exampleplugins/5.2/SwagCustomListingTheme/Resources/Themes/Frontend/CustomListing/preview.png diff --git a/exampleplugins/5.2/SwagCustomListingTheme/SwagCustomListingTheme.php b/exampleplugins/5.2/SwagCustomListingTheme/SwagCustomListingTheme.php new file mode 100644 index 0000000000..e32b67cf71 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomListingTheme/SwagCustomListingTheme.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + https://developers.shopware.com + shopware AG + + + Dieses Theme stellt ein zusätzliches Kategorieseitentemplate bereit welches für beliebige Kategorien genutzt werden kann. Wenn das Plugin aktiv ist, solltest du das Theme im Theme Manager sehen können. + This theme creates an additional listing page template which can be used on specific categories. When you have activated the plugin you should see the new theme inside your Theme Manager. + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/legacy/Frontend/SwagModelPlugin/LICENSE b/exampleplugins/5.2/SwagCustomProductBoxLayout/LICENSE similarity index 100% rename from exampleplugins/legacy/Frontend/SwagModelPlugin/LICENSE rename to exampleplugins/5.2/SwagCustomProductBoxLayout/LICENSE diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/Readme.md b/exampleplugins/5.2/SwagCustomProductBoxLayout/Readme.md new file mode 100644 index 0000000000..90768da4c7 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/Readme.md @@ -0,0 +1,7 @@ +# SwagCustomProductBoxLayout +## About SwagCustomProductBoxLayout +This skeleton contains a License file, fileheader and a basic README. + +## License + +Please see [License File](LICENSE) for more information. \ No newline at end of file diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/services.xml b/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/services.xml new file mode 100644 index 0000000000..7eb59783c1 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/services.xml @@ -0,0 +1,19 @@ + + + + + + + + %swag_custom_product_box_layout.plugin_dir% + + + + + %swag_custom_product_box_layout.plugin_dir% + + + + diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/views/backend/swag_custom_product_box_layout/store/product_box_layout.js b/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/views/backend/swag_custom_product_box_layout/store/product_box_layout.js new file mode 100644 index 0000000000..ba417cfa77 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/views/backend/swag_custom_product_box_layout/store/product_box_layout.js @@ -0,0 +1,51 @@ +/** + * Shopware 5 + * Copyright (c) shopware AG + * + * According to our dual licensing model, this program can be used either + * under the terms of the GNU Affero General Public License, version 3, + * or under a proprietary license. + * + * The texts of the GNU Affero General Public License with an additional + * permission and of our proprietary license can be found at and + * in the LICENSE file you have received along with this program. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * "Shopware" is a registered trademark of shopware AG. + * The licensing of the program under the AGPLv3 does not imply a + * trademark license. Therefore any rights, title and interest in + * our trademarks remain entirely with us. + * + * @category Shopware + * @package Base + * @subpackage Store + * @version $Id$ + * @author shopware AG + */ + +/** + * Shopware Store - Global Stores and Models + */ +//{namespace name=backend/base/product_box_layout} +//{block name="backend/base/store/product_box_layout" append} +Ext.override(Shopware.apps.Base.store.ProductBoxLayout, { + + createLayoutData: function(config) { + var me = this, + data = me.callParent(arguments); + + data.push({ + key: 'shopware', + label: '{s name=box_layout_shopware_label}Shopware{/s}', + description: '{s name=box_layout_shopware_description}This is the custom Shopware box layout{/s}', + image: '{link file="backend/_resources/images/category/layout_box_basic.png"}' + }); + + return data; + } +}); +//{/block} diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/views/frontend/listing/product-box/box-shopware.tpl b/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/views/frontend/listing/product-box/box-shopware.tpl new file mode 100644 index 0000000000..0ae69b8add --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/Resources/views/frontend/listing/product-box/box-shopware.tpl @@ -0,0 +1,12 @@ +{extends file="frontend/listing/product-box/box-basic.tpl"} + +{namespace name="frontend/listing/box_article"} + +{block name='frontend_listing_box_article_description'}{/block} + +{block name='frontend_listing_box_article_actions'}{/block} + +{block name='frontend_listing_box_article_price'} + This is my custom Shopware Product Box Layout. +{/block} + diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/Subscriber/Backend.php b/exampleplugins/5.2/SwagCustomProductBoxLayout/Subscriber/Backend.php new file mode 100644 index 0000000000..56098d6fee --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/Subscriber/Backend.php @@ -0,0 +1,38 @@ +pluginDirectory = $pluginDirectory; + } + + public static function getSubscribedEvents() + { + return array( + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Base' => 'onPostDispatchBackendBase', + ); + } + + public function onPostDispatchBackendBase(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $controller */ + $controller = $args->getSubject(); + $view = $controller->View(); + + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); + $view->extendsTemplate('backend/swag_custom_product_box_layout/store/product_box_layout.js'); + } +} diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/Subscriber/Frontend.php b/exampleplugins/5.2/SwagCustomProductBoxLayout/Subscriber/Frontend.php new file mode 100644 index 0000000000..6f40bc5286 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/Subscriber/Frontend.php @@ -0,0 +1,36 @@ +pluginDirectory = $pluginDirectory; + } + + public static function getSubscribedEvents() + { + return array( + 'Theme_Inheritance_Template_Directories_Collected' => 'onCollectTemplateDirectories', + ); + } + + public function onCollectTemplateDirectories(\Enlight_Event_EventArgs $args) + { + /** @var $controller \Enlight_Controller_Action */ + $directories = $args->getReturn(); + $directories[] = $this->pluginDirectory . '/Resources/views'; + $args->setReturn($directories); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/SwagCustomProductBoxLayout.php b/exampleplugins/5.2/SwagCustomProductBoxLayout/SwagCustomProductBoxLayout.php new file mode 100644 index 0000000000..7f948424b0 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/SwagCustomProductBoxLayout.php @@ -0,0 +1,14 @@ + + + + tests + + diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/plugin.xml b/exampleplugins/5.2/SwagCustomProductBoxLayout/plugin.xml new file mode 100644 index 0000000000..8757cb5a62 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/plugin.xml @@ -0,0 +1,18 @@ + + + + + 1.0.0 + (c) by Shopware AG + proprietary + http://store.shopware.com + Shopware AG + + + + + Erste Veröffentlichung + Initial Release + + \ No newline at end of file diff --git a/exampleplugins/5.2/SwagCustomProductBoxLayout/tests/PluginTest.php b/exampleplugins/5.2/SwagCustomProductBoxLayout/tests/PluginTest.php new file mode 100644 index 0000000000..c49761f74b --- /dev/null +++ b/exampleplugins/5.2/SwagCustomProductBoxLayout/tests/PluginTest.php @@ -0,0 +1,21 @@ + [] + ]; + + public function testCanCreateInstance() + { + /** @var Plugin $plugin */ + $plugin = Shopware()->Container()->get('kernel')->getPlugins()['SwagCustomProductBoxLayout']; + + $this->assertInstanceOf(Plugin::class, $plugin); + } +} diff --git a/exampleplugins/5.2/SwagCustomRiskRule/Resources/services.xml b/exampleplugins/5.2/SwagCustomRiskRule/Resources/services.xml new file mode 100644 index 0000000000..21489a3723 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomRiskRule/Resources/services.xml @@ -0,0 +1,17 @@ + + + + + + + %swag_custom_risk_rule.plugin_dir% + + + + + + + + diff --git a/exampleplugins/5.2/SwagCustomRiskRule/Resources/views/backend/swag_custom_risk_rule/store/risks.js b/exampleplugins/5.2/SwagCustomRiskRule/Resources/views/backend/swag_custom_risk_rule/store/risks.js new file mode 100644 index 0000000000..3ebe60cf49 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomRiskRule/Resources/views/backend/swag_custom_risk_rule/store/risks.js @@ -0,0 +1,5 @@ + +//{block name="backend/risk_management/store/risk/data"} +// {$smarty.block.parent} +{ description: '{s name=risks_store/comboBox/myCustomRule}My custom rule{/s}', value: 'MyCustomRule' }, +//{/block} diff --git a/exampleplugins/5.2/SwagCustomRiskRule/Subscriber/CustomRule.php b/exampleplugins/5.2/SwagCustomRiskRule/Subscriber/CustomRule.php new file mode 100644 index 0000000000..41936902cd --- /dev/null +++ b/exampleplugins/5.2/SwagCustomRiskRule/Subscriber/CustomRule.php @@ -0,0 +1,34 @@ + 'onMyCustomRule' + ]; + } + + /** + * @param Enlight_Event_EventArgs $args + * @return bool + */ + public function onMyCustomRule(Enlight_Event_EventArgs $args) + { + $rule = $args->get('rule'); + $user = $args->get('user'); + $basket = $args->get('basket'); + $value = $args->get('value'); + + if ($basket['AmountNumeric'] > $value) { + return true; // it's a risky customer + } + + return false; + } +} diff --git a/exampleplugins/5.2/SwagCustomRiskRule/Subscriber/RiskManagement.php b/exampleplugins/5.2/SwagCustomRiskRule/Subscriber/RiskManagement.php new file mode 100644 index 0000000000..f26d0d5b16 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomRiskRule/Subscriber/RiskManagement.php @@ -0,0 +1,48 @@ +pluginPath = $pluginPath; + } + + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_RiskManagement' => 'onRiskManagementBackend' + ]; + } + + + public function onRiskManagementBackend(Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $controller */ + $controller = $args->get('subject'); + + $view = $controller->View(); + $request = $controller->Request(); + + $view->addTemplateDir($this->pluginPath . '/Resources/views'); + + if ($request->getActionName() == 'load') { + $view->extendsTemplate('backend/swag_custom_risk_rule/store/risks.js'); + } + } +} diff --git a/exampleplugins/5.2/SwagCustomRiskRule/SwagCustomRiskRule.php b/exampleplugins/5.2/SwagCustomRiskRule/SwagCustomRiskRule.php new file mode 100644 index 0000000000..8c53dbc49b --- /dev/null +++ b/exampleplugins/5.2/SwagCustomRiskRule/SwagCustomRiskRule.php @@ -0,0 +1,11 @@ +container->get('dbal_connection'); + $query = $connection->createQueryBuilder(); + $query->select(['COUNT(codes.cashed) as amount', 'vouchers.description as name']) + ->from('s_emarketing_voucher_codes', 'codes') + ->innerJoin('codes', 's_emarketing_vouchers', 'vouchers', 'vouchers.id = codes.voucherID') + ->where('codes.cashed = 1') + ->groupBy('vouchers.id'); + + $idList = (string) $this->Request()->getParam('selectedShops'); + if (!empty($idList)) { + $selectedShopIds = explode(',', $idList); + + foreach ($selectedShopIds as $shopId) { + $query->addSelect('SUM(IF(vouchers.subshopID = ' . $connection->quote($shopId) . ', codes.cashed, 0)) as amount' . $shopId); + } + } + + $data = $query->execute()->fetchAll(); + + $this->View()->assign([ + 'success' => true, + 'data' => $data, + 'count' => count($data) + ]); + } +} diff --git a/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/app.js b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/app.js new file mode 100644 index 0000000000..05418cbc5b --- /dev/null +++ b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/app.js @@ -0,0 +1,8 @@ +// {block name="backend/analytics/application"} +// {$smarty.block.parent} + +// {include file="backend/analytics/swag_custom_statistics/store/navigation/voucher.js"} +// {include file="backend/analytics/swag_custom_statistics/view/chart/voucher.js"} +// {include file="backend/analytics/swag_custom_statistics/view/table/voucher.js"} + +// {/block} diff --git a/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation.js b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation.js new file mode 100644 index 0000000000..44df55b962 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation.js @@ -0,0 +1,12 @@ +// {block name="backend/analytics/store/navigation/items"} +// {$smarty.block.parent} +{ + id: 'voucher', + text: 'Gutscheine', + store: 'analytics-store-voucher', + iconCls: 'sprite-ticket', + comparable: true, + leaf: true, + multiShop: true +}, +// {/block} diff --git a/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation/voucher.js b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation/voucher.js new file mode 100644 index 0000000000..6c6914ba44 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation/voucher.js @@ -0,0 +1,37 @@ +//{block name="backend/analytics/swag_custom_statistics/store/navigation/voucher"} +Ext.define('Shopware.apps.Analytics.swagCustomStatistics.store.navigation.Voucher', { + extend: 'Ext.data.Store', + alias: 'widget.analytics-store-voucher', + remoteSort: true, + + fields: [ + 'amount', + 'name' + ], + + proxy: { + type: 'ajax', + + url: '{url controller=SwagCustomStatistics action=getVoucherStatistics}', + + reader: { + type: 'json', + root: 'data', + totalProperty: 'total' + } + }, + + constructor: function(config) { + var me = this; + config.fields = me.fields; + + if (config.shopStore) { + config.shopStore.each(function(shop) { + config.fields.push('amount' + shop.data.id); + }); + } + + me.callParent(arguments); + } +}); +//{/block} diff --git a/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/chart/voucher.js b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/chart/voucher.js new file mode 100644 index 0000000000..01dbd7acd9 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/chart/voucher.js @@ -0,0 +1,67 @@ +//{namespace name=backend/analytics/view/main} +//{block name="backend/analytics/swag_custom_statistics/view/chart/voucher"} +Ext.define('Shopware.apps.Analytics.swagCustomStatistics.view.chart.Voucher', { + extend: 'Shopware.apps.Analytics.view.main.Chart', + alias: 'widget.analytics-chart-voucher', + animate: true, + shadows: true, + + legend: { + position: 'right' + }, + + initComponent: function() { + var me = this; + + me.series = []; + + me.axes = [ + { + type: 'Numeric', + position: 'left', + fields: me.getAxesFields('amount'), + title: 'Eingelöst', + grid: true, + minimum: 0 + }, + { + type: 'Category', + position: 'bottom', + fields: ['name'], + title: 'Gutscheine' + } + ]; + + this.series = [ + { + type: 'column', + axis: 'left', + gutter: 80, + xField: 'name', + yField: me.getAxesFields('amount'), + title: me.getAxesTitles('{s name=chart/country/sum}Total sales{/s}'), + stacked: true, + label: { + display: 'insideEnd', + field: 'amount', + orientation: 'horizontal', + 'text-anchor': 'middle' + }, + tips: { + trackMouse: true, + width: 300, + height: 60, + renderer: function(storeItem, barItem) { + var name = storeItem.get('name'), + field = barItem.yField; + + this.setTitle(name + ' : ' + storeItem.get(field)); + } + } + } + ]; + + me.callParent(arguments); + } +}); +//{/block} diff --git a/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/table/voucher.js b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/table/voucher.js new file mode 100644 index 0000000000..92c31a5c08 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/table/voucher.js @@ -0,0 +1,36 @@ +//{namespace name=backend/analytics/view/main} +//{block name="backend/analytics/swag_custom_statistics/view/table/voucher"} +Ext.define('Shopware.apps.Analytics.swagCustomStatistics.view.table.Voucher', { + extend: 'Shopware.apps.Analytics.view.main.Table', + alias: 'widget.analytics-table-voucher', + + initComponent: function() { + var me = this; + + me.columns = { + items: me.getColumns(), + defaults: { + flex: 1, + sortable: false + } + }; + + me.initStoreIndices('amount', 'Anzahl: [0]'); + + me.callParent(arguments); + }, + + getColumns: function() { + return [ + { + dataIndex: 'name', + text: 'Name' + }, + { + dataIndex: 'amount', + text: 'Anzahl' + } + ]; + } +}); +//{/block} diff --git a/exampleplugins/5.2/SwagCustomStatistics/SwagCustomStatistics.php b/exampleplugins/5.2/SwagCustomStatistics/SwagCustomStatistics.php new file mode 100644 index 0000000000..d9a670b9c9 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomStatistics/SwagCustomStatistics.php @@ -0,0 +1,38 @@ + 'onPostDispatchBackendAnalytics', + ]; + } + + /** + * @param ActionEventArgs $args + */ + public function onPostDispatchBackendAnalytics(ActionEventArgs $args) + { + $request = $args->getRequest(); + $view = $args->getSubject()->View(); + + $view->addTemplateDir($this->getPath() . '/Resources/views/'); + + if ($request->getActionName() === 'index') { + $view->extendsTemplate('backend/analytics/swag_custom_statistics/app.js'); + } + + if ($request->getActionName() === 'load') { + $view->extendsTemplate('backend/analytics/swag_custom_statistics/store/navigation.js'); + } + } +} diff --git a/exampleplugins/5.2/SwagCustomStatistics/plugin.xml b/exampleplugins/5.2/SwagCustomStatistics/plugin.xml new file mode 100644 index 0000000000..ff9f9e8837 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomStatistics/plugin.xml @@ -0,0 +1,13 @@ + + + + + + + 1.0.0 + (c) shopware AG + shopware AG + + + diff --git a/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundle/ActiveCondition.php b/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundle/ActiveCondition.php new file mode 100644 index 0000000000..28a26dbf28 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundle/ActiveCondition.php @@ -0,0 +1,31 @@ +active = $active; + } + + public function getName() + { + return 'ActiveCondition'; + } + + public function onlyActive() + { + return $this->active; + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundleDBAL/ActiveConditionHandler.php b/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundleDBAL/ActiveConditionHandler.php new file mode 100644 index 0000000000..0d5cf24aeb --- /dev/null +++ b/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundleDBAL/ActiveConditionHandler.php @@ -0,0 +1,24 @@ +andWhere('customer.active = :active'); + + /** @var ActiveCondition $condition */ + $query->setParameter(':active', $condition->onlyActive()); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundleDBAL/SearchIndexer.php b/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundleDBAL/SearchIndexer.php new file mode 100644 index 0000000000..d5cc4a0f42 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomerSearchExtension/Bundle/CustomerSearchBundleDBAL/SearchIndexer.php @@ -0,0 +1,56 @@ +coreIndexer = $coreIndexer; + $this->connection = $connection; + } + + public function populate(array $ids) + { + $this->coreIndexer->populate($ids); + +// //fetch data +// $rows = $this->connection->createQueryBuilder()->execute()->fetchAll(); +// +// //create prepared statement for fast inserts +// $statement = $this->connection->prepare("INSERT INTO test-table"); +// +// //iterate rows and insert data +// foreach ($rows as $row) { +// $statement->execute($row); +// } + } + + public function clearIndex() + { + $this->coreIndexer->clearIndex(); +// $this->connection->executeUpdate("DELETE FROM test-table"); + } + + public function cleanupIndex() + { +// $this->coreIndexer->cleanupIndex(); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagCustomerSearchExtension/Resources/services.xml b/exampleplugins/5.2/SwagCustomerSearchExtension/Resources/services.xml new file mode 100644 index 0000000000..3f1dd30d12 --- /dev/null +++ b/exampleplugins/5.2/SwagCustomerSearchExtension/Resources/services.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/exampleplugins/5.2/SwagCustomerSearchExtension/Resources/views/backend/customer/swag_customer_stream_extension.js b/exampleplugins/5.2/SwagCustomerSearchExtension/Resources/views/backend/customer/swag_customer_stream_extension.js new file mode 100644 index 0000000000..901ab178bb --- /dev/null +++ b/exampleplugins/5.2/SwagCustomerSearchExtension/Resources/views/backend/customer/swag_customer_stream_extension.js @@ -0,0 +1,60 @@ + + +// {block name="backend/customer/view/customer_stream/condition_panel" } + + +// {$smarty.block.parent} + +Ext.define('Shopware.apps.Customer.SwagCustomerStreamExtension', { + override: 'Shopware.apps.Customer.view.customer_stream.ConditionPanel', + + registerHandlers: function() { + var me = this, + //fetch original handlers + handlers = me.callParent(arguments); + + //push own handler into + handlers.push(Ext.create('Shopware.apps.Customer.swag_customer_stream_extension.ActiveCondition')); + + //return modified handlers array + return handlers; + } +}); + + +//definition of you own condition +Ext.define('Shopware.apps.Customer.swag_customer_stream_extension.ActiveCondition', { + + getLabel: function() { + return 'My active condition'; + }, + + supports: function(conditionClass) { + return (conditionClass == 'SwagCustomerSearchExtension\\Bundle\\CustomerSearchBundle\\ActiveCondition'); + }, + + create: function(callback) { + callback(this._create()); + }, + + load: function(conditionClass, items, callback) { + callback(this._create()); + }, + + _create: function() { + return { + title: this.getLabel(), + conditionClass: 'SwagCustomerSearchExtension\\Bundle\\CustomerSearchBundle\\ActiveCondition', + items: [{ + xtype: 'checkbox', + name: 'active', + boxLabel: 'Activate for active customers, deactivate for inactive customers', + inputValue: true, + uncheckedValue: false + }] + }; + } +}); + +// {/block} + diff --git a/exampleplugins/5.2/SwagCustomerSearchExtension/SwagCustomerSearchExtension.php b/exampleplugins/5.2/SwagCustomerSearchExtension/SwagCustomerSearchExtension.php new file mode 100644 index 0000000000..7d908f514f --- /dev/null +++ b/exampleplugins/5.2/SwagCustomerSearchExtension/SwagCustomerSearchExtension.php @@ -0,0 +1,25 @@ + 'extendCustomerStream' + ]; + } + + public function extendCustomerStream(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $subject */ + $subject = $args->getSubject(); + + $subject->View()->addTemplateDir($this->getPath() . '/Resources/views'); + + $subject->View()->extendsTemplate('backend/customer/swag_customer_stream_extension.js'); + } +} \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Components/YouTubeHandler.php b/exampleplugins/5.2/SwagDigitalPublishingSample/Components/YouTubeHandler.php similarity index 58% rename from exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Components/YouTubeHandler.php rename to exampleplugins/5.2/SwagDigitalPublishingSample/Components/YouTubeHandler.php index e0e1629e8b..baa4af7f0d 100644 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Components/YouTubeHandler.php +++ b/exampleplugins/5.2/SwagDigitalPublishingSample/Components/YouTubeHandler.php @@ -1,8 +1,9 @@ + + + + + %swag_digital_publishing_sample.plugin_dir% + + + + + + %swag_digital_publishing_sample.plugin_dir% + + + + + + + + + + + + + + + diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js b/exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js similarity index 100% rename from exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js rename to exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/backend/swag_digital_publishing_sample/view/editor/extension.js b/exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/backend/swag_digital_publishing_sample/view/editor/extension.js similarity index 100% rename from exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/backend/swag_digital_publishing_sample/view/editor/extension.js rename to exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/backend/swag_digital_publishing_sample/view/editor/extension.js diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/frontend/_public/src/less/all.less b/exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/frontend/less/all.less similarity index 100% rename from exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/frontend/_public/src/less/all.less rename to exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/frontend/less/all.less diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/widgets/swag_digital_publishing/components/youtube.tpl b/exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/widgets/swag_digital_publishing/components/youtube.tpl similarity index 100% rename from exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Views/widgets/swag_digital_publishing/components/youtube.tpl rename to exampleplugins/5.2/SwagDigitalPublishingSample/Resources/views/widgets/swag_digital_publishing/components/youtube.tpl diff --git a/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/BackendSubscriber.php b/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/BackendSubscriber.php new file mode 100644 index 0000000000..b6fb4631dc --- /dev/null +++ b/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/BackendSubscriber.php @@ -0,0 +1,57 @@ +pluginBaseDirectory = $pluginBaseDirectory; + } + + /** + * Returns an array of events you want to subscribe to + * and the names of the corresponding callback methods. + * + * @return array + */ + public static function getSubscribedEvents() + { + return array( + 'Enlight_Controller_Action_PostDispatchSecure_Backend_SwagDigitalPublishing' => 'onPostDispatchBackend', + ); + } + + /** + * Extends the backend templates with the necessary template files. + * + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchBackend(\Enlight_Event_EventArgs $args) + { + /** @var Enlight_Controller_Action $subject */ + $subject = $args->getSubject(); + $view = $subject->View(); + + $view->addTemplateDir($this->pluginBaseDirectory . '/Resources/views/'); + $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/extension.js'); + $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js'); + } +} diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Subscriber/ElementHandler.php b/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/ElementHandler.php similarity index 80% rename from exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Subscriber/ElementHandler.php rename to exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/ElementHandler.php index a06bc518f9..f10e896fbe 100644 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Subscriber/ElementHandler.php +++ b/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/ElementHandler.php @@ -1,10 +1,10 @@ 'onContentBannerFilter' + ]; + } + + /** + * Filter event for the banner elements of the Digital Publishing module. + * Enables you to manipulate the banner data before it gets passed to the frontend. + * + * @param \Enlight_Event_EventArgs $args + * @return mixed + */ + public function onContentBannerFilter(\Enlight_Event_EventArgs $args) + { + $banner = $args->getReturn(); + + // Do some magic data manipulation + + return $banner; + } +} diff --git a/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/WidgetSubscriber.php b/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/WidgetSubscriber.php new file mode 100644 index 0000000000..6e17028db8 --- /dev/null +++ b/exampleplugins/5.2/SwagDigitalPublishingSample/Subscriber/WidgetSubscriber.php @@ -0,0 +1,58 @@ +pluginBaseDirectory = $pluginBaseDirectory; + } + + /** + * Returns an array of events you want to subscribe to + * and the names of the corresponding callback methods. + * + * @return array + */ + public static function getSubscribedEvents() + { + return array( + 'Enlight_Controller_Action_PostDispatchSecure_Widgets_SwagDigitalPublishing' => 'onPostDispatchWidget', + 'Enlight_Controller_Action_PostDispatchSecure_Widgets_Emotion' => 'onPostDispatchWidget', + ); + } + + /** + * Adds the template directory of the plugin to extend the frontend templates. + * + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchWidget(\Enlight_Event_EventArgs $args) + { + /** @var \Enlight_Controller_Action $subject */ + $subject = $args->getSubject(); + $view = $subject->View(); + + $view->addTemplateDir($this->pluginBaseDirectory . '/Resources/views/'); + } +} diff --git a/exampleplugins/5.2/SwagDigitalPublishingSample/SwagDigitalPublishingSample.php b/exampleplugins/5.2/SwagDigitalPublishingSample/SwagDigitalPublishingSample.php new file mode 100644 index 0000000000..61d4584287 --- /dev/null +++ b/exampleplugins/5.2/SwagDigitalPublishingSample/SwagDigitalPublishingSample.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung; + First release; + + + diff --git a/exampleplugins/legacy/Frontend/SwagESBlog/ESIndexingBundle/BlogDataIndexer.php b/exampleplugins/5.2/SwagESBlog/Bundle/ESIndexingBundle/BlogDataIndexer.php similarity index 97% rename from exampleplugins/legacy/Frontend/SwagESBlog/ESIndexingBundle/BlogDataIndexer.php rename to exampleplugins/5.2/SwagESBlog/Bundle/ESIndexingBundle/BlogDataIndexer.php index 4d35652204..c99fb9d671 100644 --- a/exampleplugins/legacy/Frontend/SwagESBlog/ESIndexingBundle/BlogDataIndexer.php +++ b/exampleplugins/5.2/SwagESBlog/Bundle/ESIndexingBundle/BlogDataIndexer.php @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/exampleplugins/legacy/Frontend/SwagESBlog/Subscriber/ORMBacklogSubscriber.php b/exampleplugins/5.2/SwagESBlog/Subscriber/ORMBacklogSubscriber.php similarity index 78% rename from exampleplugins/legacy/Frontend/SwagESBlog/Subscriber/ORMBacklogSubscriber.php rename to exampleplugins/5.2/SwagESBlog/Subscriber/ORMBacklogSubscriber.php index e750a01f30..713d22d8fc 100644 --- a/exampleplugins/legacy/Frontend/SwagESBlog/Subscriber/ORMBacklogSubscriber.php +++ b/exampleplugins/5.2/SwagESBlog/Subscriber/ORMBacklogSubscriber.php @@ -1,28 +1,6 @@ getEntityManager(); + $em = $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); // Entity deletions diff --git a/exampleplugins/5.2/SwagESBlog/SwagESBlog.php b/exampleplugins/5.2/SwagESBlog/SwagESBlog.php new file mode 100644 index 0000000000..fc0f6af372 --- /dev/null +++ b/exampleplugins/5.2/SwagESBlog/SwagESBlog.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung; + First release; + + + diff --git a/exampleplugins/legacy/Frontend/SwagESProduct/ESIndexingBundle/ProductMapping.php b/exampleplugins/5.2/SwagESProduct/Bundle/ESIndexingBundle/ProductMapping.php similarity index 94% rename from exampleplugins/legacy/Frontend/SwagESProduct/ESIndexingBundle/ProductMapping.php rename to exampleplugins/5.2/SwagESProduct/Bundle/ESIndexingBundle/ProductMapping.php index c621cd97d8..1273abdd62 100644 --- a/exampleplugins/legacy/Frontend/SwagESProduct/ESIndexingBundle/ProductMapping.php +++ b/exampleplugins/5.2/SwagESProduct/Bundle/ESIndexingBundle/ProductMapping.php @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/exampleplugins/5.2/SwagESProduct/SwagESProduct.php b/exampleplugins/5.2/SwagESProduct/SwagESProduct.php new file mode 100644 index 0000000000..ffa7e8c27f --- /dev/null +++ b/exampleplugins/5.2/SwagESProduct/SwagESProduct.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung; + First release; + + + diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Components/Emotion/Presets/MyCustomPreset.php b/exampleplugins/5.2/SwagEmotionPresetExample/Components/Emotion/Presets/MyCustomPreset.php new file mode 100644 index 0000000000..9442d68b52 --- /dev/null +++ b/exampleplugins/5.2/SwagEmotionPresetExample/Components/Emotion/Presets/MyCustomPreset.php @@ -0,0 +1,85 @@ + 'My custom preset', 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.', 'locale' => 'gb_GB'], + ['label' => 'Meine Beispiel-Vorlage', 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.', 'locale' => 'de_DE'], + ]; + } + + /** + * @return array + */ + public function getPresetData() + { + $json = SwagEmotionPresetExample::getJsonData('MyCustomPreset.json'); + return json_decode($json, true); + } + + /** + * @return array + */ + public function getRequiredPlugins() + { + return []; + } + + /** + * @return bool + */ + public function getAssetsImported() + { + return false; + } +} diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach1503f8532d4648.jpg b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach1503f8532d4648.jpg new file mode 100644 index 0000000000..8dd989d085 Binary files /dev/null and b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach1503f8532d4648.jpg differ diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach2503f8535275aa.jpg b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach2503f8535275aa.jpg new file mode 100644 index 0000000000..7161731918 Binary files /dev/null and b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach2503f8535275aa.jpg differ diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach3503f853820fa7.jpg b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach3503f853820fa7.jpg new file mode 100644 index 0000000000..c667476806 Binary files /dev/null and b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach3503f853820fa7.jpg differ diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach_teaser5038874e87338.jpg b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach_teaser5038874e87338.jpg new file mode 100644 index 0000000000..e5b5770c01 Binary files /dev/null and b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/beach_teaser5038874e87338.jpg differ diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/bienen_teaser.jpg b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/bienen_teaser.jpg new file mode 100644 index 0000000000..1ff861455f Binary files /dev/null and b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/bienen_teaser.jpg differ diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/deli_teaser503886c2336e3.jpg b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/deli_teaser503886c2336e3.jpg new file mode 100644 index 0000000000..72170b06e3 Binary files /dev/null and b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/deli_teaser503886c2336e3.jpg differ diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/flip_teaser503886e4dd480.jpg b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/flip_teaser503886e4dd480.jpg new file mode 100644 index 0000000000..931741d789 Binary files /dev/null and b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/media/MyCustomPreset/flip_teaser503886e4dd480.jpg differ diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/presetData/MyCustomPreset.json b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/presetData/MyCustomPreset.json new file mode 100644 index 0000000000..933c392ddd --- /dev/null +++ b/exampleplugins/5.2/SwagEmotionPresetExample/Resources/assets/presetData/MyCustomPreset.json @@ -0,0 +1,608 @@ +{ + "showListing": false, + "templateId": 1, + "active": false, + "name": "Meine Startseite", + "position": 1, + "device": "0,1,2,3,4", + "fullscreen": 0, + "isLandingPage": 0, + "seoTitle": "", + "seoKeywords": "", + "seoDescription": "", + "rows": 22, + "cols": 4, + "cellSpacing": 10, + "cellHeight": 185, + "articleHeight": 2, + "mode": "fluid", + "customerStreamIds": null, + "replacement": null, + "elements": [ + { + "componentId": "emotion-components-banner", + "startRow": 1, + "startCol": 4, + "endRow": 3, + "endCol": 4, + "cssClass": null, + "viewports": [ + { + "alias": "xs", + "startRow": 5, + "startCol": 3, + "endRow": 6, + "endCol": 4, + "visible": true + }, + { + "alias": "s", + "startRow": 5, + "startCol": 3, + "endRow": 6, + "endCol": 4, + "visible": true + }, + { + "alias": "m", + "startRow": 5, + "startCol": 3, + "endRow": 6, + "endCol": 4, + "visible": true + }, + { + "alias": "l", + "startRow": 1, + "startCol": 4, + "endRow": 3, + "endCol": 4, + "visible": true + }, + { + "alias": "xl", + "startRow": 1, + "startCol": 4, + "endRow": 3, + "endCol": 4, + "visible": true + } + ], + "data": [ + { + "componentId": "emotion-components-banner", + "fieldId": "file", + "value": "8b16ebc056e613024c057be590b542eb", + "key": "file", + "valueType": "" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "bannerMapping", + "value": "[{\"x\":\"0\",\"y\":\"356\",\"width\":\"251\",\"height\":\"198\",\"link\":\"SW10211\",\"resizerIndex\":0,\"path\":\"\"},{\"x\":\"0\",\"y\":\"184\",\"width\":\"251\",\"height\":\"176\",\"link\":\"SW10170\",\"resizerIndex\":1,\"path\":\"\"},{\"x\":\"0\",\"y\":\"0\",\"width\":\"251\",\"height\":\"188\",\"link\":\"SW10178\",\"resizerIndex\":2,\"path\":\"\"}]", + "key": "bannerMapping", + "valueType": "json" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "link", + "value": "", + "key": "link", + "valueType": "" + } + ], + "syncKey": "preset-element-5911b84503dd45.94762137" + }, + { + "componentId": "emotion-components-banner", + "startRow": 4, + "startCol": 1, + "endRow": 4, + "endCol": 2, + "cssClass": null, + "viewports": [ + { + "alias": "xs", + "startRow": 4, + "startCol": 1, + "endRow": 4, + "endCol": 4, + "visible": true + }, + { + "alias": "s", + "startRow": 4, + "startCol": 1, + "endRow": 4, + "endCol": 4, + "visible": true + }, + { + "alias": "m", + "startRow": 4, + "startCol": 1, + "endRow": 4, + "endCol": 4, + "visible": true + }, + { + "alias": "l", + "startRow": 4, + "startCol": 1, + "endRow": 4, + "endCol": 2, + "visible": true + }, + { + "alias": "xl", + "startRow": 4, + "startCol": 1, + "endRow": 4, + "endCol": 2, + "visible": true + } + ], + "data": [ + { + "componentId": "emotion-components-banner", + "fieldId": "file", + "value": "1728efbda81692282ba642aafd57be3a", + "key": "file", + "valueType": "" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "bannerMapping", + "value": "null", + "key": "bannerMapping", + "valueType": "json" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "link", + "value": "/Campaign/index/emotionId/6", + "key": "link", + "valueType": "" + } + ], + "syncKey": "preset-element-5911b84503df18.17545344" + }, + { + "componentId": "emotion-components-banner", + "startRow": 4, + "startCol": 3, + "endRow": 4, + "endCol": 3, + "cssClass": null, + "viewports": [ + { + "alias": "xs", + "startRow": 5, + "startCol": 1, + "endRow": 5, + "endCol": 2, + "visible": true + }, + { + "alias": "s", + "startRow": 5, + "startCol": 1, + "endRow": 5, + "endCol": 2, + "visible": true + }, + { + "alias": "m", + "startRow": 5, + "startCol": 1, + "endRow": 5, + "endCol": 2, + "visible": true + }, + { + "alias": "l", + "startRow": 4, + "startCol": 3, + "endRow": 4, + "endCol": 3, + "visible": true + }, + { + "alias": "xl", + "startRow": 4, + "startCol": 3, + "endRow": 4, + "endCol": 3, + "visible": true + } + ], + "data": [ + { + "componentId": "emotion-components-banner", + "fieldId": "file", + "value": "db85e2590b6109813dafa101ceb2faeb", + "key": "file", + "valueType": "" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "bannerMapping", + "value": "null", + "key": "bannerMapping", + "valueType": "json" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "link", + "value": "", + "key": "link", + "valueType": "" + } + ], + "syncKey": "preset-element-5911b84503e040.42536946" + }, + { + "componentId": "emotion-components-banner", + "startRow": 4, + "startCol": 4, + "endRow": 4, + "endCol": 4, + "cssClass": null, + "viewports": [ + { + "alias": "xs", + "startRow": 6, + "startCol": 1, + "endRow": 6, + "endCol": 2, + "visible": true + }, + { + "alias": "s", + "startRow": 6, + "startCol": 1, + "endRow": 6, + "endCol": 2, + "visible": true + }, + { + "alias": "m", + "startRow": 6, + "startCol": 1, + "endRow": 6, + "endCol": 2, + "visible": true + }, + { + "alias": "l", + "startRow": 4, + "startCol": 4, + "endRow": 4, + "endCol": 4, + "visible": true + }, + { + "alias": "xl", + "startRow": 4, + "startCol": 4, + "endRow": 4, + "endCol": 4, + "visible": true + } + ], + "data": [ + { + "componentId": "emotion-components-banner", + "fieldId": "file", + "value": "99c5e07b4d5de9d18c350cdf64c5aa3d", + "key": "file", + "valueType": "" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "bannerMapping", + "value": "null", + "key": "bannerMapping", + "valueType": "json" + }, + { + "componentId": "emotion-components-banner", + "fieldId": "link", + "value": "", + "key": "link", + "valueType": "" + } + ], + "syncKey": "preset-element-5911b84503e172.68706708" + }, + { + "componentId": "emotion-components-blog", + "startRow": 6, + "startCol": 1, + "endRow": 6, + "endCol": 4, + "cssClass": null, + "viewports": [ + { + "alias": "xs", + "startRow": 8, + "startCol": 1, + "endRow": 9, + "endCol": 4, + "visible": true + }, + { + "alias": "s", + "startRow": 8, + "startCol": 1, + "endRow": 9, + "endCol": 4, + "visible": true + }, + { + "alias": "m", + "startRow": 8, + "startCol": 1, + "endRow": 9, + "endCol": 4, + "visible": true + }, + { + "alias": "l", + "startRow": 6, + "startCol": 1, + "endRow": 7, + "endCol": 4, + "visible": true + }, + { + "alias": "xl", + "startRow": 6, + "startCol": 1, + "endRow": 7, + "endCol": 4, + "visible": true + } + ], + "data": [ + { + "componentId": "emotion-components-blog", + "fieldId": "entry_amount", + "value": "3", + "key": "entry_amount", + "valueType": "" + }, + { + "componentId": "emotion-components-blog", + "fieldId": "blog_entry_selection", + "value": "17", + "key": "blog_entry_selection", + "valueType": "" + } + ], + "syncKey": "preset-element-5911b84503e263.58237470" + }, + { + "componentId": "emotion-components-banner-slider", + "startRow": 1, + "startCol": 1, + "endRow": 3, + "endCol": 3, + "cssClass": null, + "viewports": [ + { + "alias": "xs", + "startRow": 1, + "startCol": 1, + "endRow": 3, + "endCol": 4, + "visible": true + }, + { + "alias": "s", + "startRow": 1, + "startCol": 1, + "endRow": 3, + "endCol": 4, + "visible": true + }, + { + "alias": "m", + "startRow": 1, + "startCol": 1, + "endRow": 3, + "endCol": 4, + "visible": true + }, + { + "alias": "l", + "startRow": 1, + "startCol": 1, + "endRow": 3, + "endCol": 3, + "visible": true + }, + { + "alias": "xl", + "startRow": 1, + "startCol": 1, + "endRow": 3, + "endCol": 3, + "visible": true + } + ], + "data": [ + { + "componentId": "emotion-components-banner-slider", + "fieldId": "banner_slider_title", + "value": "", + "key": "banner_slider_title", + "valueType": "" + }, + { + "componentId": "emotion-components-banner-slider", + "fieldId": "banner_slider_arrows", + "value": "1", + "key": "banner_slider_arrows", + "valueType": "" + }, + { + "componentId": "emotion-components-banner-slider", + "fieldId": "banner_slider_numbers", + "value": "1", + "key": "banner_slider_numbers", + "valueType": "" + }, + { + "componentId": "emotion-components-banner-slider", + "fieldId": "banner_slider_scrollspeed", + "value": "500", + "key": "banner_slider_scrollspeed", + "valueType": "" + }, + { + "componentId": "emotion-components-banner-slider", + "fieldId": "banner_slider_rotation", + "value": "1", + "key": "banner_slider_rotation", + "valueType": "" + }, + { + "componentId": "emotion-components-banner-slider", + "fieldId": "banner_slider_rotatespeed", + "value": "5000", + "key": "banner_slider_rotatespeed", + "valueType": "" + }, + { + "componentId": "emotion-components-banner-slider", + "fieldId": "banner_slider", + "value": "[{\"position\":0,\"path\":\"e995f98d56967d946471af29d7bf99f1\",\"mediaId\":734,\"link\":\"\"},{\"position\":1,\"path\":\"6cd67d9b6f0150c77bda2eda01ae484c\",\"mediaId\":735,\"link\":\"\"},{\"position\":2,\"path\":\"6bc24fc1ab650b25b4114e93a98f1eba\",\"mediaId\":736,\"link\":\"\"}]", + "key": "banner_slider", + "valueType": "json" + } + ], + "syncKey": "preset-element-5911b84503e3c1.53722051" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "startRow": 5, + "startCol": 1, + "endRow": 5, + "endCol": 4, + "cssClass": null, + "viewports": [ + { + "alias": "xs", + "startRow": 7, + "startCol": 1, + "endRow": 7, + "endCol": 4, + "visible": true + }, + { + "alias": "s", + "startRow": 7, + "startCol": 1, + "endRow": 7, + "endCol": 4, + "visible": true + }, + { + "alias": "m", + "startRow": 7, + "startCol": 1, + "endRow": 7, + "endCol": 4, + "visible": true + }, + { + "alias": "l", + "startRow": 5, + "startCol": 1, + "endRow": 5, + "endCol": 4, + "visible": true + }, + { + "alias": "xl", + "startRow": 5, + "startCol": 1, + "endRow": 5, + "endCol": 4, + "visible": true + } + ], + "data": [ + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "manufacturer_type", + "value": "selected_manufacturers", + "key": "manufacturer_type", + "valueType": "" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "manufacturer_category", + "value": "3", + "key": "manufacturer_category", + "valueType": "" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "selected_manufacturers", + "value": "[{\"position\":0,\"name\":\"The Deli Garage\",\"supplierId\":4,\"path\":\"\"},{\"position\":1,\"name\":\"stop the water while using me\",\"supplierId\":15,\"path\":\"\"},{\"position\":2,\"name\":\"Das blaue Haus\",\"supplierId\":8,\"path\":\"\"},{\"position\":3,\"name\":\"Teapavilion\",\"supplierId\":3,\"path\":\"\"},{\"position\":4,\"name\":\"Feinbrennerei Sasse\",\"supplierId\":2,\"path\":\"\"},{\"position\":5,\"name\":\"Vintage Driver\",\"supplierId\":7,\"path\":\"\"},{\"position\":6,\"name\":\"Access Oires Sisters\",\"supplierId\":5,\"path\":\"\"},{\"position\":7,\"name\":\"Beachdreams Clothes\",\"supplierId\":12,\"path\":\"\"},{\"position\":8,\"name\":\"Sonnenschirm Versand\",\"supplierId\":11,\"path\":\"\"},{\"position\":9,\"name\":\"Sun Smile and Protect\",\"supplierId\":13,\"path\":\"\"}]", + "key": "selected_manufacturers", + "valueType": "json" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "manufacturer_slider_title", + "value": "Unsere Top-Marken", + "key": "manufacturer_slider_title", + "valueType": "" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "manufacturer_slider_arrows", + "value": "", + "key": "manufacturer_slider_arrows", + "valueType": "" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "manufacturer_slider_scrollspeed", + "value": "500", + "key": "manufacturer_slider_scrollspeed", + "valueType": "" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "manufacturer_slider_rotation", + "value": "", + "key": "manufacturer_slider_rotation", + "valueType": "" + }, + { + "componentId": "emotion-components-manufacturer-slider", + "fieldId": "manufacturer_slider_rotatespeed", + "value": "5000", + "key": "manufacturer_slider_rotatespeed", + "valueType": "" + } + ], + "syncKey": "preset-element-5911b84503e568.08219710" + } + ], + "syncData": { + "assets": { + "8b16ebc056e613024c057be590b542eb": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach_teaser5038874e87338.jpg", + "1728efbda81692282ba642aafd57be3a": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/deli_teaser503886c2336e3.jpg", + "db85e2590b6109813dafa101ceb2faeb": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/flip_teaser503886e4dd480.jpg", + "99c5e07b4d5de9d18c350cdf64c5aa3d": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/bienen_teaser.jpg", + "e995f98d56967d946471af29d7bf99f1": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach1503f8532d4648.jpg", + "6cd67d9b6f0150c77bda2eda01ae484c": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach2503f8535275aa.jpg", + "6bc24fc1ab650b25b4114e93a98f1eba": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach3503f853820fa7.jpg" + } + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/SwagEmotionPresetExample.php b/exampleplugins/5.2/SwagEmotionPresetExample/SwagEmotionPresetExample.php new file mode 100644 index 0000000000..9bc9dde73c --- /dev/null +++ b/exampleplugins/5.2/SwagEmotionPresetExample/SwagEmotionPresetExample.php @@ -0,0 +1,83 @@ +container->get('shopware.emotion.preset_installer'); + + $presetInstallerService->installOrUpdate($this->getPresetInstances()); + } + + public function uninstall(UninstallContext $context) + { + /** @var PresetInstaller $presetInstallerService */ + $presetInstallerService = $this->container->get('shopware.emotion.preset_installer'); + + $presetInstallerService->uninstall([ + 'my_custom_preset', + ]); + } + + /** + * @param string $fileName + * @return string + */ + public static function getJsonData($fileName) + { + $json = '{}'; + $jsonPath = SwagEmotionPresetExample::getAssetPath() . '/presetData/' . trim($fileName, '/'); + if (file_exists($jsonPath)) { + $json = file_get_contents($jsonPath); + } + $json = str_replace('___ASSETPATH___', SwagEmotionPresetExample::getAssetPath(), $json); + return $json; + } + + /** + * @return PresetMetaDataInterface[] + */ + private function getPresetInstances() + { + return [ + new MyCustomPreset() + ]; + } +} diff --git a/exampleplugins/5.2/SwagEmotionPresetExample/plugin.xml b/exampleplugins/5.2/SwagEmotionPresetExample/plugin.xml new file mode 100644 index 0000000000..16d22a0ef9 --- /dev/null +++ b/exampleplugins/5.2/SwagEmotionPresetExample/plugin.xml @@ -0,0 +1,13 @@ + + + + + + + 1.0.0 + (c) shopware AG + shopware AG + + + diff --git a/exampleplugins/5.2/SwagEvents/Components/NameClass1.php b/exampleplugins/5.2/SwagEvents/Components/NameClass1.php new file mode 100644 index 0000000000..22a0ef3c7b --- /dev/null +++ b/exampleplugins/5.2/SwagEvents/Components/NameClass1.php @@ -0,0 +1,14 @@ +container->get('front')->Plugins()->ViewRenderer()->setNoRender(); + } + + public function notifyAction() + { + // do some magic + + $this->container->get('events')->notify( + 'SwagEvent_Controller_notifyAction', // give the event a unique name and add the payload + [ + 'payload' => 123, + 'payload2' => 'more Payload' + ] + ); + + // do some magic + } + + public function notifyUntilAction() + { + // do some magic + + $stop = $this->container->get('events')->notifyUntil( + 'SwagEvent_Controller_notifyUntilAction', + [ + // Edit the stop boolean and see the different behavior. + 'stop' => false + ] + ); + + if ($stop) { + echo '
';
+            var_export('Stop is true');
+            echo '
'; + return; + } + + echo '
';
+        var_export('Stop is false');
+        echo '
'; + die('End'); + } + + public function filterAction() + { + $result = [ + ['id' => 1], + ['id' => 2], + ['id' => 3], + ]; + + $eventManager = $this->container->get('events'); + + $result = $eventManager->filter('SwagEvent_Controller_filterAction', 'some value', ['data' => $result]); + + echo '
';
+        var_export($result);
+        echo '
'; + die('END'); + } + + public function collectAction() + { + $collection = new ArrayCollection([ + new \SwagEvents\Components\NameClass1(), + new \SwagEvents\Components\NameClass2() + ]); + + $eventManager = $this->container->get('events'); + $eventManager->collect('SwagEvent_Controller_collectAction', $collection); + + /** @var NameClassInterface $nameClass */ + foreach ($collection->toArray() as $nameClass) { + echo $nameClass->getName(); + echo '
'; + } + + echo '
';
+        \Doctrine\Common\Util\Debug::dump($collection->toArray());
+        echo '
'; + die('END'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagEvents/Resources/services.xml b/exampleplugins/5.2/SwagEvents/Resources/services.xml new file mode 100644 index 0000000000..a0e53a3dad --- /dev/null +++ b/exampleplugins/5.2/SwagEvents/Resources/services.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/exampleplugins/5.2/SwagEvents/Subscriber/FrontendListingSubscriber.php b/exampleplugins/5.2/SwagEvents/Subscriber/FrontendListingSubscriber.php new file mode 100644 index 0000000000..d4b0f0512b --- /dev/null +++ b/exampleplugins/5.2/SwagEvents/Subscriber/FrontendListingSubscriber.php @@ -0,0 +1,30 @@ + 'onFrontendListing' + ]; + } + + public function onFrontendListing(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Frontend_Listing $subject */ + $subject = $args->getSubject(); + + // Do some magic with the listing data + + // TODO: Remove after debug + echo '
';
+        var_export($subject->View()->getAssign());
+        echo '
'; + die(); + // TODO: Remove after debug + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagEvents/Subscriber/SwagEventsSubscriber.php b/exampleplugins/5.2/SwagEvents/Subscriber/SwagEventsSubscriber.php new file mode 100644 index 0000000000..91f6298152 --- /dev/null +++ b/exampleplugins/5.2/SwagEvents/Subscriber/SwagEventsSubscriber.php @@ -0,0 +1,96 @@ + 'onNotify', + 'SwagEvent_Controller_notifyUntilAction' => 'onNotifyUntil', + 'SwagEvent_Controller_filterAction' => 'onFilter', + 'SwagEvent_Controller_collectAction' => 'onCollect', + ]; + } + + + /** + * @param \Enlight_Event_EventArgs $args + */ + public function onNotify(\Enlight_Event_EventArgs $args) + { + echo '
';
+        echo 'Do some magic';
+        echo '
'; + \Doctrine\Common\Util\Debug::dump($args); + echo '
'; + var_export($args->get('payload')); + echo '
'; + var_export($args->get('payload2')); + echo '
'; + + die('END'); + } + + /** + * @param \Enlight_Event_EventArgs $args + * + * @return bool | Void + */ + public function onNotifyUntil(\Enlight_Event_EventArgs $args) + { + $stop = $args->get('stop'); + + if ($stop) { + // if you return some result you stop the callStack + return true; + } + } + + /** + * @param \Enlight_Event_EventArgs $args + * + * @return array + */ + public function onFilter(\Enlight_Event_EventArgs $args) + { + $return = $args->get('data'); + $value = $args->getReturn(); + + if ($value === 'some value') { + foreach ($return as $key => $value) { + if ($value['id'] === 2) { + $return[$key] = [ + 'id' => 178 + ]; + } + } + } + + return $return; + } + + /** + * @param \Enlight_Event_EventArgs $args + * + * @return ArrayCollection + */ + public function onCollect(\Enlight_Event_EventArgs $args) + { + return new ArrayCollection( + [ + new NameClass3(), + new NameClass4() + ] + ); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagEvents/SwagEvents.php b/exampleplugins/5.2/SwagEvents/SwagEvents.php new file mode 100644 index 0000000000..c6bdf5dfc9 --- /dev/null +++ b/exampleplugins/5.2/SwagEvents/SwagEvents.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung; + First release; + + + diff --git a/exampleplugins/5.2/SwagExtendArticleResource/plugin.xml b/exampleplugins/5.2/SwagExtendArticleResource/plugin.xml index cdde4932e6..a227141c05 100644 --- a/exampleplugins/5.2/SwagExtendArticleResource/plugin.xml +++ b/exampleplugins/5.2/SwagExtendArticleResource/plugin.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> 1.0.0 diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Components/Types/CustomType.php b/exampleplugins/5.2/SwagExtendCustomProducts/Components/Types/CustomType.php new file mode 100644 index 0000000000..185ef5e3a6 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Components/Types/CustomType.php @@ -0,0 +1,27 @@ +fileTypeWhitelist = $fileTypeWhitelist; + } + + /** + * {@inheritdoc} + */ + public function getMimeTypeWhitelist($type) + { + if ($type === FileUploadType::TYPE) { + return $this->getMimeTypeWhitelistForFiles(); + } + + return $this->fileTypeWhitelist->getMimeTypeWhitelist($type); + } + + /** + * {@inheritdoc} + */ + public function getExtensionWhitelist($type) + { + return $this->fileTypeWhitelist->getExtensionWhitelist($type); + } + + /** + * {@inheritdoc} + */ + public function getMediaOverrideType($extension) + { + return $this->fileTypeWhitelist->getMediaOverrideType($extension); + } + + /** + * Add new mimeTypes to whiteList + * + * @return array + */ + private function getMimeTypeWhitelistForFiles() + { + $newMimeTypes = [ + 'video/x-ms-asf', // .asf + 'video/x-ms-asf', // .asx + 'video/x-ms-wvx', // .wvx + 'video/x-ms-wm', // .wm + 'video/x-ms-wmx', // .wmx + 'audio/x-ms-wma', // .wma + 'audio/x-ms-wax', // .wax + 'audio/x-ms-wmv', // .wmv + 'application/x-ms-wmz', // .wmz + 'application/x-ms-wmd', // .wmd + ]; + + return array_merge( + FileTypeWhitelist::$mimeTypeWhitelist['file'], + $newMimeTypes + ); + } +} diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/backend/swag_custom_products/view/option/types/custom_type.js b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/backend/swag_custom_products/view/option/types/custom_type.js new file mode 100644 index 0000000000..211211ac8e --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/backend/swag_custom_products/view/option/types/custom_type.js @@ -0,0 +1,7 @@ +//{block name="backend/swag_custom_products/view/option/types/customType"} +// Take the original Custom Product Type and only use "Custom Type" as suffix. +// Custom Products is building this path in "Shopware.apps.SwagCustomProducts.view.option.Detail" +Ext.define('Shopware.apps.SwagCustomProducts.view.option.types.CustomType', { + extend: 'Shopware.apps.SwagCustomProducts.view.option.types.AbstractTypeContainer' +}); +//{/block} diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js new file mode 100644 index 0000000000..7df73f3e15 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js @@ -0,0 +1,4 @@ +//{block name="backend/swag_custom_products/components/typeTranslator/snippets"} +//{$smarty.block.parent} + customType: 'My CustomType Name', +//{/block} diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/frontend/swag_custom_products/options/customType.tpl b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/frontend/swag_custom_products/options/customType.tpl new file mode 100644 index 0000000000..c8c0718d8d --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/Views/frontend/swag_custom_products/options/customType.tpl @@ -0,0 +1,9 @@ +{block name="frontend_detail_swag_custom_products_options_customtype"} + +{/block} diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Resources/services.xml b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/services.xml new file mode 100644 index 0000000000..f4fdaacd1e --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Resources/services.xml @@ -0,0 +1,32 @@ + + + + + + + %swag_extend_custom_products.plugin_dir% + + + + + %swag_extend_custom_products.plugin_dir% + + + + + + + + + + + + diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/Backend.php b/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/Backend.php new file mode 100644 index 0000000000..3cbedfa8e8 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/Backend.php @@ -0,0 +1,52 @@ +path = $pluginPath; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatch_Backend_SwagCustomProducts' => 'extendBackendModule', + ]; + } + + /** + * @param \Enlight_Event_EventArgs $arguments + */ + public function extendBackendModule(\Enlight_Event_EventArgs $arguments) + { + /** @var \Shopware_Controllers_Backend_SwagCustomProducts $subject */ + $subject = $arguments->get('subject'); + + $view = $subject->View(); + + $view->addTemplateDir($this->path . '/Resources/Views/'); + + if ($arguments->get('request')->getActionName() === 'index') { + $view->extendsTemplate('backend/swag_custom_products/view/option/types/custom_type.js'); + } + + if ($arguments->get('request')->getActionName() === 'load') { + $view->extendsTemplate('backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js'); + } + } +} diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/Frontend.php b/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/Frontend.php new file mode 100644 index 0000000000..266600b07c --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/Frontend.php @@ -0,0 +1,44 @@ +path = $pluginPath; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail' => 'extendFrontendDetail', + ]; + } + + /** + * @param \Enlight_Event_EventArgs $arguments + */ + public function extendFrontendDetail(\Enlight_Event_EventArgs $arguments) + { + /** @var \Shopware_Controllers_Frontend_Detail $subject */ + $subject = $arguments->get('subject'); + + $view = $subject->View(); + + $view->addTemplateDir($this->path . '/Resources/Views/'); + } +} diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/TypeFactory.php b/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/TypeFactory.php new file mode 100644 index 0000000000..6b0ec70a76 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/Subscriber/TypeFactory.php @@ -0,0 +1,34 @@ + 'onCollectTypes', + ]; + } + + /** + * Returns our new type(s) as ArrayCollection + * + * @return ArrayCollection + */ + public function onCollectTypes() + { + return new ArrayCollection( + [ + CustomType::TYPE => new CustomType(), + ] + ); + } +} diff --git a/exampleplugins/5.2/SwagExtendCustomProducts/SwagExtendCustomProducts.php b/exampleplugins/5.2/SwagExtendCustomProducts/SwagExtendCustomProducts.php new file mode 100644 index 0000000000..2e64fb3cf6 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomProducts/SwagExtendCustomProducts.php @@ -0,0 +1,9 @@ + + + + + + + 1.0.0 + (c) by shopware AG + proprietary + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung; + First release; + + + + + + diff --git a/exampleplugins/5.2/SwagExtendCustomer/Resources/services.xml b/exampleplugins/5.2/SwagExtendCustomer/Resources/services.xml new file mode 100644 index 0000000000..10d5359e53 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomer/Resources/services.xml @@ -0,0 +1,11 @@ + + + + + %swag_extend_customer.plugin_dir% + + + + diff --git a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/app.js b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/app.js similarity index 61% rename from exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/app.js rename to exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/app.js index a3e075599b..2ed76baa31 100644 --- a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/app.js +++ b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/app.js @@ -1,4 +1,5 @@ //{block name="backend/customer/application"} // {$smarty.block.parent} +// {include file="backend/swag_extend_customer/controller/my_own_controller.js"} // {include file="backend/swag_extend_customer/view/detail/my_own_tab.js"} -//{/block} \ No newline at end of file +//{/block} diff --git a/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/controller/my_own_controller.js b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/controller/my_own_controller.js new file mode 100644 index 0000000000..9eba906171 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/controller/my_own_controller.js @@ -0,0 +1,17 @@ +// This is the controller + + +Ext.define('Shopware.apps.SwagExtendCustomer.controller.MyOwnController', { + /** + * Override the customer main controller + * @string + */ + override: 'Shopware.apps.Customer.controller.Main', + + init: function () { + var me = this; + + // me.callParent will execute the init function of the overridden controller + me.callParent(arguments); + } +}); diff --git a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/my_own_tab.js b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/view/detail/my_own_tab.js similarity index 99% rename from exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/my_own_tab.js rename to exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/view/detail/my_own_tab.js index 9d71305a5d..aa0df6012d 100644 --- a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/my_own_tab.js +++ b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/view/detail/my_own_tab.js @@ -1,5 +1,7 @@ // This tab will be shown in the customer module + + Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.MyOwnTab', { extend: 'Ext.container.Container', padding: 10, diff --git a/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/view/detail/window.js b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/view/detail/window.js new file mode 100644 index 0000000000..874ef87898 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomer/Resources/views/backend/swag_extend_customer/view/detail/window.js @@ -0,0 +1,60 @@ +//{block name="backend/customer/view/detail/window"} +// {$smarty.block.parent} +Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Window', { + override: 'Shopware.apps.Customer.view.detail.Window', + + getTabs: function() { + var me = this, + result = me.callParent(); + + result.push(Ext.create('Shopware.apps.SwagExtendCustomer.view.detail.MyOwnTab')); + + return result; + }, + + /** + * Replace the textBox for field "title" with a comboBox + * + * @returns { Ext.form.FieldSet } + */ + createPersonalFieldSet: function() { + var me = this, + fieldSet = me.callParent(arguments), + titelField = fieldSet.down('[name="title"]'), + titleContainer = titelField.up('container'); + + titelField.destroy(); + + titleContainer.insert(0, Ext.create('Ext.form.field.ComboBox', { + labelWidth: 155, + anchor: '95%', + name: 'title', + displayField: 'name', + valueField: 'name', + store: me.createTitleStore(), + fieldLabel: 'Title' + })); + + return fieldSet; + }, + + /** + * @returns { Ext.data.Store } + */ + createTitleStore: function() { + return Ext.create('Ext.data.Store', { + fields: [ + { name: 'name' } + ], + data: [ + { name: 'Sir' }, + { name: 'Madame' }, + { name: 'Lord' }, + { name: 'Dr.' }, + { name: 'Prof.' }, + { name: 'Prof. Dr.' } + ] + }) + } +}); +//{/block} diff --git a/exampleplugins/5.2/SwagExtendCustomer/Subscriber/ExtendCustomer.php b/exampleplugins/5.2/SwagExtendCustomer/Subscriber/ExtendCustomer.php new file mode 100644 index 0000000000..9a5572e724 --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomer/Subscriber/ExtendCustomer.php @@ -0,0 +1,49 @@ +pluginDirectory = $pluginDirectory; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Customer' => 'onCustomerPostDispatch' + ]; + } + + public function onCustomerPostDispatch(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $controller */ + $controller = $args->getSubject(); + + $view = $controller->View(); + $request = $controller->Request(); + + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); + + if ($request->getActionName() == 'index') { + $view->extendsTemplate('backend/swag_extend_customer/app.js'); + } + + if ($request->getActionName() == 'load') { + $view->extendsTemplate('backend/swag_extend_customer/view/detail/window.js'); + } + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagExtendCustomer/SwagExtendCustomer.php b/exampleplugins/5.2/SwagExtendCustomer/SwagExtendCustomer.php new file mode 100644 index 0000000000..58e27b72fa --- /dev/null +++ b/exampleplugins/5.2/SwagExtendCustomer/SwagExtendCustomer.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/5.2/SwagGlobalVariables/SwagGlobalVariables.php b/exampleplugins/5.2/SwagGlobalVariables/SwagGlobalVariables.php index a217ea2afe..b5f076a895 100644 --- a/exampleplugins/5.2/SwagGlobalVariables/SwagGlobalVariables.php +++ b/exampleplugins/5.2/SwagGlobalVariables/SwagGlobalVariables.php @@ -22,6 +22,6 @@ public static function getSubscribedEvents() */ public function onPostDispatch(\Enlight_Controller_ActionEventArgs $args) { - $args->getSubject()->View()->assign('sUserloggedIn', Shopware()->Modules()->Admin()->sCheckUser()); + $args->getSubject()->View()->assign('sUserLoggedIn', Shopware()->Modules()->Admin()->sCheckUser()); } -} \ No newline at end of file +} diff --git a/exampleplugins/5.2/SwagGlobalVariables/plugin.xml b/exampleplugins/5.2/SwagGlobalVariables/plugin.xml index 7d6cd3a688..c9334c1c09 100644 --- a/exampleplugins/5.2/SwagGlobalVariables/plugin.xml +++ b/exampleplugins/5.2/SwagGlobalVariables/plugin.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> diff --git a/exampleplugins/5.2/SwagLastRegistrationsWidget/SwagLastRegistrationsWidget.php b/exampleplugins/5.2/SwagLastRegistrationsWidget/SwagLastRegistrationsWidget.php index e10c880e7f..3d6e2512ff 100755 --- a/exampleplugins/5.2/SwagLastRegistrationsWidget/SwagLastRegistrationsWidget.php +++ b/exampleplugins/5.2/SwagLastRegistrationsWidget/SwagLastRegistrationsWidget.php @@ -26,7 +26,7 @@ public static function getSubscribedEvents() */ public function onGetBackendControllerPath() { - return __DIR__ . '/Controllers/Backend/SwagLastRegistrationsWidget.php'; + return $this->getPath() . '/Controllers/Backend/SwagLastRegistrationsWidget.php'; } /** diff --git a/exampleplugins/5.2/SwagLastRegistrationsWidget/plugin.xml b/exampleplugins/5.2/SwagLastRegistrationsWidget/plugin.xml index f33445444c..48b0ea6ccc 100644 --- a/exampleplugins/5.2/SwagLastRegistrationsWidget/plugin.xml +++ b/exampleplugins/5.2/SwagLastRegistrationsWidget/plugin.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> diff --git a/exampleplugins/legacy/Frontend/SwagMd5Reversed/Md5ReversedEncoder.php b/exampleplugins/5.2/SwagMd5Reversed/Components/Md5ReversedEncoder.php similarity index 96% rename from exampleplugins/legacy/Frontend/SwagMd5Reversed/Md5ReversedEncoder.php rename to exampleplugins/5.2/SwagMd5Reversed/Components/Md5ReversedEncoder.php index 873a260ecd..bf9cc1f0e6 100644 --- a/exampleplugins/legacy/Frontend/SwagMd5Reversed/Md5ReversedEncoder.php +++ b/exampleplugins/5.2/SwagMd5Reversed/Components/Md5ReversedEncoder.php @@ -1,6 +1,6 @@ + + + + + + + + + + diff --git a/exampleplugins/5.2/SwagMd5Reversed/Subscriber/AddEncoderSubscriber.php b/exampleplugins/5.2/SwagMd5Reversed/Subscriber/AddEncoderSubscriber.php new file mode 100644 index 0000000000..ddcd1c2af3 --- /dev/null +++ b/exampleplugins/5.2/SwagMd5Reversed/Subscriber/AddEncoderSubscriber.php @@ -0,0 +1,35 @@ + 'onAddEncoder' + ]; + } + + /** + * Add the encoder to the internal encoder collection + * + * @param Enlight_Event_EventArgs $args + * @return array + */ + public function onAddEncoder(Enlight_Event_EventArgs $args) + { + $passwordHashHandler = $args->getReturn(); + + $passwordHashHandler[] = new Md5ReversedEncoder(); + + return $passwordHashHandler; + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagMd5Reversed/SwagMd5Reversed.php b/exampleplugins/5.2/SwagMd5Reversed/SwagMd5Reversed.php new file mode 100644 index 0000000000..ce5049f6dd --- /dev/null +++ b/exampleplugins/5.2/SwagMd5Reversed/SwagMd5Reversed.php @@ -0,0 +1,10 @@ + + + + ./tests + + + + + ./ + + ./tests + ./vendor + + + + \ No newline at end of file diff --git a/exampleplugins/5.2/SwagMd5Reversed/plugin.xml b/exampleplugins/5.2/SwagMd5Reversed/plugin.xml new file mode 100644 index 0000000000..9aa0423c8d --- /dev/null +++ b/exampleplugins/5.2/SwagMd5Reversed/plugin.xml @@ -0,0 +1,19 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung; + First release; + + + diff --git a/exampleplugins/5.2/SwagMd5Reversed/tests/Bootstrap.php b/exampleplugins/5.2/SwagMd5Reversed/tests/Bootstrap.php new file mode 100644 index 0000000000..9b7eac2283 --- /dev/null +++ b/exampleplugins/5.2/SwagMd5Reversed/tests/Bootstrap.php @@ -0,0 +1,10 @@ +registerNamespace( + 'SwagMd5Reversed', + __DIR__ . '/../' +); diff --git a/exampleplugins/legacy/Frontend/SwagMd5Reversed/tests/Md5ReversedTest.php b/exampleplugins/5.2/SwagMd5Reversed/tests/Md5ReversedTest.php similarity index 73% rename from exampleplugins/legacy/Frontend/SwagMd5Reversed/tests/Md5ReversedTest.php rename to exampleplugins/5.2/SwagMd5Reversed/tests/Md5ReversedTest.php index b4230c856a..f4a8c486ff 100644 --- a/exampleplugins/legacy/Frontend/SwagMd5Reversed/tests/Md5ReversedTest.php +++ b/exampleplugins/5.2/SwagMd5Reversed/tests/Md5ReversedTest.php @@ -1,5 +1,6 @@ encodePassword('secret'); $this->assertEquals($hash, 'a7e86e2302d08ea6d3ff635f856468f4'); @@ -17,7 +18,7 @@ public function testHashEncoder() public function testIsValid() { - $encoder = new \Shopware\SwagMd5Reversed\Md5ReversedEncoder(); + $encoder = new SwagMd5Reversed\Components\Md5ReversedEncoder(); $isValid = $encoder->isPasswordValid('secret', 'a7e86e2302d08ea6d3ff635f856468f4'); $this->assertTrue($isValid); @@ -25,7 +26,7 @@ public function testIsValid() public function testInvalidPassword() { - $encoder = new \Shopware\SwagMd5Reversed\Md5ReversedEncoder(); + $encoder = new SwagMd5Reversed\Components\Md5ReversedEncoder(); $isValid = $encoder->isPasswordValid('secret', 'some random hash'); $this->assertFalse($isValid); diff --git a/exampleplugins/5.2/SwagModel/Bootstrap/Database.php b/exampleplugins/5.2/SwagModel/Bootstrap/Database.php new file mode 100644 index 0000000000..f9c9cacb58 --- /dev/null +++ b/exampleplugins/5.2/SwagModel/Bootstrap/Database.php @@ -0,0 +1,55 @@ +entityManager = $entityManager; + $this->schemaTool = new SchemaTool($this->entityManager); + } + + /** + * Installs all registered ORM classes + */ + public function install() + { + $this->schemaTool->updateSchema( + $this->getClassesMetaData(), + true // make sure to use the save mode + ); + } + + /** + * Drops all registered ORM classes + */ + public function uninstall() + { + $this->schemaTool->dropSchema( + $this->getClassesMetaData() + ); + } + + /** + * @return array + */ + private function getClassesMetaData() + { + return [ + $this->entityManager->getClassMetadata(MyPluginModel::class) + ]; + } +} \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagModelPlugin/Models/SwagModelPlugin/MyPluginModel.php b/exampleplugins/5.2/SwagModel/Models/MyPluginModel.php similarity index 89% rename from exampleplugins/legacy/Frontend/SwagModelPlugin/Models/SwagModelPlugin/MyPluginModel.php rename to exampleplugins/5.2/SwagModel/Models/MyPluginModel.php index 807fbfb373..35fad39e3e 100644 --- a/exampleplugins/legacy/Frontend/SwagModelPlugin/Models/SwagModelPlugin/MyPluginModel.php +++ b/exampleplugins/5.2/SwagModel/Models/MyPluginModel.php @@ -1,8 +1,8 @@ + + + + + + + + + + diff --git a/exampleplugins/5.2/SwagModel/Subscriber/ModelSubscriber.php b/exampleplugins/5.2/SwagModel/Subscriber/ModelSubscriber.php new file mode 100644 index 0000000000..2fc9e0fb54 --- /dev/null +++ b/exampleplugins/5.2/SwagModel/Subscriber/ModelSubscriber.php @@ -0,0 +1,53 @@ +getEntityManager(); + + $model = $arguments->getEntity(); + + if(!$model instanceof Article) { + return; + } + + // modify product data + } + + /** + * @param LifecycleEventArgs $arguments + */ + public function postUpdate(LifecycleEventArgs $arguments) + { + /** @var EntityManager $modelManager */ + $modelManager = $arguments->getEntityManager(); + + $model = $arguments->getEntity(); + + // modify models or do some other fancy stuff + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagModel/SwagModel.php b/exampleplugins/5.2/SwagModel/SwagModel.php new file mode 100644 index 0000000000..84d2768e8c --- /dev/null +++ b/exampleplugins/5.2/SwagModel/SwagModel.php @@ -0,0 +1,39 @@ +container->get('models') + ); + + $database->install(); + } + + /** + * @param UninstallContext $uninstallContext + */ + public function uninstall(UninstallContext $uninstallContext) + { + $database = new Database( + $this->container->get('models') + ); + + if ($uninstallContext->keepUserData()) { + return; + } + + $database->uninstall(); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagModel/plugin.xml b/exampleplugins/5.2/SwagModel/plugin.xml new file mode 100644 index 0000000000..96a0329952 --- /dev/null +++ b/exampleplugins/5.2/SwagModel/plugin.xml @@ -0,0 +1,19 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung; + First release; + + + diff --git a/exampleplugins/5.2/SwagPaymentExample/plugin.xml b/exampleplugins/5.2/SwagPaymentExample/plugin.xml index 4c30bf31e0..abb5c337d2 100644 --- a/exampleplugins/5.2/SwagPaymentExample/plugin.xml +++ b/exampleplugins/5.2/SwagPaymentExample/plugin.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> diff --git a/exampleplugins/legacy/Frontend/SwagPluginSystem/StoreFrontBundle/ListProductService.php b/exampleplugins/5.2/SwagPluginSystem/Bundle/StoreFrontBundle/ListProductService.php similarity index 96% rename from exampleplugins/legacy/Frontend/SwagPluginSystem/StoreFrontBundle/ListProductService.php rename to exampleplugins/5.2/SwagPluginSystem/Bundle/StoreFrontBundle/ListProductService.php index 932cb998bb..03e53bc934 100644 --- a/exampleplugins/legacy/Frontend/SwagPluginSystem/StoreFrontBundle/ListProductService.php +++ b/exampleplugins/5.2/SwagPluginSystem/Bundle/StoreFrontBundle/ListProductService.php @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + %swag_plugin_system.plugin_dir% + + + + + diff --git a/exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/detail/actions.tpl b/exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/detail/actions.tpl similarity index 100% rename from exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/detail/actions.tpl rename to exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/detail/actions.tpl diff --git a/exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/_public/src/less/all.less b/exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/less/all.less similarity index 100% rename from exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/_public/src/less/all.less rename to exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/less/all.less diff --git a/exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/listing/product-box/product-badges.tpl b/exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/listing/product-box/product-badges.tpl similarity index 100% rename from exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/listing/product-box/product-badges.tpl rename to exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/listing/product-box/product-badges.tpl diff --git a/exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/swag_plugin_system/detail-link.tpl b/exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/swag_plugin_system/detail-link.tpl similarity index 100% rename from exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/swag_plugin_system/detail-link.tpl rename to exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/swag_plugin_system/detail-link.tpl diff --git a/exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/swag_plugin_system/listing-badge.tpl b/exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/swag_plugin_system/listing-badge.tpl similarity index 100% rename from exampleplugins/legacy/Frontend/SwagPluginSystem/Views/frontend/swag_plugin_system/listing-badge.tpl rename to exampleplugins/5.2/SwagPluginSystem/Resources/views/frontend/swag_plugin_system/listing-badge.tpl diff --git a/exampleplugins/5.2/SwagPluginSystem/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagPluginSystem/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..4a7edd12fe --- /dev/null +++ b/exampleplugins/5.2/SwagPluginSystem/Subscriber/TemplateRegistration.php @@ -0,0 +1,43 @@ +pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagPluginSystem/SwagPluginSystem.php b/exampleplugins/5.2/SwagPluginSystem/SwagPluginSystem.php new file mode 100644 index 0000000000..2a7655dc8d --- /dev/null +++ b/exampleplugins/5.2/SwagPluginSystem/SwagPluginSystem.php @@ -0,0 +1,10 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Controllers/Backend/SwagProduct.php b/exampleplugins/5.2/SwagProductAssoc/Controllers/Backend/SwagProductAssoc.php similarity index 84% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Controllers/Backend/SwagProduct.php rename to exampleplugins/5.2/SwagProductAssoc/Controllers/Backend/SwagProductAssoc.php index ce664a3628..0df2c54646 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Controllers/Backend/SwagProduct.php +++ b/exampleplugins/5.2/SwagProductAssoc/Controllers/Backend/SwagProductAssoc.php @@ -1,8 +1,10 @@ getManager()->createQueryBuilder(); $builder->select(array('products', 'categories')) - ->from('Shopware\CustomModels\Product\Product', 'products') + ->from(Product::class, 'products') ->innerJoin('products.categories', 'categories') ->where('products.id = :id') ->setParameter('id', $productId); diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Models/Product/Attribute.php b/exampleplugins/5.2/SwagProductAssoc/Models/Attribute.php similarity index 92% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Models/Product/Attribute.php rename to exampleplugins/5.2/SwagProductAssoc/Models/Attribute.php index 7dbf66d2c3..ec97c08efa 100644 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Models/Product/Attribute.php +++ b/exampleplugins/5.2/SwagProductAssoc/Models/Attribute.php @@ -1,11 +1,9 @@ setOneToOne( $attribute, - '\Shopware\CustomModels\Product\Attribute', + Attribute::class, 'attribute', 'product' ); } /** - * @return \Shopware\CustomModels\Product\Variant[] + * @return Variant[] */ public function getVariants() { @@ -277,14 +276,14 @@ public function getVariants() } /** - * @param \Shopware\CustomModels\Product\Variant[] $variants + * @param Variant[] $variants * @return \Shopware\Components\Model\ModelEntity */ public function setVariants($variants) { return $this->setOneToMany( $variants, - '\Shopware\CustomModels\Product\Variant', + Variant::class, 'variants', 'product' ); diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Models/Product/Variant.php b/exampleplugins/5.2/SwagProductAssoc/Models/Variant.php similarity index 93% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Models/Product/Variant.php rename to exampleplugins/5.2/SwagProductAssoc/Models/Variant.php index fcd1050ca8..8a50965863 100644 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Models/Product/Variant.php +++ b/exampleplugins/5.2/SwagProductAssoc/Models/Variant.php @@ -1,11 +1,9 @@ + + + + + SwagProductAssoc + + + SwagProductAssoc + index + sprite-application-block + Article + + + diff --git a/exampleplugins/5.2/SwagProductAssoc/Resources/services.xml b/exampleplugins/5.2/SwagProductAssoc/Resources/services.xml new file mode 100644 index 0000000000..45f8ecec92 --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/services.xml @@ -0,0 +1,12 @@ + + + + + %swag_product_assoc.plugin_dir% + + + + + diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/app.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/app.js similarity index 82% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/app.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/app.js index 82f2fa3aa2..0d90d9f9a9 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/app.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/app.js @@ -1,8 +1,8 @@ -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductAssoc', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductAssoc', loadPath: '{url action=load}', bulkLoad: true, @@ -18,7 +18,7 @@ Ext.define('Shopware.apps.SwagProduct', { 'detail.Category', 'detail.Attribute', - 'detail.Variant', + 'detail.Variant' ], models: [ diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/controller/main.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/controller/main.js similarity index 72% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/controller/main.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/controller/main.js index 31b1e5f39d..315580f0d7 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/controller/main.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/controller/main.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductAssoc.controller.Main', { extend: 'Enlight.app.Controller', init: function() { diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/attribute.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/attribute.js similarity index 77% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/attribute.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/attribute.js index ea739afd99..5ace9f3350 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/attribute.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/attribute.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Attribute', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Attribute', { extend: 'Shopware.data.Model', configure: function() { return { - detail: 'Shopware.apps.SwagProduct.view.detail.Attribute' + detail: 'Shopware.apps.SwagProductAssoc.view.detail.Attribute' }; }, diff --git a/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/category.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/category.js new file mode 100755 index 0000000000..a39ef01a40 --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/category.js @@ -0,0 +1,12 @@ + +Ext.define('Shopware.apps.SwagProductAssoc.model.Category', { + + extend: 'Shopware.apps.Base.model.Category', + + configure: function() { + return { + related: 'Shopware.apps.SwagProductAssoc.view.detail.Category' + } + } +}); + diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/product.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/product.js similarity index 75% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/product.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/product.js index ccf9c2ab33..5be9628222 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/product.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/product.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { - controller: 'SwagProduct', - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + controller: 'SwagProductAssoc', + detail: 'Shopware.apps.SwagProductAssoc.view.detail.Product' }; }, @@ -43,7 +43,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { relation: 'ManyToMany', type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Category', + model: 'Shopware.apps.SwagProductAssoc.model.Category', name: 'getCategory', associationKey: 'categories' }, @@ -51,17 +51,17 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { relation: 'OneToOne', type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Attribute', + model: 'Shopware.apps.SwagProductAssoc.model.Attribute', name: 'getAttribute', associationKey: 'attribute' }, { relation: 'OneToMany', - storeClass: 'Shopware.apps.SwagProduct.store.Variant', + storeClass: 'Shopware.apps.SwagProductAssoc.store.Variant', loadOnDemand: true, type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Variant', + model: 'Shopware.apps.SwagProductAssoc.model.Variant', name: 'getVariants', associationKey: 'variants' }, diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/variant.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/variant.js similarity index 77% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/variant.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/variant.js index af5528e997..f9e63cc29e 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/variant.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/model/variant.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Variant', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Variant', { extend: 'Shopware.data.Model', configure: function() { return { - listing: 'Shopware.apps.SwagProduct.view.detail.Variant' + listing: 'Shopware.apps.SwagProductAssoc.view.detail.Variant' }; }, diff --git a/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/store/product.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/store/product.js new file mode 100755 index 0000000000..690467372f --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/store/product.js @@ -0,0 +1,11 @@ + +Ext.define('Shopware.apps.SwagProductAssoc.store.Product', { + extend: 'Shopware.store.Listing', + + configure: function () { + return { + controller: 'SwagProductAssoc' + }; + }, + model: 'Shopware.apps.SwagProductAssoc.model.Product' +}); \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/store/variant.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/store/variant.js new file mode 100644 index 0000000000..123468e207 --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/store/variant.js @@ -0,0 +1,10 @@ + +Ext.define('Shopware.apps.SwagProductAssoc.store.Variant', { + extend: 'Shopware.store.Association', + model: 'Shopware.apps.SwagProductAssoc.model.Variant', + configure: function() { + return { + controller: 'SwagProductAssoc' + }; + } +}); diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/attribute.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/attribute.js similarity index 70% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/attribute.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/attribute.js index afb19d9428..1816b26249 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/attribute.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/attribute.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Attribute', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Attribute', { extend: 'Shopware.model.Container', // padding: 20, diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/category.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/category.js similarity index 69% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/category.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/category.js index 32fafab19e..75027dfc90 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/category.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/category.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Category', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Category', { extend: 'Shopware.grid.Association', alias: 'widget.product-view-detail-category', height: 200, @@ -7,7 +7,7 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Category', { configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductAssoc', columns: { name: {} } diff --git a/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/product.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/product.js new file mode 100755 index 0000000000..5bb94b082f --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/product.js @@ -0,0 +1,39 @@ + + +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Product', { + extend: 'Shopware.model.Container', + alias: 'widget.product-detail-container', + padding: 20, + + configure: function () { + return { + controller: 'SwagProductAssoc', + fieldSets: [ + { + title: '', + fields: { + name: { + + }, + taxId: { + allowBlank: false + }, + active: { + + }, + description: { + + }, + descriptionLong: { + + }, + lastStock: { + + } + } + }, + ] +// associations: [ 'variants', 'categories', 'attribute' ] + }; + } +}); diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/variant.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/variant.js similarity index 65% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/variant.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/variant.js index 4d5a035a76..fd3a10b2d6 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/variant.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/variant.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Variant', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Variant', { extend: 'Shopware.grid.Panel', alias: 'widget.shopware-product-variant-grid', title: 'Variant', diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/window.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/window.js similarity index 81% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/window.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/window.js index 09471be5db..87b7eef909 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/window.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/detail/window.js @@ -1,6 +1,6 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/list/product.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/list/product.js similarity index 54% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/list/product.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/list/product.js index a12daa72e9..98ee736f02 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/list/product.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/list/product.js @@ -1,13 +1,13 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', configure: function() { return { - detailWindow: 'Shopware.apps.SwagProduct.view.detail.Window' + detailWindow: 'Shopware.apps.SwagProductAssoc.view.detail.Window' }; } }); diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/list/window.js b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/list/window.js similarity index 52% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/list/window.js rename to exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/list/window.js index 6de6b02802..13bef37af4 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/list/window.js +++ b/exampleplugins/5.2/SwagProductAssoc/Resources/views/backend/swag_product_assoc/view/list/window.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductAssoc.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 340, @@ -8,8 +8,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product' + listingGrid: 'Shopware.apps.SwagProductAssoc.view.list.Product', + listingStore: 'Shopware.apps.SwagProductAssoc.store.Product' }; } }); \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductAssoc/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagProductAssoc/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..a03c038ebc --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/Subscriber/TemplateRegistration.php @@ -0,0 +1,42 @@ +pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductAssoc/SwagProductAssoc.php b/exampleplugins/5.2/SwagProductAssoc/SwagProductAssoc.php new file mode 100644 index 0000000000..ef80d171a8 --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/SwagProductAssoc.php @@ -0,0 +1,153 @@ +createDatabase(); + + $this->addDemoData(); + } + + /** + * {@inheritdoc} + */ + public function activate(ActivateContext $activateContext) + { + $activateContext->scheduleClearCache(ActivateContext::CACHE_LIST_DEFAULT); + } + + /** + * {@inheritdoc} + */ + public function uninstall(UninstallContext $uninstallContext) + { + if (!$uninstallContext->keepUserData()) { + $this->removeDatabase(); + } + } + + private function createDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->updateSchema($classes, true); // make sure use the save mode + } + + private function removeDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->dropSchema($classes); + } + + /** + * @param ModelManager $modelManager + * @return array + */ + private function getClasses(ModelManager $modelManager) + { + return [ + $modelManager->getClassMetadata(Product::class), + $modelManager->getClassMetadata(Variant::class), + $modelManager->getClassMetadata(Attribute::class) + ]; + } + + private function addDemoData() + { + $connection = $this->container->get('dbal_connection'); + + $this->createProductDemoData($connection); + $this->createProductVariantDemoData($connection); + $this->createProductAttributeDemoData($connection); + + $sql = " + SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_categories (product_id, category_id) + SELECT + a.articleID as product_id, + a.categoryID as category_id + FROM s_articles_categories a + "; + + $connection->exec($sql); + } + + private function createProductDemoData(Connection $connection) + { + $sql = 'INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate, tax_id) + SELECT + a.id, + a.name, + a.active, + a.description, + a.description_long as descriptionLong, + a.laststock as lastStock, + a.datum as createDate, + a.taxID as tax_id + FROM s_articles a + '; + + $connection->exec($sql); + } + + private function createProductVariantDemoData(Connection $connection) + { + $sql = 'SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_variant (id, product_id, number, additionalText, active, inStock, stockMin, weight) + SELECT + a.id, + a.articleID, + a.ordernumber, + a.additionaltext, + a.active, + a.instock, + a.stockmin, + a.weight + FROM s_articles_details a + '; + + $connection->exec($sql); + } + + private function createProductAttributeDemoData(Connection $connection) + { + $sql = 'SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_attribute + SELECT + a.id, + a.articleID as product_id, + a.attr1, + a.attr2, + a.attr3, + a.attr4, + a.attr5 + FROM s_articles_attributes a + '; + + $connection->exec($sql); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductAssoc/plugin.xml b/exampleplugins/5.2/SwagProductAssoc/plugin.xml new file mode 100644 index 0000000000..0e09d08f78 --- /dev/null +++ b/exampleplugins/5.2/SwagProductAssoc/plugin.xml @@ -0,0 +1,18 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/5.2/SwagProductBasic/Controllers/Backend/SwagProductBasic.php b/exampleplugins/5.2/SwagProductBasic/Controllers/Backend/SwagProductBasic.php new file mode 100755 index 0000000000..79be60cd85 --- /dev/null +++ b/exampleplugins/5.2/SwagProductBasic/Controllers/Backend/SwagProductBasic.php @@ -0,0 +1,9 @@ + + + + + + SwagBasicProduct + + + SwagProductBasic + index + sprite-application-block + Article + + + diff --git a/exampleplugins/5.2/SwagProductBasic/Resources/services.xml b/exampleplugins/5.2/SwagProductBasic/Resources/services.xml new file mode 100644 index 0000000000..ae9bfb8f2d --- /dev/null +++ b/exampleplugins/5.2/SwagProductBasic/Resources/services.xml @@ -0,0 +1,12 @@ + + + + + %swag_product_basic.plugin_dir% + + + + + diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/app.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/app.js similarity index 80% rename from exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/app.js rename to exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/app.js index 578d32b126..4a348881d3 100755 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/app.js +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/app.js @@ -1,8 +1,9 @@ +// -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductBasic', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductBasic', loadPath: '{url action=load}', bulkLoad: true, diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/controller/main.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/controller/main.js similarity index 72% rename from exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/controller/main.js rename to exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/controller/main.js index 978ad020f5..d2c217eb45 100755 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/controller/main.js +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/controller/main.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductBasic.controller.Main', { extend: 'Enlight.app.Controller', init: function() { diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/model/product.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/model/product.js similarity index 73% rename from exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/model/product.js rename to exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/model/product.js index 99ee852899..424fcc6aa6 100755 --- a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/model/product.js +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/model/product.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductBasic.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { - controller: 'SwagProduct', - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + controller: 'SwagProductBasic', + detail: 'Shopware.apps.SwagProductBasic.view.detail.Product' }; }, diff --git a/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/store/product.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/store/product.js new file mode 100755 index 0000000000..b1450486ca --- /dev/null +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/store/product.js @@ -0,0 +1,13 @@ +// + +Ext.define('Shopware.apps.SwagProductBasic.store.Product', { + extend:'Shopware.store.Listing', + + configure: function() { + return { + controller: 'SwagProductBasic' + }; + }, + + model: 'Shopware.apps.SwagProductBasic.model.Product' +}); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/detail/product.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/detail/product.js similarity index 52% rename from exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/detail/product.js rename to exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/detail/product.js index 85c05316b8..3f91411d9f 100755 --- a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/detail/product.js +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/detail/product.js @@ -1,12 +1,12 @@ +// - -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductBasic.view.detail.Product', { extend: 'Shopware.model.Container', padding: 20, configure: function() { return { - controller: 'SwagProduct' + controller: 'SwagProductBasic' }; } }); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/detail/window.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/detail/window.js similarity index 70% rename from exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/detail/window.js rename to exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/detail/window.js index 6d47e708ce..feee852aea 100755 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/detail/window.js +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/detail/window.js @@ -1,6 +1,6 @@ +// - -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductBasic.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/product.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/list/product.js similarity index 53% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/product.js rename to exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/list/product.js index a12daa72e9..1fb50974be 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/product.js +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/list/product.js @@ -1,13 +1,13 @@ +// - -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductBasic.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', configure: function() { return { - detailWindow: 'Shopware.apps.SwagProduct.view.detail.Window' + detailWindow: 'Shopware.apps.SwagProductBasic.view.detail.Window' }; } }); diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/list/window.js b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/list/window.js similarity index 50% rename from exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/list/window.js rename to exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/list/window.js index cb68dc8d54..83e9556d3a 100755 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/list/window.js +++ b/exampleplugins/5.2/SwagProductBasic/Resources/views/backend/swag_product_basic/view/list/window.js @@ -1,5 +1,6 @@ +// -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductBasic.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 450, @@ -7,8 +8,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product' + listingGrid: 'Shopware.apps.SwagProductBasic.view.list.Product', + listingStore: 'Shopware.apps.SwagProductBasic.store.Product' }; } }); \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductBasic/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagProductBasic/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..4b7305b67f --- /dev/null +++ b/exampleplugins/5.2/SwagProductBasic/Subscriber/TemplateRegistration.php @@ -0,0 +1,42 @@ +pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductBasic/SwagProductBasic.php b/exampleplugins/5.2/SwagProductBasic/SwagProductBasic.php new file mode 100644 index 0000000000..8e3f7f7821 --- /dev/null +++ b/exampleplugins/5.2/SwagProductBasic/SwagProductBasic.php @@ -0,0 +1,90 @@ +createDatabase(); + + $this->addDemoData(); + } + + /** + * {@inheritdoc} + */ + public function activate(ActivateContext $activateContext) + { + $activateContext->scheduleClearCache(InstallContext::CACHE_LIST_ALL); + } + + /** + * {@inheritdoc} + */ + public function uninstall(UninstallContext $uninstallContext) + { + if (!$uninstallContext->keepUserData()) { + $this->removeDatabase(); + } + } + + private function createDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->updateSchema($classes, true); // make sure to use the save mode + } + + private function removeDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->dropSchema($classes); + } + + /** + * @param ModelManager $modelManager + * @return array + */ + private function getClasses(ModelManager $modelManager) + { + return [ + $modelManager->getClassMetadata(Product::class) + ]; + } + + private function addDemoData() + { + $sql = " + INSERT IGNORE INTO s_product (`name`, active, description, descriptionLong, lastStock, createDate) + SELECT + a.name, + a.active, + a.description, + a.description_long as descriptionLong, + a.laststock as lastStock, + a.datum as createDate + FROM s_articles a + "; + + $this->container->get('dbal_connection')->exec($sql); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductBasic/plugin.xml b/exampleplugins/5.2/SwagProductBasic/plugin.xml new file mode 100644 index 0000000000..74abf184b3 --- /dev/null +++ b/exampleplugins/5.2/SwagProductBasic/plugin.xml @@ -0,0 +1,18 @@ + + + + + + 1.0.0 + (c) by shopware AG + proprietary + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/5.2/SwagProductDetail/Controllers/Backend/SwagProductDetail.php b/exampleplugins/5.2/SwagProductDetail/Controllers/Backend/SwagProductDetail.php new file mode 100755 index 0000000000..68fe92b6b0 --- /dev/null +++ b/exampleplugins/5.2/SwagProductDetail/Controllers/Backend/SwagProductDetail.php @@ -0,0 +1,9 @@ + + + + + + SwagProductDetail + + + SwagProductDetail + index + sprite-application-block + Article + + + diff --git a/exampleplugins/5.2/SwagProductDetail/Resources/services.xml b/exampleplugins/5.2/SwagProductDetail/Resources/services.xml new file mode 100644 index 0000000000..27121cd553 --- /dev/null +++ b/exampleplugins/5.2/SwagProductDetail/Resources/services.xml @@ -0,0 +1,12 @@ + + + + + %swag_product_detail.plugin_dir% + + + + + diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/app.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/app.js similarity index 81% rename from exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/app.js rename to exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/app.js index 578d32b126..cc38fdfd4f 100755 --- a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/app.js +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/app.js @@ -1,8 +1,8 @@ -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductDetail', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductDetail', loadPath: '{url action=load}', bulkLoad: true, diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/controller/main.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/controller/main.js similarity index 98% rename from exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/controller/main.js rename to exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/controller/main.js index f29fbbbcf0..9405993569 100755 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/controller/main.js +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/controller/main.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductDetail.controller.Main', { extend: 'Enlight.app.Controller', init: function() { diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/model/product.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/model/product.js similarity index 73% rename from exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/model/product.js rename to exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/model/product.js index 99ee852899..9b5ee65614 100755 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/model/product.js +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/model/product.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductDetail.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { - controller: 'SwagProduct', - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + controller: 'SwagProductDetail', + detail: 'Shopware.apps.SwagProductDetail.view.detail.Product' }; }, diff --git a/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/store/product.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/store/product.js new file mode 100755 index 0000000000..9317342eba --- /dev/null +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/store/product.js @@ -0,0 +1,10 @@ + +Ext.define('Shopware.apps.SwagProductDetail.store.Product', { + extend:'Shopware.store.Listing', + configure: function() { + return { + controller: 'SwagProductDetail' + }; + }, + model: 'Shopware.apps.SwagProductDetail.model.Product' +}); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/detail/product.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/detail/product.js similarity index 94% rename from exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/detail/product.js rename to exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/detail/product.js index 08c21fbcb8..a9b68196ee 100755 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/detail/product.js +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/detail/product.js @@ -1,6 +1,6 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductDetail.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', @@ -13,7 +13,7 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductDetail', fieldSets: [{ title: 'Product data', fields: { diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/detail/window.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/detail/window.js similarity index 93% rename from exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/detail/window.js rename to exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/detail/window.js index 61adec2aad..2ca33dfb47 100755 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/detail/window.js +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/detail/window.js @@ -1,6 +1,6 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductDetail.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/list/product.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/list/product.js similarity index 93% rename from exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/list/product.js rename to exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/list/product.js index bb513aeecd..5f7ad6de63 100755 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/list/product.js +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/list/product.js @@ -1,13 +1,13 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductDetail.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', configure: function() { return { - detailWindow: 'Shopware.apps.SwagProduct.view.detail.Window', + detailWindow: 'Shopware.apps.SwagProductDetail.view.detail.Window', columns: { name: { header: 'Product name' }, description: { flex: 3 }, diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/list/window.js b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/list/window.js similarity index 52% rename from exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/list/window.js rename to exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/list/window.js index 6de6b02802..433ad27875 100755 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/view/list/window.js +++ b/exampleplugins/5.2/SwagProductDetail/Resources/views/backend/swag_product_detail/view/list/window.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductDetail.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 340, @@ -8,8 +8,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product' + listingGrid: 'Shopware.apps.SwagProductDetail.view.list.Product', + listingStore: 'Shopware.apps.SwagProductDetail.store.Product' }; } }); \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductDetail/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagProductDetail/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..0b784486e7 --- /dev/null +++ b/exampleplugins/5.2/SwagProductDetail/Subscriber/TemplateRegistration.php @@ -0,0 +1,42 @@ +pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductDetail/SwagProductDetail.php b/exampleplugins/5.2/SwagProductDetail/SwagProductDetail.php new file mode 100644 index 0000000000..5c12b3f18b --- /dev/null +++ b/exampleplugins/5.2/SwagProductDetail/SwagProductDetail.php @@ -0,0 +1,87 @@ +createDatabase(); + + $this->addDemoData(); + } + + /** + * {@inheritdoc} + */ + public function activate(ActivateContext $activateContext) + { + $activateContext->scheduleClearCache(InstallContext::CACHE_LIST_ALL); + } + + public function uninstall(UninstallContext $uninstallContext) + { + if (!$uninstallContext->keepUserData()) { + $this->removeDatabase(); + } + } + + private function createDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->updateSchema($classes, true); // make sure to use the save mode + } + + private function removeDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->dropSchema($classes); + } + + /**s + * @param ModelManager $modelManager + * @return array + */ + private function getClasses(ModelManager $modelManager) + { + return [ + $modelManager->getClassMetadata(Product::class) + ]; + } + + private function addDemoData() + { + $sql = " + INSERT IGNORE INTO s_product (`name`, active, description, descriptionLong, lastStock, createDate) + SELECT + a.name, + a.active, + a.description, + a.description_long as descriptionLong, + a.laststock as lastStock, + a.datum as createDate + FROM s_articles a + "; + + $this->container->get('dbal_connection')->exec($sql); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductDetail/plugin.xml b/exampleplugins/5.2/SwagProductDetail/plugin.xml new file mode 100644 index 0000000000..1bea942f49 --- /dev/null +++ b/exampleplugins/5.2/SwagProductDetail/plugin.xml @@ -0,0 +1,18 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/5.2/SwagProductListing/Controllers/Backend/SwagProductListing.php b/exampleplugins/5.2/SwagProductListing/Controllers/Backend/SwagProductListing.php new file mode 100755 index 0000000000..0d77cc88d0 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListing/Controllers/Backend/SwagProductListing.php @@ -0,0 +1,9 @@ + + + + + + SwagProductListing + + + SwagProductListing + index + sprite-application-block + Article + + + diff --git a/exampleplugins/5.2/SwagProductListing/Resources/services.xml b/exampleplugins/5.2/SwagProductListing/Resources/services.xml new file mode 100644 index 0000000000..0f2a8a2202 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListing/Resources/services.xml @@ -0,0 +1,12 @@ + + + + + %swag_product_listing.plugin_dir% + + + + + diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/app.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/app.js similarity index 80% rename from exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/app.js rename to exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/app.js index 578d32b126..e35340e1de 100755 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/app.js +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/app.js @@ -1,8 +1,8 @@ -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductListing', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductListing', loadPath: '{url action=load}', bulkLoad: true, diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/controller/main.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/controller/main.js similarity index 96% rename from exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/controller/main.js rename to exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/controller/main.js index 81a8050017..1e844f872d 100755 --- a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/controller/main.js +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/controller/main.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductListing.controller.Main', { extend: 'Enlight.app.Controller', init: function() { var me = this; diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/model/product.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/model/product.js similarity index 73% rename from exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/model/product.js rename to exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/model/product.js index 99ee852899..1d5227f9f0 100755 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Views/backend/swag_product/model/product.js +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/model/product.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductListing.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { - controller: 'SwagProduct', - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + controller: 'SwagProductListing', + detail: 'Shopware.apps.SwagProductListing.view.detail.Product' }; }, diff --git a/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/store/product.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/store/product.js new file mode 100755 index 0000000000..8a1c4997f0 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/store/product.js @@ -0,0 +1,10 @@ + +Ext.define('Shopware.apps.SwagProductListing.store.Product', { + extend:'Shopware.store.Listing', + configure: function() { + return { + controller: 'SwagProductListing' + }; + }, + model: 'Shopware.apps.SwagProductListing.model.Product' +}); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/detail/product.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/detail/product.js similarity index 52% rename from exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/detail/product.js rename to exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/detail/product.js index 85c05316b8..30d1312f90 100755 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/detail/product.js +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/detail/product.js @@ -1,12 +1,12 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.detail.Product', { extend: 'Shopware.model.Container', padding: 20, configure: function() { return { - controller: 'SwagProduct' + controller: 'SwagProductListing' }; } }); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/detail/window.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/detail/window.js similarity index 71% rename from exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/detail/window.js rename to exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/detail/window.js index 6d47e708ce..9cf1eb3350 100755 --- a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/detail/window.js +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/detail/window.js @@ -1,6 +1,6 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductListing.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/list/product.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/list/product.js similarity index 92% rename from exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/list/product.js rename to exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/list/product.js index af65d6c821..1c56b176b2 100755 --- a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/list/product.js +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/list/product.js @@ -1,13 +1,13 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', configure: function() { return { - detailWindow: 'Shopware.apps.SwagProduct.view.detail.Window', + detailWindow: 'Shopware.apps.SwagProductListing.view.detail.Window', columns: { name: { header: 'Product name' }, description: { flex: 3 }, diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/list/window.js b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/list/window.js similarity index 51% rename from exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/list/window.js rename to exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/list/window.js index 6de6b02802..c53d63d2fd 100755 --- a/exampleplugins/legacy/Backend/SwagProductListing/Views/backend/swag_product/view/list/window.js +++ b/exampleplugins/5.2/SwagProductListing/Resources/views/backend/swag_product_listing/view/list/window.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 340, @@ -8,8 +8,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product' + listingGrid: 'Shopware.apps.SwagProductListing.view.list.Product', + listingStore: 'Shopware.apps.SwagProductListing.store.Product' }; } }); \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductListing/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagProductListing/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..dc04bda5e2 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListing/Subscriber/TemplateRegistration.php @@ -0,0 +1,42 @@ +pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductListing/SwagProductListing.php b/exampleplugins/5.2/SwagProductListing/SwagProductListing.php new file mode 100644 index 0000000000..a51c7c825b --- /dev/null +++ b/exampleplugins/5.2/SwagProductListing/SwagProductListing.php @@ -0,0 +1,90 @@ +createDatabase(); + + $this->addDemoData(); + } + + /** + * {@inheritdoc} + */ + public function activate(ActivateContext $activateContext) + { + $activateContext->scheduleClearCache(InstallContext::CACHE_LIST_ALL); + } + + /** + * {@inheritdoc} + */ + public function uninstall(UninstallContext $uninstallContext) + { + if (!$uninstallContext->keepUserData()) { + $this->removeDatabase(); + } + } + + private function createDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->updateSchema($classes, true); // make sure to use the save mode + } + + private function removeDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $classes = $this->getClasses($modelManager); + + $tool->dropSchema($classes); + } + + /** + * @param ModelManager $modelManager + * @return array + */ + private function getClasses(ModelManager $modelManager) + { + return [ + $modelManager->getClassMetadata(Product::class) + ]; + } + + private function addDemoData() + { + $sql = " + INSERT IGNORE INTO s_product (`name`, active, description, descriptionLong, lastStock, createDate) + SELECT + a.name, + a.active, + a.description, + a.description_long as descriptionLong, + a.laststock as lastStock, + a.datum as createDate + FROM s_articles a + "; + + $this->container->get('dbal_connection')->exec($sql); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductListing/plugin.xml b/exampleplugins/5.2/SwagProductListing/plugin.xml new file mode 100644 index 0000000000..fa9f7652ea --- /dev/null +++ b/exampleplugins/5.2/SwagProductListing/plugin.xml @@ -0,0 +1,18 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Controllers/Backend/SwagProduct.php b/exampleplugins/5.2/SwagProductListingExtension/Controllers/Backend/SwagProductListingExtension.php similarity index 83% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Controllers/Backend/SwagProduct.php rename to exampleplugins/5.2/SwagProductListingExtension/Controllers/Backend/SwagProductListingExtension.php index ce664a3628..b2a5a08a49 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Controllers/Backend/SwagProduct.php +++ b/exampleplugins/5.2/SwagProductListingExtension/Controllers/Backend/SwagProductListingExtension.php @@ -1,8 +1,10 @@ getManager()->createQueryBuilder(); $builder->select(array('products', 'categories')) - ->from('Shopware\CustomModels\Product\Product', 'products') + ->from(Product::class, 'products') ->innerJoin('products.categories', 'categories') ->where('products.id = :id') ->setParameter('id', $productId); diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Models/Product/Attribute.php b/exampleplugins/5.2/SwagProductListingExtension/Models/Attribute.php similarity index 92% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Models/Product/Attribute.php rename to exampleplugins/5.2/SwagProductListingExtension/Models/Attribute.php index 7dbf66d2c3..6aa0765cab 100644 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Models/Product/Attribute.php +++ b/exampleplugins/5.2/SwagProductListingExtension/Models/Attribute.php @@ -1,11 +1,9 @@ setOneToOne( $attribute, - '\Shopware\CustomModels\Product\Attribute', + Attribute::class, 'attribute', 'product' ); } /** - * @return \Shopware\CustomModels\Product\Variant[] + * @return Variant[] */ public function getVariants() { @@ -277,14 +276,14 @@ public function getVariants() } /** - * @param \Shopware\CustomModels\Product\Variant[] $variants + * @param Variant[] $variants * @return \Shopware\Components\Model\ModelEntity */ public function setVariants($variants) { return $this->setOneToMany( $variants, - '\Shopware\CustomModels\Product\Variant', + Variant::class, 'variants', 'product' ); diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Models/Product/Variant.php b/exampleplugins/5.2/SwagProductListingExtension/Models/Variant.php similarity index 93% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Models/Product/Variant.php rename to exampleplugins/5.2/SwagProductListingExtension/Models/Variant.php index fcd1050ca8..0ab8d4d56d 100644 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Models/Product/Variant.php +++ b/exampleplugins/5.2/SwagProductListingExtension/Models/Variant.php @@ -1,11 +1,9 @@ + + + + + SwagProductListingExtension + + + SwagProductListingExtension + index + sprite-application-block + Article + + + diff --git a/exampleplugins/5.2/SwagProductListingExtension/Resources/services.xml b/exampleplugins/5.2/SwagProductListingExtension/Resources/services.xml new file mode 100644 index 0000000000..03686bd8f5 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/services.xml @@ -0,0 +1,12 @@ + + + + + %swag_product_listing_extension.plugin_dir% + + + + + diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/app.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/app.js similarity index 85% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/app.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/app.js index 6642e55e7b..5616653e54 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/app.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/app.js @@ -1,8 +1,8 @@ -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductListingExtension', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductListingExtension', loadPath: '{url action=load}', bulkLoad: true, diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/controller/main.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/controller/main.js similarity index 69% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/controller/main.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/controller/main.js index 31b1e5f39d..71fa00697c 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/controller/main.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/controller/main.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductListingExtension.controller.Main', { extend: 'Enlight.app.Controller', init: function() { diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/attribute.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/attribute.js similarity index 74% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/attribute.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/attribute.js index ea739afd99..f82b2e7bf6 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/attribute.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/attribute.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Attribute', { +Ext.define('Shopware.apps.SwagProductListingExtension.model.Attribute', { extend: 'Shopware.data.Model', configure: function() { return { - detail: 'Shopware.apps.SwagProduct.view.detail.Attribute' + detail: 'Shopware.apps.SwagProductListingExtension.view.detail.Attribute' }; }, diff --git a/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/category.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/category.js new file mode 100755 index 0000000000..6276df5ca0 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/category.js @@ -0,0 +1,12 @@ + +Ext.define('Shopware.apps.SwagProductListingExtension.model.Category', { + + extend: 'Shopware.apps.Base.model.Category', + + configure: function() { + return { + related: 'Shopware.apps.SwagProductListingExtension.view.detail.Category' + } + } +}); + diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/product.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/product.js similarity index 72% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/product.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/product.js index ccf9c2ab33..fa9ec3dbee 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/product.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/product.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductListingExtension.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { - controller: 'SwagProduct', - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + controller: 'SwagProductListingExtension', + detail: 'Shopware.apps.SwagProductListingExtension.view.detail.Product' }; }, @@ -43,7 +43,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { relation: 'ManyToMany', type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Category', + model: 'Shopware.apps.SwagProductListingExtension.model.Category', name: 'getCategory', associationKey: 'categories' }, @@ -51,17 +51,17 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { relation: 'OneToOne', type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Attribute', + model: 'Shopware.apps.SwagProductListingExtension.model.Attribute', name: 'getAttribute', associationKey: 'attribute' }, { relation: 'OneToMany', - storeClass: 'Shopware.apps.SwagProduct.store.Variant', + storeClass: 'Shopware.apps.SwagProductListingExtension.store.Variant', loadOnDemand: true, type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Variant', + model: 'Shopware.apps.SwagProductListingExtension.model.Variant', name: 'getVariants', associationKey: 'variants' }, diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/variant.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/variant.js similarity index 75% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/variant.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/variant.js index af5528e997..5c8e1a1cc5 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/variant.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/model/variant.js @@ -1,11 +1,11 @@ -Ext.define('Shopware.apps.SwagProduct.model.Variant', { +Ext.define('Shopware.apps.SwagProductListingExtension.model.Variant', { extend: 'Shopware.data.Model', configure: function() { return { - listing: 'Shopware.apps.SwagProduct.view.detail.Variant' + listing: 'Shopware.apps.SwagProductListingExtension.view.detail.Variant' }; }, diff --git a/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/store/product.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/store/product.js new file mode 100755 index 0000000000..7282652bf8 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/store/product.js @@ -0,0 +1,11 @@ + +Ext.define('Shopware.apps.SwagProductListingExtension.store.Product', { + extend:'Shopware.store.Listing', + + configure: function() { + return { + controller: 'SwagProductListingExtension' + }; + }, + model: 'Shopware.apps.SwagProductListingExtension.model.Product' +}); \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/store/variant.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/store/variant.js new file mode 100644 index 0000000000..55a76e5951 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/store/variant.js @@ -0,0 +1,10 @@ + +Ext.define('Shopware.apps.SwagProductListingExtension.store.Variant', { + extend: 'Shopware.store.Association', + model: 'Shopware.apps.SwagProductListingExtension.model.Variant', + configure: function() { + return { + controller: 'SwagProductListingExtension' + }; + } +}); diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/attribute.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/attribute.js similarity index 66% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/attribute.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/attribute.js index afb19d9428..1d6f68644d 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/attribute.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/attribute.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Attribute', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.detail.Attribute', { extend: 'Shopware.model.Container', // padding: 20, diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/category.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/category.js similarity index 65% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/category.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/category.js index 32fafab19e..15e2003d9c 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/category.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/category.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Category', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.detail.Category', { extend: 'Shopware.grid.Association', alias: 'widget.product-view-detail-category', height: 200, @@ -7,7 +7,7 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Category', { configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductListingExtension', columns: { name: {} } diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/product.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/product.js similarity index 64% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/product.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/product.js index 1bde5a5f30..bbfa00cd67 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/product.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/product.js @@ -1,13 +1,13 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', padding: 20, configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductListingExtension', // associations: [ 'variants', 'categories', 'attribute' ] }; } diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/variant.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/variant.js similarity index 62% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/variant.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/variant.js index 4d5a035a76..44349d62e0 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/detail/variant.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/variant.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Variant', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.detail.Variant', { extend: 'Shopware.grid.Panel', alias: 'widget.shopware-product-variant-grid', title: 'Variant', diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/window.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/window.js similarity index 79% rename from exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/window.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/window.js index 09471be5db..69d7ffd233 100755 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/window.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/detail/window.js @@ -1,6 +1,6 @@ -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/extensions/filter.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/extensions/filter.js similarity index 67% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/extensions/filter.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/extensions/filter.js index 9224f42ce5..6093ed3cc7 100644 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/extensions/filter.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/extensions/filter.js @@ -1,14 +1,14 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Filter', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.extensions.Filter', { extend: 'Shopware.listing.FilterPanel', alias: 'widget.product-listing-filter-panel', width: 270, configure: function() { return { - controller: 'SwagProduct', - model: 'Shopware.apps.SwagProduct.model.Product', + controller: 'SwagProductListingExtension', + model: 'Shopware.apps.SwagProductListingExtension.model.Product', fields: { name: {}, taxId: 'Tax rate', diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/extensions/info.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/extensions/info.js similarity index 88% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/extensions/info.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/extensions/info.js index 75e2628edd..173d86e4c8 100644 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/extensions/info.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/extensions/info.js @@ -1,6 +1,6 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Info', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.extensions.Info', { extend: 'Shopware.listing.InfoPanel', alias: 'widget.product-listing-info-panel', width: 270, @@ -9,7 +9,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Info', { var me = this; return { - model: 'Shopware.apps.SwagProduct.model.Product', + model: 'Shopware.apps.SwagProductListingExtension.model.Product', // fields: { // name: '

' + // 'The product name is: ' + diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/list/product.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/product.js similarity index 50% rename from exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/list/product.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/product.js index a12daa72e9..47cf9d9a24 100755 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Views/backend/swag_product/view/list/product.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/product.js @@ -1,13 +1,13 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', configure: function() { return { - detailWindow: 'Shopware.apps.SwagProduct.view.detail.Window' + detailWindow: 'Shopware.apps.SwagProductListingExtension.view.detail.Window' }; } }); diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/window.js b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/window.js similarity index 61% rename from exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/window.js rename to exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/window.js index f8a02e5cca..91acde530a 100755 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/view/list/window.js +++ b/exampleplugins/5.2/SwagProductListingExtension/Resources/views/backend/swag_product_listing_extension/view/list/window.js @@ -1,5 +1,5 @@ -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 450, @@ -8,8 +8,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product', + listingGrid: 'Shopware.apps.SwagProductListingExtension.view.list.Product', + listingStore: 'Shopware.apps.SwagProductListingExtension.store.Product', extensions: [ { xtype: 'product-listing-info-panel' }, diff --git a/exampleplugins/5.2/SwagProductListingExtension/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagProductListingExtension/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..b8935e3ce4 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListingExtension/Subscriber/TemplateRegistration.php @@ -0,0 +1,42 @@ +pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductListingExtension/SwagProductListingExtension.php b/exampleplugins/5.2/SwagProductListingExtension/SwagProductListingExtension.php new file mode 100644 index 0000000000..09f32e0ec2 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListingExtension/SwagProductListingExtension.php @@ -0,0 +1,153 @@ +createDatabase(); + + $this->addDemoData(); + } + + /** + * {@inheritdoc} + */ + public function activate(ActivateContext $activateContext) + { + $activateContext->scheduleClearCache(ActivateContext::CACHE_LIST_DEFAULT); + } + + /** + * {@inheritdoc} + */ + public function uninstall(UninstallContext $uninstallContext) + { + if (!$uninstallContext->keepUserData()) { + $this->removeDatabase(); + } + } + + private function createDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->updateSchema($classes, true); // make sure use the save mode + } + + private function removeDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->dropSchema($classes); + } + + /** + * @param ModelManager $modelManager + * @return array + */ + private function getClasses(ModelManager $modelManager) + { + return [ + $modelManager->getClassMetadata(Product::class), + $modelManager->getClassMetadata(Variant::class), + $modelManager->getClassMetadata(Attribute::class) + ]; + } + + private function addDemoData() + { + $connection = $this->container->get('dbal_connection'); + + $this->createProductDemoData($connection); + $this->createProductVariantDemoData($connection); + $this->createProductAttributeDemoData($connection); + + $sql = " + SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_categories (product_id, category_id) + SELECT + a.articleID as product_id, + a.categoryID as category_id + FROM s_articles_categories a + "; + + $connection->exec($sql); + } + + private function createProductDemoData(Connection $connection) + { + $sql = 'INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate, tax_id) + SELECT + a.id, + a.name, + a.active, + a.description, + a.description_long as descriptionLong, + a.laststock as lastStock, + a.datum as createDate, + a.taxID as tax_id + FROM s_articles a + '; + + $connection->exec($sql); + } + + private function createProductVariantDemoData(Connection $connection) + { + $sql = 'SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_variant (id, product_id, number, additionalText, active, inStock, stockMin, weight) + SELECT + a.id, + a.articleID, + a.ordernumber, + a.additionaltext, + a.active, + a.instock, + a.stockmin, + a.weight + FROM s_articles_details a + '; + + $connection->exec($sql); + } + + private function createProductAttributeDemoData(Connection $connection) + { + $sql = 'SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_attribute + SELECT + a.id, + a.articleID as product_id, + a.attr1, + a.attr2, + a.attr3, + a.attr4, + a.attr5 + FROM s_articles_attributes a + '; + + $connection->exec($sql); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagProductListingExtension/plugin.xml b/exampleplugins/5.2/SwagProductListingExtension/plugin.xml new file mode 100644 index 0000000000..a56309d923 --- /dev/null +++ b/exampleplugins/5.2/SwagProductListingExtension/plugin.xml @@ -0,0 +1,18 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/5.2/SwagReCaptcha/Resources/config.xml b/exampleplugins/5.2/SwagReCaptcha/Resources/config.xml index abf23668b6..20390ab5a6 100644 --- a/exampleplugins/5.2/SwagReCaptcha/Resources/config.xml +++ b/exampleplugins/5.2/SwagReCaptcha/Resources/config.xml @@ -1,5 +1,6 @@ - + sitekey @@ -12,4 +13,4 @@ - \ No newline at end of file + diff --git a/exampleplugins/5.2/SwagReCaptcha/Resources/services.xml b/exampleplugins/5.2/SwagReCaptcha/Resources/services.xml index 371e5ab554..5f1d22cc63 100644 --- a/exampleplugins/5.2/SwagReCaptcha/Resources/services.xml +++ b/exampleplugins/5.2/SwagReCaptcha/Resources/services.xml @@ -6,7 +6,7 @@ - + \ No newline at end of file diff --git a/exampleplugins/5.2/SwagReCaptcha/plugin.xml b/exampleplugins/5.2/SwagReCaptcha/plugin.xml index 8a8d06252a..6a2ffe508c 100644 --- a/exampleplugins/5.2/SwagReCaptcha/plugin.xml +++ b/exampleplugins/5.2/SwagReCaptcha/plugin.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> @@ -13,4 +13,4 @@ Veröffentlichung Release - \ No newline at end of file + diff --git a/exampleplugins/5.2/SwagSearchBundle/Resources/services.xml b/exampleplugins/5.2/SwagSearchBundle/Resources/services.xml new file mode 100644 index 0000000000..a15cf0e106 --- /dev/null +++ b/exampleplugins/5.2/SwagSearchBundle/Resources/services.xml @@ -0,0 +1,29 @@ + + + + + %swag_search_bundle.plugin_dir% + + + + + + + + + + + + + + + + + + + + + + diff --git a/exampleplugins/legacy/Frontend/SwagSearchBundle/Snippets/frontend/listing/facet_labels.ini b/exampleplugins/5.2/SwagSearchBundle/Resources/snippets/frontend/listing/facet_labels.ini similarity index 100% rename from exampleplugins/legacy/Frontend/SwagSearchBundle/Snippets/frontend/listing/facet_labels.ini rename to exampleplugins/5.2/SwagSearchBundle/Resources/snippets/frontend/listing/facet_labels.ini diff --git a/exampleplugins/legacy/Frontend/SwagSearchBundle/Views/frontend/listing/actions/action-sorting.tpl b/exampleplugins/5.2/SwagSearchBundle/Resources/views/frontend/listing/actions/action-sorting.tpl similarity index 100% rename from exampleplugins/legacy/Frontend/SwagSearchBundle/Views/frontend/listing/actions/action-sorting.tpl rename to exampleplugins/5.2/SwagSearchBundle/Resources/views/frontend/listing/actions/action-sorting.tpl diff --git a/exampleplugins/legacy/Frontend/SwagSearchBundle/SearchBundleDBAL/Condition/EsdCondition.php b/exampleplugins/5.2/SwagSearchBundle/SearchBundleDBAL/Condition/EsdCondition.php similarity index 83% rename from exampleplugins/legacy/Frontend/SwagSearchBundle/SearchBundleDBAL/Condition/EsdCondition.php rename to exampleplugins/5.2/SwagSearchBundle/SearchBundleDBAL/Condition/EsdCondition.php index 1f911a20e4..bf7fba27e9 100644 --- a/exampleplugins/legacy/Frontend/SwagSearchBundle/SearchBundleDBAL/Condition/EsdCondition.php +++ b/exampleplugins/5.2/SwagSearchBundle/SearchBundleDBAL/Condition/EsdCondition.php @@ -1,6 +1,6 @@ pluginDir = $pluginDir; + } + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Listing' => 'extendListingTemplate' + ]; + } + + /** + * @param $args \Enlight_Event_EventArgs + */ + public function extendListingTemplate(\Enlight_Event_EventArgs $args) + { + $args->get('subject')->View()->addTemplateDir( + $this->pluginDir . '/Resources/views' + ); + } +} diff --git a/exampleplugins/5.2/SwagSearchBundle/SwagSearchBundle.php b/exampleplugins/5.2/SwagSearchBundle/SwagSearchBundle.php new file mode 100644 index 0000000000..ed47bf931c --- /dev/null +++ b/exampleplugins/5.2/SwagSearchBundle/SwagSearchBundle.php @@ -0,0 +1,10 @@ +logger = $logger; + } + + /** + * @param $netPrice float + * @param $tax float + * @return float + */ + public function calculate($netPrice, $tax) + { + $this->logger->debug('Calculating price for tax: ' . $tax); + return $netPrice * $tax; + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagService/Resources/services.xml b/exampleplugins/5.2/SwagService/Resources/services.xml new file mode 100644 index 0000000000..1189edc22b --- /dev/null +++ b/exampleplugins/5.2/SwagService/Resources/services.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/exampleplugins/5.2/SwagService/Subscriber/Frontend.php b/exampleplugins/5.2/SwagService/Subscriber/Frontend.php new file mode 100644 index 0000000000..bb8b36a7f3 --- /dev/null +++ b/exampleplugins/5.2/SwagService/Subscriber/Frontend.php @@ -0,0 +1,34 @@ +taxCalculator = $taxCalculator; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch_Frontend' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->taxCalculator->calculate(13.99, 1.19); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagService/SwagService.php b/exampleplugins/5.2/SwagService/SwagService.php new file mode 100644 index 0000000000..54ba528a94 --- /dev/null +++ b/exampleplugins/5.2/SwagService/SwagService.php @@ -0,0 +1,10 @@ + + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> diff --git a/exampleplugins/5.2/SwagSloganOfTheDay/Components/SloganPrinter.php b/exampleplugins/5.2/SwagSloganOfTheDay/Components/SloganPrinter.php new file mode 100644 index 0000000000..38e8053235 --- /dev/null +++ b/exampleplugins/5.2/SwagSloganOfTheDay/Components/SloganPrinter.php @@ -0,0 +1,17 @@ + + + + + swagSloganContent + + + Dieser Slogan wird in in der Storefront angezeigt + This slogan will be shown on the storefront + + + swagSloganFontSize + + + 12 + + + + + + + + swagSloganItalic + + + false + + + diff --git a/exampleplugins/5.2/SwagSloganOfTheDay/Resources/services.xml b/exampleplugins/5.2/SwagSloganOfTheDay/Resources/services.xml new file mode 100644 index 0000000000..50ca419cd3 --- /dev/null +++ b/exampleplugins/5.2/SwagSloganOfTheDay/Resources/services.xml @@ -0,0 +1,20 @@ + + + + + + + %swag_slogan_of_the_day.plugin_name% + %swag_slogan_of_the_day.plugin_dir% + + + + + + + + + + diff --git a/exampleplugins/legacy/Frontend/SwagSloganOfTheDay/Views/frontend/index/index.tpl b/exampleplugins/5.2/SwagSloganOfTheDay/Resources/views/frontend/index/index.tpl similarity index 65% rename from exampleplugins/legacy/Frontend/SwagSloganOfTheDay/Views/frontend/index/index.tpl rename to exampleplugins/5.2/SwagSloganOfTheDay/Resources/views/frontend/index/index.tpl index cc196ae2e3..6e72e1bbe3 100644 --- a/exampleplugins/legacy/Frontend/SwagSloganOfTheDay/Views/frontend/index/index.tpl +++ b/exampleplugins/5.2/SwagSloganOfTheDay/Resources/views/frontend/index/index.tpl @@ -8,14 +8,14 @@ text-align:center; } .slogan { - {if $italic}font-style:italic;{/if} - font-size:{$sloganSize}px; + {if $swagSloganItalic}font-style:italic;{/if} + font-size:{$swagSloganFontSize}px; }

- {$slogan} + {$swagSloganContent}
{$smarty.block.parent} -{/block} \ No newline at end of file +{/block} diff --git a/exampleplugins/5.2/SwagSloganOfTheDay/Subscriber/RouteSubscriber.php b/exampleplugins/5.2/SwagSloganOfTheDay/Subscriber/RouteSubscriber.php new file mode 100644 index 0000000000..b33951a80b --- /dev/null +++ b/exampleplugins/5.2/SwagSloganOfTheDay/Subscriber/RouteSubscriber.php @@ -0,0 +1,45 @@ + 'onPostDispatch' + ]; + } + + public function __construct($pluginName, $pluginDirectory, SloganPrinter $sloganPrinter, ConfigReader $configReader) + { + $this->pluginDirectory = $pluginDirectory; + $this->sloganPrinter = $sloganPrinter; + + $this->config = $configReader->getByPluginName($pluginName); + } + + public function onPostDispatch(\Enlight_Controller_ActionEventArgs $args) + { + /** @var \Enlight_Controller_Action $controller */ + $controller = $args->get('subject'); + $view = $controller->View(); + + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); + + $view->assign('swagSloganFontSize', $this->config['swagSloganFontSize']); + $view->assign('swagSloganItalic', $this->config['swagSloganItalic']); + $view->assign('swagSloganContent', $this->config['swagSloganContent']); + + if (!$this->config['swagSloganContent']) { + $view->assign('swagSloganContent', $this->sloganPrinter->getSlogan()); + } + } +} diff --git a/exampleplugins/5.2/SwagSloganOfTheDay/SwagSloganOfTheDay.php b/exampleplugins/5.2/SwagSloganOfTheDay/SwagSloganOfTheDay.php new file mode 100644 index 0000000000..b7beec5f36 --- /dev/null +++ b/exampleplugins/5.2/SwagSloganOfTheDay/SwagSloganOfTheDay.php @@ -0,0 +1,10 @@ + - - + + - - \ No newline at end of file + diff --git a/exampleplugins/5.2/SwagTestExample/Service/TestExampleService.php b/exampleplugins/5.2/SwagTestExample/Service/TestExampleService.php index b32a2ac0e6..f777f3deda 100644 --- a/exampleplugins/5.2/SwagTestExample/Service/TestExampleService.php +++ b/exampleplugins/5.2/SwagTestExample/Service/TestExampleService.php @@ -21,7 +21,7 @@ class TestExampleService /** * @param ListProductService $listProductService - * @param ContextService $contextService + * @param ContextService $contextService */ public function __construct(ListProductService $listProductService, ContextService $contextService) { diff --git a/exampleplugins/5.2/SwagTestExample/phpunit.xml.dist b/exampleplugins/5.2/SwagTestExample/phpunit.xml.dist index 4c910f7bb8..3a3dcd8d8f 100644 --- a/exampleplugins/5.2/SwagTestExample/phpunit.xml.dist +++ b/exampleplugins/5.2/SwagTestExample/phpunit.xml.dist @@ -1,9 +1,5 @@ - + ./Tests diff --git a/exampleplugins/5.2/SwagTestExample/plugin.xml b/exampleplugins/5.2/SwagTestExample/plugin.xml index 674c01b197..4ab03f1afd 100644 --- a/exampleplugins/5.2/SwagTestExample/plugin.xml +++ b/exampleplugins/5.2/SwagTestExample/plugin.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> @@ -10,5 +10,5 @@ proprietary http://store.shopware.com shopware AG - + diff --git a/exampleplugins/5.2/SwagTutorialTheme/Resources/Themes/Frontend/TutorialTheme/Theme.php b/exampleplugins/5.2/SwagTutorialTheme/Resources/Themes/Frontend/TutorialTheme/Theme.php new file mode 100755 index 0000000000..d917148a76 --- /dev/null +++ b/exampleplugins/5.2/SwagTutorialTheme/Resources/Themes/Frontend/TutorialTheme/Theme.php @@ -0,0 +1,30 @@ + +
+ + + + {$smarty.block.parent} +{/block} + + +{* Menu (Off canvas left) trigger *} +{block name='frontend_index_offcanvas_left_trigger'} + +{/block} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagTutorialTheme/Resources/Themes/Frontend/TutorialTheme/preview.png b/exampleplugins/5.2/SwagTutorialTheme/Resources/Themes/Frontend/TutorialTheme/preview.png new file mode 100755 index 0000000000..ca16411754 Binary files /dev/null and b/exampleplugins/5.2/SwagTutorialTheme/Resources/Themes/Frontend/TutorialTheme/preview.png differ diff --git a/exampleplugins/5.2/SwagTutorialTheme/SwagTutorialTheme.php b/exampleplugins/5.2/SwagTutorialTheme/SwagTutorialTheme.php new file mode 100755 index 0000000000..ca33950416 --- /dev/null +++ b/exampleplugins/5.2/SwagTutorialTheme/SwagTutorialTheme.php @@ -0,0 +1,8 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/5.2/SwagVimeoElement/Bootstrap/EmotionElementInstaller.php b/exampleplugins/5.2/SwagVimeoElement/Bootstrap/EmotionElementInstaller.php new file mode 100644 index 0000000000..bfe6d1970a --- /dev/null +++ b/exampleplugins/5.2/SwagVimeoElement/Bootstrap/EmotionElementInstaller.php @@ -0,0 +1,91 @@ +emotionComponentInstaller = $emotionComponentInstaller; + $this->pluginName = $pluginName; + } + + public function install() + { + $vimeoElement = $this->emotionComponentInstaller->createOrUpdate( + $this->pluginName, + 'SwagVimeoElement', + [ + 'name' => 'Vimeo Video', + 'xtype' => 'emotion-components-vimeo', + 'template' => 'emotion_vimeo', + 'cls' => 'emotion-vimeo-element', + 'description' => 'A simple vimeo video element for the shopping worlds.' + ] + ); + + $vimeoElement->createTextField([ + 'name' => 'vimeo_video_id', + 'fieldLabel' => 'Video ID', + 'supportText' => 'Enter the ID of the video you want to embed.', + 'allowBlank' => false + ]); + + $vimeoElement->createHiddenField([ + 'name' => 'vimeo_video_thumbnail' + ]); + + $vimeoElement->createTextField([ + 'name' => 'vimeo_interface_color', + 'fieldLabel' => 'Interface Color', + 'supportText' => 'Enter the #hex color code for the video player interface.', + 'defaultValue' => '#0096FF' + ]); + + $vimeoElement->createCheckboxField([ + 'name' => 'vimeo_autoplay', + 'fieldLabel' => 'Autoplay', + 'defaultValue' => false + ]); + + $vimeoElement->createCheckboxField([ + 'name' => 'vimeo_loop', + 'fieldLabel' => 'Loop', + 'defaultValue' => false + ]); + + $vimeoElement->createCheckboxField([ + 'name' => 'vimeo_show_title', + 'fieldLabel' => 'Show title', + 'defaultValue' => false + ]); + + $vimeoElement->createCheckboxField([ + 'name' => 'vimeo_show_portrait', + 'fieldLabel' => 'Show portrait', + 'defaultValue' => false + ]); + + $vimeoElement->createCheckboxField([ + 'name' => 'vimeo_show_author', + 'fieldLabel' => 'Show author', + 'defaultValue' => false + ]); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagVimeoElement/Resources/services.xml b/exampleplugins/5.2/SwagVimeoElement/Resources/services.xml new file mode 100644 index 0000000000..ce58b1c0e4 --- /dev/null +++ b/exampleplugins/5.2/SwagVimeoElement/Resources/services.xml @@ -0,0 +1,18 @@ + + + + + + %swag_vimeo_element.plugin_dir% + + + + + + %swag_vimeo_element.plugin_dir% + + + + diff --git a/exampleplugins/5.2/SwagVimeoElement/Resources/snippets/backend/emotion/swag_vimeo_element.ini b/exampleplugins/5.2/SwagVimeoElement/Resources/snippets/backend/emotion/swag_vimeo_element.ini new file mode 100644 index 0000000000..90d201a94e --- /dev/null +++ b/exampleplugins/5.2/SwagVimeoElement/Resources/snippets/backend/emotion/swag_vimeo_element.ini @@ -0,0 +1,7 @@ +[en_GB] +interfaceColorFieldLabel = "Interface Color" +interfaceColorSupportText = "Enter the #hex color code for the video player interface." + +[de_DE] +interfaceColorFieldLabel = "Interface Farbe" +interfaceColorSupportText = "Gib den #hex Farbcode für das Video Player Interface ein." \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagVimeoElement/Views/backend/emotion/swag_vimeo_element/view/detail/elements/vimeo_video.js b/exampleplugins/5.2/SwagVimeoElement/Resources/views/backend/emotion/swag_vimeo_element/view/detail/elements/vimeo_video.js similarity index 100% rename from exampleplugins/legacy/Backend/SwagVimeoElement/Views/backend/emotion/swag_vimeo_element/view/detail/elements/vimeo_video.js rename to exampleplugins/5.2/SwagVimeoElement/Resources/views/backend/emotion/swag_vimeo_element/view/detail/elements/vimeo_video.js diff --git a/exampleplugins/legacy/Backend/SwagVimeoElement/Views/emotion_components/backend/vimeo-video.js b/exampleplugins/5.2/SwagVimeoElement/Resources/views/emotion_components/backend/vimeo-video.js similarity index 85% rename from exampleplugins/legacy/Backend/SwagVimeoElement/Views/emotion_components/backend/vimeo-video.js rename to exampleplugins/5.2/SwagVimeoElement/Resources/views/emotion_components/backend/vimeo-video.js index 11121837b8..c8f2c96bd6 100644 --- a/exampleplugins/legacy/Backend/SwagVimeoElement/Views/emotion_components/backend/vimeo-video.js +++ b/exampleplugins/5.2/SwagVimeoElement/Resources/views/emotion_components/backend/vimeo-video.js @@ -1,3 +1,4 @@ +// {namespace name="backend/emotion/swag_vimeo_element"} //{block name="emotion_components/backend/vimeo_video"} Ext.define('Shopware.apps.Emotion.view.components.VimeoVideo', { @@ -12,6 +13,17 @@ Ext.define('Shopware.apps.Emotion.view.components.VimeoVideo', { */ alias: 'widget.emotion-components-vimeo', + /** + * Contains the translations of each input field which was created with the EmotionComponentInstaller. + * Use the name of the field as identifier + */ + snippets: { + 'vimeo_interface_color': { + 'fieldLabel': '{s name=interfaceColorFieldLabel}{/s}', + 'supportText': '{s name=interfaceColorSupportText}{/s}' + } + }, + /** * The constructor method of each component. */ @@ -89,4 +101,4 @@ Ext.define('Shopware.apps.Emotion.view.components.VimeoVideo', { xhr.send(); } }); -//{/block} \ No newline at end of file +//{/block} diff --git a/exampleplugins/legacy/Backend/SwagVimeoElement/Views/emotion_components/widgets/emotion/components/emotion_vimeo.tpl b/exampleplugins/5.2/SwagVimeoElement/Resources/views/emotion_components/widgets/emotion/components/emotion_vimeo.tpl similarity index 100% rename from exampleplugins/legacy/Backend/SwagVimeoElement/Views/emotion_components/widgets/emotion/components/emotion_vimeo.tpl rename to exampleplugins/5.2/SwagVimeoElement/Resources/views/emotion_components/widgets/emotion/components/emotion_vimeo.tpl diff --git a/exampleplugins/5.2/SwagVimeoElement/Subscriber/TemplateRegistration.php b/exampleplugins/5.2/SwagVimeoElement/Subscriber/TemplateRegistration.php new file mode 100644 index 0000000000..f2bd09ccbf --- /dev/null +++ b/exampleplugins/5.2/SwagVimeoElement/Subscriber/TemplateRegistration.php @@ -0,0 +1,42 @@ +pluginDirectory = $pluginDirectory; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Emotion' => 'onPostDispatchBackendEmotion' + ]; + } + + /** + * @param \Enlight_Controller_ActionEventArgs $args + */ + public function onPostDispatchBackendEmotion(\Enlight_Controller_ActionEventArgs $args) + { + $view = $args->getSubject()->View(); + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); + $view->extendsTemplate('backend/emotion/swag_vimeo_element/view/detail/elements/vimeo_video.js'); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagVimeoElement/SwagVimeoElement.php b/exampleplugins/5.2/SwagVimeoElement/SwagVimeoElement.php new file mode 100644 index 0000000000..0103706c20 --- /dev/null +++ b/exampleplugins/5.2/SwagVimeoElement/SwagVimeoElement.php @@ -0,0 +1,38 @@ +getName(), + $this->container->get('shopware.emotion_component_installer') + ); + + $emotionElementInstaller->install(); + } + + public function activate(ActivateContext $activateContext) + { + $activateContext->scheduleClearCache(ActivateContext::CACHE_LIST_ALL); + } + + public function deactivate(DeactivateContext $deactivateContext) + { + $deactivateContext->scheduleClearCache(DeactivateContext::CACHE_LIST_ALL); + } + + public function uninstall(UninstallContext $uninstallContext) + { + $uninstallContext->scheduleClearCache(UninstallContext::CACHE_LIST_ALL); + } +} \ No newline at end of file diff --git a/exampleplugins/5.2/SwagVimeoElement/plugin.xml b/exampleplugins/5.2/SwagVimeoElement/plugin.xml new file mode 100644 index 0000000000..5d69af9c38 --- /dev/null +++ b/exampleplugins/5.2/SwagVimeoElement/plugin.xml @@ -0,0 +1,18 @@ + + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + + + Erstveröffentlichung + First release + + diff --git a/exampleplugins/b2b/B2bAcl/B2bAcl.php b/exampleplugins/b2b/B2bAcl/B2bAcl.php new file mode 100755 index 0000000000..59f3172e7f --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/B2bAcl.php @@ -0,0 +1,75 @@ +Container()->get('dbal_connection'); + $connection->exec( + 'CREATE TABLE IF NOT EXISTS `b2b_offer` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `s_user_id` INT(11) NULL DEFAULT NULL, + `name` VARCHAR(255) NOT NULL COLLATE \'utf8_unicode_ci\', + `description` TEXT NULL COLLATE \'utf8_unicode_ci\', + + PRIMARY KEY (`id`), + + CONSTRAINT b2b_offer_s_user_id_FK FOREIGN KEY (`s_user_id`) + REFERENCES `s_user` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE + ) + COLLATE = utf8_unicode_ci;' + ); + + AclRoutingUpdateService::create()->addConfig(Offer\AclConfig::getAclConfigArray()); + AclDdlService::create()->createTable(new Offer\OfferAclTable()); + + parent::install($context); + } + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Dispatcher_ControllerPath_Frontend_B2bOffer' => 'registerController', + 'Enlight_Controller_Action_PostDispatchSecure' => 'onPostDispatchSecure', + ]; + } + + /** + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchSecure(\Enlight_Event_EventArgs $args) + { + $this->container->get('template')->addTemplateDir( + $this->getPath() . '/Resources/views/' + ); + $this->container->get('snippets')->addConfigDir( + $this->getPath() . '/Resources/snippets/' + ); + } + + /** + * @param \Enlight_Event_EventArgs $args + * @return string + */ + public function registerController(\Enlight_Event_EventArgs $args) + { + $this->container->get('template')->addTemplateDir( + $this->getPath() . '/Resources/views/' + ); + $this->container->get('snippets')->addConfigDir( + $this->getPath() . '/Resources/snippets/' + ); + + return $this->getPath() . '/Controllers/Frontend/B2bOffer.php'; + } +} diff --git a/exampleplugins/b2b/B2bAcl/B2bOfferController.php b/exampleplugins/b2b/B2bAcl/B2bOfferController.php new file mode 100644 index 0000000000..c9d242ab64 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/B2bOfferController.php @@ -0,0 +1,205 @@ +offerRepository = $offerRepository; + $this->authenticationService = $authenticationService; + $this->offerCrudService = $offerCrudService; + $this->gridHelper = $gridHelper; + } + + public function indexAction() + { + //nth + } + + /** + * @param Request $request + * @return array + */ + public function gridAction(Request $request): array + { + $ownershipContext = $this->authenticationService->getIdentity()->getOwnershipContext(); + $searchStruct = new OfferSearchStruct(); + + $this->gridHelper + ->extractSearchDataInStoreFront($request, $searchStruct); + + $offers = $this->offerRepository + ->fetchList($ownershipContext, $searchStruct); + + $totalCount = $this->offerRepository + ->fetchTotalCount($ownershipContext, $searchStruct); + + $maxPage = $this->gridHelper + ->getMaxPage($totalCount); + + $currentPage = (int) $request + ->getParam('page', 1); + + $gridState = $this->gridHelper + ->getGridState($request, $searchStruct, $offers, $maxPage, $currentPage); + + return [ + 'gridState' => $gridState, + ]; + } + + /** + * @return array + */ + public function newAction(): array + { + return $this->gridHelper->getValidationResponse('offer'); + } + + /** + * @param Request $request + */ + public function createAction(Request $request) + { + $request->checkPost(); + $post = $request->getPost(); + + $serviceRequest = $this->offerCrudService->createNewRecordRequest($post); + + $identity = $this->authenticationService->getIdentity(); + + try { + $offer = $this->offerCrudService + ->create($serviceRequest, $identity->getOwnershipContext()); + } catch (ValidationException $e) { + $this->gridHelper->pushValidationException($e); + throw new B2bControllerRedirectException( + 'new', + 'b2boffer', + 'frontend' + ); + } + + throw new B2bControllerRedirectException( + 'detail', + 'b2boffer', + 'frontend', + ['id' => $offer->id] + ); + } + + /** + * @param Request $request + * @return array + */ + public function detailAction(Request $request): array + { + $id = (int) $request->requireParam('id'); + + return [ + 'offer' => $this->offerRepository->fetchOneById($id), + 'id' => (int) $request->requireParam('id'), + ]; + } + + /** + * @param Request $request + * @return array + */ + public function editAction(Request $request): array + { + $id = (int) $request->requireParam('id'); + + if (!$this->gridHelper->getValidationResponse('offer')) { + return ['offer' => $this->offerRepository->fetchOneById((int) $id)]; + } + } + + /** + * @param Request $request + */ + public function updateAction(Request $request) + { + $request->checkPost(); + + $post = $request->getPost(); + + $ownershipContext = $this->authenticationService->getIdentity()->getOwnershipContext(); + $serviceRequest = $this->offerCrudService->createExistingRecordRequest($post); + + try { + $this->offerCrudService + ->update($serviceRequest, $ownershipContext); + } catch (ValidationException $e) { + $this->gridHelper + ->pushValidationException($e); + } + + throw new B2bControllerRedirectException( + 'edit', + 'b2boffer', + 'frontend', + ['id' => $serviceRequest->requireParam('id')] + ); + } + + /** + * @param Request $request + */ + public function removeAction(Request $request) + { + $request->checkPost(); + + $serviceRequest = $this->offerCrudService + ->createExistingRecordRequest($request->getPost()); + + try { + $this->offerCrudService->remove($serviceRequest); + } catch (CanNotRemoveExistingRecordException $e) { + // nth + } + + throw new B2bControllerRedirectException('grid', 'b2boffer'); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Controllers/Frontend/B2bOffer.php b/exampleplugins/b2b/B2bAcl/Controllers/Frontend/B2bOffer.php new file mode 100644 index 0000000000..f1b71f4da6 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Controllers/Frontend/B2bOffer.php @@ -0,0 +1,14 @@ + + [ + 'B2bOffer' => + [ + 'index' => 'list', + 'grid' => 'list', + 'create' => 'create', + 'update' => 'update', + 'remove' => 'delete', + 'new' => 'create', + 'detail' => 'detail', + 'edit' => 'detail', + ], + ], + ]; + } +} diff --git a/exampleplugins/b2b/B2bAcl/Offer/AclTableContactContextResolver.php b/exampleplugins/b2b/B2bAcl/Offer/AclTableContactContextResolver.php new file mode 100644 index 0000000000..c26e71f6af --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Offer/AclTableContactContextResolver.php @@ -0,0 +1,30 @@ +identityClassName, ContactIdentity::class, true)) { + return $context->identityId; + } + + if ($context instanceof ContactEntity && $context->id) { + return $context->id; + } + + throw new AclUnsupportedContextException(); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Offer/OfferAclTable.php b/exampleplugins/b2b/B2bAcl/Offer/OfferAclTable.php new file mode 100644 index 0000000000..11b2385e6d --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Offer/OfferAclTable.php @@ -0,0 +1,32 @@ +offerRepository = $offerRepository; + $this->offerValidationService = $offerValidationService; + $this->aclRepository = $aclRepository; + } + + /** + * @param array $data + * @return CrudServiceRequest + */ + public function createNewRecordRequest(array $data): CrudServiceRequest + { + return new CrudServiceRequest( + $data, + [ + 'name', + 'description', + ] + ); + } + + /** + * @param array $data + * @return CrudServiceRequest + */ + public function createExistingRecordRequest(array $data): CrudServiceRequest + { + return new CrudServiceRequest( + $data, + [ + 'id', + 'name', + 'description', + ] + ); + } + + /** + * @param CrudServiceRequest $request + * @param OwnershipContext $ownershipContext + * @throws \Shopware\B2B\Common\Repository\CanNotInsertExistingRecordException + * @throws \Shopware\B2B\Common\Validator\ValidationException + * @return OfferEntity + */ + public function create(CrudServiceRequest $request, OwnershipContext $ownershipContext): OfferEntity + { + $data = $request->getFilteredData(); + $data['sUserId'] = $ownershipContext->shopOwnerUserId; + + $offer = new OfferEntity(); + + $offer->setData($data); + + $validation = $this->offerValidationService + ->createInsertValidation($offer); + + $this->testValidation($offer, $validation); + + $offer = $this->offerRepository + ->addOffer($offer); + + try { + $this->aclRepository->allow( + $ownershipContext, + $offer->id + ); + } catch (AclUnsupportedContextException $e) { + return $offer; + } + + return $offer; + } + + /** + * @param CrudServiceRequest $request + * @param OwnershipContext $ownershipContext + * @throws \Shopware\B2B\Common\Validator\ValidationException + * @throws \Shopware\B2B\Common\Repository\CanNotUpdateExistingRecordException + * @return OfferEntity + */ + public function update(CrudServiceRequest $request, OwnershipContext $ownershipContext): OfferEntity + { + $data = $request->getFilteredData(); + $offer = new OfferEntity(); + $offer->setData($data); + $offer->id = (int) $offer->id; + $offer->sUserId = $ownershipContext->shopOwnerUserId; + + $validation = $this->offerValidationService + ->createUpdateValidation($offer); + + $this->testValidation($offer, $validation); + + $this->offerRepository + ->updateOffer($offer); + + return $offer; + } + + /** + * @param CrudServiceRequest $request + * @throws \Shopware\B2B\Common\Repository\CanNotRemoveUsedRecordException + * @throws \Shopware\B2B\Common\Repository\CanNotRemoveExistingRecordException + * @return OfferEntity + */ + public function remove(CrudServiceRequest $request): OfferEntity + { + $data = $request->getFilteredData(); + $offer = new OfferEntity(); + $offer->setData($data); + + $this->offerRepository + ->removeOffer($offer); + + return $offer; + } +} diff --git a/exampleplugins/b2b/B2bAcl/Offer/OfferEntity.php b/exampleplugins/b2b/B2bAcl/Offer/OfferEntity.php new file mode 100644 index 0000000000..7616268b2c --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Offer/OfferEntity.php @@ -0,0 +1,94 @@ +id; + } + + /** + * {@inheritdoc} + */ + public function toDatabaseArray(): array + { + return [ + 'id' => (int) $this->id, + 's_user_id' => (int) $this->sUserId, + 'name' => $this->name, + 'description' => $this->description, + ]; + } + + /** + * {@inheritdoc} + */ + public function fromDatabaseArray(array $data): CrudEntity + { + $this->id = (int) $data['id']; + $this->sUserId = (int) $data['s_user_id']; + $this->name = $data['name']; + $this->description = $data['description']; + + return $this; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + foreach ($data as $key => $value) { + if (!property_exists($this, $key)) { + continue; + } + + $this->{$key} = $value; + } + } + + /** + * @return array + */ + public function toArray(): array + { + return get_object_vars($this); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Offer/OfferRepository.php b/exampleplugins/b2b/B2bAcl/Offer/OfferRepository.php new file mode 100644 index 0000000000..4ac03482d7 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Offer/OfferRepository.php @@ -0,0 +1,242 @@ +connection = $connection; + $this->dbalHelper = $dbalHelper; + $this->aclRepository = $aclRepository; + } + + /** + * @param OwnershipContext $context + * @param OfferSearchStruct $searchStruct + * @throws \InvalidArgumentException + * @return OfferEntity[] + */ + public function fetchList(OwnershipContext $context, OfferSearchStruct $searchStruct): array + { + $query = $this->connection->createQueryBuilder() + ->select(self::TABLE_ALIAS . '.*') + ->from(self::TABLE_NAME, self::TABLE_ALIAS) + ->where(self::TABLE_ALIAS . '.s_user_id = :owner') + ->setParameter('owner', $context->shopOwnerUserId); + + $this->applyAcl($context, $query); + + if (!$searchStruct->orderBy) { + $searchStruct->orderBy = self::TABLE_ALIAS . '.id'; + $searchStruct->orderDirection = 'DESC'; + } + + $this->dbalHelper->applySearchStruct($searchStruct, $query); + + $statement = $query->execute(); + + $offersData = $statement + ->fetchAll(\PDO::FETCH_ASSOC); + + $offers = []; + foreach ($offersData as $offerData) { + $offers[] = (new OfferEntity())->fromDatabaseArray($offerData); + } + + return $offers; + } + + /** + * @param OwnershipContext $context + * @param OfferSearchStruct $searchStruct + * @return int + */ + public function fetchTotalCount(OwnershipContext $context, OfferSearchStruct $searchStruct): int + { + $query = $this->connection->createQueryBuilder() + ->select('COUNT(*)') + ->from(self::TABLE_NAME, self::TABLE_ALIAS) + ->where(self::TABLE_ALIAS . '.s_user_id = :owner') + ->setParameter('owner', $context->shopOwnerUserId); + + $this->applyAcl($context, $query); + $this->dbalHelper->applyFilters($searchStruct, $query); + + $statement = $query->execute(); + + return (int) $statement->fetchColumn(0); + } + + /** + * @param int $id + * @return OfferEntity + */ + public function fetchOneById(int $id): OfferEntity + { + $statement = $this->connection->createQueryBuilder() + ->select('*') + ->from(self::TABLE_NAME, self::TABLE_ALIAS) + ->where(self::TABLE_ALIAS . '.id = :id') + ->setParameter('id', $id) + ->execute(); + + $offerData = $statement->fetch(\PDO::FETCH_ASSOC); + + $offer = new OfferEntity(); + + return $offer->fromDatabaseArray($offerData); + } + + /** + * @param OfferEntity $offerEntity + * @throws \Shopware\B2B\Common\Repository\CanNotInsertExistingRecordException + * @return OfferEntity + */ + public function addOffer(OfferEntity $offerEntity): OfferEntity + { + if (!$offerEntity->isNew()) { + throw new CanNotInsertExistingRecordException('The Offer provided already exists'); + } + + $this->connection->insert( + self::TABLE_NAME, + $offerEntity->toDatabaseArray() + ); + + $offerEntity->id = (int) $this->connection->lastInsertId(); + + return $offerEntity; + } + + /** + * @param OfferEntity $offerEntity + * @throws \Shopware\B2B\Common\Repository\CanNotUpdateExistingRecordException + * @return OfferEntity + */ + public function updateOffer(OfferEntity $offerEntity): OfferEntity + { + if ($offerEntity->isNew()) { + throw new CanNotUpdateExistingRecordException('The Offer provided does not exist'); + } + + $this->connection->update( + self::TABLE_NAME, + $offerEntity->toDatabaseArray(), + ['id' => $offerEntity->id] + ); + + return $offerEntity; + } + + /** + * @param OfferEntity $offerEntity + * @throws \Shopware\B2B\Common\Repository\CanNotRemoveUsedRecordException + * @throws \Shopware\B2B\Common\Repository\CanNotRemoveExistingRecordException + * @return OfferEntity + */ + public function removeOffer(OfferEntity $offerEntity): OfferEntity + { + if ($offerEntity->isNew()) { + throw new CanNotRemoveExistingRecordException('The Offer provided does not exist'); + } + + $this->connection->delete( + self::TABLE_NAME, + ['id' => $offerEntity->id] + ); + + $offerEntity->id = null; + + return $offerEntity; + } + + /** + * @return string query alias for filter construction + */ + public function getMainTableAlias(): string + { + return self::TABLE_ALIAS; + } + + /** + * @return string[] + */ + public function getFullTextSearchFields(): array + { + return [ + 'name', + 'description', + ]; + } + + /** + * @param OwnershipContext $context + * @param QueryBuilder $query + */ + private function applyAcl(OwnershipContext $context, QueryBuilder $query) + { + try { + $aclQuery = $this->aclRepository->getUnionizedSqlQuery($context); + + $query->innerJoin( + self::TABLE_ALIAS, + '(' . $aclQuery->sql . ')', + 'acl_query', + self::TABLE_ALIAS . '.id = acl_query.referenced_entity_id' + ); + + foreach ($aclQuery->params as $name => $value) { + $query->setParameter($name, $value); + } + } catch (AclUnsupportedContextException $e) { + // nth + } + } + + /** + * @return array + */ + public function getAdditionalSearchResourceAndFields(): array + { + return []; + } +} diff --git a/exampleplugins/b2b/B2bAcl/Offer/OfferSearchStruct.php b/exampleplugins/b2b/B2bAcl/Offer/OfferSearchStruct.php new file mode 100644 index 0000000000..5dfc1aadb0 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Offer/OfferSearchStruct.php @@ -0,0 +1,9 @@ +validationBuilder = $validationBuilder; + $this->validator = $validator; + } + + /** + * @param OfferEntity $offer + * @return Validator + */ + public function createInsertValidation(OfferEntity $offer): Validator + { + return $this->createCrudValidation($offer) + ->validateThat('id', $offer->id) + ->isBlank() + ->getValidator($this->validator); + } + + /** + * @param OfferEntity $offer + * @return Validator + */ + public function createUpdateValidation(OfferEntity $offer): Validator + { + return $this->createCrudValidation($offer) + ->validateThat('id', $offer->id) + ->isInt() + + ->getValidator($this->validator); + } + + /** + * @param OfferEntity $offer + * @return ValidationBuilder + */ + private function createCrudValidation(OfferEntity $offer): ValidationBuilder + { + return $this->validationBuilder + + ->validateThat('name', $offer->name) + ->isNotBlank(); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Resources/services.xml b/exampleplugins/b2b/B2bAcl/Resources/services.xml new file mode 100755 index 0000000000..a458794a76 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/services.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + B2bAcl\Offer\OfferRepository::TABLE_NAME + + + + + + + + + + + + + + diff --git a/exampleplugins/b2b/B2bAcl/Resources/snippets/frontend/plugins/b2b_debtor_plugin.ini b/exampleplugins/b2b/B2bAcl/Resources/snippets/frontend/plugins/b2b_debtor_plugin.ini new file mode 100644 index 0000000000..178865b0cc --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/snippets/frontend/plugins/b2b_debtor_plugin.ini @@ -0,0 +1,15 @@ +[en_GB] +_acl_offer = 'Offers' +ManageOffers = 'Manage Offers' +CreateOffer = 'Create Offer' +OfferDetail = 'Offer detail' +OfferName = 'Name' +OfferDescription = 'Description' + +[de_DE] +_acl_offer = 'Angebote' +ManageOffers = 'Angebote verwalten' +CreateOffer = 'Angebot erstellen' +OfferDetail = 'Angebotsdetail' +OfferName = 'Name' +OfferDescription = 'Beschreibung' diff --git a/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/_form.tpl b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/_form.tpl new file mode 100644 index 0000000000..6c4cabe5bc --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/_form.tpl @@ -0,0 +1,36 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+ +

+ {s name="MasterData"}Master data{/s} +

+ +
+
+ {s name="OfferName"}Name{/s}: * +
+
+ +
+
+
+
+ {s name="OfferDescription"}Description{/s}: +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
diff --git a/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/detail.tpl b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/detail.tpl new file mode 100644 index 0000000000..72a4cb8e03 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/detail.tpl @@ -0,0 +1,24 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+
+ +
+

{s name="ManageOffers"}{/s}

+
+ + + +
+
+ + {include file="frontend/b2boffer/_form.tpl"} +
+
+
+
diff --git a/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/edit.tpl b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/edit.tpl new file mode 100644 index 0000000000..57363c0455 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/edit.tpl @@ -0,0 +1,23 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+
+ +
+

{s name="OfferDetail"}{/s}

+
+ + {foreach $errors as $error} + + {/foreach} + +
+
+ + {include file="frontend/b2boffer/_form.tpl"} +
+
+
+
diff --git a/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/grid.tpl b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/grid.tpl new file mode 100644 index 0000000000..449006b919 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/grid.tpl @@ -0,0 +1,24 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+
+ +
+
+

{s name="ManageOffers"}Manage Offers{/s}

+
+
+ + + +
+
+
+
+ + {include file="frontend/_grid/offer-grid.tpl"} + +
+
\ No newline at end of file diff --git a/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/index.tpl b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/index.tpl new file mode 100644 index 0000000000..be381f1bb2 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/index.tpl @@ -0,0 +1,16 @@ +{extends file="frontend/_base/index.tpl"} + +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{* B2b Account Main Content *} +{block name="frontend_index_content_b2b"} + + {* B2B Account Header *} + {include file="frontend/b2bcontact/topbar.tpl"} + +
+ +
+ +
+{/block} diff --git a/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/new.tpl b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/new.tpl new file mode 100644 index 0000000000..a73efac55d --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/new.tpl @@ -0,0 +1,22 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+
+ +
+

{s name="CreateOffer"}Create Offer{/s}

+
+ + + +
+
+ {include file="frontend/b2boffer/_form.tpl"} +
+
+
+
diff --git a/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/offer-grid.tpl b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/offer-grid.tpl new file mode 100644 index 0000000000..e61c22cde4 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Resources/views/frontend/b2boffer/offer-grid.tpl @@ -0,0 +1,41 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{extends file="parent:frontend/_grid/grid.tpl"} + +{block name="b2b_grid_col_sort"} + + + + +{/block} + +{block name="b2b_grid_table_head"} + + {s name="OfferName"}Name{/s} + {s name="OfferDescription"}Description{/s} + {s name="Action"}Action{/s} + +{/block} + +{block name="b2b_grid_table_row"} + + {$row->name} + {$row->description} + +
+ + +
+ + +{/block} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bAcl/Tests/Bootstrap.php b/exampleplugins/b2b/B2bAcl/Tests/Bootstrap.php new file mode 100644 index 0000000000..2fee5dc7f3 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Tests/Bootstrap.php @@ -0,0 +1,6 @@ +getContainer()->get('b2b_offer.crud_service'); + } + + public function test_crud_service_instance() + { + self::assertInstanceOf(OfferCrudService::class, $this->getCrudService()); + } + + public function test_crud_service() + { + $this->importFixtures($this->loadCommonFixtureSql()); + + $data = ['name' => 'name', 'description' => 'description']; + $createRequest = $this->getCrudService()->createNewRecordRequest($data); + + self::assertInstanceOf(CrudServiceRequest::class, $createRequest); + + $ownershipContext = self::createContactIdentity() + ->getOwnershipContext(); + + $offerEntity = $this->getCrudService() + ->create($createRequest, $ownershipContext); + + self::assertInstanceOf(OfferEntity::class, $offerEntity); + + $offerEntity->description = 'updated Description'; + + $existingRecordRequest = $this->getCrudService()->createExistingRecordRequest($offerEntity->toArray()); + + $offerEntity = $this->getCrudService()->update($existingRecordRequest, $ownershipContext); + + self::assertInstanceOf(OfferEntity::class, $offerEntity); + self::assertEquals('updated Description', $offerEntity->description); + + $existingRecordRequest = $this->getCrudService()->createExistingRecordRequest($offerEntity->toArray()); + + $offerEntity = $this->getCrudService()->remove($existingRecordRequest); + + self::assertInstanceOf(OfferEntity::class, $offerEntity); + self::assertNull($offerEntity->id); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Tests/Integration/OfferRepositoryTest.php b/exampleplugins/b2b/B2bAcl/Tests/Integration/OfferRepositoryTest.php new file mode 100644 index 0000000000..55bd4d9457 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Tests/Integration/OfferRepositoryTest.php @@ -0,0 +1,45 @@ +getContainer()->get('b2b_offer.repository'); + } + + public function test_repository_instance() + { + self::assertInstanceOf(OfferRepository::class, $this->getRepository()); + } + + public function test_repository() + { + $identity = new MockIdentity(); + $ownershipContext = $identity->getOwnershipContext(); + + $ownershipContext->shopOwnerUserId = 250; + $ownershipContext->identityId = 11; + $ownershipContext->identityClassName = \Shopware\B2B\Contact\Framework\ContactIdentity::class; + + $searchStruct = new OfferSearchStruct(); + $result = $this->getRepository()->fetchList($ownershipContext, $searchStruct); + + self::assertCount(0, $result); + + $result = $this->getRepository()->fetchTotalCount($ownershipContext, $searchStruct); + + self::assertEquals(0, $result); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Tests/Unit/AclConfigTest.php b/exampleplugins/b2b/B2bAcl/Tests/Unit/AclConfigTest.php new file mode 100644 index 0000000000..4f70a73715 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Tests/Unit/AclConfigTest.php @@ -0,0 +1,14 @@ +expectException(AclUnsupportedContextException::class); + $resolver->extractId(''); + } + + public function test_extract_id_for_contact_entity() + { + $context = new ContactEntity(); + $context->id = 42; + $resolver = new AclTableContactContextResolver(); + self::assertEquals(42, $resolver->extractId($context)); + } + + public function test_extract_id_based_on_ownership_context() + { + $context = new OwnershipContext(1, 1, 'foo', 1, 1, ContactIdentity::class); + $resolver = new AclTableContactContextResolver(); + self::assertEquals(1, $resolver->extractId($context)); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Tests/Unit/OfferAclTableTest.php b/exampleplugins/b2b/B2bAcl/Tests/Unit/OfferAclTableTest.php new file mode 100644 index 0000000000..e5ec6223e6 --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Tests/Unit/OfferAclTableTest.php @@ -0,0 +1,22 @@ +setAccessible(true); + $resolvers = $method->invoke(new OfferAclTable()); + + self::assertInternalType('array', $resolvers); + self::assertContainsOnlyInstancesOf(AclTableContactContextResolver::class, $resolvers); + } +} diff --git a/exampleplugins/b2b/B2bAcl/Tests/Unit/OrderEntityTest.php b/exampleplugins/b2b/B2bAcl/Tests/Unit/OrderEntityTest.php new file mode 100644 index 0000000000..d4640ba00e --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/Tests/Unit/OrderEntityTest.php @@ -0,0 +1,53 @@ + '123', + 's_user_id' => '250', + 'name' => 'foo', + 'description' => 'bar', + ]; + + $entity->fromDatabaseArray($data); + + $this->assertInstanceOf(OfferEntity::class, $entity); + $this->assertEquals('123', $entity->id); + $this->assertEquals('foo', $entity->name); + $this->assertEquals('bar', $entity->description); + $this->assertEquals('250', $entity->sUserId); + + $toDatabaseArray = $entity->toDatabaseArray(); + + self::assertEquals($data, $toDatabaseArray); + + self::assertFalse($entity->isNew()); + } + + public function test_set_data() + { + /** @var OfferEntity $entity */ + $entity = new OfferEntity(); + + $data = [ + 's_user_id' => '250', + 'name' => 'foo', + 'description' => 'bar', + 'skippingParameter' => true, + ]; + + $entity->setData($data); + + self::assertNotEquals($data, $entity->toDatabaseArray()); + self::assertJson(json_encode($entity)); + } +} diff --git a/exampleplugins/b2b/B2bAcl/phpunit.xml.dist b/exampleplugins/b2b/B2bAcl/phpunit.xml.dist new file mode 100644 index 0000000000..b22ba8eb9f --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + ./Tests + + + + + Offer/ + + ./Tests + + + + diff --git a/exampleplugins/b2b/B2bAcl/plugin.png b/exampleplugins/b2b/B2bAcl/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bAcl/plugin.png differ diff --git a/exampleplugins/b2b/B2bAcl/plugin.xml b/exampleplugins/b2b/B2bAcl/plugin.xml new file mode 100644 index 0000000000..8ca0dafaed --- /dev/null +++ b/exampleplugins/b2b/B2bAcl/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bAjaxPanel/B2bAjaxPanel.php b/exampleplugins/b2b/B2bAjaxPanel/B2bAjaxPanel.php new file mode 100755 index 0000000000..1de17d6336 --- /dev/null +++ b/exampleplugins/b2b/B2bAjaxPanel/B2bAjaxPanel.php @@ -0,0 +1,9 @@ +get('template')->addTemplateDir($path . '/Resources/views/'); + $this->get('snippets')->addConfigDir($path . '/Resources/snippets/'); + } + + public function indexAction() + { + // nothing to do + } + + public function navAction() + { + // nothing to do + } + + public function subAction() + { + $this->View()->assign('isPost', $this->Request()->isPost()); + $this->View()->assign('name', $this->Request()->getParam('name', 'nobody')); + } +} diff --git a/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/index.tpl b/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/index.tpl new file mode 100644 index 0000000000..0583fbd41c --- /dev/null +++ b/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/index.tpl @@ -0,0 +1,20 @@ +{extends file="parent:frontend/index/index.tpl"} + +{* Reset sidebar categories *} +{block name="frontend_index_content_left"}{/block} + +{* B2b Account Main Content *} +{block name="frontend_index_content"} + +

Ajax Panel Demo

+ +
+
+
+ +
+
+
+
+
+{/block} diff --git a/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/nav.tpl b/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/nav.tpl new file mode 100644 index 0000000000..fb46948171 --- /dev/null +++ b/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/nav.tpl @@ -0,0 +1,2 @@ +Say "Hello Tom" +Say "Hello Carl" diff --git a/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/sub.tpl b/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/sub.tpl new file mode 100644 index 0000000000..135308edc7 --- /dev/null +++ b/exampleplugins/b2b/B2bAjaxPanel/Resources/views/frontend/b2b_ajax_panel/sub.tpl @@ -0,0 +1,10 @@ +
+ + +
+
+

+ {if $isPost}This is a POST request!! - {/if} + + Hello "{$name}" +

diff --git a/exampleplugins/b2b/B2bAjaxPanel/Tests/B2bAjaxPanelTest.php b/exampleplugins/b2b/B2bAjaxPanel/Tests/B2bAjaxPanelTest.php new file mode 100644 index 0000000000..eb0216e7cb --- /dev/null +++ b/exampleplugins/b2b/B2bAjaxPanel/Tests/B2bAjaxPanelTest.php @@ -0,0 +1,55 @@ +request('GET', '/B2bAjaxPanel/index'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains('B2bAjaxPanel/nav', $client->getResponse()->getContent()); + self::assertContains('B2bAjaxPanel/sub', $client->getResponse()->getContent()); + } + + public function test_nav_action_is_accessible() + { + $client = self::createClient(); + + $client->request('GET', '/B2bAjaxPanel/nav'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains('btn', $client->getResponse()->getContent()); + self::assertContains('b2b--tab-link', $client->getResponse()->getContent()); + self::assertContains('name/Carl', $client->getResponse()->getContent()); + self::assertContains('name/Tom', $client->getResponse()->getContent()); + self::assertContains('B2bAjaxPanel/sub', $client->getResponse()->getContent()); + } + + public function test_sub_action_is_accessible() + { + $client = self::createClient(); + + $client->request('GET', '/B2bAjaxPanel/sub'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains('nobody', $client->getResponse()->getContent()); + } + + public function test_sub_action_is_post_accessible() + { + $client = self::createClient(); + + $client->request('GET', '/B2bAjaxPanel/sub/name/somebody'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains('somebody', $client->getResponse()->getContent()); + } +} diff --git a/exampleplugins/b2b/B2bAjaxPanel/Tests/Bootstrap.php b/exampleplugins/b2b/B2bAjaxPanel/Tests/Bootstrap.php new file mode 100644 index 0000000000..e3f6e72852 --- /dev/null +++ b/exampleplugins/b2b/B2bAjaxPanel/Tests/Bootstrap.php @@ -0,0 +1,7 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bAjaxPanel/plugin.png b/exampleplugins/b2b/B2bAjaxPanel/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bAjaxPanel/plugin.png differ diff --git a/exampleplugins/b2b/B2bAjaxPanel/plugin.xml b/exampleplugins/b2b/B2bAjaxPanel/plugin.xml new file mode 100644 index 0000000000..1c0f0c6aec --- /dev/null +++ b/exampleplugins/b2b/B2bAjaxPanel/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bAuditLog/AuditLogController.php b/exampleplugins/b2b/B2bAuditLog/AuditLogController.php new file mode 100644 index 0000000000..3cec617598 --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/AuditLogController.php @@ -0,0 +1,131 @@ +authenticationService = $authenticationService; + $this->auditLogRepository = $auditLogRepository; + $this->auditLogGridHelper = $auditLogGridHelper; + $this->auditLogService = $auditLogService; + } + + public function indexAction() + { + // nth + } + + /** + * @param Request $request + * @return array + */ + public function gridAction(Request $request): array + { + $auditLogSearchStruct = new AuditLogSearchStruct(); + + $this->auditLogGridHelper + ->extractSearchDataInStoreFront($request, $auditLogSearchStruct); + + $auditLogs = $this->auditLogRepository + ->fetchList(self::REFERENCE_TABLE, self::REFERENCE_ID, $auditLogSearchStruct); + + $totalCount = $this->auditLogRepository + ->fetchTotalCount(self::REFERENCE_TABLE, self::REFERENCE_ID, $auditLogSearchStruct); + + $maxPage = $this->auditLogGridHelper + ->getMaxPage($totalCount); + + $currentPage = (int) $request->getParam('page', 1); + + $gridState = $this->auditLogGridHelper + ->getGridState($request, $auditLogSearchStruct, $auditLogs, $maxPage, $currentPage); + + return [ + 'gridState' => $gridState, + ]; + } + + /** + * @param Request $request + * @return array + */ + public function createAction(Request $request) + { + $identity = $this->authenticationService + ->getIdentity(); + + $auditLogValue = new AuditLogValueDiffEntity(); + $auditLogValue->newValue = 'newValue'; + $auditLogValue->oldValue = 'oldValue'; + $auditLogValue->comment = 'audit log comment'; + + $auditLog = new AuditLogEntity(); + $auditLog->logValue = $auditLogValue; + $auditLog->logType = 'logType'; + + $auditLogIndex = new AuditLogIndexEntity(); + $auditLogIndex->referenceId = self::REFERENCE_ID; + $auditLogIndex->referenceTable = self::REFERENCE_TABLE; + + $auditLogSubIndex = new AuditLogIndexEntity(); + $auditLogSubIndex->referenceId = self::REFERENCE_SUB_ID; + $auditLogSubIndex->referenceTable = self::REFERENCE_SUB_TABLE; + + $auditLog = $this->auditLogService + ->createAuditLog($auditLog, $identity, [$auditLogIndex, $auditLogSubIndex]); + + return [ + 'auditLog' => $auditLog, + ]; + } +} diff --git a/exampleplugins/b2b/B2bAuditLog/B2bAuditLog.php b/exampleplugins/b2b/B2bAuditLog/B2bAuditLog.php new file mode 100755 index 0000000000..df7846499c --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/B2bAuditLog.php @@ -0,0 +1,43 @@ +addConfiguration(new SwagB2bPluginConfiguration()); + $containerBuilder->registerConfigurations($container); + + parent::build($container); + } + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure' => 'onPostDispatchSecure', + ]; + } + + /** + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchSecure(\Enlight_Event_EventArgs $args) + { + $this->container->get('Template')->addTemplateDir( + $this->getPath() . '/Resources/views/' + ); + } +} diff --git a/exampleplugins/b2b/B2bAuditLog/Controllers/Frontend/B2bAuditLog.php b/exampleplugins/b2b/B2bAuditLog/Controllers/Frontend/B2bAuditLog.php new file mode 100644 index 0000000000..b44abbcc54 --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/Controllers/Frontend/B2bAuditLog.php @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/create.tpl b/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/create.tpl new file mode 100644 index 0000000000..36775aebed --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/create.tpl @@ -0,0 +1,15 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+
+
+

Audit Log

+
+
+ +
+
+
+ diff --git a/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/grid.tpl b/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/grid.tpl new file mode 100644 index 0000000000..659510e070 --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/grid.tpl @@ -0,0 +1,19 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{extends file="parent:frontend/_grid/grid.tpl"} + +{block name="b2b_grid_table_head"} + + old Value + new value + comment + +{/block} + +{block name="b2b_grid_table_row"} + + {$row->logValue->oldValue} + {$row->logValue->newValue} + {$row->logValue->comment} + +{/block} diff --git a/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/index.tpl b/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/index.tpl new file mode 100644 index 0000000000..67863b6e0d --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/Resources/views/frontend/b2bauditlog/index.tpl @@ -0,0 +1,28 @@ +{extends file="parent:frontend/_base/index.tpl"} + +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{* B2b Account Main Content *} +{block name="frontend_index_content_b2b"} +
+
+
+
+
+

Audit Log Example

+
+
+ +
+
+
+
+
+
+
+
+ +
+{/block} diff --git a/exampleplugins/b2b/B2bAuditLog/Tests/Bootstrap.php b/exampleplugins/b2b/B2bAuditLog/Tests/Bootstrap.php new file mode 100644 index 0000000000..e3f6e72852 --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/Tests/Bootstrap.php @@ -0,0 +1,7 @@ +disableCommonFixtures(false); + $this->importFixtures($this->loadCommonFixtureSql()); + $this->performB2bDebtorLogin(); + + $client = self::createClient(); + $client->request('GET', '/b2bauditlog/'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains('b2bauditlog', $client->getResponse()->getContent()); + } + + public function test_create_action() + { + $this->disableCommonFixtures(false); + $this->importFixtures($this->loadCommonFixtureSql()); + $this->performB2bDebtorLogin(); + + $client = self::createClient(); + $client->request('GET', '/b2bauditlog/create'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains('Audit Log successfully created!', $client->getResponse()->getContent()); + } +} diff --git a/exampleplugins/b2b/B2bAuditLog/phpunit.xml.dist b/exampleplugins/b2b/B2bAuditLog/phpunit.xml.dist new file mode 100644 index 0000000000..eabead5f6e --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bAuditLog/plugin.png b/exampleplugins/b2b/B2bAuditLog/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bAuditLog/plugin.png differ diff --git a/exampleplugins/b2b/B2bAuditLog/plugin.xml b/exampleplugins/b2b/B2bAuditLog/plugin.xml new file mode 100644 index 0000000000..35f04f0bac --- /dev/null +++ b/exampleplugins/b2b/B2bAuditLog/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bAuth/B2bAuth.php b/exampleplugins/b2b/B2bAuth/B2bAuth.php new file mode 100755 index 0000000000..fea6b79898 --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/B2bAuth.php @@ -0,0 +1,43 @@ +addConfiguration(new SwagB2bPluginConfiguration()); + $containerBuilder->registerConfigurations($container); + + parent::build($container); + } + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure' => 'onPostDispatchSecure', + ]; + } + + /** + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchSecure(\Enlight_Event_EventArgs $args) + { + $this->container->get('Template')->addTemplateDir( + $this->getPath() . '/Resources/views/' + ); + } +} diff --git a/exampleplugins/b2b/B2bAuth/Controllers/Frontend/B2bAuth.php b/exampleplugins/b2b/B2bAuth/Controllers/Frontend/B2bAuth.php new file mode 100644 index 0000000000..e21f969834 --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/Controllers/Frontend/B2bAuth.php @@ -0,0 +1,78 @@ +get('b2b_front_auth.authentication_service'), $this->get('b2b_front_auth.login_service')) { + /** + * @var AuthenticationService + */ + private $authenticationService; + + /** + * @var LoginService + */ + private $loginService; + + /** + * @param AuthenticationService $authenticationService + * @param LoginService $loginService + */ + public function __construct( + AuthenticationService $authenticationService, + LoginService $loginService + ) { + $this->authenticationService = $authenticationService; + $this->loginService = $loginService; + } + + public function indexAction() + { + //nth + } + + /** + * @return array + */ + public function contactAction(): array + { + $this->loginService->setIdentityFor('contact1@example.com'); + + $identity = $this->authenticationService->getIdentity(); + + return [ + 'identity' => $identity, + 'contextOwner' => $this->authenticationService->getIdentityByAuthId($identity->getOwnershipContext()->contextOwnerId), + ]; + } + + /** + * @return array + */ + public function debtorAction(): array + { + $this->loginService->setIdentityFor('debtor@example.com'); + + $identity = $this->authenticationService->getIdentity(); + + return [ + 'identity' => $identity, + 'contextOwner' => $this->authenticationService->getIdentityByAuthId($identity->getOwnershipContext()->contextOwnerId), + ]; + } + }; + } +} diff --git a/exampleplugins/b2b/B2bAuth/Resources/services.xml b/exampleplugins/b2b/B2bAuth/Resources/services.xml new file mode 100755 index 0000000000..b760b7d775 --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/Resources/services.xml @@ -0,0 +1,5 @@ + + + diff --git a/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/contact.tpl b/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/contact.tpl new file mode 100644 index 0000000000..410b7d095a --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/contact.tpl @@ -0,0 +1,27 @@ +{extends file="parent:frontend/index/index.tpl"} + +{* Reset sidebar categories *} +{block name="frontend_index_content_left"}{/block} + +{* B2B Top Navigation *} +{block name="frontend_index_content_top"} +{/block} + +{* B2b Account Main Content *} +{block name="frontend_index_content"} + +
+

Contact (current Identity)

+
+
{$identity|@print_r}
+
+
+ +
+

Context (Identity of the context owner - the debtor)

+
+
{$contextOwner|@print_r}
+
+
+ +{/block} diff --git a/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/debtor.tpl b/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/debtor.tpl new file mode 100644 index 0000000000..06c4d132b0 --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/debtor.tpl @@ -0,0 +1,27 @@ +{extends file="parent:frontend/index/index.tpl"} + +{* Reset sidebar categories *} +{block name="frontend_index_content_left"}{/block} + +{* B2B Top Navigation *} +{block name="frontend_index_content_top"} +{/block} + +{* B2b Account Main Content *} +{block name="frontend_index_content"} + +
+

Debtor (current Identity)

+
+
{$identity|@print_r}
+
+
+ +
+

Context (Identity of the context owner - also the debtor)

+
+
{$contextOwner|@print_r}
+
+
+ +{/block} diff --git a/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/index.tpl b/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/index.tpl new file mode 100644 index 0000000000..55ef0bdfe4 --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/Resources/views/frontend/b2bauth/index.tpl @@ -0,0 +1,19 @@ +{extends file="parent:frontend/index/index.tpl"} + +{* Reset sidebar categories *} +{block name="frontend_index_content_left"}{/block} + +{* B2B Top Navigation *} +{block name="frontend_index_content_top"} +{/block} + +{* B2b Account Main Content *} +{block name="frontend_index_content"} +
+

Be someone else

+ +
+{/block} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bAuth/Tests/Bootstrap.php b/exampleplugins/b2b/B2bAuth/Tests/Bootstrap.php new file mode 100644 index 0000000000..e3f6e72852 --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/Tests/Bootstrap.php @@ -0,0 +1,7 @@ +disableCommonFixtures(false); + $this->importFixtures($this->loadCommonFixtureSql()); + + $client = self::createClient(); + $this->performB2bContactLogin(); + + $client->request('GET', '/b2bauth/contact'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains(ContactIdentity::class, $client->getResponse()->getContent()); + self::assertContains(DebtorIdentity::class, $client->getResponse()->getContent()); + } + + public function test_debtor_action() + { + $this->disableCommonFixtures(false); + $this->importFixtures($this->loadCommonFixtureSql()); + + $client = self::createClient(); + $this->performB2bDebtorLogin(); + + $client->request('GET', '/b2bauth/debtor'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains(DebtorIdentity::class, $client->getResponse()->getContent()); + } +} diff --git a/exampleplugins/b2b/B2bAuth/phpunit.xml.dist b/exampleplugins/b2b/B2bAuth/phpunit.xml.dist new file mode 100644 index 0000000000..eabead5f6e --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bAuth/plugin.png b/exampleplugins/b2b/B2bAuth/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bAuth/plugin.png differ diff --git a/exampleplugins/b2b/B2bAuth/plugin.xml b/exampleplugins/b2b/B2bAuth/plugin.xml new file mode 100644 index 0000000000..b854906688 --- /dev/null +++ b/exampleplugins/b2b/B2bAuth/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bContingentRuleItem/B2bContingentRuleItem.php b/exampleplugins/b2b/B2bContingentRuleItem/B2bContingentRuleItem.php new file mode 100755 index 0000000000..8ef96472d9 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/B2bContingentRuleItem.php @@ -0,0 +1,68 @@ +Container()->get('dbal_connection'); + $connection->exec( + 'CREATE TABLE IF NOT EXISTS b2b_contingent_group_rule_weekday ( + contingent_rule_id INT(11) NOT NULL, + weekday_id INT(11) NOT NULL, + + PRIMARY KEY (`contingent_rule_id`), + + CONSTRAINT b2b_contingent_group_rule_weekday_contingent_rule_id_FK FOREIGN KEY (`contingent_rule_id`) + REFERENCES `b2b_contingent_group_rule` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE + ) + COLLATE=\'utf8_unicode_ci\'' + ); + } + + /** + * @param ContainerBuilder $container + */ + public function build(ContainerBuilder $container) + { + $containerBuilder = B2BContainerBuilder::create(); + $containerBuilder->addConfiguration(new SwagB2bPluginConfiguration()); + $containerBuilder->registerConfigurations($container); + + $restrictTypes = $container->getParameter('b2b_contingent_rule.restrict_types'); + $restrictTypes[] = WeekdayRuleType::NAME; + $container->setParameter('b2b_contingent_rule.restrict_types', $restrictTypes); + parent::build($container); + } + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure' => 'onPostDispatchSecure', + ]; + } + + /** + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchSecure(\Enlight_Event_EventArgs $args) + { + $this->container->get('Template')->addTemplateDir( + $this->getPath() . '/Resources/views/' + ); + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/B2bContingentRuleWeekdayController.php b/exampleplugins/b2b/B2bContingentRuleItem/B2bContingentRuleWeekdayController.php new file mode 100644 index 0000000000..e9be1328b5 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/B2bContingentRuleWeekdayController.php @@ -0,0 +1,44 @@ +contingentRuleRepository = $contingentRuleRepository; + } + + /** + * @param Request $request + */ + public function newAction(Request $request) + { + // nth + } + + /** + * @param Request $request + * @return array + */ + public function editAction(Request $request) + { + $id = (int) $request->getParam('id'); + + return [ + 'rule' => $this->contingentRuleRepository + ->fetchOneById($id), + ]; + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/Controllers/Frontend/B2bContingentRuleWeekday.php b/exampleplugins/b2b/B2bContingentRuleItem/Controllers/Frontend/B2bContingentRuleWeekday.php new file mode 100644 index 0000000000..1c1d51aa09 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/Controllers/Frontend/B2bContingentRuleWeekday.php @@ -0,0 +1,13 @@ + + + + + + + + + + + + + + + + diff --git a/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/_grid/contingentrule-grid-weekday.tpl b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/_grid/contingentrule-grid-weekday.tpl new file mode 100644 index 0000000000..b7e5f19b5e --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/_grid/contingentrule-grid-weekday.tpl @@ -0,0 +1,26 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + + +
    +
  • + {s name="Weekday"}Weekday{/s}: + + {if $row->weekdayId == 1} + Monday + {elseif $row->weekdayId == 2} + Tuesday + {elseif $row->weekdayId == 3} + Wednesday + {elseif $row->weekdayId == 4} + Thursday + {elseif $row->weekdayId == 5} + Friday + {elseif $row->weekdayId == 6} + Saturday + {elseif $row->weekdayId == 7} + Sunday + {/if} + +
  • +
+ diff --git a/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/_form.tpl b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/_form.tpl new file mode 100644 index 0000000000..7014609436 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/_form.tpl @@ -0,0 +1,19 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+
+ {s name="Weekday"}Weekday{/s}: * +
+
+ +
+
diff --git a/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/edit.tpl b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/edit.tpl new file mode 100644 index 0000000000..1f76aa2cd2 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/edit.tpl @@ -0,0 +1 @@ +{include file="frontend/b2bcontingentruleweekday/_form.tpl"} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/new.tpl b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/new.tpl new file mode 100644 index 0000000000..1f76aa2cd2 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/Resources/views/frontend/b2bcontingentruleweekday/new.tpl @@ -0,0 +1 @@ +{include file="frontend/b2bcontingentruleweekday/_form.tpl"} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleAccessStrategy.php b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleAccessStrategy.php new file mode 100644 index 0000000000..f0bd3acdc6 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleAccessStrategy.php @@ -0,0 +1,52 @@ +weekdayId = $weekdayId; + $this->weekdayEntity = $weekdayEntity; + } + + /** + * {@inheritdoc} + */ + public function isAllowed(CartAccessContext $context, MessageCollection $messageCollection): bool + { + $allowed = (int) date('N') !== $this->weekdayId; + if ($allowed) { + return true; + } + + $messageCollection->addError( + __CLASS__, + 'WeekdayError', + [ + 'allowedValue' => $this->weekdayEntity->toArray(), + ] + ); + + return false; + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleEntity.php b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleEntity.php new file mode 100644 index 0000000000..94ea13116d --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleEntity.php @@ -0,0 +1,35 @@ + $this->weekdayId] + ); + } + + /** + * {@inheritdoc} + */ + public function fromDatabaseArray(array $data): CrudEntity + { + $this->weekdayId = (int) $data['weekday_id']; + + return parent::fromDatabaseArray($data); + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleRepository.php b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleRepository.php new file mode 100644 index 0000000000..75b1440fa6 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleRepository.php @@ -0,0 +1,45 @@ +connection = $connection; + } + + /** + * @return string + */ + public function createSubQuery(): string + { + return $this->connection->createQueryBuilder() + ->select(self::TABLE_ALIAS . '.*') + ->from(self::TABLE_NAME, self::TABLE_ALIAS) + ->getSQL(); + } + + /** + * @return string + */ + public function getTableName(): string + { + return self::TABLE_NAME; + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleType.php b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleType.php new file mode 100644 index 0000000000..3ba29074f4 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleType.php @@ -0,0 +1,74 @@ +getTypeName()); + } + + /** + * {@inheritdoc} + */ + public function createValidationExtender(ContingentRuleEntity $entity): ContingentRuleTypeValidationExtender + { + return new WeekdayRuleValidationExtender($entity); + } + + /** + * {@inheritdoc} + * @throws \Shopware\B2B\ContingentRule\Framework\UnsupportedContingentRuleEntityTypeException + */ + public function createCartAccessStrategy( + OwnershipContext $ownershipContext, + ContingentRuleEntity $entity + ): CartAccessStrategyInterface { + if (!$entity instanceof WeekdayRuleEntity) { + throw new UnsupportedContingentRuleEntityTypeException($entity); + } + + return new WeekdayRuleAccessStrategy($entity->weekdayId, $entity); + } + + /** + * {@inheritdoc} + */ + public function getRepository(Connection $connection): ContingentRuleTypeRepositoryInterface + { + return new WeekdayRuleRepository($connection); + } + + /** + * @return string[] + */ + public function getRequestKeys(): array + { + return [ + 'weekdayId', + ]; + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleValidationExtender.php b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleValidationExtender.php new file mode 100644 index 0000000000..c4c7fcee09 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/RuleItem/WeekdayRuleValidationExtender.php @@ -0,0 +1,33 @@ +weekdayEntity = $weekdayEntity; + } + + /** + * @param ValidationBuilder $validationBuilder + * @return ValidationBuilder + */ + public function extendValidator(ValidationBuilder $validationBuilder): ValidationBuilder + { + return $validationBuilder + ->validateThat('weekdayId', $this->weekdayEntity->weekdayId) + ->isNotBlank(); + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/Tests/Bootstrap.php b/exampleplugins/b2b/B2bContingentRuleItem/Tests/Bootstrap.php new file mode 100644 index 0000000000..e3f6e72852 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/Tests/Bootstrap.php @@ -0,0 +1,7 @@ +performB2bDebtorLogin(); + + $client->request('GET', '/b2bcontingentruleweekday/new'); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains('Weekday', $client->getResponse()->getContent()); + } + + public function test_edit_action_is_accessible() + { + $client = self::createClient(); + + $this->performB2bDebtorLogin(); + + $this->expectException(InvalidArgumentException::class); + $client->request('GET', '/b2bcontingentruleweekday/edit'); + } +} diff --git a/exampleplugins/b2b/B2bContingentRuleItem/Tests/Unit/WeekdayRuleEntityTest.php b/exampleplugins/b2b/B2bContingentRuleItem/Tests/Unit/WeekdayRuleEntityTest.php new file mode 100644 index 0000000000..91a73d7191 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/Tests/Unit/WeekdayRuleEntityTest.php @@ -0,0 +1,16 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bContingentRuleItem/plugin.png b/exampleplugins/b2b/B2bContingentRuleItem/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bContingentRuleItem/plugin.png differ diff --git a/exampleplugins/b2b/B2bContingentRuleItem/plugin.xml b/exampleplugins/b2b/B2bContingentRuleItem/plugin.xml new file mode 100644 index 0000000000..76304c018a --- /dev/null +++ b/exampleplugins/b2b/B2bContingentRuleItem/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bContingents/B2bContingents.php b/exampleplugins/b2b/B2bContingents/B2bContingents.php new file mode 100755 index 0000000000..9730b2c2ea --- /dev/null +++ b/exampleplugins/b2b/B2bContingents/B2bContingents.php @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/exampleplugins/b2b/B2bContingents/Tests/Bootstrap.php b/exampleplugins/b2b/B2bContingents/Tests/Bootstrap.php new file mode 100644 index 0000000000..2fee5dc7f3 --- /dev/null +++ b/exampleplugins/b2b/B2bContingents/Tests/Bootstrap.php @@ -0,0 +1,6 @@ +createCartAccessForIdentity(DebtorFactoryTrait::createDebtorIdentity()) + ); + } +} diff --git a/exampleplugins/b2b/B2bContingents/phpunit.xml.dist b/exampleplugins/b2b/B2bContingents/phpunit.xml.dist new file mode 100644 index 0000000000..94ed9f231b --- /dev/null +++ b/exampleplugins/b2b/B2bContingents/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bContingents/plugin.png b/exampleplugins/b2b/B2bContingents/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bContingents/plugin.png differ diff --git a/exampleplugins/b2b/B2bContingents/plugin.xml b/exampleplugins/b2b/B2bContingents/plugin.xml new file mode 100644 index 0000000000..3b8a942179 --- /dev/null +++ b/exampleplugins/b2b/B2bContingents/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bContingentsBudgets/B2bContingentsBudgets.php b/exampleplugins/b2b/B2bContingentsBudgets/B2bContingentsBudgets.php new file mode 100644 index 0000000000..a85adc6cf5 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/B2bContingentsBudgets.php @@ -0,0 +1,20 @@ +addConfiguration(new SwagB2bPluginFrontendConfiguration()); + $containerBuilder->registerConfigurations($container); + + parent::build($container); + } +} diff --git a/exampleplugins/b2b/B2bContingentsBudgets/Budgets/BudgetCartAccessFactory.php b/exampleplugins/b2b/B2bContingentsBudgets/Budgets/BudgetCartAccessFactory.php new file mode 100644 index 0000000000..24c78f34d8 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/Budgets/BudgetCartAccessFactory.php @@ -0,0 +1,44 @@ +budgetService = $budgetService; + $this->currencyService = $currencyService; + } + + public function createCartAccessForIdentity(Identity $identity, string $environmentName): CartAccessStrategyInterface + { + switch ($environmentName) { + case CartService::ENVIRONMENT_NAME_ORDER: + return new BudgetCartOrderAccessStrategy($identity, $this->budgetService, $this->currencyService->createCurrencyContext()); + case CartService::ENVIRONMENT_NAME_LISTING: + return new BudgetCartListingAccessStrategy($this->budgetService, $identity->getOwnershipContext(), $this->currencyService->createCurrencyContext()); + case CartService::ENVIRONMENT_NAME_MODIFY: + default: + return new CartAccessStrategyAlwaysAllowed(); + } + } +} diff --git a/exampleplugins/b2b/B2bContingentsBudgets/Budgets/BudgetCartOrderAccessStrategy.php b/exampleplugins/b2b/B2bContingentsBudgets/Budgets/BudgetCartOrderAccessStrategy.php new file mode 100644 index 0000000000..e7ee05576a --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/Budgets/BudgetCartOrderAccessStrategy.php @@ -0,0 +1,75 @@ +identity = $identity; + $this->budgetService = $budgetService; + $this->currencyContext = $currencyContext; + } + + public function checkAccess(CartAccessContext $context, CartAccessResult $cartAccessResult) + { + $amountNet = 0; + + if ($context->orderClearanceEntity->list->amountNet) { + $amountNet = $context->orderClearanceEntity->list->amountNet; + } + + $budgets = $this->budgetService->getUserSelectableBudgetsWithStatus( + $this->identity->getOwnershipContext(), + $amountNet, + $this->currencyContext + ); + + if (!count($budgets)) { + return; + } + + $canSpend = false; + array_walk($budgets, function ($budget) use (&$canSpend) { + if ($budget->currentStatus->isSufficient) { + $canSpend = true; + } + }); + + if ($canSpend) { + return; + } + + $cartAccessResult->addError( + __CLASS__, + 'BudgetSpendError' + ); + } + + public function addInformation(CartAccessResult $cartAccessResult) + { + //nth + } +} diff --git a/exampleplugins/b2b/B2bContingentsBudgets/ContingentRules/RuleCartAccessFactory.php b/exampleplugins/b2b/B2bContingentsBudgets/ContingentRules/RuleCartAccessFactory.php new file mode 100644 index 0000000000..6261dc673f --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/ContingentRules/RuleCartAccessFactory.php @@ -0,0 +1,99 @@ +contingentGroupRepository = $contingentGroupRepository; + $this->contingentRuleRepository = $contingentRuleRepository; + $this->typeFactory = $typeFactory; + $this->allowedTypes = $allowedTypes; + $this->currencyService = $currencyService; + } + + public function createCartAccessForIdentity(Identity $identity, string $environmentName): CartAccessStrategyInterface + { + $context = $identity->getOwnershipContext(); + + $contingentGroupIds = $this->contingentGroupRepository + ->fetchContingentGroupIdsForContact($context->identityId); + + $groupStrategies = []; + foreach ($contingentGroupIds as $contingentGroupId) { + $types = $this->contingentGroupRepository + ->fetchRuleTypesFromContingentGroup($contingentGroupId); + + $types = array_filter($types, function (string $typeName) { + return in_array($typeName, $this->allowedTypes, true); + }); + + $typeStrategies = []; + foreach ($types as $type) { + $ruleData = $this->contingentRuleRepository + ->fetchActiveRuleItemsForRuleType($type, $contingentGroupId, $this->currencyService->createCurrencyContext()); + + $ruleStrategies = []; + foreach ($ruleData as $rule) { + $ruleStrategies[] = $this->typeFactory + ->createCartAccessStrategy($type, $context, $rule); + } + + if (!$ruleStrategies) { + continue; + } + + $typeStrategies[] = new BlackListCartAccess(... $ruleStrategies); + } + + if (!$typeStrategies) { + continue; + } + + $groupStrategies[] = new BlackListCartAccess(... $typeStrategies); + } + + return new WhiteListCartAccess(... $groupStrategies); + } +} diff --git a/exampleplugins/b2b/B2bContingentsBudgets/Resources/services.xml b/exampleplugins/b2b/B2bContingentsBudgets/Resources/services.xml new file mode 100644 index 0000000000..79f33bcb97 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/Resources/services.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + %b2b_contingent_rule.release_types% + + + + + diff --git a/exampleplugins/b2b/B2bContingentsBudgets/Tests/Integration/RuleCartAccessFactoryTest.php b/exampleplugins/b2b/B2bContingentsBudgets/Tests/Integration/RuleCartAccessFactoryTest.php new file mode 100644 index 0000000000..e3f719a745 --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/Tests/Integration/RuleCartAccessFactoryTest.php @@ -0,0 +1,75 @@ +getContainer() + ->get('b2b_contingent_rule.rule_cart_access_factory'); + } + + /** + * @covers ::createCartAccessForIdentity + */ + public function test_it_creates_whitelist() + { + $whitelist = $this->createRuleCartAccessFactory() + ->createCartAccessForIdentity(self::createContactIdentity(), 'does-not-matter-in-this-test-scenario'); + + self::assertInstanceOf(WhiteListCartAccess::class, $whitelist); + } + + /** + * @covers ::createCartAccessForIdentity + */ + public function test_it_creates_whitelist_if_no_contingent_groups_rules_exist() + { + self::getKernel()->getContainer()->get('dbal_connection')->exec('DELETE FROM b2b_contingent_group_rule'); + + $whitelist = $this->createRuleCartAccessFactory() + ->createCartAccessForIdentity(self::createContactIdentity(), 'does-not-matter-in-this-test-scenario'); + + self::assertInstanceOf(WhiteListCartAccess::class, $whitelist); + } + + /** + * @covers ::createCartAccessForIdentity + */ + public function test_it_creates_whitelist_if_no_contingent_groups_exist() + { + self::getKernel()->getContainer()->get('dbal_connection')->exec('DELETE FROM b2b_contingent_group'); + + $whitelist = $this->createRuleCartAccessFactory() + ->createCartAccessForIdentity(self::createContactIdentity(), 'does-not-matter-in-this-test-scenario'); + + self::assertInstanceOf(WhiteListCartAccess::class, $whitelist); + } + + /** + * @covers ::createCartAccessForIdentity + */ + public function test_it_creates_whitelist_if_no_contingent_group_rule_time_restriction_exist() + { + self::getKernel()->getContainer()->get('dbal_connection')->exec('DELETE FROM b2b_contingent_group_rule_time_restriction'); + + $whitelist = $this->createRuleCartAccessFactory() + ->createCartAccessForIdentity(self::createContactIdentity(), 'does-not-matter-in-this-test-scenario'); + + self::assertInstanceOf(WhiteListCartAccess::class, $whitelist); + } +} diff --git a/exampleplugins/b2b/B2bContingentsBudgets/Tests/Unit/BudgetCartOrderAccessStrategyTest.php b/exampleplugins/b2b/B2bContingentsBudgets/Tests/Unit/BudgetCartOrderAccessStrategyTest.php new file mode 100644 index 0000000000..f10a519f1d --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/Tests/Unit/BudgetCartOrderAccessStrategyTest.php @@ -0,0 +1,44 @@ +getContainer()->get('b2b_budget.service'); + + $strategry = new BudgetCartOrderAccessStrategy(self::createContactIdentity(), $budgetService, self::createCurrencyContext()); + + $result = new CartAccessResult(); + $conext = new CartAccessContext(); + + $conext->orderClearanceEntity = new OrderClearanceEntity(); + $conext->orderClearanceEntity->list = new LineItemList(); + $conext->orderClearanceEntity->list->amountNet = 0; + + $strategry->checkAccess($conext, $result); + + self::assertEquals(0, $result->errorCount); + } +} diff --git a/exampleplugins/b2b/B2bContingentsBudgets/phpunit.xml.dist b/exampleplugins/b2b/B2bContingentsBudgets/phpunit.xml.dist new file mode 100644 index 0000000000..5ab7d8543b --- /dev/null +++ b/exampleplugins/b2b/B2bContingentsBudgets/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/B2bCustomerFrontendApi.php b/exampleplugins/b2b/B2bCustomerFrontendApi/B2bCustomerFrontendApi.php new file mode 100755 index 0000000000..79c6865f67 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/B2bCustomerFrontendApi.php @@ -0,0 +1,25 @@ +addConfiguration(new StoreFrontAuthenticationFrameworkConfiguration()); + $containerBuilder->addConfiguration(new ContactFrameworkConfiguration()); + $containerBuilder->registerConfigurations($container); + + parent::build($container); + } +} diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/Controllers/Frontend/B2bCustomerApi.php b/exampleplugins/b2b/B2bCustomerFrontendApi/Controllers/Frontend/B2bCustomerApi.php new file mode 100644 index 0000000000..510455ffd0 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/Controllers/Frontend/B2bCustomerApi.php @@ -0,0 +1,22 @@ +Response()->setHeader('Content-type', 'application/json', true); + } +} diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/Controllers/Frontend/B2bCustomerDirectApi.php b/exampleplugins/b2b/B2bCustomerFrontendApi/Controllers/Frontend/B2bCustomerDirectApi.php new file mode 100644 index 0000000000..fb91f66dd3 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/Controllers/Frontend/B2bCustomerDirectApi.php @@ -0,0 +1,57 @@ +get('b2b_front_auth.authentication_service'); + if (!$authenticationService->isB2b()) { + $this->forward('index', 'account'); + } + } + + /** + * @throws \InvalidArgumentException + */ + public function indexAction() + { + $request = $this->Request(); + + $identity = $this->get('b2b_front_auth.authentication_service')->getIdentity(); + + if (!($identity instanceof DebtorIdentity)) { + throw new \InvalidArgumentException('No debtor identity'); + } + + $contactId = (int) $request->getParam('contactId'); + + if (!$contactId) { + throw new \InvalidArgumentException('No contact id given'); + } + + /** @var ContactRepository $contactRepository */ + $contactRepository = $this->get('b2b_contact.repository'); + + $contact = $contactRepository->fetchOneById($contactId); + + $this->View()->assign( + 'data', + [ + 'success' => true, + 'contact' => $contact, + ] + ); + } + + public function postDispatch() + { + parent::postDispatch(); + + $this->Response()->setHeader('Content-type', 'application/json', true); + } +} diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/CustomerApi/CustomerApiController.php b/exampleplugins/b2b/B2bCustomerFrontendApi/CustomerApi/CustomerApiController.php new file mode 100644 index 0000000000..9d1c218248 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/CustomerApi/CustomerApiController.php @@ -0,0 +1,75 @@ +authenticationService = $authenticationService; + $this->contactRepository = $contactRepository; + $this->gridHelper = $gridHelper; + } + + /** + * @param Request $request + * @throws \InvalidArgumentException + * @return array + */ + public function indexAction(Request $request): array + { + $identity = $this->authenticationService->getIdentity(); + + if (!($identity instanceof DebtorIdentity)) { + throw new \InvalidArgumentException('No debtor identity'); + } + + $ownerShipContext = $identity->getOwnershipContext(); + + $searchStruct = new ContactSearchStruct(); + + $this->gridHelper->extractSearchDataInRestApi($request, $searchStruct); + + $contacts = $this->contactRepository->fetchList($ownerShipContext, $searchStruct); + + $totalCount = $this->contactRepository->fetchTotalCount($ownerShipContext, $searchStruct); + + return [ + 'data' => [ + 'success' => true, + 'contacts' => $contacts, + 'totalCount' => $totalCount, + ], + ]; + } +} diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/services.xml b/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/services.xml new file mode 100755 index 0000000000..f6a7e6605f --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/services.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/views/frontend/b2bcustomerapi/index.tpl b/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/views/frontend/b2bcustomerapi/index.tpl new file mode 100644 index 0000000000..7ddf04e143 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/views/frontend/b2bcustomerapi/index.tpl @@ -0,0 +1 @@ +{$data|json_encode} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/views/frontend/b2bcustomerdirectapi/index.tpl b/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/views/frontend/b2bcustomerdirectapi/index.tpl new file mode 100644 index 0000000000..7ddf04e143 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/Resources/views/frontend/b2bcustomerdirectapi/index.tpl @@ -0,0 +1 @@ +{$data|json_encode} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/Subscriber/FrontendTemplateExtender.php b/exampleplugins/b2b/B2bCustomerFrontendApi/Subscriber/FrontendTemplateExtender.php new file mode 100644 index 0000000000..3cf4919863 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/Subscriber/FrontendTemplateExtender.php @@ -0,0 +1,27 @@ + 'addViewDirectories', + 'Enlight_Controller_Action_PreDispatch_Frontend_B2bCustomerDirectApi' => 'addViewDirectories', + ]; + } + + /** + * @param \Enlight_Controller_ActionEventArgs $args + */ + public function addViewDirectories(\Enlight_Controller_ActionEventArgs $args) + { + $args->getSubject()->View()->addTemplateDir(__DIR__ . '/../Resources/views'); + } +} diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/plugin.png b/exampleplugins/b2b/B2bCustomerFrontendApi/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bCustomerFrontendApi/plugin.png differ diff --git a/exampleplugins/b2b/B2bCustomerFrontendApi/plugin.xml b/exampleplugins/b2b/B2bCustomerFrontendApi/plugin.xml new file mode 100644 index 0000000000..b538d3c8d8 --- /dev/null +++ b/exampleplugins/b2b/B2bCustomerFrontendApi/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bLogin/B2bLogin.php b/exampleplugins/b2b/B2bLogin/B2bLogin.php new file mode 100755 index 0000000000..b1c16a7a23 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/B2bLogin.php @@ -0,0 +1,55 @@ + 'addViewDirectories', + 'Enlight_Controller_Action_PostDispatchSecure_Widgets' => 'addViewDirectories', + ]; + } + + /** + * @param \Enlight_Controller_ActionEventArgs $args + */ + public function addViewDirectories(\Enlight_Controller_ActionEventArgs $args) + { + $args->getSubject()->View()->addTemplateDir(__DIR__ . '/../Resources/views'); + } + + /** + * @param InstallContext $context + */ + public function install(InstallContext $context) + { + $connection = Shopware()->Container()->get('dbal_connection'); + $connection->exec(' + ALTER TABLE b2b_debtor_contact + ADD staff_id VARCHAR(255); + + UNIQUE INDEX `dcstaff_id` (`staff_id`), + '); + + $connection->exec(' + SET @a = 0; + UPDATE b2b_debtor_contact SET staff_id = CONCAT(\'A\', \'-\', @a:=@a+1); + '); + + $attributeService = Shopware()->Container()->get('shopware_attribute.crud_service'); + $attributeService->update('s_user_attributes', 'staff_id', 'string'); + + $connection->exec(' + SET @a = 0; + UPDATE s_user_attributes SET staff_id = CONCAT(\'B\', \'-\', @a:=@a+1); + '); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Contact/B2bContactAuthenticationIdentityLoader.php b/exampleplugins/b2b/B2bLogin/Contact/B2bContactAuthenticationIdentityLoader.php new file mode 100644 index 0000000000..28985b0591 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Contact/B2bContactAuthenticationIdentityLoader.php @@ -0,0 +1,68 @@ +contactRepository = $contactRepository; + $this->debtorRepository = $debtorRepository; + parent::__construct($contactRepository, $debtorRepository); + } + + public function fetchIdentityByStaffId(string $staffId, LoginContextService $contextService, bool $isApi = false): Identity + { + $entity = $this->contactRepository->fetchOneByStaffId($staffId); + + /** @var DebtorIdentity $debtorIdentity */ + $debtorIdentity = $this + ->debtorRepository + ->fetchIdentityById($entity->debtor->id, $contextService); + + $authId = $contextService->getAuthId(ContactRepository::class, $entity->email, $debtorIdentity->getAuthId()); + + $this->contactRepository->setAuthId($entity->id, $authId); + + return new ContactIdentity($authId, (int) $entity->id, ContactRepository::TABLE_NAME, $entity, $debtorIdentity); + } + + /** + * @param CredentialsEntity $credentialsEntity + * @param LoginContextService $contextService + * @param bool $isApi + * @return Identity + */ + public function fetchIdentityByCredentials(CredentialsEntity $credentialsEntity, LoginContextService $contextService, bool $isApi = false): Identity + { + if (!$credentialsEntity->staffId) { + throw new NotFoundException('Unable to handle context'); + } + + return $this->fetchIdentityByStaffId($credentialsEntity->staffId, $contextService); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Contact/B2bContactCrudService.php b/exampleplugins/b2b/B2bLogin/Contact/B2bContactCrudService.php new file mode 100644 index 0000000000..1f0c338392 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Contact/B2bContactCrudService.php @@ -0,0 +1,127 @@ +contactRepository = $contactRepository; + $this->validationService = $validationService; + $this->aclRepository = $aclRepository; + $this->passwordProvider = $passwordProvider; + } + + /** + * @param CrudServiceRequest $request + * @param Identity $identity + * @throws \Shopware\B2B\Common\Validator\ValidationException + * @return ContactEntity + */ + public function create(CrudServiceRequest $request, Identity $identity): ContactEntity + { + $data = $request->getFilteredData(); + + $contact = new ContactEntity(); + + $contact->setData($data); + + $this->checkPassword($contact, $request, true); + $this->passwordProvider->setPassword($contact, $request->requireParam('passwordNew')); + + $validation = $this->validationService + ->createInsertValidation($contact); + + $this->testValidation($contact, $validation); + + if (empty($contact->email)) { + $contact->email = uniqid('', true); + } + + $contact = $this->contactRepository + ->addContact($contact); + + $this->aclRepository + ->allowAll( + $contact, + [ + $identity->getMainShippingAddress()->id, + $identity->getMainBillingAddress()->id, + ] + ); + + return $contact; + } + + /** + * @param CrudServiceRequest $request + * @throws \Shopware\B2B\Common\Validator\ValidationException + * @return ContactEntity + */ + public function update(CrudServiceRequest $request): ContactEntity + { + $data = $request->getFilteredData(); + $contact = new ContactEntity(); + $contact->setData($data); + + $this->checkPassword($contact, $request, false); + + if ($request->hasValueForParam('passwordNew')) { + $this->passwordProvider->setPassword($contact, $request->requireParam('passwordNew')); + } + + $validation = $this->validationService + ->createUpdateValidation($contact); + + $this->testValidation($contact, $validation); + + if (empty($contact->email)) { + $contact->email = uniqid('', true); + } + + $this->contactRepository + ->updateContact($contact); + + return $contact; + } +} diff --git a/exampleplugins/b2b/B2bLogin/Contact/B2bContactRepository.php b/exampleplugins/b2b/B2bLogin/Contact/B2bContactRepository.php new file mode 100644 index 0000000000..f2f2ae9d85 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Contact/B2bContactRepository.php @@ -0,0 +1,73 @@ +connection = $connection; + $this->debtorRepository = $debtorRepository; + $this->dbalHelper = $dbalHelper; + $this->authenticationRepository = $authenticationRepository; + parent::__construct($connection, $dbalHelper, $debtorRepository, $authenticationRepository); + } + + /** + * @param string $staffId + * @return ContactEntity + */ + public function fetchOneByStaffId(string $staffId): ContactEntity + { + $statement = $this->connection->createQueryBuilder() + ->select('*') + ->from(self::TABLE_NAME, self::TABLE_ALIAS) + ->where(self::TABLE_ALIAS . '.staff_id = :staffId') + ->setParameter('staffId', $staffId) + ->execute(); + + $contactData = $statement->fetch(\PDO::FETCH_ASSOC); + + return parent::createContactByContactData($contactData, $staffId); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Contact/B2bContactValidationService.php b/exampleplugins/b2b/B2bLogin/Contact/B2bContactValidationService.php new file mode 100644 index 0000000000..7988ce1681 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Contact/B2bContactValidationService.php @@ -0,0 +1,105 @@ +validationBuilder = $validationBuilder; + $this->validator = $validator; + $this->contactRepository = $contactRepository; + $this->debtorRepository = $debtorRepository; + } + + /** + * @param ContactEntity $contact + * @return Validator + */ + public function createInsertValidation(ContactEntity $contact): Validator + { + $validation = $this->createCrudValidation($contact) + ->validateThat('id', $contact->id) + ->isBlank(); + + if (empty($contact->email)) { + return $validation->getValidator($this->validator); + }; + + $validation->validateThat('email', $contact->email) + ->isUnique(function () use ($contact) { + return 0 === $this->contactRepository->hasByEmail($contact->email); + }) + ->isUnique(function () use ($contact) { + return !$this->debtorRepository->hasDebtorWithEmail($contact->email); + }) + ->isNotBlank() + ->isEmail() + + ->getValidator($this->validator); + } + + /** + * @param ContactEntity $contact + * @return Validator + */ + public function createUpdateValidation(ContactEntity $contact): Validator + { + $validation = $this->createCrudValidation($contact) + ->validateThat('id', $contact->id) + ->isNotBlank(); + + if (empty($contact->email)) { + return $validation->getValidator($this->validator); + }; + + $validation->validateThat('email', $contact->email) + ->isUnique(function () use ($contact) { + return 2 > $this->contactRepository->hasByEmail($contact->email); + }) + ->isUnique(function () use ($contact) { + return 3 > $this->debtorRepository->fetchDebtorWithEmailCount($contact->email); + }) + ->isEmail() + ->getValidator($this->validator); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Debtor/B2bDebtorAuthenticationIdentityLoader.php b/exampleplugins/b2b/B2bLogin/Debtor/B2bDebtorAuthenticationIdentityLoader.php new file mode 100644 index 0000000000..eeb478db08 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Debtor/B2bDebtorAuthenticationIdentityLoader.php @@ -0,0 +1,59 @@ +debtorRepository = $debtorRepository; + parent::__construct($debtorRepository); + } + + /** + * @param string $email + * @param LoginContextService $contextService + * @param bool $isApi + * @param string $staffId + * @return Identity + */ + public function fetchIdentityByStaffId(string $staffId, LoginContextService $contextService, bool $isApi = false): Identity + { + $entity = $this->debtorRepository->fetchOneByStaffId($staffId); + + $authId = $contextService->getAuthId(DebtorRepository::class, $entity->email); + + return new DebtorIdentity($authId, (int) $entity->id, DebtorRepository::TABLE_NAME, $entity, $isApi); + } + + /** + * @param CredentialsEntity $credentialsEntity + * @param LoginContextService $contextService + * @param bool $isApi + * @return Identity + */ + public function fetchIdentityByCredentials(CredentialsEntity $credentialsEntity, LoginContextService $contextService, bool $isApi = false): Identity + { + if (!$credentialsEntity->staffId) { + throw new NotFoundException('Unable to handle context'); + } + + return $this->fetchIdentityByStaffId($credentialsEntity->staffId, $contextService); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Debtor/B2bDebtorRepository.php b/exampleplugins/b2b/B2bLogin/Debtor/B2bDebtorRepository.php new file mode 100644 index 0000000000..03e7f8b566 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Debtor/B2bDebtorRepository.php @@ -0,0 +1,54 @@ +connection = $connection; + parent::__construct($connection); + } + + /** + * @param string $staffId + * @throws NotFoundException + * @return DebtorEntity + */ + public function fetchOneByStaffId(string $staffId): DebtorEntity + { + $statement = $this->connection->createQueryBuilder() + ->select(self::TABLE_ALIAS . '.*') + ->from(self::TABLE_NAME, 'user') + ->innerJoin(self::TABLE_ALIAS, 's_user_attributes', 'attributes', 'attributes.userID = user.id') + ->where('attributes.staff_id = :staffId') + ->andWhere('attributes.b2b_is_debtor = 1') + ->setParameter('staffId', $staffId) + ->execute(); + + $user = $statement->fetch(\PDO::FETCH_ASSOC); + + if (!$user) { + throw new NotFoundException(sprintf('Debtor not found for %s', $staffId)); + } + + return (new DebtorEntity())->fromDatabaseArray($user); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Login/B2bCredentialsBuilder.php b/exampleplugins/b2b/B2bLogin/Login/B2bCredentialsBuilder.php new file mode 100644 index 0000000000..e37fc11686 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Login/B2bCredentialsBuilder.php @@ -0,0 +1,22 @@ +staffId = $args->get('post')['staffId']; + + return $entity; + } +} diff --git a/exampleplugins/b2b/B2bLogin/Login/LoginSubscriber.php b/exampleplugins/b2b/B2bLogin/Login/LoginSubscriber.php new file mode 100644 index 0000000000..5adf10072f --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Login/LoginSubscriber.php @@ -0,0 +1,41 @@ + array('updateEmail', 2), + 'Shopware_Modules_Admin_CheckUser_Failure' => array('resetEmail', 2), + 'Shopware_Modules_Admin_Login_Successful' => array('resetEmail', 2), + ]; + } + + /** + * @param Enlight_Event_EventArgs $args + */ + public function updateEmail(Enlight_Event_EventArgs $args) + { + $credentialBuilder = Shopware()->Container()->get('b2b_front_auth.credentials_builder'); + $identityChain = Shopware()->Container()->get('b2b_front_auth.identity_chain_repository'); + $context = Shopware()->Container()->get('b2b_front_auth.login_context'); + $post = $args->get('post'); + $credential = $credentialBuilder->createCredentials($args); + try { + $entity = $identityChain->fetchIdentityByCredentials($credential, $context, false); + } catch (NotFoundException $e) { + return; + } + $post['email'] = $entity->getEntity()->email; + Shopware()->Front()->Request()->setPost($post); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Resources/services.xml b/exampleplugins/b2b/B2bLogin/Resources/services.xml new file mode 100755 index 0000000000..ec2b74b9da --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Resources/services.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/exampleplugins/b2b/B2bLogin/Resources/views/frontend/_grid/contact-grid.tpl b/exampleplugins/b2b/B2bLogin/Resources/views/frontend/_grid/contact-grid.tpl new file mode 100644 index 0000000000..3d08315cb1 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Resources/views/frontend/_grid/contact-grid.tpl @@ -0,0 +1,29 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{extends file="parent:frontend/_grid/contact-grid.tpl"} + +{block name="b2b_grid_table_row"} + + {$row->firstName} + {$row->lastName} + {if strpos($row->email, "@")}{$row->email}{/if} + + {if $row->active} + + {else} + + {/if} + + + + +
+ + + +
+ + +{/block} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bLogin/Resources/views/frontend/b2bcontact/_form.tpl b/exampleplugins/b2b/B2bLogin/Resources/views/frontend/b2bcontact/_form.tpl new file mode 100644 index 0000000000..d001d59fa2 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Resources/views/frontend/b2bcontact/_form.tpl @@ -0,0 +1,68 @@ +{namespace name=frontend/plugins/b2b_debtor_plugin} + +
+
+ {s name="Firstname"}Firstname{/s}: * +
+
+ +
+
+ +
+
+ {s name="Surname"}Surname{/s}: * +
+
+ +
+
+ +
+
+ {s name="Email"}E-Mail{/s}: +
+
+ email}{/if}" placeholder="{s name="Email"}E-Mail{/s}"> +
+
+ +
+
+ {s name="Department"}Department{/s}: +
+
+ +
+
+ +
+
+ {s name="Password"}Password{/s}: * +
+
+ +
+
+ +
+
+ {s name="Confirm"}Confirm{/s}: * +
+
+ +
+
+ +
+
+ {s name="Active"}Active{/s}: +
+
+ + active || $isNew}checked="checked"{/if}> + + +
+
\ No newline at end of file diff --git a/exampleplugins/b2b/B2bLogin/Resources/views/frontend/register/login.tpl b/exampleplugins/b2b/B2bLogin/Resources/views/frontend/register/login.tpl new file mode 100644 index 0000000000..8a64fdf7fb --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Resources/views/frontend/register/login.tpl @@ -0,0 +1,9 @@ +{extends file='parent:frontend/register/login.tpl'} + +{block name='frontend_register_login_input_email'} + +{/block} + +{block name="frontend_register_login_input_lostpassword"}{/block} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bLogin/Subscriber/FrontendTemplateExtender.php b/exampleplugins/b2b/B2bLogin/Subscriber/FrontendTemplateExtender.php new file mode 100644 index 0000000000..b1207b44d2 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Subscriber/FrontendTemplateExtender.php @@ -0,0 +1,42 @@ +container = $container; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PreDispatch_Frontend' => 'addViewDirectories', + 'Enlight_Controller_Action_PostDispatchSecure_Widgets' => 'addViewDirectories', + ]; + } + + /** + * @param \Enlight_Controller_ActionEventArgs $args + */ + public function addViewDirectories(\Enlight_Controller_ActionEventArgs $args) + { + $args->getSubject()->View()->addTemplateDir(__DIR__ . '/../Resources/views'); + $args->getSubject()->View()->addTemplateDir(__DIR__ . '/../Resources/extendedViews'); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Tests/Bootstrap.php b/exampleplugins/b2b/B2bLogin/Tests/Bootstrap.php new file mode 100644 index 0000000000..45c8931ad9 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Tests/Bootstrap.php @@ -0,0 +1,6 @@ +importFixturesFileOnce(__DIR__ . '/../test_fixtures.sql'); + } + + public function test_fails_for_unknown_user() + { + $client = self::createClient(); + self::getKernel()->getContainer()->get('front')->setResponse('Enlight_Controller_Response_ResponseTestCase'); + $client->request( + 'POST', + 'account/login/sTarget/account/sTargetAction/index', + [ + 'staffId' => 'foo', + 'password' => 'bar', + ] + ); + + self::assertEquals(200, $client->getResponse()->getStatusCode()); + self::assertContains( + 'Ihre Zugangsdaten konnten keinem Benutzer zugeordnet werden', + $client->getResponse()->getContent() + ); + + self::assertEquals(1, $client->getResponse()->headers->has('B2b-no-login')); + self::assertFalse($client->getResponse()->headers->has('B2b-login')); + self::assertEquals(200, $client->getResponse()->getStatusCode()); + } + + public function test_success_login_for_debtor() + { + self::getKernel()->getContainer()->get('front')->setResponse('Enlight_Controller_Response_ResponseTestCase'); + $client = self::createClient(); + $client->request( + 'POST', + 'account/login/sTarget/account/sTargetAction/index', + [ + 'staffId' => 'B-1', + 'password' => 'shopware', + ] + ); + self::assertEquals(302, $client->getResponse()->getStatusCode()); + + self::getKernel()->getContainer()->get('front')->setResponse('Enlight_Controller_Response_ResponseTestCase'); + $client->request('GET', 'account'); + self::assertNotContains( + 'Ihre Zugangsdaten konnten keinem Benutzer zugeordnet werden', + $client->getResponse()->getContent() + ); + self::assertEquals( + 302, + $client->getResponse()->getStatusCode(), + print_r($client->getResponse()->getStatusCode(), true) + ); + + + self::assertEquals(null, $client->getResponse()->headers->get('B2b-login')); + self::assertTrue( + self::getKernel()->getContainer()->get('b2b_front_auth.authentication_service')->is( + DebtorIdentity::class + ) + ); + } + + public function test_success_login_for_existing_contact() + { + $client = self::createClient(); + self::getKernel()->getContainer()->get('front')->setResponse('Enlight_Controller_Response_ResponseTestCase'); + $client->request( + 'POST', + 'account/login/sTarget/account/sTargetAction/index', + [ + 'staffId' => 'A-1', + 'password' => 'shopware', + ] + ); + self::assertEquals(302, $client->getResponse()->getStatusCode()); + + self::getKernel()->getContainer()->get('front')->setResponse('Enlight_Controller_Response_ResponseTestCase'); + $client->request('GET', 'account'); + self::assertEquals(302, $client->getResponse()->getStatusCode()); + self::assertNotContains( + 'Ihre Zugangsdaten konnten keinem Benutzer zugeordnet werden', + $client->getResponse()->getContent() + ); + self::assertTrue( + self::getKernel()->getContainer()->get('b2b_front_auth.authentication_service')->is( + ContactIdentity::class + ) + ); + } + + public function test_success_login_for_new_contact() + { + $client = self::createClient(); + self::getKernel()->getContainer()->get('front')->setResponse('Enlight_Controller_Response_ResponseTestCase'); + $client->request( + 'POST', + 'account/login/sTarget/account/sTargetAction/index', + [ + 'staffId' => 'A-3', + 'password' => 'shopware', + ] + ); + + self::getKernel()->getContainer()->get('front')->setResponse('Enlight_Controller_Response_ResponseTestCase'); + $client->request('GET', 'account'); + self::assertNotContains( + 'Ihre Zugangsdaten konnten keinem Benutzer zugeordnet werden', + $client->getResponse()->getContent() + ); + self::assertTrue( + self::getKernel()->getContainer()->get('b2b_front_auth.authentication_service') + ->is(ContactIdentity::class) + ); + } +} diff --git a/exampleplugins/b2b/B2bLogin/Tests/test_fixtures.sql b/exampleplugins/b2b/B2bLogin/Tests/test_fixtures.sql new file mode 100644 index 0000000000..860a4e2533 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/Tests/test_fixtures.sql @@ -0,0 +1,394 @@ +INSERT INTO `s_core_customergroups` (`id`, `groupkey`, `description`, `tax`, `taxinput`, `mode`, `discount`, `minimumorder`, `minimumordersurcharge`) VALUES + (98, 'Emo', 'EK-Emotion', '1', '1', '0', '0', '10', '5') + ,(99, 'NoEmo', 'EK-No-Emotion', '1', '1', '0', '0', '10', '5') + ,(97, 'NoEId', 'EK-No-Emotion-Id', '1', '1', '0', '0', '10', '5') + ,(96, 'NoEAt', 'EK-No-Emotion-Attr', '1', '1', '0', '0', '10', '5') +; + +INSERT INTO `s_core_customergroups_attributes` (`id`, `customerGroupID`, `b2b_landingpage`) VALUES + (98, 98, 99) + ,(99, 99, 99999) + ,(97, 97, NULL) +; + +INSERT INTO `s_emotion` (`id`, `active`, `name`, `cols`, `cell_spacing`, `cell_height`, `article_height`, `rows`, `userID`, `show_listing`, `is_landingpage`, `create_date`, `modified`, `template_id`, `device`, `fullscreen`, `mode`, `position`) VALUES + (99, 1, 'Test Einkaufswelt', '4', '10', '185', '2', '22', '1', '0', '0', '2012-08-29 08:41:30', '2016-05-31 08:23:09', '1', '0,1,2,3,4', '0', 'fluid', '1') +; + +REPLACE INTO `s_user` (`id`, `customernumber`, `password`, `email`, `active`, `accountmode`, `confirmationkey`, `paymentID`, `firstlogin`, `lastlogin`, `sessionID`, `newsletter`, `validation`, `affiliate`, `customergroup`, `paymentpreset`, `language`, `subshopID`, `referer`, `pricegroupID`, `internalcomment`, `failedlogins`, `lockeduntil`, `default_billing_address_id`, `default_shipping_address_id`, `salutation`, `firstname`, `lastname`) VALUES + (250, '1324', 'a256a310bc1e5db755fd392c524028a8', 'debtor@example.com', 1, 0, '', 5, '2011-11-23', '2012-01-04 14:12:05', '', 0, '', 0, 'EK', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'Mustermann'), + (249, '1324', 'a256a310bc1e5db755fd392c524028a8', 'debtor2@example.com', 1, 0, '', 5, '2011-11-23', '2012-01-04 14:12:05', '', 0, '', 0, 'EK', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'Mustermann'), + (251, '1234', 'a256a310bc1e5db755fd392c524028a8', 'contact1@example.com', 1, 0, '', 5, '2011-11-23', '2012-01-04 14:12:05', '', 0, '', 0, 'EK', 0, '1', 1, '', NULL, '', 0, NULL, 1, 3, 'mr', 'Max', 'Contact1'), + (252, '1234', 'a256a310bc1e5db755fd392c524028a8', 'contact2@example.com', 1, 0, '', 5, '2011-11-23', '2012-01-04 14:12:05', '', 0, '', 0, 'EK', 0, '1', 1, '', NULL, '', 0, NULL, 1, 3, 'mr', 'Max', 'Contact2'), + (253, '1324', 'a256a310bc1e5db755fd392c524028a8', 'debtor2@example.com', 1, 0, '', 5, '2011-11-23', '2012-01-04 14:12:05', '', 0, '', 0, 'EK', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'Mustermann'), + (254, '1324', 'a256a310bc1e5db755fd392c524028a8', 'contact3@example.com', 1, 0, '', 5, '2011-11-23', '2012-01-04 14:12:05', '', 0, '', 0, 'EK', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'Mustermann'), + (255, '2324', 'a256a310bc1e5db755fd392c524028a8', 'salesrepresentative@example.com',1, 0, '', 5, '2011-11-23', '2016-11-18 15:04:17', '', 0, '', 0, 'EK', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'Sales Representative'), + (256, '2324', 'a256a310bc1e5db755fd392c524028a8', 'shoppingworld@example.com',1, 0, '', 5, '2011-11-23', '2016-11-18 15:04:17', '', 0, '', 0, 'Emo', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'Shoppingworld'), + (257, '2324', 'a256a310bc1e5db755fd392c524028a8', 'no-shoppingworld@example.com',1, 0, '', 5, '2011-11-23', '2016-11-18 15:04:17', '', 0, '', 0, 'NoEmo', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'No Shoppingworld'), + (258, '2324', 'a256a310bc1e5db755fd392c524028a8', 'no-shoppingworld-id@example.com',1, 0, '', 5, '2011-11-23', '2016-11-18 15:04:17', '', 0, '', 0, 'NoEId', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'No Shoppingworld Id'), + (259, '2324', 'a256a310bc1e5db755fd392c524028a8', 'no-shoppingworld-attr@example.com',1, 0, '', 5, '2011-11-23', '2016-11-18 15:04:17', '', 0, '', 0, 'NoEAt', 0, '1', 1, '', NULL, '', 0, NULL, 28, 35, 'mr', 'Max', 'No Shoppingworld Attr'); + +REPLACE INTO b2b_store_front_auth (`id`, `context_owner_id`, `provider_key`, `provider_context`) VALUES + (1, 1, 'Shopware\\B2B\\Debtor\\Framework\\DebtorRepository', 'debtor@example.com') + ,(2, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'contact1@example.com') + ,(3, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'contact2@example.com') + ,(4, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'contact3@example.com') + ,(5, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'contact4@example.com') + ,(6, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'contact5@example.com') + ,(40, 40, 'Shopware\\B2B\\Debtor\\Framework\\DebtorRepository', 'debtor2@example.com') + ,(41, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'shoppingworld@example.com') + ,(42, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'no-shoppingworld@example.com') + ,(43, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'no-shoppingworld-id@example.com') + ,(44, 1, 'Shopware\\B2B\\Contact\\Framework\\ContactRepository', 'no-shoppingworld-attr@example.com') +; + +REPLACE INTO `b2b_debtor_contact` (`id`, `password`, `email`, `active`, `language`, `salutation`, `firstname`, `lastname`, `context_owner_id`, `auth_id`, staff_id) VALUES + (11, 'a256a310bc1e5db755fd392c524028a8', 'contact1@example.com', 1, 0, 'mr', 'Tom', 'Contact1', 1, 2, 'A-1'), + (22, 'a256a310bc1e5db755fd392c524028a8', 'contact2@example.com', 1, 0, 'mr', 'Tom', 'Contact2', 1, 3, 'A-2'), + (33, 'a256a310bc1e5db755fd392c524028a8', 'contact3@example.com', 1, 0, 'mr', 'Max', 'Contact3', 1, 4, 'A-3'), + (44, 'a256a310bc1e5db755fd392c524028a8', 'contact4@example.com', 1, 0, 'mr', 'Max', 'Contact4', 1, 5, 'A-4'), + (45, 'a256a310bc1e5db755fd392c524028a8', 'contact5@example.com', 1, 0, 'mr', 'Max', 'Contact5', 1, 6, 'A-5'), + (46, 'a256a310bc1e5db755fd392c524028a8', 'contact6@example.com', 1, 0, 'mr', 'Max', 'Contact6', 1, NULL, 'A-6'), + (47, 'a256a310bc1e5db755fd392c524028a8', 'contact7@example.com', 1, 0, 'mr', 'Max', 'Contact7', 1, NULL, 'A-7'), + (48, 'a256a310bc1e5db755fd392c524028a8', 'contact8@example.com', 1, 0, 'mr', 'Max', 'Contact8', 1, NULL, 'A-8'), + (49, 'a256a310bc1e5db755fd392c524028a8', 'contact9@example.com', 1, 0, 'mr', 'Max', 'Contact9', 1, NULL, 'A-9'), + (50, 'a256a310bc1e5db755fd392c524028a8', 'contact10@example.com', 1, 0, 'mr', 'Max', 'Contact10', 1, NULL, 'A-10'), + (51, 'a256a310bc1e5db755fd392c524028a8', 'contact11@example.com', 1, 0, 'mr', 'Max', 'Contact11', 1, NULL, 'A-11'), + (52, 'a256a310bc1e5db755fd392c524028a8', 'contact12@example.com', 1, 0, 'mr', 'Max', 'Contact12', 1, NULL, 'A-12'), + (53, 'a256a310bc1e5db755fd392c524028a8', 'contact13@example.com', 1, 0, 'mr', 'Max', 'Contact13', 1, NULL, 'A-13'), + (54, 'a256a310bc1e5db755fd392c524028a8', 'contact14@example.com', 1, 0, 'mr', 'Max', 'Contact14', 1, NULL, 'A-14'), + (55, 'a256a310bc1e5db755fd392c524028a8', 'contact15@example.com', 1, 0, 'mr', 'Max', 'Contact15', 1, NULL, 'A-15'), + (56, 'a256a310bc1e5db755fd392c524028a8', 'contact16@example.com', 1, 0, 'mr', 'Max', 'Contact16', 1, NULL, 'A-16'), + (57, 'a256a310bc1e5db755fd392c524028a8', 'contact17@example.com', 1, 0, 'mr', 'Max', 'Contact17', 1, NULL, 'A-17'), + (58, 'a256a310bc1e5db755fd392c524028a8', 'contact18@example.com', 1, 0, 'mr', 'Max', 'Contact18', 1, NULL, 'A-18'), + (59, 'a256a310bc1e5db755fd392c524028a8', 'contact19@example.com', 1, 0, 'mr', 'Max', 'Contact19', 1, NULL, 'A-19'), + (60, 'a256a310bc1e5db755fd392c524028a8', 'contact20@example.com', 1, 0, 'mr', 'Max', 'Contact20', 1, NULL, 'A-20'), + (61, 'a256a310bc1e5db755fd392c524028a8', 'contact21@example.com', 0, 0, 'mr', 'Max', 'Contact21', 1, NULL, 'A-21'), + (62, 'a256a310bc1e5db755fd392c524028a8', 'contact23@example.com', 0, 0, 'mr', 'Max', 'Contact23', 40, NULL, 'A-22'), + (63, 'a256a310bc1e5db755fd392c524028a8', 'shoppingworld@example.com', 0, 0, 'mr', 'Max', 'Shoppingworld', 1, 41, 'A-23'), + (64, 'a256a310bc1e5db755fd392c524028a8', 'no-shoppingworld@example.com', 0, 0, 'mr', 'Max', 'No Shoppingworld', 1, 42, 'A-24'), + (65, 'a256a310bc1e5db755fd392c524028a8', 'no-shoppingworld-id@example.com', 0, 0, 'mr', 'Max', 'No Shoppingworld Id', 1, 43, 'A-25'), + (65, 'a256a310bc1e5db755fd392c524028a8', 'no-shoppingworld-attr@example.com', 0, 0, 'mr', 'Max', 'No Shoppingworld Attr', 1, 44, 'A-26'); + +REPLACE INTO s_user_attributes (id, userID, b2b_is_debtor, b2b_is_sales_representative, b2b_sales_representative_media_id, b2b_sales_representative_id, staff_id) VALUES + (449, 249, 1, 0, NULL, 255, 'B-1'), + (450, 250, 1, 0, NULL, 255, 'B-2'), + (451, 255, 0, 1, NULL, NULL, 'C-3'), + (452, 251, 0, 0, NULL, 255, null); + +REPLACE INTO `s_user_addresses` (`id`, `user_id`, `company`, `department`, `salutation`, `firstname`, `lastname`, `street`, `zipcode`, `city`, `phone`, `country_id`, `state_id`, `ustid`) VALUES + (28, 250, 'Debtor GMBH GmbH', NULL, 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (41, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (42, 250, 'Debtor GMBH GmbH', 'Zuletztkauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (43, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (44, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (45, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (46, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (47, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (48, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (49, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (50, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (51, 250, 'Debtor GMBH GmbH', 'Einkauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (35, 250, 'Debtor FQM', 'Einkauf', 'mr', 'Händler', 'Kundengruppe-Netto', 'Musterweg 1', '55555', 'Musterstadt', '012345 / 6789', 2, 3, NULL); + +# @todo necessary until SW-16481 is resolved +INSERT INTO `s_user_billingaddress` (`id`, `userID`, `company`, `department`, `salutation`, `firstname`, `lastname`, `street`, `zipcode`, `city`, `phone`, `countryID`, `stateID`, `ustid`) VALUES + (28, 250, 'Debtor GMBH GmbH', 'foo', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL); + +INSERT INTO `s_user_addresses_attributes` (`address_id`, `b2b_type`) VALUES + (28, 'billing'), + (41, 'billing'), + (42, 'billing'), + (43, 'billing'), + (44, 'billing'), + (45, 'billing'), + (46, 'billing'), + (47, 'billing'), + (48, 'billing'), + (49, 'billing'), + (50, 'billing'), + (51, 'billing'), + (35, 'shipping'); + +INSERT INTO `b2b_acl_contact_address` (`entity_id`, `referenced_entity_id`, `grantable`) VALUES + (33, 28, 1), + (33, 41, 1), + (33, 42, 1), + (33, 43, 1), + (33, 44, 1), + (33, 45, 1), + (33, 46, 1), + (33, 47, 1), + (33, 48, 1), + (33, 49, 1), + (33, 50, 1), + (33, 51, 1), + (11, 28, 1), + (11, 41, 1), + (11, 42, 0), + (11, 43, 1), + (11, 44, 1), + (11, 45, 0), + (11, 46, 1), + (11, 47, 1), + (11, 48, 0), + (11, 49, 1), + (11, 50, 1), + (11, 51, 1), + (11, 35, 1); + +INSERT INTO `s_categories` (`id`, `parent`, `path`, `description`, `position`, `left`, `right`, `level`, `added`, `changed`, `metakeywords`, `metadescription`, `cmsheadline`, `cmstext`, `template`, `active`, `blog`, `external`, `hidefilter`, `hidetop`, `mediaID`, `product_box_layout`, `meta_title`, `stream_id`) VALUES + (500, 3, '|3|', 'B2B', 0, 0, 0, 0, '2012-07-30 15:24:59', '2012-07-30 15:24:59', NULL, '', 'B2B', NULL, NULL, 1, 0, NULL, 0, 0, 0, NULL, NULL, NULL); + +INSERT INTO `s_articles` (`id`, `supplierID`, `name`, `description`, `description_long`, `shippingtime`, `datum`, `active`, `taxID`, `pseudosales`, `topseller`, `keywords`, `changetime`, `pricegroupID`, `pricegroupActive`, `filtergroupID`, `laststock`, `crossbundlelook`, `notification`, `template`, `mode`, `main_detail_id`, `available_from`, `available_to`, `configurator_set_id`) VALUES + (500, 2, 'B2B Product 1', 'B2B Product 1', 'B2B Product 1', NULL, '2012-08-15', 1, 1, 20, 0, 'b2b', '2012-08-30 16:57:00', 1, 0, 1, 0, 0, 0, '', 0, 1600, NULL, NULL, NULL), + (501, 2, 'B2B Product 2', 'B2B Product 2', 'B2B Product 2', NULL, '2012-08-15', 1, 1, 30, 0, 'b2b', '2012-08-20 15:16:45', NULL, 0, 1, 0, 0, 0, '', 0, 1601, NULL, NULL, NULL); + +INSERT INTO `s_articles_details` (`id`, `articleID`, `ordernumber`, `suppliernumber`, `kind`, `additionaltext`, `sales`, `active`, `instock`, `stockmin`, `weight`, `position`, `width`, `height`, `length`, `ean`, `unitID`, `purchasesteps`, `maxpurchase`, `minpurchase`, `purchaseunit`, `referenceunit`, `packunit`, `releasedate`, `shippingfree`, `shippingtime`, `purchaseprice`) VALUES + (1600, 500, 'B2B01', '', 1, '', 0, 1, 25, 0, 0.000, 0, NULL, NULL, NULL, NULL, 1, NULL, NULL, 1, 0.7000, 1.000, 'Flasche(n)', '2012-06-13', 0, '', 1.0), + (1601, 501, 'B2B02', '', 1, '', 0, 1, 5, 0, 0.000, 0, NULL, NULL, NULL, NULL, 1, NULL, NULL, 1, 0.7000, 1.000, 'Flasche(n)', '2012-05-07', 0, '10', 1.0); + +INSERT INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES + (500, 500), + (501, 500); + +INSERT INTO `s_articles_categories_ro` (`articleID`, `categoryID`, `parentCategoryID`) VALUES + (500, 500, 3), + (500, 3, 14), + (500, 14, 14), + (501, 500, 3), + (501, 3, 14), + (501, 14, 14); + +INSERT INTO `s_articles_attributes` (`articleID`, `articledetailsID`) VALUES + (500, 1600), + (501, 1601); + +INSERT INTO `s_articles_prices` (`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `pseudoprice`, `baseprice`, `percent`) VALUES + ('EK', 1, 'beliebig', 500, 1600, 15.957983193277, 0, 0, 0.00), + ('EK', 1, 'beliebig', 501, 1601, 15.957983193277, 0, 0, 0.00); + +DELETE FROM s_core_sessions; + +INSERT INTO `b2b_role` (`id`, `name`, `context_owner_id`) VALUES + (11, 'Einkauf', 1), + (22, 'Einkauf', 1), + (33, 'Verkauf', 1), + (44, 'Core', 1), + (55, 'Enterprise', 1), + (66, 'Qa', 1), + (77, 'Einkauf', 1), + (88, 'Verkauf', 1), + (99, 'Core', 1), + (111, 'Enterprise', 1), + (122, 'Aa', 1), + (133, 'Za', 1), + (144, 'Debtor2role', 40); + +INSERT INTO b2b_acl_role_route_privilege (`entity_id`, `referenced_entity_id`, `grantable`) + (SELECT 33 AS entity_id, id AS referenced_entity_id, 1 AS grantable FROM b2b_acl_route_privilege); + +INSERT INTO b2b_acl_contact_route_privilege (`entity_id`, `referenced_entity_id`, `grantable`) + (SELECT 33 AS entity_id, id AS referenced_entity_id, 1 AS grantable FROM b2b_acl_route_privilege); + +INSERT INTO `b2b_role_contact` (`id`, `role_id`, `debtor_contact_id`) VALUES +(11, 11, 11), +(33, 33, 33); + +INSERT INTO `b2b_acl_route_privilege` (`id`, `resource_name`, `privilege_type`) VALUES + (5, 'resource', 'privilege'); + +INSERT INTO `b2b_acl_route` (`id`, `controller`, `action`, `privilege_id`) VALUES + (1, 'unit', 'test', 5); + +INSERT INTO b2b_contingent_group (id, context_owner_id, name, description) VALUES + (3, 1, 'IT', 'IT 250'), + (4, 1, 'Vertrieb', 'Vertrieb 250'), + (5, 1, 'Einkauf', 'Einkauf 250'), + (6, 40, 'Vertrieb', 'Vertrieb 251'), + (7, 40, 'Einkauf', 'Einkauf 251'), + (8, 1, 'Technik', 'Technik'), + (9, 1, 'Einkauf', 'Einkauf'), + (10, 1, 'Enterprise', 'Enterprise'), + (11, 1, 'Marketing', 'Marketing'), + (12, 1, 'Technical E', 'Technical E'), + (13, 1, 'Core Development', 'Core Development'), + (14, 1, 'Performance Marketing', 'Performance Marketing'), + (15, 1, 'Financial Department', 'Performance Marketing'), + (16, 1, 'Denter', 'Dentist'), + (100, 1, 'Qa', 'Qa'); + +INSERT INTO b2b_role_contact (id, role_id, debtor_contact_id) VALUES + (101, 11, 33); + +INSERT INTO b2b_acl_role_contingent_group (id, entity_id, referenced_entity_id, grantable) VALUES + (9, 11, 3, 1), + (11, 33, 3, 1); + +INSERT INTO b2b_role_contingent_group (id, role_id, contingent_group_id) VALUES + (11, 11, 10); + +INSERT INTO b2b_acl_contact_contingent_group (id, entity_id, referenced_entity_id, grantable) VALUES + (30, 33, 4, 1), + (31, 33, 5, 0), + (39, 33, 6, 0), + (38, 33, 7, 0), + (37, 33, 8, 0), + (36, 33, 9, 0), + (35, 33, 10, 0), + (34, 33, 11, 0), + (33, 33, 12, 0), + (32, 33, 13, 0); + +INSERT INTO b2b_contingent_group_rule (id, contingent_group_id, type) VALUES + (16, 10, 'OrderQuantity'), + (17, 10, 'OrderItemQuantity'), + (18, 10, 'OrderAmount'), + (19, 11, 'OrderAmount'), + (20, 11, 'OrderQuantity'), + (21, 13, 'OrderAmount'), + (22, 13, 'OrderQuantity'), + (23, 10, 'Category'), + (24, 10, 'ProductPrice'), + (25, 10, 'ProductOrderNumber') +; + +INSERT INTO b2b_contingent_group_rule_time_restriction (contingent_rule_id, time_restriction, value) VALUES + (16, 'DAYOFYEAR', 3.00), + (17, 'WEEKOFYEAR', 13.00), + (18, 'MONTH', 1337.00), + (19, 'QUARTER', 10.00), + (20, 'YEAR', 20.00), + (21, 'QUARTER', 10.00), + (22, 'YEAR', 20.00); + +INSERT INTO b2b_contingent_group_rule_category (contingent_rule_id, category_id) VALUES + (23, 500); + +INSERT INTO b2b_contingent_group_rule_product_price (contingent_rule_id, product_price) VALUES + (24, 123); + +INSERT INTO b2b_contingent_group_rule_product_order_number (contingent_rule_id, product_order_number) VALUES + (25, 'SW123'); + +INSERT INTO `b2b_contact_contingent_group` (`id`, `contact_id`, `contingent_group_id`) VALUES + (10, 11, 13); + +INSERT INTO s_order (id, ordernumber, userID, invoice_amount, invoice_amount_net, invoice_shipping, invoice_shipping_net, ordertime, status, cleared, paymentID, transactionID, comment, customercomment, internalcomment, net, taxfree, partnerID, temporaryID, referer, cleareddate, trackingcode, language, dispatchID, currency, currencyFactor, subshopID, remote_addr, deviceType) VALUES + (1, '20003', 251, 3122.99, 2624.36, 3.9, 3.28, '2016-11-05 08:39:23', -2, 17, 5, '', '', '', '', 0, 0, '', '', '', null, '', '1', 9, 'EUR', 1, 1, '10.100.200.1', 'desktop'), + (2, '20004', 251, 3122.99, 2624.36, 3.9, 3.28, '2016-11-09 08:39:23', -2, 17, 5, '', '', '', '', 0, 0, '', '', '', null, '', '1', 9, 'EUR', 1, 1, '10.100.200.1', 'desktop'), + (3, '20005', 254, 321.89, 270.5, 3.9, 3.28, '2016-11-11 14:38:01', -2, 17, 5, '', '', '', '', 0, 0, '', '', '', null, '', '1', 9, 'EUR', 1, 1, '10.100.200.1', 'desktop'), + (4, '20006', 254, 321.89, 270.5, 3.9, 3.28, '2016-11-11 14:38:01', -2, 17, 5, '', '', '', '', 0, 0, '', '', '', null, '', '1', 9, 'EUR', 1, 1, '10.100.200.1', 'desktop'); + +INSERT INTO s_order_attributes (orderID, attribute1, attribute2, attribute3, attribute4, attribute5, attribute6, b2b_auth_id) VALUES + (1, null, null, null, null, null, null, 4), + (2, null, null, null, null, null, null, 4), + (3, null, null, null, null, null, null, 3), + (4, null, null, null, null, null, null, 1); + +INSERT INTO `b2b_line_item_list` (`id`, `context_owner_id`, `amount_net`, `amount`) VALUES + (4, 1, '2624.36', '3122.99'), + (5, 1, '2624.36', '3122.99'), + (6, 1, '270.5', '321.89'), + (7, 1, '270.5', '321.89'); + +INSERT INTO `b2b_line_item_reference` (`id`, `reference_number`, `quantity`, `comment`, `list_id`, `amount_net`, `amount`) +VALUES + (4, 'SW10170', 8, '', 4, '33.571428571429', '39,95'), + (5, 'SW10165', 5, '', 4, '16.798319327731', '19,99'), + (6, 'SHIPPINGDISCOUNT', 1, '', 4, '-1.68', '-2,00'), + (7, 'SW10170', 8, '', 5, '33.571428571429', '39,95'), + (8, 'SW10165', 5, '', 5, '16.798319327731', '19,99'), + (9, 'SW10170', 8, '', 6, '33.571428571429', '39,95'), + (10, 'SW10165', 5, '', 6, '16.798319327731', '19,99'), + (11, 'SW10170', 8, '', 7, '33.571428571429', '39,95'), + (12, 'SW10165', 5, '', 7, '16.798319327731', '19,99'); + +INSERT INTO `b2b_order_context` (`id`, `ordernumber`, `list_id`, `created_at`, `shipping_address_id`, `billing_address_id`, `payment_id`, `shipping_id`, `comment`, `device_type`, `s_order_id`, `status_id`, `auth_id`, `order_reference`, `currency_factor`, `requested_delivery_date`, `cleared_at`) VALUES + (1, '20003', 4, '2016-11-05 13:16:40', 35, 51, 5, 9, 'THE_TEST_COMMENT', 'desktop', 1, -2, 4, 'Reference', 1, 'every monday', NULL), + (2, '20004', 5, '2016-11-19 13:56:13', 35, 51, 5, 9, '', 'desktop', 2, -2, 4, '', 1, 'every monday', '2017-01-05 13:16:40'), + (3, '20005', 6, '2017-01-05 13:57:02', 35, 51, 5, 9, '', 'desktop', 3, -2, 4, '', 1, 'every monday', '2017-01-05 13:16:40'), + (4, '20006', 7, '2017-01-05 13:57:02', 35, 51, 5, 9, '', 'desktop', 4, -2, 1, '', 1, 'every monday', '2017-01-05 13:16:40'); + + +INSERT INTO b2b_audit_log_author (hash, salutation, title, firstname, lastname, email, is_api) +VALUES ('a804b81a0c49fce89e95e1110985ed8f', 'mr', NULL, 'Tom', 'Contact1', 'contact1@example.com', 1), + ('c2b6a5abf5355c920150c7c12d46af03', 'mr', NULL, 'Tom', 'Contact2', 'contact2@example.com', 0); + +INSERT INTO `b2b_audit_log` (`id`, `log_value`, `log_type`, `event_date`, `author_hash`) +VALUES + (5, 'O:63:\"Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueOrderCommentEntity\":2:{s:8:\"oldValue\";s:0:\"\";s:8:\"newValue\";s:9:\"Kommentar\";}', + 'Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueOrderCommentEntity', '2017-03-06 11:16:11', 'a804b81a0c49fce89e95e1110985ed8f'), + (6, 'O:67:\"Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemQuantityEntity\":4:{s:11:\"orderNumber\";s:7:\"SW10072\";s:11:\"productName\";s:16:\"Schlüsselkasten\";s:8:\"oldValue\";i:78;s:8:\"newValue\";i:75;}', + 'Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemQuantityEntity', '2017-03-06 11:18:25', 'a804b81a0c49fce89e95e1110985ed8f'), + (7, 'O:66:\"Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemCommentEntity\":4:{s:11:\"orderNumber\";s:7:\"SW10072\";s:11:\"productName\";s:16:\"Schlüsselkasten\";s:8:\"oldValue\";s:0:\"\";s:8:\"newValue\";s:12:\"item comment\";}', + 'Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemCommentEntity', '2017-03-06 11:18:31', 'a804b81a0c49fce89e95e1110985ed8f'), + (8, 'O:65:\"Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemRemoveEntity\":4:{s:11:\"orderNumber\";s:16:\"SHIPPINGDISCOUNT\";s:11:\"productName\";s:9:\"NOT FOUND\";s:8:\"oldValue\";i:15;s:8:\"newValue\";N;}', + 'Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemRemoveEntity', '2017-03-06 11:20:40', 'a804b81a0c49fce89e95e1110985ed8f'), + (9, 'O:55:\"Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueDiffEntity\":2:{s:8:\"oldValue\";s:2:\"-2\";s:8:\"newValue\";s:1:\"0\";}', + 'Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueDiffEntity', '2017-03-06 11:22:32', 'a804b81a0c49fce89e95e1110985ed8f'), + (10, 'O:66:\"Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemCommentEntity\":4:{s:11:\"orderNumber\";s:7:\"SW10067\";s:11:\"productName\";s:19:\"Kommode Shabby Chic\";s:8:\"oldValue\";s:0:\"\";s:8:\"newValue\";s:12:\"item comment\";}', + 'Shopware\\B2B\\AuditLog\\Framework\\AuditLogValueLineItemCommentEntity', '2017-03-06 15:07:24', 'a804b81a0c49fce89e95e1110985ed8f'); + +INSERT INTO `b2b_audit_log_index` (`audit_log_id`, `reference_table`, `reference_id`) +VALUES + (5, 'b2b_order_context', 1), + (6, 'b2b_order_context', 1), + (7, 'b2b_order_context', 1), + (8, 'b2b_order_context', 1), + (9, 'b2b_order_context', 1), + (10, 'b2b_order_context', 1); + +INSERT INTO `b2b_line_item_list` (`id`, `context_owner_id`, `amount_net`, `amount`) VALUES + (400, 1, '354.16', '421.45'), + (500, 1, '43.6', '51.88'); + +INSERT INTO `b2b_line_item_reference` (`id`, `reference_number`, `quantity`, `comment`, `list_id`, `amount_net`, `amount`) VALUES + (400, 'SW10170', 8, '', 400, '33.571428571429', '39,95'), + (500, 'SW10165', 5, '', 400, '16.798319327731', '19,99'), + (700, 'SW10088', 2, '', 500, '21', '24,99') +; + +INSERT INTO `b2b_order_list` (`id`, `name`, `list_id`, `context_owner_id`) VALUES + (1, 'List 1' , 400, 1) + ,(2, 'List 2', 500, 1) +; + +INSERT INTO `s_order_basket` (`id`, `sessionID`, `userID`, `articlename`, `articleID`, `ordernumber`, `shippingfree`, `quantity`, `price`, `netprice`, `tax_rate`, `datum`, `modus`, `esdarticle`, `partnerID`, `lastviewport`, `useragent`, `config`, `currencyFactor`) VALUES + (673, '91ee4151ebab33707b83b96b80b38d91', 250, 'Warenkorbrabatt', 0, 'SHIPPINGDISCOUNT', 0, 1, -2, -1.68, 19, '2017-03-28 10:53:31', 4, 0, '', 'b2borderlist', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', '', 1), + (670, '91ee4151ebab33707b83b96b80b38d91', 250, 'Schüssel Style Blumen', 500, 'SW10170', 0, 2, 24.99, 21, 19, '2017-03-28 10:53:27', 0, 0, '', 'b2borderlist', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', '', 1); + +INSERT INTO `b2b_acl_contact_order_list` (entity_id, referenced_entity_id, grantable) VALUES (11, 2, 0); + +INSERT INTO `b2b_acl_role_order_list` (entity_id, referenced_entity_id, grantable) VALUES (66, 2, 0); + +INSERT INTO `b2b_budget` (`id`, `identifier`, `name`, `context_owner_id`, `owner_id`, `amount`, `refresh_type`, `notify_author`, `notify_author_percentage`) VALUES + (1, 'id-1', 'NoRenewal', 1, 2, 500, 'none', 0, 0) + ,(2, 'id-2', 'Monthly', 1, 2, 600, 'monthly', 0, 0) + ,(3, 'id-3', 'Yearly', 1, 2, 400, 'yearly', 0, 0) + ,(4, 'Invisible1','Debtor only', 1, 2, 40000000, 'yearly', 0, 0) + ,(5, 'id-5','Mailing', 1, 2, 100, 'monthly', 1, 50) +; + +INSERT INTO `b2b_budget_transaction` (`id`, `budget_id`, `auth_id`, `refresh_group`, `amount`) VALUES + (1, 1, 2, 0, 450) + ,(2, 2, 2, 201611, 450) + ,(3, 2, 2, 201611, 10) + ,(4, 2, 2, 201612, 600) + ,(5, 2, 2, 201750, 450) + ,(6, 5, 2, DATE_FORMAT(NOW(), '%Y%m'), 90) +; + +INSERT INTO `b2b_acl_role_budget` (`id`, `entity_id`, `referenced_entity_id`, `grantable`) VALUES + (1, 11, 1, 1) + ,(2, 11, 2, 0) + ,(3, 11, 3, 1) +; + +INSERT INTO `b2b_acl_contact_budget` (entity_id, referenced_entity_id, grantable) VALUES (11, 2, 0); + +INSERT INTO `b2b_acl_role_budget` (entity_id, referenced_entity_id, grantable) VALUES (66, 2, 0); + +REPLACE INTO `s_core_config_mails` (`name`, `frommail`, `fromname`, `subject`, `content`, `contentHTML`, `ishtml`, `attachment`, `mailtype`, `context`, `dirty`) VALUES + ('b2bBudgetNotify', 'test@example.com', '{config name=shopName}', 'Notify Budget Mail Subject', 'Notify Budget Mail Content', '', '0', '', '2', 'N;', '0') +; \ No newline at end of file diff --git a/exampleplugins/b2b/B2bLogin/phpunit.xml.dist b/exampleplugins/b2b/B2bLogin/phpunit.xml.dist new file mode 100644 index 0000000000..19e9f6e4e0 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bLogin/plugin.png b/exampleplugins/b2b/B2bLogin/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bLogin/plugin.png differ diff --git a/exampleplugins/b2b/B2bLogin/plugin.xml b/exampleplugins/b2b/B2bLogin/plugin.xml new file mode 100644 index 0000000000..e713f6c2a5 --- /dev/null +++ b/exampleplugins/b2b/B2bLogin/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bPrice/B2bPrice.php b/exampleplugins/b2b/B2bPrice/B2bPrice.php new file mode 100755 index 0000000000..4039362297 --- /dev/null +++ b/exampleplugins/b2b/B2bPrice/B2bPrice.php @@ -0,0 +1,9 @@ +price = random_int(1, 1337); + + return $price; + } + + /** + * {@inheritdoc} + */ + public function fetchPricesByDebtorIdAndOrderNumber(int $debtorId, array $orderNumbers): array + { + $prices = []; + foreach ($orderNumbers as $orderNumber) { + $priceEntity = new PriceEntity(); + $priceEntity->price = random_int(1, 1337); + $priceEntity->orderNumber = $orderNumber; + $prices[] = $priceEntity; + } + + return $prices; + } + + /** + * {@inheritdoc} + */ + public function addPrice(PriceEntity $priceEntity): PriceEntity + { + throw new NotImplementedException(); + } + + /** + * {@inheritdoc} + */ + public function removePrice(PriceEntity $priceEntity): PriceEntity + { + throw new NotImplementedException(); + } + + /** + * {@inheritdoc} + */ + public function updatePrice(PriceEntity $priceEntity): PriceEntity + { + throw new NotImplementedException(); + } + + /** + * {@inheritdoc} + */ + public function fetchPricesByDebtorId(int $id, PriceSearchStruct $searchStruct): array + { + throw new NotImplementedException(); + } + + /** + * @param int $id + * @return PriceEntity + */ + public function fetchOneById(int $id): PriceEntity + { + throw new NotImplementedException(); + } + + /** + * @param int $id + * @param PriceSearchStruct $searchStruct + * @return int + */ + public function fetchTotalCount(int $id, PriceSearchStruct $searchStruct): int + { + throw new NotImplementedException(); + } + + /** + * @param PriceEntity $priceEntity + * @return bool + */ + public function checkForUniquePriceToRange(PriceEntity $priceEntity): bool + { + throw new NotImplementedException(); + } + + /** + * @param PriceEntity $priceEntity + * @return bool + */ + public function checkForUniquePriceFromRange(PriceEntity $priceEntity): bool + { + throw new NotImplementedException(); + } +} diff --git a/exampleplugins/b2b/B2bPrice/Resources/services.xml b/exampleplugins/b2b/B2bPrice/Resources/services.xml new file mode 100644 index 0000000000..da552191d2 --- /dev/null +++ b/exampleplugins/b2b/B2bPrice/Resources/services.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/exampleplugins/b2b/B2bPrice/Tests/Bootstrap.php b/exampleplugins/b2b/B2bPrice/Tests/Bootstrap.php new file mode 100644 index 0000000000..2fee5dc7f3 --- /dev/null +++ b/exampleplugins/b2b/B2bPrice/Tests/Bootstrap.php @@ -0,0 +1,6 @@ +fetchPricesByDebtorIdAndOrderNumber(250, ['SW10001', 'SW10002']) + ); + } +} diff --git a/exampleplugins/b2b/B2bPrice/phpunit.xml.dist b/exampleplugins/b2b/B2bPrice/phpunit.xml.dist new file mode 100644 index 0000000000..43c5e18b5a --- /dev/null +++ b/exampleplugins/b2b/B2bPrice/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + ./Tests + + diff --git a/exampleplugins/b2b/B2bPrice/plugin.png b/exampleplugins/b2b/B2bPrice/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bPrice/plugin.png differ diff --git a/exampleplugins/b2b/B2bPrice/plugin.xml b/exampleplugins/b2b/B2bPrice/plugin.xml new file mode 100644 index 0000000000..c5ca9753f7 --- /dev/null +++ b/exampleplugins/b2b/B2bPrice/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bRestApi/B2bRestApi.php b/exampleplugins/b2b/B2bRestApi/B2bRestApi.php new file mode 100755 index 0000000000..4caff34261 --- /dev/null +++ b/exampleplugins/b2b/B2bRestApi/B2bRestApi.php @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/exampleplugins/b2b/B2bRestApi/RestApi/Controller/RestApiController.php b/exampleplugins/b2b/B2bRestApi/RestApi/Controller/RestApiController.php new file mode 100644 index 0000000000..b3b051bb27 --- /dev/null +++ b/exampleplugins/b2b/B2bRestApi/RestApi/Controller/RestApiController.php @@ -0,0 +1,13 @@ + 'hello ' . $name]; + } +} diff --git a/exampleplugins/b2b/B2bRestApi/RestApi/Routing/RestApiRouteProvider.php b/exampleplugins/b2b/B2bRestApi/RestApi/Routing/RestApiRouteProvider.php new file mode 100644 index 0000000000..cf524a69cd --- /dev/null +++ b/exampleplugins/b2b/B2bRestApi/RestApi/Routing/RestApiRouteProvider.php @@ -0,0 +1,24 @@ +query('GET', '/rest/api/enterprise'); + + self::assertArrayHasKey('message', $result); + self::assertEquals('hello enterprise', $result['message']); + } +} diff --git a/exampleplugins/b2b/B2bRestApi/Tests/Unit/RestApiRouteProviderTest.php b/exampleplugins/b2b/B2bRestApi/Tests/Unit/RestApiRouteProviderTest.php new file mode 100644 index 0000000000..aea2fc3bc6 --- /dev/null +++ b/exampleplugins/b2b/B2bRestApi/Tests/Unit/RestApiRouteProviderTest.php @@ -0,0 +1,14 @@ +getRoutes()); + } +} diff --git a/exampleplugins/b2b/B2bRestApi/phpunit.xml.dist b/exampleplugins/b2b/B2bRestApi/phpunit.xml.dist new file mode 100644 index 0000000000..dfa0db6b84 --- /dev/null +++ b/exampleplugins/b2b/B2bRestApi/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + ./Tests + + + + + RestApi/ + + ./Tests + + + + diff --git a/exampleplugins/b2b/B2bRestApi/plugin.png b/exampleplugins/b2b/B2bRestApi/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bRestApi/plugin.png differ diff --git a/exampleplugins/b2b/B2bRestApi/plugin.xml b/exampleplugins/b2b/B2bRestApi/plugin.xml new file mode 100644 index 0000000000..49a1b482f7 --- /dev/null +++ b/exampleplugins/b2b/B2bRestApi/plugin.xml @@ -0,0 +1,17 @@ + + + + + + + 1.0.0 + shopware AG + https://en.shopware.com + + + + Veröffentlichung + Release + + diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/B2bSalesRepresentativePlugin.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/B2bSalesRepresentativePlugin.php new file mode 100644 index 0000000000..8883564dae --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/B2bSalesRepresentativePlugin.php @@ -0,0 +1,22 @@ +setParameter($this->getContainerPrefix() . '.plugin_dir', $this->getPath()); + $container->setParameter($this->getContainerPrefix() . '.plugin_name', $this->getName()); + $this->loadFiles($container); + } +} diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/Contact/ExtendContactAuthenticationIdentityLoader.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/Contact/ExtendContactAuthenticationIdentityLoader.php new file mode 100644 index 0000000000..a968b33994 --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/Contact/ExtendContactAuthenticationIdentityLoader.php @@ -0,0 +1,41 @@ +leftJoin( + StoreFrontAuthenticationRepository::TABLE_ALIAS, + '(SELECT + contact.id, + contact.firstname, + contact.lastname, + contact.salutation, + contact.email, + contact.active, + address.phone, + address.company, + address.city, + address.zipcode, + s_user.customernumber + FROM b2b_debtor_contact as contact + INNER JOIN b2b_store_front_auth auth + ON contact.context_owner_id = auth.id + INNER JOIN s_user + ON auth.provider_context = s_user.id + INNER JOIN s_user_addresses address + ON s_user.default_billing_address_id = address.id)', + 'contact', + StoreFrontAuthenticationRepository::TABLE_ALIAS . '.provider_context = contact.id AND ' + . StoreFrontAuthenticationRepository::TABLE_ALIAS . '.provider_key = :contactProviderKey' + ) + ->setParameter('contactProviderKey', ContactRepository::class); + } +} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/Debtor/ExtendDebtorAuthenticationIdentityLoader.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/Debtor/ExtendDebtorAuthenticationIdentityLoader.php new file mode 100644 index 0000000000..ad390ce109 --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/Debtor/ExtendDebtorAuthenticationIdentityLoader.php @@ -0,0 +1,41 @@ +leftJoin( + StoreFrontAuthenticationRepository::TABLE_ALIAS, + '(SELECT + s_user.id, + s_user.firstname, + s_user.lastname, + s_user.salutation, + s_user.email, + s_user.active, + s_user.customernumber, + address.`phone`, + address.company, + address.zipcode, + address.city + FROM s_user + INNER JOIN s_user_addresses address + ON s_user.default_billing_address_id = address.id)', + 'debtor', + StoreFrontAuthenticationRepository::TABLE_ALIAS . '.provider_context = debtor.id AND ' + . StoreFrontAuthenticationRepository::TABLE_ALIAS . '.provider_key = :debtorProviderKey' + ) + ->setParameter('debtorProviderKey', DebtorRepository::class); + } +} diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendClientIdentityChainLoader.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendClientIdentityChainLoader.php new file mode 100644 index 0000000000..a85736667a --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendClientIdentityChainLoader.php @@ -0,0 +1,27 @@ +fieldNames as $fieldName) { + if (!array_key_exists($fieldName, $additionalSelect)) { + $additionalSelect[$fieldName] = ''; + } + $additionalSelect[$fieldName] .= 'IFNULL(' . $joinTable['joinAlias'] . '.' . $fieldName . ','; + } + $countIdentities += 1; + } + + return $this->formatAdditionalSelect($additionalSelect, $countIdentities); + } +} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeClientEntity.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeClientEntity.php new file mode 100644 index 0000000000..48d7b6df9b --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeClientEntity.php @@ -0,0 +1,89 @@ +getPostalSettings(); + + $this->authId = $identity->getAuthId(); + $this->firstName = $postal->firstName; + $this->lastName = $postal->lastName; + $this->email = $postal->email; + $this->active = $identity->getLoginCredentials()->active; + $this->phone = $address->phone; + $this->billingAddress = $identity->getMainBillingAddress(); + $this->zipcode = $address->zipcode; + $this->city = $address->city; + + if (empty($identity->getEntity()->customernumber)) { + $customernumber = $identity->getEntity()->debtor->customernumber; + } else { + $customernumber = $identity->getEntity()->customernumber; + } + + $this->customernumber = $customernumber; + $this->company = $address->company; + } +} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeClientRepository.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeClientRepository.php new file mode 100644 index 0000000000..add7f3c13d --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeClientRepository.php @@ -0,0 +1,131 @@ +connection = $connection; + $this->dbalHelper = $dbalHelper; + $this->authenticationIdentityLoader = $authenticationIdentityLoader; + $this->loginContextService = $loginContextService; + $this->authRepository = $authRepository; + $this->addressRepository = $addressRepository; + + parent::__construct($connection, $authenticationIdentityLoader, $dbalHelper, $loginContextService, $authRepository, $addressRepository); + } + + /** + * @return SalesRepresentativeClientEntity[] + */ + public function fetchClientsList(SalesRepresentativeSearchStruct $searchStruct, SalesRepresentativeEntity $salesRepresentativeEntity): array + { + $query = $this->connection->createQueryBuilder() + ->select('*') + ->from(self::TABLE_NAME, self::TABLE_ALIAS) + ->innerJoin( + self::TABLE_ALIAS, + StoreFrontAuthenticationRepository::TABLE_NAME, + StoreFrontAuthenticationRepository::TABLE_ALIAS, + StoreFrontAuthenticationRepository::TABLE_ALIAS . '.id = ' . self::TABLE_ALIAS . '.client_id' + ) + ->where(self::TABLE_ALIAS . '.sales_representative_id = :id') + ->setParameter('id', $salesRepresentativeEntity->id); + + if (!$searchStruct->orderBy) { + $searchStruct->orderBy = self::TABLE_ALIAS . '.client_id'; + $searchStruct->orderDirection = 'DESC'; + } + + $this->authenticationIdentityLoader->addSubSelect($query); + + $this->dbalHelper->applySearchStruct($searchStruct, $query); + + $clientIds = $query->execute()->fetchAll(\PDO::FETCH_COLUMN, 2); + + return $this->fetchClientsByAuthIds($clientIds); + } + + /** + * @internal + * @param int[] $clientIds + * @return SalesRepresentativeClientEntity[] + */ + protected function fetchClientsByAuthIds(array $clientIds): array + { + $clients = []; + foreach ($clientIds as $authId) { + try { + $auth = $this->authRepository->fetchAuthenticationById((int) $authId); + + $identity = $this->authenticationIdentityLoader + ->fetchIdentityByAuthentication($auth, $this->loginContextService); + + $client = new ExtendSalesRepresentativeClientEntity( + $identity, + $this->addressRepository->fetchOneById($identity->getMainShippingAddress()->id, $identity) + ); + $clients[] = $client; + } catch (NotFoundException $e) { + continue; + } + } + + return $clients; + } +} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeRepository.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeRepository.php new file mode 100644 index 0000000000..e480dc217c --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Components/SalesRepresentative/ExtendSalesRepresentativeRepository.php @@ -0,0 +1,36 @@ + [ + 'contact' => [ + 'phone', + 'email', + 'firstname', + 'lastname', + 'customernumber', + 'zipcode', + 'city', + 'company', + ], + 'debtor' => [ + 'phone', + 'email', + 'firstname', + 'lastname', + 'customernumber', + 'zipcode', + 'city', + 'company', + ], + ], + ]; + } +} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/services.xml b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/services.xml new file mode 100644 index 0000000000..dc5a8720bc --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/services.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/snippets/frontend/plugins/salesrepresentative.ini b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/snippets/frontend/plugins/salesrepresentative.ini new file mode 100644 index 0000000000..6750e2c3ed --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/snippets/frontend/plugins/salesrepresentative.ini @@ -0,0 +1,41 @@ +[en_GB] +FirstName = "Firstname" +FirstNameAsc = "Firstname ascending" +FirstNameDesc = "Firstname descending" +LastName = "Lastname" +LastNameAsc = "Lastname ascending" +LastNameDesc = "Lastname descending" +Email = "E-Mail" +EmailAsc = "E-Mail ascending" +EmailDesc = "E-Mail descending" +State = "State" +StateAsc = "Status aufsteigend" +StateDesc = "Status absteigend" +Company = "Company" +CompanyAsc = "Company ascending" +CompanyDesc = "Company descending" +CustomernumberAsc = "Customernumber ascending" +CustomernumberDesc = "Customernumber descending" +Customernumber = "Customernumber" +Actions = "Actions" + +[de_DE] +FirstName = "Vorname" +FirstNameAsc = "Vorname aufsteigend" +FirstNameDesc = "Vorname absteigend" +LastName = "Nachname" +LastNameAsc = "Nachname aufsteigend" +LastNameDesc = "Nachname absteigend" +Email = "E-Mail" +EmailAsc = "E-Mail aufsteigend" +EmailDesc = "E-Mail absteigend" +State = "Status" +StateAsc = "State ascending" +StateDesc = "State descending" +Company = "Firma" +CompanyAsc = "Firma aufsteigend" +CompanyDesc = "Firma absteigend" +CustomernumberAsc = "Kundennummer aufsteigend" +CustomernumberDesc = "Kundennummer absteigend" +Customernumber = "Kundennummer" +Actions = "Aktionen" \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/views/frontend/_grid/salesrepresentative-grid.tpl b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/views/frontend/_grid/salesrepresentative-grid.tpl new file mode 100644 index 0000000000..f2218ef4f8 --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Resources/views/frontend/_grid/salesrepresentative-grid.tpl @@ -0,0 +1,60 @@ +{namespace name=frontend/plugins/salesrepresentative} + +{extends file="parent:frontend/_grid/salesrepresentative-grid.tpl"} + +{block name="b2b_grid_col_sort"} + + + + + + + + + + + + +{/block} + +{block name="b2b_grid_table_head"} + + {s name="FirstName"}Firstname{/s} + {s name="SurName"}Surname{/s} + {s name="Email"}E-Mail{/s} + {s name="Phone"}Phone{/s} + {s name="State"}State{/s} + {s name="Company"}Company{/s} + {s name="Customernumber"}customernumber{/s} + {s name="Actions"}Actions{/s} + +{/block} + +{block name="b2b_grid_table_row"} + + {$row->firstName} + {$row->lastName} + {$row->email} + {$row->phone} + + {if $row->active} + + {else} + + {/if} + + {$row->company} + {$row->customernumber} + + {if $row->active} +
+ + + +
+ {/if} + + +{/block} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bSalesRepresentativePlugin/Subscriber/FrontendTemplateExtender.php b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Subscriber/FrontendTemplateExtender.php new file mode 100644 index 0000000000..f0e8218a45 --- /dev/null +++ b/exampleplugins/b2b/B2bSalesRepresentativePlugin/Subscriber/FrontendTemplateExtender.php @@ -0,0 +1,29 @@ + ['addViewDirectories', -1], + 'Enlight_Controller_Action_PostDispatchSecure_Widgets' => ['addViewDirectories', -1], + ]; + } + + public function addViewDirectories(\Enlight_Controller_ActionEventArgs $args) + { + $args->getSubject()->View()->addTemplateDir(__DIR__ . '/../Resources/views'); + $args->getSubject()->View()->addTemplateDir(__DIR__ . '/../Resources/extendedViews'); + } +} diff --git a/exampleplugins/b2b/B2bServiceExtension/B2bServiceExtension.php b/exampleplugins/b2b/B2bServiceExtension/B2bServiceExtension.php new file mode 100755 index 0000000000..44a72ed492 --- /dev/null +++ b/exampleplugins/b2b/B2bServiceExtension/B2bServiceExtension.php @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/exampleplugins/b2b/B2bServiceExtension/Tests/Integration/RoleRepositoryTest.php b/exampleplugins/b2b/B2bServiceExtension/Tests/Integration/RoleRepositoryTest.php new file mode 100644 index 0000000000..26571d5402 --- /dev/null +++ b/exampleplugins/b2b/B2bServiceExtension/Tests/Integration/RoleRepositoryTest.php @@ -0,0 +1,269 @@ +importFixturesFileOnce(__DIR__ . '/../test_fixtures.sql'); + + return self::getKernel()->getContainer() + ->get('b2b_role.repository'); + } + + public function test_removeRole_throws_if_record_is_new() + { + $role = static::createNewRoleEntity(); + $this->expectException(CanNotRemoveExistingRecordException::class); + $this->getRepository()->removeRole($role); + } + + public function test_removeRole_removes_a_role() + { + $role = static::createNewRoleEntity(); + $role->id = 11; + + $repository = $this->getRepository(); + + $connection = self::getKernel()->getContainer()->get('dbal_connection'); + + self::assertEquals(1, $connection->fetchColumn('SELECT COUNT(*) FROM b2b_role WHERE id=11;')); + + $role = $repository->removeRole($role); + + self::assertEquals(0, $connection->fetchColumn('SELECT COUNT(*) FROM b2b_role WHERE id=11;')); + $id = $role->id; + + self::assertNull($id); + } + + public function test_fetchTotalCount_returns_an_int_only() + { + $count = $this->getRepository()->fetchTotalCount(new RoleSearchStruct(), $this->createOwnershipContext()); + self::assertEquals(12, $count); + } + + public function test_fetchTotalCount_roles_filters() + { + $searchStruct = new RoleSearchStruct(); + $searchStruct->filters[] = new LikeFilter('role', 'name', 'Qa'); + + $count = $this->getRepository()->fetchTotalCount($searchStruct, $this->createOwnershipContext()); + self::assertEquals(1, $count); + } + + public function test_fetchList_returns_only_roles() + { + $role = $this->getRepository()->fetchList(new RoleSearchStruct(), $this->createOwnershipContext()); + + // check if default sorting is id desc + $id = $role[0]->id; + foreach ($role as $value) { + static::assertGreaterThanOrEqual((int) $value->id, (int) $id); + } + + self::assertCount(12, $role); + self::assertContainsOnlyInstancesOf(RoleEntity::class, $role); + } + + public function test_fetchList_roles_limit_and_offset() + { + $searchStruct = new RoleSearchStruct(); + $searchStruct->offset = 0; + $searchStruct->limit = 1; + $searchStruct->orderBy = 'name'; + $searchStruct->filters[] = new EqualsFilter('role', 'id', 11); + + $role = $this->getRepository() + ->fetchList($searchStruct, $this->createOwnershipContext()); + + self::assertCount(1, $role); + self::assertEquals(11, $role[0]->id); + } + + public function test_fetchList_roles_order_by_and_direction_asc() + { + $searchStruct = new RoleSearchStruct(); + $searchStruct->orderBy = 'name'; + $searchStruct->orderDirection = 'asc'; + + $role = $this->getRepository() + ->fetchList($searchStruct, $this->createOwnershipContext()); + + self::assertCount(12, $role); + } + + public function test_fetchList_roles_order_by_invalid() + { + $searchStruct = new RoleSearchStruct(); + $searchStruct->orderBy = 'lastname::foooooo'; + $searchStruct->orderDirection = 'desc'; + + $this->expectException(\Doctrine\DBAL\DBALException::class); + + $roles = $this->getRepository() + ->fetchList($searchStruct, $this->createOwnershipContext()); + } + + public function test_fetchList_returns_arrays_for_empty_resultsets() + { + $searchStruct = new RoleSearchStruct(); + $searchStruct->filters[] = new EqualsFilter('role', 'id', PHP_INT_MAX); + + $role = $this->getRepository() + ->fetchList($searchStruct, $this->createOwnershipContext()); + self::assertCount(0, $role); + } + + public function test_addRole_throws_if_the_role_already_has_an_id() + { + $role = $this->createNewRoleEntity(); + + $role->id = PHP_INT_MAX; + $this->expectException(CanNotInsertExistingRecordException::class); + $this->getRepository()->addRole($role); + } + + public function test_addRole_creates_a_valid_new_role() + { + $role = $this->createNewRoleEntity(); + $role->name = 'newRole'; + + $repository = $this->getRepository(); + + $connection = self::getKernel()->getContainer()->get('dbal_connection'); + + self::assertEquals( + 0, + (int) $connection->fetchColumn('SELECT COUNT(*) FROM b2b_role WHERE name = "newRole";') + ); + + $role = $repository->addRole($role); + + self::assertEquals( + 1, + (int) $connection->fetchColumn('SELECT COUNT(*) FROM b2b_role WHERE name = "newRole";') + ); + + $id = $role->id; + self::assertNotEmpty($id); + } + + public function test_fetchOneById_throws_NotFoundException_for_invalid_id() + { + $this->expectException(NotFoundException::class); + $this->getRepository()->fetchOneById((int) uniqid()); + } + + public function test_fetchOneById_for_valid_ids() + { + $user = $this->getRepository()->fetchOneById(11); + static::assertEquals('Einkauf', $user->name); + } + + public function test_updateRole_throws_for_a_new_role() + { + $role = $this->createNewRoleEntity(); + $this->expectException(CanNotUpdateExistingRecordException::class); + $this->getRepository()->updateRole($role); + } + + public function test_updateRole_update_role() + { + $role = $this->createExistingRoleEntity(); + + $role = $this->getRepository()->updateRole($role); + $id = $role->id; + self::assertEquals(11, $id); + } + + public function test_fetchList_returns_only_roles_with_assignment_id() + { + $search = new RoleSearchStruct(); + + $roles = $this->getRepository()->fetchAllRolesAndCheckForContactAssignment(11, $search, $this->createOwnershipContext()); + + self::assertCount(12, $roles); + + self::assertEquals(11, $roles[11]->assignmentId); + self::assertContainsOnlyInstancesOf(RoleAssignmentEntity::class, $roles); + + self::assertEquals(11, $roles[11]->toDatabaseArray()['assignmentId']); + } + + public function test_fetchList_has_no_selection() + { + $search = new RoleSearchStruct(); + + $roles = $this->getRepository()->fetchAllRolesAndCheckForContactAssignment((int) uniqid(), $search, $this->createOwnershipContext()); + + self::assertCount(12, $roles); + foreach ($roles as $role) { + self::assertEquals(null, $role->assignmentId); + } + self::assertContainsOnlyInstancesOf(RoleAssignmentEntity::class, $roles); + } + + public function test_fetchList_has_no_role_for_another_debtor() + { + $search = new RoleSearchStruct(); + + $roles = $this->getRepository()->fetchAllRolesAndCheckForContactAssignment(11, $search, $this->createOwnershipContext()); + + self::assertCount(12, $roles); + + self::assertArrayNotHasKey(144, $roles); + } + + /** + * @return RoleEntity + */ + public static function createExistingRoleEntity(): RoleEntity + { + $role = self::createNewRoleEntity(); + $role->id = 11; + + return $role; + } + + /** + * @return RoleEntity + */ + public static function createNewRoleEntity(): RoleEntity + { + $role = new RoleEntity(); + $role->name = 'rolle'; + $role->contextOwnerId = self::createContactIdentity()->getOwnershipContext()->contextOwnerId; + + return $role; + } + + /** + * @return OwnershipContext + */ + private function createOwnershipContext(): OwnershipContext + { + return self::createContactIdentity()->getOwnershipContext(); + } +} diff --git a/exampleplugins/b2b/B2bServiceExtension/Tests/test_fixtures.sql b/exampleplugins/b2b/B2bServiceExtension/Tests/test_fixtures.sql new file mode 100644 index 0000000000..e25acff0ff --- /dev/null +++ b/exampleplugins/b2b/B2bServiceExtension/Tests/test_fixtures.sql @@ -0,0 +1,116 @@ +REPLACE INTO `s_user_addresses` (`id`, `user_id`, `company`, `department`, `salutation`, `firstname`, `lastname`, `street`, `zipcode`, `city`, `phone`, `country_id`, `state_id`, `ustid`) VALUES + (28, 250, 'Debtor GMBH GmbH', NULL, 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (41, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (42, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (43, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (44, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (45, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (46, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (47, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (48, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (49, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (50, 250, 'Debtor GMBH GmbH', 'Ankauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (51, 250, 'Debtor GMBH GmbH', 'Einkauf', 'mr', 'Max', 'Mustermann', 'Musterstr. 55', '55555', 'Musterhausen', '05555 / 555555', 2, 3, NULL), + (35, 250, 'Debtor FQM', 'Einkauf', 'mr', 'Händler', 'Kundengruppe-Netto', 'Musterweg 1', '55555', 'Musterstadt', '012345 / 6789', 2, 3, NULL); + +INSERT INTO `s_user_addresses_attributes` (`address_id`, `b2b_type`) VALUES + (28, 'billing'), + (41, 'billing'), + (42, 'billing'), + (43, 'billing'), + (44, 'billing'), + (45, 'billing'), + (46, 'billing'), + (47, 'billing'), + (48, 'billing'), + (49, 'billing'), + (50, 'billing'), + (51, 'billing'), + (35, 'shipping'); + +INSERT INTO `b2b_acl_contact_address` (`entity_id`, `referenced_entity_id`, `grantable`) VALUES + (33, 28, 1), + (33, 41, 1), + (33, 42, 1), + (33, 43, 1), + (33, 44, 1), + (33, 45, 1), + (33, 46, 1), + (33, 47, 1), + (33, 48, 1), + (33, 49, 1), + (33, 50, 1), + (33, 51, 1), + (11, 28, 1), + (11, 41, 1), + (11, 42, 0), + (11, 43, 1), + (11, 44, 1), + (11, 45, 0), + (11, 46, 1), + (11, 47, 1), + (11, 48, 0), + (11, 49, 1), + (11, 50, 1), + (11, 51, 1), + (11, 35, 1); + + +INSERT INTO `s_categories` (`id`, `parent`, `path`, `description`, `position`, `left`, `right`, `level`, `added`, `changed`, `metakeywords`, `metadescription`, `cmsheadline`, `cmstext`, `template`, `active`, `blog`, `external`, `hidefilter`, `hidetop`, `mediaID`, `product_box_layout`, `meta_title`, `stream_id`) VALUES + (500, 3, '|3|', 'B2B', 0, 0, 0, 0, '2012-07-30 15:24:59', '2012-07-30 15:24:59', NULL, '', 'B2B', NULL, NULL, 1, 0, NULL, 0, 0, 0, NULL, NULL, NULL); + +INSERT INTO `s_articles` (`id`, `supplierID`, `name`, `description`, `description_long`, `shippingtime`, `datum`, `active`, `taxID`, `pseudosales`, `topseller`, `keywords`, `changetime`, `pricegroupID`, `pricegroupActive`, `filtergroupID`, `laststock`, `crossbundlelook`, `notification`, `template`, `mode`, `main_detail_id`, `available_from`, `available_to`, `configurator_set_id`) VALUES + (500, 2, 'B2B Product 1', 'B2B Product 1', 'B2B Product 1', NULL, '2012-08-15', 1, 1, 20, 0, 'b2b', '2012-08-30 16:57:00', 1, 0, 1, 0, 0, 0, '', 0, 1600, NULL, NULL, NULL), + (501, 2, 'B2B Product 2', 'B2B Product 2', 'B2B Product 2', NULL, '2012-08-15', 1, 1, 30, 0, 'b2b', '2012-08-20 15:16:45', NULL, 0, 1, 0, 0, 0, '', 0, 1601, NULL, NULL, NULL); + +INSERT INTO `s_articles_details` (`id`, `articleID`, `ordernumber`, `suppliernumber`, `kind`, `additionaltext`, `sales`, `active`, `instock`, `stockmin`, `weight`, `position`, `width`, `height`, `length`, `ean`, `unitID`, `purchasesteps`, `maxpurchase`, `minpurchase`, `purchaseunit`, `referenceunit`, `packunit`, `releasedate`, `shippingfree`, `shippingtime`, `purchaseprice`) VALUES + (1600, 500, 'B2B01', '', 1, '', 0, 1, 25, 0, 0.000, 0, NULL, NULL, NULL, NULL, 1, NULL, NULL, 1, 0.7000, 1.000, 'Flasche(n)', '2012-06-13', 0, '', 1.0), + (1601, 501, 'B2B02', '', 1, '', 0, 1, 5, 0, 0.000, 0, NULL, NULL, NULL, NULL, 1, NULL, NULL, 1, 0.7000, 1.000, 'Flasche(n)', '2012-05-07', 0, '10', 1.0); + +INSERT INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES + (500, 500), + (501, 500); + +INSERT INTO `s_articles_categories_ro` (`articleID`, `categoryID`, `parentCategoryID`) VALUES + (500, 500, 3), + (500, 3, 14), + (500, 14, 14), + (501, 500, 3), + (501, 3, 14), + (501, 14, 14); + +INSERT INTO `s_articles_attributes` (`articleID`, `articledetailsID`) VALUES + (500, 1600), + (501, 1601); + +INSERT INTO `s_articles_prices` (`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `pseudoprice`, `baseprice`, `percent`) VALUES + ('EK', 1, 'beliebig', 500, 1600, 15.957983193277, 0, 0, 0.00), + ('EK', 1, 'beliebig', 501, 1601, 15.957983193277, 0, 0, 0.00); + +INSERT INTO `b2b_acl_route_privilege` (`id`, `resource_name`, `privilege_type`) VALUES + (5, 'resource', 'privilege'); + +INSERT INTO `b2b_acl_route` (`id`, `controller`, `action`, `privilege_id`) VALUES + (1, 'unit', 'test', 5); + +DELETE FROM s_core_sessions; + +INSERT INTO `b2b_role` (`id`, `name`, `context_owner_id`) VALUES + (11, 'Einkauf', 1), + (22, 'Einkauf', 1), + (33, 'Verkauf', 1), + (44, 'Core', 1), + (55, 'Enterprise', 1), + (66, 'Qa', 1), + (77, 'Einkauf', 1), + (88, 'Verkauf', 1), + (99, 'Core', 1), + (111, 'Enterprise', 1), + (122, 'Aa', 1), + (133, 'Za', 1), + (144, 'Debtor2role', 40); + +INSERT INTO `b2b_role_contact` (`id`, `role_id`, `debtor_contact_id`) VALUES +(11, 11, 11), +(22, 22,22), +(33, 33,33); diff --git a/exampleplugins/b2b/B2bServiceExtension/YourRoleRepository.php b/exampleplugins/b2b/B2bServiceExtension/YourRoleRepository.php new file mode 100644 index 0000000000..0cb9080983 --- /dev/null +++ b/exampleplugins/b2b/B2bServiceExtension/YourRoleRepository.php @@ -0,0 +1,29 @@ +myService = array_pop($args); + + parent::__construct(... $args); + } + + /** + * @param RoleEntity $role + * @return RoleEntity + */ + public function updateRole(RoleEntity $role): RoleEntity + { + return parent::updateRole($role); + } +} diff --git a/exampleplugins/b2b/B2bTemplateExtension/composer.json b/exampleplugins/b2b/B2bTemplateExtension/composer.json new file mode 100644 index 0000000000..8ea9060b3e --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/composer.json @@ -0,0 +1,19 @@ +{ + "name": "swag/b2b-template-extension", + "description": "B2B suite template extension example", + "type": "shopware-platform-plugin", + "license": "MIT", + "version": "1.0.0", + "autoload": { + "psr-4": { + "B2bTemplateExtension\\": "src/" + } + }, + "extra": { + "shopware-plugin-class": "B2bTemplateExtension\\B2bTemplateExtension", + "label": { + "de-DE": "B2B-Suite Template-Erweiterungsbeispiel", + "en-GB": "B2B suite template extension example" + } + } +} diff --git a/exampleplugins/b2b/B2bTemplateExtension/src/Framework/Adapter/Twig/NamespaceHierarchy/TemplateNamespaceHierarchyBuilder.php b/exampleplugins/b2b/B2bTemplateExtension/src/Framework/Adapter/Twig/NamespaceHierarchy/TemplateNamespaceHierarchyBuilder.php new file mode 100644 index 0000000000..31d8fd5671 --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/src/Framework/Adapter/Twig/NamespaceHierarchy/TemplateNamespaceHierarchyBuilder.php @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bcontact/_form.html.twig b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bcontact/_form.html.twig new file mode 100644 index 0000000000..d9955df75c --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bcontact/_form.html.twig @@ -0,0 +1,6 @@ +{% sw_extends '@SwagB2bPlatform/storefront/frontend/b2bcontact/_form.html.twig' %} + +{% block b2b_contact_form_rows_salutation %} + {{ parent() }} +

ContactData TEST TEST

+{% endblock %} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bcontact/index.html.twig b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bcontact/index.html.twig new file mode 100644 index 0000000000..a8236f611b --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bcontact/index.html.twig @@ -0,0 +1,6 @@ +{% sw_extends '@SwagB2bPlatform/storefront/frontend/b2bcontact/index.html.twig' %} + +{% block b2b_contact %} +

TEST TEST

+ {{ parent() }} +{% endblock %} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bdashboard/index.html.twig b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bdashboard/index.html.twig new file mode 100644 index 0000000000..2efff76f6b --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bdashboard/index.html.twig @@ -0,0 +1,6 @@ +{% sw_extends '@SwagB2bPlatform/storefront/frontend/b2bdashboard/index.html.twig' %} + +{% block b2b_dashboard_blocks %} + {{ parent() }} +

TEST

+{% endblock %} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bstatistic/grid.html.twig b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bstatistic/grid.html.twig new file mode 100644 index 0000000000..5542b160f8 --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bstatistic/grid.html.twig @@ -0,0 +1,6 @@ +{% sw_extends '@SwagB2bPlatform/storefront/frontend/b2bstatistic/grid.html.twig' %} + +{% block b2b_grid_form %} + {{ parent() }} +

TEST GRID

+{% endblock %} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bstatistic/index.html.twig b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bstatistic/index.html.twig new file mode 100644 index 0000000000..bf466b1a30 --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/frontend/b2bstatistic/index.html.twig @@ -0,0 +1,6 @@ +{% sw_extends '@SwagB2bPlatform/storefront/frontend/b2bstatistic/index.html.twig' %} + +{% block b2b_statistic_canvas %} + {{ parent() }} +

TEST

+{% endblock %} \ No newline at end of file diff --git a/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/layout/header/logo.html.twig b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/layout/header/logo.html.twig new file mode 100644 index 0000000000..0733b06443 --- /dev/null +++ b/exampleplugins/b2b/B2bTemplateExtension/src/Resources/views/storefront/layout/header/logo.html.twig @@ -0,0 +1,7 @@ +{% sw_extends '@Storefront/storefront/layout/header/logo.html.twig' %} + +{% block layout_header_logo_inner %} + {{ parent() }} +

header

+{% endblock %} + diff --git a/exampleplugins/b2b/B2bThemeInheritance/composer.json b/exampleplugins/b2b/B2bThemeInheritance/composer.json new file mode 100644 index 0000000000..3ef056d463 --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/composer.json @@ -0,0 +1,24 @@ +{ + "name": "swag/swag-b2b-inheritance-sample-theme", + "description": "B2B Inheritance-Sample-Theme", + "type": "shopware-platform-plugin", + "version": "0.0.0-dev", + "license": "MIT", + "authors": [ + { + "name": "shopware AG" + } + ], + "autoload": { + "psr-4": { + "SwagB2bInheritanceSampleTheme\\": "src/" + } + }, + "extra": { + "shopware-plugin-class": "SwagB2bInheritanceSampleTheme\\SwagB2bInheritanceSampleTheme", + "label": { + "de-DE": "Beispiel B2B-Theme (ableitend)", + "en-GB": "Sample B2B-Theme (inheritance)" + } + } +} diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/dist/storefront/js/swag-b2b-inheritance-sample-theme.js b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/dist/storefront/js/swag-b2b-inheritance-sample-theme.js new file mode 100644 index 0000000000..9571e53c43 --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/dist/storefront/js/swag-b2b-inheritance-sample-theme.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([["swag-b2b-inheritance-sample-theme"],{eGzE:function(e,n){}},[["eGzE","runtime"]]]); \ No newline at end of file diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/assets/.gitkeep b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/main.js b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/main.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/scss/base.scss b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/scss/base.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/scss/overrides.scss b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/scss/overrides.scss new file mode 100644 index 0000000000..61b108c49f --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/app/storefront/src/scss/overrides.scss @@ -0,0 +1,8 @@ +/* +Override variable defaults +================================================== +This file is used to override default SCSS variables from the Shopware Storefront or Bootstrap. + +Because of the !default flags, theme variable overrides have to be declared beforehand. +https://getbootstrap.com/docs/4.0/getting-started/theming/#variable-defaults +*/ \ No newline at end of file diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/config/plugin.png b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/config/plugin.png new file mode 100644 index 0000000000..122712f8ac Binary files /dev/null and b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/config/plugin.png differ diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/theme.json b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/theme.json new file mode 100644 index 0000000000..2e856059b2 --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/theme.json @@ -0,0 +1,21 @@ +{ + "name": "B2B Inheritance-Sample-Theme", + "author": "Shopware AG", + "views": [ + "@Storefront", + "@Plugins", + "@SwagB2bInheritanceSampleTheme" + ], + "style": [ + "app/storefront/src/scss/overrides.scss", + "@Storefront", + "app/storefront/src/scss/base.scss" + ], + "script": [ + "@Storefront", + "app/storefront/dist/storefront/js/swag-b2b-inheritance-sample-theme.js" + ], + "asset": [ + "app/storefront/src/assets" + ] +} diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/_partials/_b2bnavigation/_navigation.html.twig b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/_partials/_b2bnavigation/_navigation.html.twig new file mode 100644 index 0000000000..c358c1a2c7 --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/_partials/_b2bnavigation/_navigation.html.twig @@ -0,0 +1,6 @@ +{% sw_extends '@SwagB2bPlatform/storefront/_partials/_b2bnavigation/_navigation.html.twig' %} + +{% block b2b_navigation_mainmenu %} + {{ parent() }} +

B2B Inheritance Sample

+{% endblock %} diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/frontend/b2bdashboard/index.html.twig b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/frontend/b2bdashboard/index.html.twig new file mode 100644 index 0000000000..bd8ea8215f --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/frontend/b2bdashboard/index.html.twig @@ -0,0 +1,13 @@ +{% sw_extends '@SwagB2bPlatform/storefront/frontend/b2bdashboard/index.html.twig' %} + +{% block b2b_dashboard_first_entry %} +
+

B2B Inheritance Sample

+ +

Custom file (extends from base):

+
    +
  • custom/plugins/SwagB2bInheritanceSampleTheme/src/Resources/views/storefront/frontend/b2bdashboard/index.html.twig
  • +
+ +
+{% endblock %} diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/layout/header/search.html.twig b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/layout/header/search.html.twig new file mode 100644 index 0000000000..d13486b50b --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/src/Resources/views/storefront/layout/header/search.html.twig @@ -0,0 +1,14 @@ +{% sw_extends "@Storefront/storefront/layout/header/search.html.twig" %} + +{% block layout_header_search_input_group %} + {{ parent() }} +
+

Inheritance Sample

+ +

Custom file (extends from base):

+
    +
  • custom/plugins/SwagB2bInheritanceSampleTheme/src/Resources/views/storefront/layout/header/search.html.twig
  • +
+ +
+{% endblock %} diff --git a/exampleplugins/b2b/B2bThemeInheritance/src/SwagB2bInheritanceSampleTheme.php b/exampleplugins/b2b/B2bThemeInheritance/src/SwagB2bInheritanceSampleTheme.php new file mode 100644 index 0000000000..9098312a37 --- /dev/null +++ b/exampleplugins/b2b/B2bThemeInheritance/src/SwagB2bInheritanceSampleTheme.php @@ -0,0 +1,14 @@ +subscribeEvent('Enlight_Controller_Front_StartDispatch', 'registerSubscriber'); - $this->subscribeEvent('Shopware_Console_Add_Command', 'registerSubscriber'); - - return true; - } - - public function uninstall() - { - return true; - } - - public function enable() - { - return array('success' => true, 'invalidateCache' => array('frontend', 'backend', 'theme')); - } - - public function registerSubscriber() - { - $this->registerPluginNamespace(); - $this->Application()->Events()->addSubscriber(new Resources($this)); - } - - public function registerPluginNamespace() - { - $this->Application()->Loader()->registerNamespace( - 'Shopware\SwagDigitalPublishingSample', - $this->Path() - ); - } -} diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Subscriber/Resources.php b/exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Subscriber/Resources.php deleted file mode 100644 index 9250ce90fb..0000000000 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample/Subscriber/Resources.php +++ /dev/null @@ -1,105 +0,0 @@ -bootstrap = $bootstrap; - } - - /** - * Returns an array of events you want to subscribe to - * and the names of the corresponding callback methods. - * - * @return array - */ - public static function getSubscribedEvents() - { - return array( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_SwagDigitalPublishing' => 'onPostDispatchBackend', - 'Enlight_Controller_Action_PostDispatchSecure_Widgets_SwagDigitalPublishing' => 'onPostDispatchFrontend', - 'Enlight_Controller_Action_PostDispatchSecure_Widgets_Emotion' => 'onPostDispatchFrontend', - 'SwagDigitalPublishing_ContentBanner_FilterResult' => 'onContentBannerFilter', - 'Theme_Compiler_Collect_Plugin_Less' => 'onAddLessFiles', - ); - } - - /** - * Extends the backend templates with the necessary template files. - * - * @param \Enlight_Event_EventArgs $args - */ - public function onPostDispatchBackend(\Enlight_Event_EventArgs $args) - { - $subject = $args->getSubject(); - $view = $subject->View(); - - $view->addTemplateDir($this->bootstrap->Path() . 'Views/'); - $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/extension.js'); - $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js'); - } - - /** - * Adds the template directory of the plugin to extend the frontend templates. - * - * @param \Enlight_Event_EventArgs $args - */ - public function onPostDispatchFrontend(\Enlight_Event_EventArgs $args) - { - $subject = $args->getSubject(); - $view = $subject->View(); - - $view->addTemplateDir($this->bootstrap->Path() . 'Views/'); - } - - /** - * Returns a collection of less files you want to add to the theme compiler. - * - * @return ArrayCollection - */ - public function onAddLessFiles() - { - $less = new LessDefinition(array(), array( __DIR__ . '/../Views/frontend/_public/src/less/all.less'), __DIR__); - - return new ArrayCollection(array($less)); - } - - /** - * Filter event for the banner elements of the Digital Publishing module. - * Enables you to manipulate the banner data before it gets passed to the frontend. - * - * @param \Enlight_Event_EventArgs $args - * @return mixed - */ - public function onContentBannerFilter(\Enlight_Event_EventArgs $args) - { - $banner = $args->getReturn(); - - // Do some magic data manipulation - - return $banner; - } -} diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Bootstrap.php b/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Bootstrap.php deleted file mode 100644 index 4337b477b1..0000000000 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Bootstrap.php +++ /dev/null @@ -1,50 +0,0 @@ -subscribeEvent('Enlight_Controller_Front_StartDispatch', 'registerSubscriber'); - $this->subscribeEvent('Shopware_Console_Add_Command', 'registerSubscriber'); - - return true; - } - - public function uninstall() - { - return true; - } - - public function enable() - { - return array('success' => true, 'invalidateCache' => array('frontend', 'backend', 'theme')); - } - - public function registerSubscriber() - { - $this->registerPluginNamespace(); - $this->Application()->Events()->addSubscriber(new Resources($this)); - $this->Application()->Events()->addSubscriber(new ElementHandler()); - } - - public function registerPluginNamespace() - { - $this->Application()->Loader()->registerNamespace( - 'Shopware\SwagDigitalPublishingSample120', - $this->Path() - ); - } -} \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Subscriber/Resources.php b/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Subscriber/Resources.php deleted file mode 100644 index 4842bcbb8e..0000000000 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Subscriber/Resources.php +++ /dev/null @@ -1,105 +0,0 @@ -bootstrap = $bootstrap; - } - - /** - * Returns an array of events you want to subscribe to - * and the names of the corresponding callback methods. - * - * @return array - */ - public static function getSubscribedEvents() - { - return array( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_SwagDigitalPublishing' => 'onPostDispatchBackend', - 'Enlight_Controller_Action_PostDispatchSecure_Widgets_SwagDigitalPublishing' => 'onPostDispatchFrontend', - 'Enlight_Controller_Action_PostDispatchSecure_Widgets_Emotion' => 'onPostDispatchFrontend', - 'SwagDigitalPublishing_ContentBanner_FilterResult' => 'onContentBannerFilter', - 'Theme_Compiler_Collect_Plugin_Less' => 'onAddLessFiles', - ); - } - - /** - * Extends the backend templates with the necessary template files. - * - * @param \Enlight_Event_EventArgs $args - */ - public function onPostDispatchBackend(\Enlight_Event_EventArgs $args) - { - $subject = $args->getSubject(); - $view = $subject->View(); - - $view->addTemplateDir($this->bootstrap->Path() . 'Views/'); - $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/extension.js'); - $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js'); - } - - /** - * Adds the template directory of the plugin to extend the frontend templates. - * - * @param \Enlight_Event_EventArgs $args - */ - public function onPostDispatchFrontend(\Enlight_Event_EventArgs $args) - { - $subject = $args->getSubject(); - $view = $subject->View(); - - $view->addTemplateDir($this->bootstrap->Path() . 'Views/'); - } - - /** - * Returns a collection of less files you want to add to the theme compiler. - * - * @return ArrayCollection - */ - public function onAddLessFiles() - { - $less = new LessDefinition(array(), array( __DIR__ . '/../Views/frontend/_public/src/less/all.less'), __DIR__); - - return new ArrayCollection(array($less)); - } - - /** - * Filter event for the banner elements of the Digital Publishing module. - * Enables you to manipulate the banner data before it gets passed to the frontend. - * - * @param \Enlight_Event_EventArgs $args - * @return mixed - */ - public function onContentBannerFilter(\Enlight_Event_EventArgs $args) - { - $banner = $args->getReturn(); - - // Do some magic data manipulation - - return $banner; - } -} diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js b/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js deleted file mode 100644 index 9b441815b6..0000000000 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js +++ /dev/null @@ -1,74 +0,0 @@ -//{block name="backend/swag_digital_publishing/view/editor/abstract_element_handler"} -// {$smarty.block.parent} -Ext.define('Shopware.apps.SwagDigitalPublishing.view.editor.elements.YouTubeElementHandler', { - - extend: 'Shopware.apps.SwagDigitalPublishing.view.editor.elements.AbstractElementHandler', - - name: 'youtube', - - label: 'YouTube Video', - - iconCls: 'sprite-film-youtube', - - createFormItems: function(elementRecord, data) { - var me = this; - - me.generalFieldset = Ext.create('Ext.form.FieldSet', { - title: 'YouTube Settings', - layout: 'anchor', - defaults: { - anchor : '100%', - labelWidth: 100 - }, - items: [{ - xtype: 'textfield', - name: 'youTubeId', - translatable: true, - fieldLabel: 'YouTube ID', - value: data['youTubeId'] || '', - listeners: { - change: Ext.bind(me.updateElementRecord, me, [ me.formPanel, elementRecord ]) - } - }, { - xtype: 'numberfield', - name: 'maxWidth', - fieldLabel: 'Max width', - value: data['maxWidth'] || 280, - allowDecimals: false, - minValue: 0, - listeners: { - change: Ext.bind(me.updateElementRecord, me, [ me.formPanel, elementRecord ]) - } - }, { - xtype: 'numberfield', - name: 'maxHeight', - fieldLabel: 'Max height', - value: data['maxHeight'] || 158, - allowDecimals: false, - minValue: 0, - listeners: { - change: Ext.bind(me.updateElementRecord, me, [ me.formPanel, elementRecord ]) - } - }, { - xtype: 'checkbox', - name: 'controls', - fieldLabel: 'Show Controls', - checked: !!data['controls'], - listeners: { - change: Ext.bind(me.updateElementRecord, me, [ me.formPanel, elementRecord ]) - } - }, { - xtype: 'checkbox', - name: 'showinfo', - fieldLabel: 'Show Info', - checked: !!data['showinfo'], - listeners: { - change: Ext.bind(me.updateElementRecord, me, [ me.formPanel, elementRecord ]) - } - }] - }); - - return me.generalFieldset; - } -}); -//{/block} \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/backend/swag_digital_publishing_sample/view/editor/extension.js b/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/backend/swag_digital_publishing_sample/view/editor/extension.js deleted file mode 100644 index 3495b6db64..0000000000 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/backend/swag_digital_publishing_sample/view/editor/extension.js +++ /dev/null @@ -1,35 +0,0 @@ -//{block name="backend/swag_digital_publishing/view/editor/container"} -// {$smarty.block.parent} -Ext.define('Shopware.apps.SwagDigitalPublishing.view.editor.YouTubeExtension', { - - /** - * Override the container class of the editor to add your custom element handlers. - */ - override: 'Shopware.apps.SwagDigitalPublishing.view.editor.Container', - - initComponent: function() { - var me = this; - - /** - * Create an instance of your custom element handler. - */ - var youTubeHandler = Ext.create('Shopware.apps.SwagDigitalPublishing.view.editor.elements.YouTubeElementHandler'); - - /** - * Check if there is already a registered handler with the same name. - */ - if (me.getElementHandlerByName(youTubeHandler.getName()) === null) { - - /** - * Add the custom handler to the list of available handlers. - */ - me.elementHandlers.push(youTubeHandler); - } - - /** - * Call the parent init method. - */ - me.callParent(arguments); - } -}); -//{/block} \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/frontend/_public/src/less/all.less b/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/frontend/_public/src/less/all.less deleted file mode 100644 index c2e0155f33..0000000000 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/frontend/_public/src/less/all.less +++ /dev/null @@ -1,9 +0,0 @@ -.dig-pub--youtube { - display: block; - width: 100%; - - .youtube--frame { - max-width: 100%; - max-height: 100%; - } -} \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/widgets/swag_digital_publishing/components/youtube.tpl b/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/widgets/swag_digital_publishing/components/youtube.tpl deleted file mode 100644 index 40423fb298..0000000000 --- a/exampleplugins/legacy/Backend/SwagDigitalPublishingSample120/Views/widgets/swag_digital_publishing/components/youtube.tpl +++ /dev/null @@ -1,20 +0,0 @@ -
- - {$controls = 0} - {$showinfo = 0} - - {if $element.controls} - {$controls = 1} - {/if} - - {if $element.showinfo} - {$showinfo = 1} - {/if} - - -
\ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagExtendCustomer/Bootstrap.php b/exampleplugins/legacy/Backend/SwagExtendCustomer/Bootstrap.php deleted file mode 100644 index 7788511593..0000000000 --- a/exampleplugins/legacy/Backend/SwagExtendCustomer/Bootstrap.php +++ /dev/null @@ -1,43 +0,0 @@ -subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_Customer', - 'onCustomerPostDispatch' - ); - - return true; - } - - public function onCustomerPostDispatch(Enlight_Event_EventArgs $args) - { - /** @var \Enlight_Controller_Action $controller */ - $controller = $args->getSubject(); - $view = $controller->View(); - $request = $controller->Request(); - - $view->addTemplateDir(__DIR__ . '/Views'); - - if ($request->getActionName() == 'index') { - $view->extendsTemplate('backend/swag_extend_customer/app.js'); - } - - if ($request->getActionName() == 'load') { - $view->extendsTemplate('backend/swag_extend_customer/view/detail/billing.js'); - $view->extendsTemplate('backend/swag_extend_customer/view/detail/window.js'); - } - } -} diff --git a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/billing.js b/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/billing.js deleted file mode 100644 index 253d694cd1..0000000000 --- a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/billing.js +++ /dev/null @@ -1,20 +0,0 @@ -//{block name="backend/customer/view/detail/billing"} -// {$smarty.block.parent} -Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Billing', { - override:'Shopware.apps.Customer.view.detail.Billing', - - /** - * This extjs override will call the original method first - * and then change the xtype of the 3rd field - */ - createBillingFormRight: function() { - var me = this, - result = me.callParent(arguments); - - result[2].xtype = 'numberfield'; - - return result; - } - -}); -//{/block} \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/window.js b/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/window.js deleted file mode 100644 index 0e713b070c..0000000000 --- a/exampleplugins/legacy/Backend/SwagExtendCustomer/Views/backend/swag_extend_customer/view/detail/window.js +++ /dev/null @@ -1,26 +0,0 @@ -//{block name="backend/customer/view/detail/window"} -// {$smarty.block.parent} -Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Window', { - override: 'Shopware.apps.Customer.view.detail.Window', - - getTabs: function() { - var me = this, - result = me.callParent(); - - result.push(Ext.create('Shopware.apps.SwagExtendCustomer.view.detail.MyOwnTab')); - - return result; - } - -}); -//{/block} - - - - - - - - - - diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Bootstrap.php b/exampleplugins/legacy/Backend/SwagProductAssoc/Bootstrap.php deleted file mode 100755 index 8ab4fcb5cf..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Bootstrap.php +++ /dev/null @@ -1,146 +0,0 @@ - 'Shopware Product Overview - Association' - ); - } - - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Dispatcher_ControllerPath_Backend_SwagProduct', - 'getBackendController' - ); - - $this->createMenuItem(array( - 'label' => 'Shopware Product Overview - Association', - 'controller' => 'SwagProduct', - 'class' => 'sprite-application-block', - 'action' => 'Index', - 'active' => 1, - 'parent' => $this->Menu()->findOneBy(['label' => 'Marketing']) - )); - - $this->updateSchema(); - - return true; - } - - protected function updateSchema() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product'), - $em->getClassMetadata('Shopware\CustomModels\Product\Variant'), - $em->getClassMetadata('Shopware\CustomModels\Product\Attribute') - ); - - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore - } - $tool->createSchema($classes); - - $this->addDemoData(); - } - - public function uninstall() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product'), - $em->getClassMetadata('Shopware\CustomModels\Product\Variant'), - $em->getClassMetadata('Shopware\CustomModels\Product\Attribute') - ); - $tool->dropSchema($classes); - - return true; - } - - public function getBackendController(Enlight_Event_EventArgs $args) - { - $this->Application()->Template()->addTemplateDir( - $this->Path() . 'Views/' - ); - - $this->registerCustomModels(); - - return $this->Path() . '/Controllers/Backend/SwagProduct.php'; - } - - protected function addDemoData() - { - $sql = " - INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate, tax_id) - SELECT - a.id, - a.name, - a.active, - a.description, - a.description_long as descriptionLong, - a.laststock as lastStock, - a.datum as createDate, - a.taxID as tax_id - FROM s_articles a - "; - Shopware()->Db()->query($sql); - - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_variant (id, product_id, number, additionalText, active, inStock, stockMin, weight) - SELECT - a.id, - a.articleID, - a.ordernumber, - a.additionaltext, - a.active, - a.instock, - a.stockmin, - a.weight - FROM s_articles_details a - "; - Shopware()->Db()->query($sql); - - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_attribute - SELECT - a.id, - a.articleID as product_id, - a.attr1, - a.attr2, - a.attr3, - a.attr4, - a.attr5 - FROM s_articles_attributes a - "; - Shopware()->Db()->query($sql); - - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_categories (product_id, category_id) - SELECT - a.articleID as product_id, - a.categoryID as category_id - FROM s_articles_categories a - "; - Shopware()->Db()->query($sql); - - - -// `id`, `product_id`, `additionalText`, `active`, `inStock`, `stockMin`, `weight` - } -} diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/category.js b/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/category.js deleted file mode 100755 index 6265976b67..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/model/category.js +++ /dev/null @@ -1,12 +0,0 @@ - -Ext.define('Shopware.apps.SwagProduct.model.Category', { - - extend: 'Shopware.apps.Base.model.Category', - - configure: function() { - return { - related: 'Shopware.apps.SwagProduct.view.detail.Category' - } - } -}); - diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/store/product.js b/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/store/product.js deleted file mode 100755 index f8324c07a0..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/store/product.js +++ /dev/null @@ -1,9 +0,0 @@ - -Ext.define('Shopware.apps.SwagProduct.store.Product', { - extend:'Shopware.store.Listing', - - configure: function() { - return { controller: 'SwagProduct' }; - }, - model: 'Shopware.apps.SwagProduct.model.Product' -}); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/store/variant.js b/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/store/variant.js deleted file mode 100644 index 3d63466a7a..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/store/variant.js +++ /dev/null @@ -1,10 +0,0 @@ - -Ext.define('Shopware.apps.SwagProduct.store.Variant', { - extend: 'Shopware.store.Association', - model: 'Shopware.apps.SwagProduct.model.Variant', - configure: function() { - return { - controller: 'SwagProduct' - }; - } -}); diff --git a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/product.js b/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/product.js deleted file mode 100755 index 1bde5a5f30..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductAssoc/Views/backend/swag_product/view/detail/product.js +++ /dev/null @@ -1,14 +0,0 @@ - - -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { - extend: 'Shopware.model.Container', - alias: 'widget.product-detail-container', - padding: 20, - - configure: function() { - return { - controller: 'SwagProduct', -// associations: [ 'variants', 'categories', 'attribute' ] - }; - } -}); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Bootstrap.php b/exampleplugins/legacy/Backend/SwagProductBasics/Bootstrap.php deleted file mode 100755 index 7a13b89b2a..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Bootstrap.php +++ /dev/null @@ -1,96 +0,0 @@ - 'Shopware Product Overview - Basics' - ); - } - - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Dispatcher_ControllerPath_Backend_SwagProduct', - 'getBackendController' - ); - - $this->createMenuItem(array( - 'label' => 'Shopware Product Overview - Basics', - 'controller' => 'SwagProduct', - 'class' => 'sprite-application-block', - 'action' => 'Index', - 'active' => 1, - 'parent' => $this->Menu()->findOneBy(['label' => 'Marketing']) - )); - - $this->updateSchema(); - - return true; - } - - protected function updateSchema() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); - - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore - } - $tool->createSchema($classes); - - $this->addDemoData(); - } - - public function uninstall() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); - $tool->dropSchema($classes); - - return true; - } - - public function getBackendController(Enlight_Event_EventArgs $args) - { - $this->Application()->Template()->addTemplateDir( - $this->Path() . 'Views/' - ); - - $this->registerCustomModels(); - - return $this->Path() . '/Controllers/Backend/SwagProduct.php'; - } - - protected function addDemoData() - { - $sql = " - INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate) - SELECT - a.id, - a.name, - a.active, - a.description, - a.description_long as descriptionLong, - a.laststock as lastStock, - a.datum as createDate - FROM s_articles a - "; - Shopware()->Db()->query($sql); - } -} diff --git a/exampleplugins/legacy/Backend/SwagProductBasics/Controllers/Backend/SwagProduct.php b/exampleplugins/legacy/Backend/SwagProductBasics/Controllers/Backend/SwagProduct.php deleted file mode 100755 index aad0e08175..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductBasics/Controllers/Backend/SwagProduct.php +++ /dev/null @@ -1,7 +0,0 @@ - 'Shopware Product Overview - Detail' - ); - } - - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Dispatcher_ControllerPath_Backend_SwagProduct', - 'getBackendController' - ); - - $this->createMenuItem(array( - 'label' => 'Shopware Product Overview - Detail', - 'controller' => 'SwagProduct', - 'class' => 'sprite-application-block', - 'action' => 'Index', - 'active' => 1, - 'parent' => $this->Menu()->findOneBy(['label' => 'Marketing']) - )); - - $this->updateSchema(); - - return true; - } - - protected function updateSchema() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); - - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore - } - $tool->createSchema($classes); - - $this->addDemoData(); - } - - public function uninstall() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); - $tool->dropSchema($classes); - - return true; - } - - public function getBackendController(Enlight_Event_EventArgs $args) - { - $this->Application()->Template()->addTemplateDir( - $this->Path() . 'Views/' - ); - - $this->registerCustomModels(); - - return $this->Path() . '/Controllers/Backend/SwagProduct.php'; - } - - protected function addDemoData() - { - $sql = " - INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate) - SELECT - a.id, - a.name, - a.active, - a.description, - a.description_long as descriptionLong, - a.laststock as lastStock, - a.datum as createDate - FROM s_articles a - "; - Shopware()->Db()->query($sql); - } -} diff --git a/exampleplugins/legacy/Backend/SwagProductDetail/Controllers/Backend/SwagProduct.php b/exampleplugins/legacy/Backend/SwagProductDetail/Controllers/Backend/SwagProduct.php deleted file mode 100755 index aad0e08175..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductDetail/Controllers/Backend/SwagProduct.php +++ /dev/null @@ -1,7 +0,0 @@ - 'Shopware Product Overview - Listing' - ); - } - - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Dispatcher_ControllerPath_Backend_SwagProduct', - 'getBackendController' - ); - - $this->createMenuItem(array( - 'label' => 'Shopware Product Overview - Listing', - 'controller' => 'SwagProduct', - 'class' => 'sprite-application-block', - 'action' => 'Index', - 'active' => 1, - 'parent' => $this->Menu()->findOneBy(['label' => 'Marketing']) - )); - - $this->updateSchema(); - - return true; - } - - protected function updateSchema() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); - - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore - } - $tool->createSchema($classes); - - $this->addDemoData(); - } - - public function uninstall() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); - $tool->dropSchema($classes); - - return true; - } - - public function getBackendController(Enlight_Event_EventArgs $args) - { - $this->Application()->Template()->addTemplateDir( - $this->Path() . 'Views/' - ); - - $this->registerCustomModels(); - - return $this->Path() . '/Controllers/Backend/SwagProduct.php'; - } - - protected function addDemoData() - { - $sql = " - INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate) - SELECT - a.id, - a.name, - a.active, - a.description, - a.description_long as descriptionLong, - a.laststock as lastStock, - a.datum as createDate - FROM s_articles a - "; - Shopware()->Db()->query($sql); - } -} diff --git a/exampleplugins/legacy/Backend/SwagProductListing/Controllers/Backend/SwagProduct.php b/exampleplugins/legacy/Backend/SwagProductListing/Controllers/Backend/SwagProduct.php deleted file mode 100755 index aad0e08175..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductListing/Controllers/Backend/SwagProduct.php +++ /dev/null @@ -1,7 +0,0 @@ - 'Shopware Product Overview - Listing Extension' - ); - } - - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Dispatcher_ControllerPath_Backend_SwagProduct', - 'getBackendController' - ); - - $this->createMenuItem(array( - 'label' => 'Shopware Product Overview - Listing Extension', - 'controller' => 'SwagProduct', - 'class' => 'sprite-application-block', - 'action' => 'Index', - 'active' => 1, - 'parent' => $this->Menu()->findOneBy(['label' => 'Marketing']) - )); - - $this->updateSchema(); - - return true; - } - - protected function updateSchema() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product'), - $em->getClassMetadata('Shopware\CustomModels\Product\Variant'), - $em->getClassMetadata('Shopware\CustomModels\Product\Attribute') - ); - - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore - } - $tool->createSchema($classes); - - $this->addDemoData(); - } - - public function uninstall() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product'), - $em->getClassMetadata('Shopware\CustomModels\Product\Variant'), - $em->getClassMetadata('Shopware\CustomModels\Product\Attribute') - ); - $tool->dropSchema($classes); - - return true; - } - - public function getBackendController(Enlight_Event_EventArgs $args) - { - $this->Application()->Template()->addTemplateDir( - $this->Path() . 'Views/' - ); - - $this->registerCustomModels(); - - return $this->Path() . '/Controllers/Backend/SwagProduct.php'; - } - - protected function addDemoData() - { - $sql = " - INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate, tax_id) - SELECT - a.id, - a.name, - a.active, - a.description, - a.description_long as descriptionLong, - a.laststock as lastStock, - a.datum as createDate, - a.taxID as tax_id - FROM s_articles a - "; - Shopware()->Db()->query($sql); - - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_variant (id, product_id, number, additionalText, active, inStock, stockMin, weight) - SELECT - a.id, - a.articleID, - a.ordernumber, - a.additionaltext, - a.active, - a.instock, - a.stockmin, - a.weight - FROM s_articles_details a - "; - Shopware()->Db()->query($sql); - - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_attribute - SELECT - a.id, - a.articleID as product_id, - a.attr1, - a.attr2, - a.attr3, - a.attr4, - a.attr5 - FROM s_articles_attributes a - "; - Shopware()->Db()->query($sql); - - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_categories (product_id, category_id) - SELECT - a.articleID as product_id, - a.categoryID as category_id - FROM s_articles_categories a - "; - Shopware()->Db()->query($sql); - } -} diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/category.js b/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/category.js deleted file mode 100755 index 6265976b67..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/model/category.js +++ /dev/null @@ -1,12 +0,0 @@ - -Ext.define('Shopware.apps.SwagProduct.model.Category', { - - extend: 'Shopware.apps.Base.model.Category', - - configure: function() { - return { - related: 'Shopware.apps.SwagProduct.view.detail.Category' - } - } -}); - diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/store/product.js b/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/store/product.js deleted file mode 100755 index f8324c07a0..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/store/product.js +++ /dev/null @@ -1,9 +0,0 @@ - -Ext.define('Shopware.apps.SwagProduct.store.Product', { - extend:'Shopware.store.Listing', - - configure: function() { - return { controller: 'SwagProduct' }; - }, - model: 'Shopware.apps.SwagProduct.model.Product' -}); \ No newline at end of file diff --git a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/store/variant.js b/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/store/variant.js deleted file mode 100644 index 3d63466a7a..0000000000 --- a/exampleplugins/legacy/Backend/SwagProductListingExtension/Views/backend/swag_product/store/variant.js +++ /dev/null @@ -1,10 +0,0 @@ - -Ext.define('Shopware.apps.SwagProduct.store.Variant', { - extend: 'Shopware.store.Association', - model: 'Shopware.apps.SwagProduct.model.Variant', - configure: function() { - return { - controller: 'SwagProduct' - }; - } -}); diff --git a/exampleplugins/legacy/Backend/SwagVimeoElement/Bootstrap.php b/exampleplugins/legacy/Backend/SwagVimeoElement/Bootstrap.php deleted file mode 100644 index e3a0704c35..0000000000 --- a/exampleplugins/legacy/Backend/SwagVimeoElement/Bootstrap.php +++ /dev/null @@ -1,129 +0,0 @@ -createEmotionComponent([ - 'name' => 'Vimeo Video', - 'xtype' => 'emotion-components-vimeo', - 'template' => 'emotion_vimeo', - 'cls' => 'emotion-vimeo-element', - 'description' => 'A simple vimeo video element for the shopping worlds.' - ]); - - $vimeoElement->createTextField([ - 'name' => 'vimeo_video_id', - 'fieldLabel' => 'Video ID', - 'supportText' => 'Enter the ID of the video you want to embed.', - 'allowBlank' => false - ]); - - $vimeoElement->createHiddenField([ - 'name' => 'vimeo_video_thumbnail' - ]); - - $vimeoElement->createTextField([ - 'name' => 'vimeo_interface_color', - 'fieldLabel' => 'Interface Color', - 'supportText' => 'Enter the #hex color code for the video player interface.', - 'defaultValue' => '#0096FF' - ]); - - $vimeoElement->createCheckboxField([ - 'name' => 'vimeo_autoplay', - 'fieldLabel' => 'Autoplay', - 'defaultValue' => false - ]); - - $vimeoElement->createCheckboxField([ - 'name' => 'vimeo_loop', - 'fieldLabel' => 'Loop', - 'defaultValue' => false - ]); - - $vimeoElement->createCheckboxField([ - 'name' => 'vimeo_show_title', - 'fieldLabel' => 'Show title', - 'defaultValue' => false - ]); - - $vimeoElement->createCheckboxField([ - 'name' => 'vimeo_show_portrait', - 'fieldLabel' => 'Show portrait', - 'defaultValue' => false - ]); - - $vimeoElement->createCheckboxField([ - 'name' => 'vimeo_show_author', - 'fieldLabel' => 'Show author', - 'defaultValue' => false - ]); - - /** - * Subscribe to the post dispatch event of the emotion backend module to extend the components. - */ - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_Emotion', - 'onPostDispatchBackendEmotion' - ); - - return true; - } - - /** - * Extends the backend template to add the grid component for the emotion designer. - * - * @param Enlight_Controller_ActionEventArgs $args - */ - public function onPostDispatchBackendEmotion(Enlight_Controller_ActionEventArgs $args) - { - /** @var \Shopware_Controllers_Backend_Emotion $controller */ - $controller = $args->getSubject(); - $view = $controller->View(); - - $view->addTemplateDir($this->Path() . 'Views/'); - $view->extendsTemplate('backend/emotion/swag_vimeo_element/view/detail/elements/vimeo_video.js'); - } - - public function uninstall() - { - return true; - } - - public function enable() - { - return [ - 'success' => true, - 'invalidateCache' => ['backend', 'frontend', 'theme'] - ]; - } - - public function disable() - { - return [ - 'success' => true, - 'invalidateCache' => ['backend', 'frontend', 'theme'] - ]; - } -} diff --git a/exampleplugins/legacy/Backend/SwagVimeoElement/plugin.png b/exampleplugins/legacy/Backend/SwagVimeoElement/plugin.png deleted file mode 100644 index b12d795b00..0000000000 Binary files a/exampleplugins/legacy/Backend/SwagVimeoElement/plugin.png and /dev/null differ diff --git a/exampleplugins/legacy/Frontend/SwagAttributeFilter/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagAttributeFilter/Bootstrap.php deleted file mode 100644 index d350c4847f..0000000000 --- a/exampleplugins/legacy/Frontend/SwagAttributeFilter/Bootstrap.php +++ /dev/null @@ -1,31 +0,0 @@ -subscribeEvent('Enlight_Controller_Front_StartDispatch', 'registerSubscriber'); - $this->subscribeEvent('Shopware_Console_Add_Command', 'registerSubscriber'); - return true; - } - - public function afterInit() - { - $this->get('Loader')->registerNamespace( - 'ShopwarePlugins\SwagAttributeFilter', - $this->Path() - ); - } - - public function registerSubscriber() - { - $this->get('events')->addSubscriber( - new AttributeFilterSubscriber( - Shopware()->Container(), - $this->Path() - ) - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagAttributeFilter/Subscribers/AttributeFilterSubscriber.php b/exampleplugins/legacy/Frontend/SwagAttributeFilter/Subscribers/AttributeFilterSubscriber.php deleted file mode 100644 index 0bac8fde62..0000000000 --- a/exampleplugins/legacy/Frontend/SwagAttributeFilter/Subscribers/AttributeFilterSubscriber.php +++ /dev/null @@ -1,48 +0,0 @@ -container = $container; - $this->pluginDir = $pluginDir; - } - - /** - * @inheritdoc - */ - public static function getSubscribedEvents() - { - return [ - 'Shopware_SearchBundleDBAL_Collect_Facet_Handlers' => 'registerFacetHandler', - 'Shopware_SearchBundle_Collect_Criteria_Request_Handlers' => 'registerRequestHandler', - - 'Shopware_SearchBundleDBAL_Collect_Condition_Handlers' => 'registerConditionHandler', - ]; - } - - public function registerRequestHandler() - { - return new CriteriaRequestHandler(); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagController/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagController/Bootstrap.php deleted file mode 100644 index 3d9ee4d7ff..0000000000 --- a/exampleplugins/legacy/Frontend/SwagController/Bootstrap.php +++ /dev/null @@ -1,52 +0,0 @@ - true, 'invalidateCache' => array()) in order to let the installation - * be successfull - * - * - update: Triggered when the user updates the plugin. You will get passes the former version of the plugin as param - * In order to let the update be successful, return "true" - * - * - uninstall: Triggered when the plugin is reinstalled or uninstalled. Clean up your tables here. - */ -class Shopware_Plugins_Frontend_SwagController_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - public function getVersion() - { - $info = json_decode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR .'plugin.json'), true); - if ($info) { - return $info['currentVersion']; - } else { - throw new Exception('The plugin has an invalid version file.'); - } - } - - public function getLabel() - { - return 'SwagController'; - } - - public function uninstall() - { - return true; - } - - public function update($oldVersion) - { - return true; - } - - public function install() - { - $this->registerController('Frontend', 'SwagControllerTest'); - - return true; - } -} diff --git a/exampleplugins/legacy/Frontend/SwagController/plugin.json b/exampleplugins/legacy/Frontend/SwagController/plugin.json deleted file mode 100644 index 9825f140bf..0000000000 --- a/exampleplugins/legacy/Frontend/SwagController/plugin.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "label": { - "de": "SwagController", - "en": "SwagController" - }, - "copyright": "(c) by Shopware AG", - "license": "proprietary", - "link": "http://store.shopware.com", - "author": "Shopware AG", - - "currentVersion": "1.0.0", - - "changelog": { - "de": { - "1.0.0": "Erstveröffentlichung" - }, - "en": { - "1.0.0": "First release" - } - }, - - "compatibility": { - "minimumVersion": "4.3.0", - "maximumVersion": null, - "blacklist": [ - ] - } -} \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagCustomDetailTheme/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagCustomDetailTheme/Bootstrap.php deleted file mode 100644 index 21c698f1aa..0000000000 --- a/exampleplugins/legacy/Frontend/SwagCustomDetailTheme/Bootstrap.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->getVersion(), - 'label' => $this->getLabel(), - 'supplier' => 'shopware AG', - 'description' => 'This theme creates an additional detail page template which can be used on specific products. When you have activated the plugin you should see the new theme inside your Theme Manager.', - 'link' => 'https://developers.shopware.com' - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Bootstrap.php deleted file mode 100644 index 58fd6a9027..0000000000 --- a/exampleplugins/legacy/Frontend/SwagCustomListingTheme/Bootstrap.php +++ /dev/null @@ -1,33 +0,0 @@ - $this->getVersion(), - 'label' => $this->getLabel(), - 'supplier' => 'shopware AG', - 'description' => 'This theme creates an additional listing page template which can be used on specific categories. When you have activated the plugin you should see the new theme inside your Theme Manager.', - 'link' => 'https://developers.shopware.com' - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagCustomRiskRule/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagCustomRiskRule/Bootstrap.php deleted file mode 100644 index cfddf489b4..0000000000 --- a/exampleplugins/legacy/Frontend/SwagCustomRiskRule/Bootstrap.php +++ /dev/null @@ -1,52 +0,0 @@ -subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_RiskManagement', - 'onRiskManagementBackend' - ); - - $this->subscribeEvent( - 'Shopware_Modules_Admin_Execute_Risk_Rule_sRiskMyCustomRule', - 'onMyCustomRule' - ); - - return true; - } - - /** - * @param Enlight_Controller_ActionEventArgs $args - */ - public function onRiskManagementBackend(Enlight_Controller_ActionEventArgs $args) - { - $subject = $args->getSubject(); - $request = $subject->Request(); - - $view = $subject->View(); - $view->addTemplateDir(__DIR__.'/Views/'); - - if ($request->getActionName() === 'load') { - $view->extendsTemplate('backend/my_risk_rule/store/risks.js'); - } - } - - /** - * @param Enlight_Event_EventArgs $args - * @return bool - */ - public function onMyCustomRule(Enlight_Event_EventArgs $args) - { - $rule = $args->get('rule'); - $user = $args->get('user'); - $basket = $args->get('basket'); - $value = $args->get('value'); - - if ($basket['AmountNumeric'] > $value) { - return true; // it's a risky customer - } - - return false; - } -} diff --git a/exampleplugins/legacy/Frontend/SwagCustomRiskRule/Views/backend/my_risk_rule/store/risks.js b/exampleplugins/legacy/Frontend/SwagCustomRiskRule/Views/backend/my_risk_rule/store/risks.js deleted file mode 100644 index 227debe368..0000000000 --- a/exampleplugins/legacy/Frontend/SwagCustomRiskRule/Views/backend/my_risk_rule/store/risks.js +++ /dev/null @@ -1,4 +0,0 @@ -//{block name="backend/risk_management/store/risk/data"} -// {$smarty.block.parent} -{ description: 'My Custom Rule', value: 'MyCustomRule' }, -//{/block} diff --git a/exampleplugins/legacy/Frontend/SwagESBlog/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagESBlog/Bootstrap.php deleted file mode 100644 index 398c496f02..0000000000 --- a/exampleplugins/legacy/Frontend/SwagESBlog/Bootstrap.php +++ /dev/null @@ -1,82 +0,0 @@ -subscribeEvent('Enlight_Bootstrap_InitResource_swag_es_blog_search.blog_indexer', 'registerIndexerService'); - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Indexer', 'addIndexer'); - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Mapping', 'addMapping'); - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Synchronizer', 'addSynchronizer'); - $this->subscribeEvent('Enlight_Controller_Front_StartDispatch', 'addBacklogSubscriber'); - $this->subscribeEvent('Enlight_Bootstrap_AfterInitResource_shopware_search.product_search', 'decorateProductSearch'); - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Settings', 'addSettings'); - return true; - } - - public function afterInit() - { - $this->get('Loader')->registerNamespace('ShopwarePlugins\SwagESBlog', $this->Path()); - } - - public function addSettings() - { - return new BlogSettings(); - } - - - public function registerIndexerService() - { - return new BlogDataIndexer( - $this->get('dbal_connection'), - $this->get('shopware_elastic_search.client'), - new BlogProvider($this->get('dbal_connection')) - ); - } - - public function addIndexer() - { - return $this->get('swag_es_blog_search.blog_indexer'); - } - - public function addMapping() - { - return new BlogMapping($this->get('shopware_elastic_search.field_mapping')); - } - - public function addBacklogSubscriber() - { - $subscriber = new ORMBacklogSubscriber(Shopware()->Container()); - - /** @var ModelManager $entityManager */ - $entityManager = $this->get('models'); - $entityManager->getEventManager()->addEventSubscriber($subscriber); - } - - public function addSynchronizer() - { - return new BlogSynchronizer( - $this->get('swag_es_blog_search.blog_indexer'), - $this->get('dbal_connection') - ); - } - - public function decorateProductSearch() - { - $service = new BlogSearch( - $this->get('shopware_elastic_search.client'), - $this->get('shopware_search.product_search'), - $this->get('shopware_elastic_search.index_factory') - ); - Shopware()->Container()->set('shopware_search.product_search', $service); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagESProduct/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagESProduct/Bootstrap.php deleted file mode 100644 index 45d6ce11e4..0000000000 --- a/exampleplugins/legacy/Frontend/SwagESProduct/Bootstrap.php +++ /dev/null @@ -1,78 +0,0 @@ -subscribeEvent('Enlight_Bootstrap_AfterInitResource_shopware_elastic_search.product_mapping', 'decorateProductMapping'); - $this->subscribeEvent('Enlight_Bootstrap_AfterInitResource_shopware_elastic_search.product_provider', 'decorateProductProvider'); - $this->subscribeEvent('Enlight_Bootstrap_AfterInitResource_shopware_search_es.search_term_query_builder', 'decorateSearchTermQueryBuilder'); - - $this->subscribeEvent('Shopware_SearchBundle_Collect_Criteria_Request_Handlers', 'addCriteriaRequestHandler'); - $this->subscribeEvent('Shopware_SearchBundleES_Collect_Handlers', 'addSearchHandlers'); - - return true; - } - - public function addCriteriaRequestHandler() - { - return new CriteriaRequestHandler(); - } - - public function addSearchHandlers() - { - return new ArrayCollection([ - new SalesFacetHandler(), - new SalesConditionHandler(), - new SalesSortingHandler() - ]); - } - - public function decorateSearchTermQueryBuilder() - { - $searchTermQueryBuilder = new SearchTermQueryBuilder( - $this->get('shopware_search_es.search_term_query_builder') - ); - - Shopware()->Container()->set( - 'shopware_search_es.search_term_query_builder', - $searchTermQueryBuilder - ); - } - - public function afterInit() - { - $this->get('Loader')->registerNamespace('ShopwarePlugins\SwagESProduct', $this->Path()); - } - - public function decorateProductMapping() - { - /** @var \Shopware\Bundle\ESIndexingBundle\MappingInterface $mapping */ - $mapping = $this->get('shopware_elastic_search.product_mapping'); - Shopware()->Container()->set( - 'shopware_elastic_search.product_mapping', - new ProductMapping($mapping) - ); - } - - public function decorateProductProvider() - { - /** @var ProductProviderInterface $provider */ - $provider = $this->get('shopware_elastic_search.product_provider'); - - Shopware()->Container()->set( - 'shopware_elastic_search.product_provider', - new ProductProvider($provider) - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagLegacyTutorialTheme/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagLegacyTutorialTheme/Bootstrap.php new file mode 100755 index 0000000000..96c90d8b18 --- /dev/null +++ b/exampleplugins/legacy/Frontend/SwagLegacyTutorialTheme/Bootstrap.php @@ -0,0 +1,19 @@ + + + + + + {$smarty.block.parent} +{/block} + + +{* Menu (Off canvas left) trigger *} +{block name='frontend_index_offcanvas_left_trigger'} + +{/block} \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagLegacyTutorialTheme/Themes/Frontend/TutorialTheme/preview.png b/exampleplugins/legacy/Frontend/SwagLegacyTutorialTheme/Themes/Frontend/TutorialTheme/preview.png new file mode 100755 index 0000000000..ca16411754 Binary files /dev/null and b/exampleplugins/legacy/Frontend/SwagLegacyTutorialTheme/Themes/Frontend/TutorialTheme/preview.png differ diff --git a/exampleplugins/legacy/Frontend/SwagMd5Reversed/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagMd5Reversed/Bootstrap.php deleted file mode 100644 index 7829842c35..0000000000 --- a/exampleplugins/legacy/Frontend/SwagMd5Reversed/Bootstrap.php +++ /dev/null @@ -1,72 +0,0 @@ -assertVersionGreaterThen('4.3.0')) { - throw new \RuntimeException('At least Shopware 4.3.0 is required'); - } - - // this event collects all password encoders - $this->subscribeEvent( - 'Shopware_Components_Password_Manager_AddEncoder', - 'onAddEncoder' - ); - - return true; - } - - /** - * Add our own encoder to the internal encoder collection - * - * @param Enlight_Event_EventArgs $args - * @return array|mixed - */ - public function onAddEncoder(Enlight_Event_EventArgs $args) - { - $this->registerMyComponents(); - - $hashes = $args->getReturn(); - - $hashes[] = new \Shopware\SwagMd5Reversed\Md5ReversedEncoder(); - - return $hashes; - } - - /** - * Register the plugin's namespace - */ - public function registerMyComponents() - { - $this->Application()->Loader()->registerNamespace( - 'Shopware\SwagMd5Reversed', - $this->Path() - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagMd5Reversed/phpunit.xml.dist b/exampleplugins/legacy/Frontend/SwagMd5Reversed/phpunit.xml.dist deleted file mode 100644 index 05b8693f3a..0000000000 --- a/exampleplugins/legacy/Frontend/SwagMd5Reversed/phpunit.xml.dist +++ /dev/null @@ -1,6 +0,0 @@ - - - - tests - - diff --git a/exampleplugins/legacy/Frontend/SwagMd5Reversed/plugin.json b/exampleplugins/legacy/Frontend/SwagMd5Reversed/plugin.json deleted file mode 100644 index d6329cf5f3..0000000000 --- a/exampleplugins/legacy/Frontend/SwagMd5Reversed/plugin.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "label": { - "de": "SwagMd5Reversed", - "en": "SwagMd5Reversed" - }, - "copyright": "(c) by Shopware AG", - "license": "proprietary", - "link": "http://store.shopware.com", - "author": "Shopware AG", - - "currentVersion": "1.0.0", - - "changelog": { - "de": { - "1.0.0": "Erstveröffentlichung" - }, - "en": { - "1.0.0": "First release" - } - }, - - "compatibility": { - "minimumVersion": "4.3.0", - "maximumVersion": null, - "blacklist": [ - ] - } -} \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagMd5Reversed/tests/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagMd5Reversed/tests/Bootstrap.php deleted file mode 100644 index 9bef9c93cd..0000000000 --- a/exampleplugins/legacy/Frontend/SwagMd5Reversed/tests/Bootstrap.php +++ /dev/null @@ -1,14 +0,0 @@ -Loader(); - -$pluginDir = __DIR__ . '/../'; - -$loader->registerNamespace( - 'Shopware\\SwagMd5Reversed', - $pluginDir -); diff --git a/exampleplugins/legacy/Frontend/SwagModelPlugin/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagModelPlugin/Bootstrap.php deleted file mode 100644 index 91d8ecf59e..0000000000 --- a/exampleplugins/legacy/Frontend/SwagModelPlugin/Bootstrap.php +++ /dev/null @@ -1,114 +0,0 @@ - true, 'invalidateCache' => array()) in order to let the installation - * be successfull - * - * - update: Triggered when the user updates the plugin. You will get passes the former version of the plugin as param - * In order to let the update be successful, return "true" - * - * - uninstall: Triggered when the plugin is reinstalled or uninstalled. Clean up your tables here. - */ -class Shopware_Plugins_Frontend_SwagModelPlugin_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - public function getVersion() - { - $info = json_decode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR .'plugin.json'), true); - if ($info) { - return $info['currentVersion']; - } else { - throw new Exception('The plugin has an invalid version file.'); - } - } - - public function getLabel() - { - return 'SwagModelPlugin'; - } - - public function uninstall() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\SwagModelPlugin\MyPluginModel') - ); - $tool->dropSchema($classes); - - return true; - } - - public function update($oldVersion) - { - return true; - } - - public function install() - { - if (!$this->assertMinimumVersion('4.3.0')) { - throw new \RuntimeException('At least Shopware 4.3.0 is required'); - } - - - $this->subscribeEvent( - 'Enlight_Controller_Front_DispatchLoopStartup', - 'onStartDispatch' - ); - - $this->updateSchema(); - return true; - } - - /** - * Creates the database scheme from an existing doctrine model. - * - * Will remove the table first, so handle with care. - */ - protected function updateSchema() - { - $this->registerCustomModels(); - - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); - - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\SwagModelPlugin\MyPluginModel') - ); - - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore - } - $tool->createSchema($classes); - } - - /** - * This callback function is triggered at the very beginning of the dispatch process and allows - * us to register additional events on the fly. This way you won't ever need to reinstall you - * plugin for new events - any event and hook can simply be registerend in the event subscribers - */ - public function onStartDispatch(Enlight_Event_EventArgs $args) - { - $this->registerMyComponents(); - $this->registerCustomModels(); - } - - public function registerMyComponents() - { - $this->Application()->Loader()->registerNamespace( - 'Shopware\SwagModelPlugin', - $this->Path() - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagModelPlugin/Readme.md b/exampleplugins/legacy/Frontend/SwagModelPlugin/Readme.md deleted file mode 100644 index 7320a532e7..0000000000 --- a/exampleplugins/legacy/Frontend/SwagModelPlugin/Readme.md +++ /dev/null @@ -1,7 +0,0 @@ -# SwagModelPlugin -## About SwagModelPlugin -This skeleton contains a License file, file header and a basic README. - -## License - -Please see [License File](LICENSE) for more information. \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagModelPlugin/phpunit.xml.dist b/exampleplugins/legacy/Frontend/SwagModelPlugin/phpunit.xml.dist deleted file mode 100644 index 2ad2c8aa46..0000000000 --- a/exampleplugins/legacy/Frontend/SwagModelPlugin/phpunit.xml.dist +++ /dev/null @@ -1,5 +0,0 @@ - - - tests - - diff --git a/exampleplugins/legacy/Frontend/SwagModelPlugin/plugin.json b/exampleplugins/legacy/Frontend/SwagModelPlugin/plugin.json deleted file mode 100644 index 48ce945312..0000000000 --- a/exampleplugins/legacy/Frontend/SwagModelPlugin/plugin.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "label": { - "de": "SwagModelPlugin", - "en": "SwagModelPlugin" - }, - "copyright": "(c) by Shopware AG", - "license": "proprietary", - "link": "http://store.shopware.com", - "author": "Shopware AG", - - "currentVersion": "1.0.0", - - "changelog": { - "de": { - "1.0.0": "Erstveröffentlichung" - }, - "en": { - "1.0.0": "First release" - } - }, - - "compatibility": { - "minimumVersion": "4.3.0", - "maximumVersion": null, - "blacklist": [ - ] - } -} \ No newline at end of file diff --git a/exampleplugins/legacy/Frontend/SwagModelPlugin/tests/Test.php b/exampleplugins/legacy/Frontend/SwagModelPlugin/tests/Test.php deleted file mode 100644 index dc88b378ef..0000000000 --- a/exampleplugins/legacy/Frontend/SwagModelPlugin/tests/Test.php +++ /dev/null @@ -1,33 +0,0 @@ - array( - ) - ); - - public function setUp() - { - parent::setUp(); - - $helper = \TestHelper::Instance(); - $loader = $helper->Loader(); - - - $pluginDir = getcwd() . '/../'; - - $loader->registerNamespace( - 'Shopware\\SwagModelPlugin', - $pluginDir - ); - } - - public function testCanCreateInstance() - { - /** @var Shopware_Plugins_Frontend_SwagModelPlugin_Bootstrap $plugin */ - $plugin = Shopware()->Plugins()->Frontend()->SwagModelPlugin(); - - $this->assertInstanceOf('Shopware_Plugins_Frontend_SwagModelPlugin_Bootstrap', $plugin); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagPluginSystem/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagPluginSystem/Bootstrap.php deleted file mode 100644 index 6806718663..0000000000 --- a/exampleplugins/legacy/Frontend/SwagPluginSystem/Bootstrap.php +++ /dev/null @@ -1,86 +0,0 @@ -subscribeEvent( - 'Enlight_Bootstrap_InitResource_shopware_storefront.seo_category_service', - 'registerSeoCategoryService' - ); - - $this->subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service', - 'registerListProductService' - ); - - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend', - 'addTemplateDir' - ); - - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Widgets', - 'addTemplateDir' - ); - - $this->subscribeEvent( - 'Theme_Compiler_Collect_Plugin_Less', - 'onCollectLessFiles' - ); - - return true; - } - - public function afterInit() - { - $this->get('Loader')->registerNamespace( - 'ShopwarePlugins\SwagPluginSystem', - $this->Path() - ); - } - - /** - * @return \Shopware\Components\Theme\LessDefinition - */ - public function onCollectLessFiles() - { - return new \Shopware\Components\Theme\LessDefinition( - [], - [__DIR__ . '/Views/frontend/_public/src/less/all.less'] - ); - } - - public function addTemplateDir() - { - Shopware()->Container()->get('template')->addTemplateDir(__DIR__ . '/Views/'); - } - - public function registerSeoCategoryService() - { - $seoCategoryService = new SeoCategoryService( - Shopware()->Container()->get('dbal_connection'), - Shopware()->Container()->get('shopware_storefront.category_service') - ); - Shopware()->Container()->set('shopware_storefront.seo_category_service', $seoCategoryService); - } - - public function registerListProductService() - { - Shopware()->Container()->set( - 'shopware_storefront.list_product_service', - new ListProductService( - Shopware()->Container()->get('shopware_storefront.list_product_service'), - Shopware()->Container()->get('shopware_storefront.seo_category_service') - ) - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagSearchBundle/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagSearchBundle/Bootstrap.php deleted file mode 100644 index 8bd8bfb3eb..0000000000 --- a/exampleplugins/legacy/Frontend/SwagSearchBundle/Bootstrap.php +++ /dev/null @@ -1,31 +0,0 @@ -subscribeEvent('Enlight_Controller_Front_StartDispatch', 'registerSubscriber'); - $this->subscribeEvent('Shopware_Console_Add_Command', 'registerSubscriber'); - return true; - } - - public function afterInit() - { - $this->get('Loader')->registerNamespace( - 'ShopwarePlugins\SwagSearchBundle', - $this->Path() - ); - } - - public function registerSubscriber() - { - $this->get('events')->addSubscriber( - new SearchBundleSubscriber( - Shopware()->Container(), - $this->Path() - ) - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagSearchBundle/Subscribers/SearchBundleSubscriber.php b/exampleplugins/legacy/Frontend/SwagSearchBundle/Subscribers/SearchBundleSubscriber.php deleted file mode 100644 index 943e844cf0..0000000000 --- a/exampleplugins/legacy/Frontend/SwagSearchBundle/Subscribers/SearchBundleSubscriber.php +++ /dev/null @@ -1,81 +0,0 @@ -container = $container; - $this->pluginDir = $pluginDir; - } - - /** - * @inheritdoc - */ - public static function getSubscribedEvents() - { - return [ - 'Shopware_SearchBundleDBAL_Collect_Facet_Handlers' => 'registerFacetHandler', - 'Shopware_SearchBundle_Collect_Criteria_Request_Handlers' => 'registerRequestHandler', - - 'Shopware_SearchBundleDBAL_Collect_Sorting_Handlers' => 'registerSortingHandler', - 'Shopware_SearchBundleDBAL_Collect_Condition_Handlers' => 'registerConditionHandler', - - - - 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Listing' => 'extendListingTemplate' - ]; - } - - public function registerRequestHandler() - { - return new CriteriaRequestHandler(); - } - - public function registerConditionHandler() - { - return new EsdConditionHandler(); - } - - public function registerSortingHandler() - { - return new RandomSortingHandler(); - } - - public function registerFacetHandler() - { - return new EsdFacetHandler( - $this->container->get('shopware_searchdbal.dbal_query_builder_factory'), - $this->container->get('snippets') - ); - } - - public function extendListingTemplate(\Enlight_Event_EventArgs $args) - { - $args->getSubject()->View()->addTemplateDir( - $this->pluginDir . '/Views/' - ); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagService/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagService/Bootstrap.php deleted file mode 100644 index fa6a79046e..0000000000 --- a/exampleplugins/legacy/Frontend/SwagService/Bootstrap.php +++ /dev/null @@ -1,95 +0,0 @@ - true, 'invalidateCache' => array()) in order to let the installation - * be successfull - * - * - update: Triggered when the user updates the plugin. You will get passes the former version of the plugin as param - * In order to let the update be successful, return "true" - * - * - uninstall: Triggered when the plugin is reinstalled or uninstalled. Clean up your tables here. - */ -class Shopware_Plugins_Frontend_SwagService_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - /** - * Register the namespace of our plugin globally - */ - public function afterInit() - { - $this->Application()->Loader()->registerNamespace( - 'ShopwarePlugins\SwagService', - $this->Path() - ); - } - - /** - * Return the version of the plugin. - * - * @return mixed - * @throws Exception - */ - public function getVersion() - { - return '1.0.0'; - } - - /** - * Return the label of the plugin - * - * @return string - */ - public function getLabel() - { - return 'SwagService'; - } - - /** - * Register Service + an example controller PreDispatch method - * - * @return bool - */ - public function install() - { - $this->subscribeEvent( - 'Enlight_Bootstrap_InitResource_swag_service_plugin.tax_calculator', - 'onInitTaxCalculator' - ); - - $this->subscribeEvent( - 'Enlight_Controller_Action_PreDispatch_Frontend', - 'onFrontendPreDispatch' - ); - - return true; - } - - /** - * Return an instance of our new service class - * - * @return \ShopwarePlugins\SwagService\Component\TaxCalculator - */ - public function onInitTaxCalculator() - { - return new \ShopwarePlugins\SwagService\Component\TaxCalculator( - $this->get('pluginlogger') - ); - } - - /** - * This will call the service on any Frontend PreDispatch event - * You should see the log message in your /var/log or /logs log file - */ - public function onFrontendPreDispatch() - { - /** @var \ShopwarePlugins\SwagService\Component\TaxCalculator $taxCalculator */ - $taxCalculator = $this->get('swag_service_plugin.tax_calculator'); - $taxCalculator->calculate(13.99, 1.19); - } -} diff --git a/exampleplugins/legacy/Frontend/SwagService/Component/TaxCalculator.php b/exampleplugins/legacy/Frontend/SwagService/Component/TaxCalculator.php deleted file mode 100644 index d0ea7cd09e..0000000000 --- a/exampleplugins/legacy/Frontend/SwagService/Component/TaxCalculator.php +++ /dev/null @@ -1,22 +0,0 @@ -logger = $logger; - } - - public function calculate($netPrice, $tax) - { - // Bypass the two fingers crossed handler, by setting an `alert` message - // By default Shopware would not show `debug` or `info` level debug messages in the log - $this->logger->alert('Calculating price for tax: ' . $tax); - return $netPrice * $tax; - } -} diff --git a/exampleplugins/legacy/Frontend/SwagSloganOfTheDay/Bootstrap.php b/exampleplugins/legacy/Frontend/SwagSloganOfTheDay/Bootstrap.php deleted file mode 100644 index e73c78d210..0000000000 --- a/exampleplugins/legacy/Frontend/SwagSloganOfTheDay/Bootstrap.php +++ /dev/null @@ -1,76 +0,0 @@ -subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend', - 'onFrontendPostDispatch' - ); - - $this->createConfig(); - - return true; - } - - private function createConfig() - { - $this->Form()->setElement( - 'select', - 'font-size', - array( - 'label' => 'Font size', - 'store' => array( - array(12, '12px'), - array(18, '18px'), - array(25, '25px') - ), - 'value' => 12 - ) - ); - - $this->Form()->setElement('boolean', 'italic', array( - 'value' => true, - 'label' => 'Italic' - )); - } - - public function onFrontendPostDispatch(Enlight_Event_EventArgs $args) - { - /** @var \Enlight_Controller_Action $controller */ - $controller = $args->get('subject'); - $view = $controller->View(); - - $view->addTemplateDir( - __DIR__ . '/Views' - ); - - $view->assign('sloganSize', $this->Config()->get('font-size')); - $view->assign('italic', $this->Config()->get('italic')); - $view->assign('slogan', $this->getSlogan()); - } - - public function getSlogan() - { - return array_rand( - array_flip( - array( - 'An apple a day keeps the doctor away', - 'Let’s get ready to rumble', - 'A rolling stone gathers no moss', - ) - ) - ); - } -} diff --git a/exampleplugins/search/SesVariantSearch/CriteriaRequestHandler.php b/exampleplugins/search/SesVariantSearch/CriteriaRequestHandler.php new file mode 100755 index 0000000000..2e40994709 --- /dev/null +++ b/exampleplugins/search/SesVariantSearch/CriteriaRequestHandler.php @@ -0,0 +1,25 @@ +getControllerName() === 'search') { + $criteria->addBaseCondition(new VariantCondition()); + } + + if ($request->getControllerName() !== 'suggest') { + $criteria->addBaseCondition(new VariantCondition()); + } + } +} diff --git a/exampleplugins/search/SesVariantSearch/Resources/services.xml b/exampleplugins/search/SesVariantSearch/Resources/services.xml new file mode 100755 index 0000000000..7a13287956 --- /dev/null +++ b/exampleplugins/search/SesVariantSearch/Resources/services.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/exampleplugins/search/SesVariantSearch/SesVariantSearch.php b/exampleplugins/search/SesVariantSearch/SesVariantSearch.php new file mode 100755 index 0000000000..9aed048986 --- /dev/null +++ b/exampleplugins/search/SesVariantSearch/SesVariantSearch.php @@ -0,0 +1,9 @@ +add('groupBy', 'variant.id', false); + } + + public function supports(CriteriaPartInterface $criteriaPart) + { + return $criteriaPart instanceof VariantCondition; + } + + public function handle( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context + ) { + $criteria->removeCondition('isMain'); + } +} diff --git a/exampleplugins/search/SesVariantSearch/plugin.xml b/exampleplugins/search/SesVariantSearch/plugin.xml new file mode 100755 index 0000000000..e4f17197dd --- /dev/null +++ b/exampleplugins/search/SesVariantSearch/plugin.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/init.sh b/init.sh index 9b767b3cf9..18db696f28 100755 --- a/init.sh +++ b/init.sh @@ -13,5 +13,5 @@ else $composerBin self-update fi -echo "Installing sculpin dependencies" +echo "Installing Sculpin dependencies" $composerBin install --no-dev diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..e999ca9dab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1553 @@ +{ + "name": "devdocs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devdocs", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "grunt": "^1.4.1", + "grunt-contrib-less": "^3.0.0", + "grunt-contrib-watch": "^1.1.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==", + "dependencies": { + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==" + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/continuable-cache": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", + "integrity": "sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", + "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", + "dependencies": { + "string-template": "~0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getobject": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", + "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globule": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", + "dependencies": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/grunt": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", + "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", + "dependencies": { + "dateformat": "~4.6.2", + "eventemitter2": "~0.4.13", + "exit": "~0.1.2", + "findup-sync": "~5.0.0", + "glob": "~7.1.6", + "grunt-cli": "~1.4.3", + "grunt-known-options": "~2.0.0", + "grunt-legacy-log": "~3.0.0", + "grunt-legacy-util": "~2.0.1", + "iconv-lite": "~0.6.3", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "nopt": "~3.0.6" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/grunt-cli": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", + "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", + "dependencies": { + "grunt-known-options": "~2.0.0", + "interpret": "~1.1.0", + "liftup": "~3.0.1", + "nopt": "~4.0.1", + "v8flags": "~3.2.0" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-cli/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/grunt-contrib-less": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-3.0.0.tgz", + "integrity": "sha512-fBB8MUDCo5EgT7WdOVQnZq4GF+XCeFdnkhaxI7uepp8P973sH1jdodjF87c6d9WSHKgArJAGP5JEtthhdKVovg==", + "dependencies": { + "async": "^3.2.0", + "chalk": "^4.1.0", + "less": "^4.1.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-contrib-watch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", + "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", + "dependencies": { + "async": "^2.6.0", + "gaze": "^1.1.0", + "lodash": "^4.17.10", + "tiny-lr": "^1.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-watch/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/grunt-known-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", + "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", + "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", + "dependencies": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", + "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", + "dependencies": { + "chalk": "~4.1.0", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-legacy-util": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", + "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", + "dependencies": { + "async": "~3.2.0", + "exit": "~0.1.2", + "getobject": "~1.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.21", + "underscore.string": "~3.3.5", + "which": "~2.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", + "engines": { + "node": "*" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==" + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/liftup": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", + "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^4.0.0", + "fined": "^1.2.0", + "flagged-respawn": "^1.0.1", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.1", + "rechoir": "^0.7.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/liftup/node_modules/findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/livereload-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "optional": true + }, + "node_modules/qs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", + "dependencies": { + "bytes": "1", + "string_decoder": "0.10" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-json-parse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "optional": true + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "node_modules/string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tiny-lr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", + "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", + "dependencies": { + "body": "^5.1.0", + "debug": "^3.1.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.3.0", + "object-assign": "^4.1.0", + "qs": "^6.4.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/underscore.string/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..e15f3a03c5 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "devdocs", + "private": true, + "version": "1.0.0", + "description": "Shopware Developer Documentation", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/shopware5/devdocs.git" + }, + "keywords": [ + "devdocs", + "shopware" + ], + "author": "klarstil", + "license": "MIT", + "bugs": { + "url": "https://github.com/shopware5/devdocs/issues" + }, + "homepage": "https://github.com/shopware5/devdocs#readme", + "dependencies": { + "grunt": "^1.4.1", + "grunt-contrib-less": "^3.0.0", + "grunt-contrib-watch": "^1.1.0" + } +} diff --git a/source/99-paper-cuts/img/header.jpg b/source/99-paper-cuts/img/header.jpg deleted file mode 100644 index 6be7b18bc5..0000000000 Binary files a/source/99-paper-cuts/img/header.jpg and /dev/null differ diff --git a/source/99-paper-cuts/index.md b/source/99-paper-cuts/index.md deleted file mode 100644 index b5c0c79d30..0000000000 --- a/source/99-paper-cuts/index.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -layout: default -title: 99 Paper Cuts -indexed: true -github_link: 99-paper-cuts/index.md ---- - - - -## What is the 99 Papercut Project? - -As an open source project, Shopware’s strength is attributed to the feedback and involvement from our vibrant Community. So in honor of this philosophy, we created the project “99 Papercuts” to encourage your direct involvement in making Shopware the best possible eCommerce software for your needs. - -In the first phase of the project, we asked for your suggestions on how Shopware could be tweaked and made even better. To make this project a huge success, we need the added manpower from your contributions in this second phase. - -## Get involved in making Shopware an even better solution! - - - -Starting on Monday, 5th September and continuing until Monday, 19th September, we would like to tackle as many papercut bugs as possible, together with developers and coders from our Community. - -## How you can contribute: - -1. Join our [IRC Channel](/contributing/irc) for direct communication with us and the rest of the community. -2. Find a papercut that you want to fix from the existing papercuts. - - When you start working please write a comment like "Working on it" into the ticket. -3. Create a Pull Request on [Github](https://github.com/shopware/shopware). - - Please read [How to create a Pull Request](/contributing/contributing-code/) - - The pull request must be based on the `99-papercuts` branch (`git checkout -b my-new-feature upstream/99-papercuts`) - - Prefix the Pull Request Title with `[Papercut]` like this: `[Papercut] Fix spelling of newsletter` - - The commit message must be formatted with the Issue number like this: `SW-9999 - Fix spelling of newsletter` - - When you have opened the pull request please add a comment with the pull request URL to the ticket -4. Our team will review and test your code, please watch out for feedback in the pull request -5. If the code is successfully reviewed and tested it will be merged into the `99-papercuts` branch -6. `GOTO 2.` - -## Take your pick from submitted papercuts - -Don't know where to start? You can also see all existing papercuts in the Issue Tracker. Papercuts with the most votes can be considered priority! - -## Direct contact & help from Shopware developers - -The best tool to directly communicate with the Community and shopware developers is the [\#shopware IRC Channel](/contributing/irc). -During the time of the papercut project, we will be paying close attention to this channel and supporting you with your pull requests. - -## Brag about it on Twitter -Please use the hashtag [#99papercuts](https://twitter.com/hashtag/99papercuts) to make some noise on Twitter. We look forward to putting your work in the spotlight! diff --git a/source/_layouts/default.twig.html b/source/_layouts/default.twig.html index bcc1816b3e..2d892d7d26 100644 --- a/source/_layouts/default.twig.html +++ b/source/_layouts/default.twig.html @@ -1,6 +1,17 @@ + + + + @@ -13,21 +24,45 @@ {% if page.menu_chapter %} + {% elseif page.robots.hide == true %} + {% endif %} - - + + + + + + + + + + + + + + + + + + + + + +
@@ -37,7 +72,7 @@
@@ -52,45 +87,86 @@ + {% block content_main %}
+ Top
+ {% endblock %} + - + + {% endblock %}
- - - + + + - + + + {% block scripts_after %} {% endblock %} -{% if site.google_tag_manager_id %} - - - - -{% endif %} +{% include 'usercentrics.twig' %} diff --git a/source/_layouts/guides.twig.html b/source/_layouts/guides.twig.html new file mode 100644 index 0000000000..05b15a844b --- /dev/null +++ b/source/_layouts/guides.twig.html @@ -0,0 +1,45 @@ +{% extends "default.twig.html" %} + {% block content_main %} +
+ + + +
+ + {% block content_wrapper %}{% block content %}{% endblock %}{% endblock %} + + {% if page.history is defined %} +

Version History

+ + + + + + + + + {% for changeDate, changeText in page.history|reverse(true) %} + + + + + {% endfor %} + +
DateChanges
{{ changeDate|date("Y-m-d") }}{{ changeText }}
+ {% endif %} +
+ +
+ Top +
+ +
+
+
+
+{% endblock %} + diff --git a/source/_layouts/labs.twig.html b/source/_layouts/labs.twig.html new file mode 100644 index 0000000000..1a4cb17c2f --- /dev/null +++ b/source/_layouts/labs.twig.html @@ -0,0 +1,5 @@ +{% extends "default.twig.html" %} + +{% block footer_main %} + {{ parent() }} +{% endblock %} \ No newline at end of file diff --git a/source/_layouts/landingpage.twig.html b/source/_layouts/landingpage.twig.html new file mode 100644 index 0000000000..f8f4920a19 --- /dev/null +++ b/source/_layouts/landingpage.twig.html @@ -0,0 +1,17 @@ +{% extends "default.twig.html" %} + +{% block content_main %} + + +
+ {% block content_wrapper %}{% block content %}{% endblock %}{% endblock %} +
+{% endblock %} + +{% block footer_main %} + {{ parent() }} +{% endblock %} \ No newline at end of file diff --git a/source/_layouts/start.twig.html b/source/_layouts/start.twig.html new file mode 100644 index 0000000000..1b84eff5df --- /dev/null +++ b/source/_layouts/start.twig.html @@ -0,0 +1,368 @@ + + + + + + + + + + + {% if page.title is defined %}{{ page.title }}{% else %}{{ page.project.name }}{% endif %} + + {% block head_meta %} + + {% endblock %} + + {% if page.menu_chapter %} + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ + + + + + {% block content_main %} + + +
+
+
+
Getting started
+
+
+

Set up

+
Start developing with Shopware.
+ +
+
+
+

Themes

+
Learn how to create themes.
+ +
+
+
+

Plugins

+
Learn how to extend Shopware.
+ +
+
+
+
+ +
+
+ + + + +
+ + + +
+ + {% block content_wrapper %}{% block content %}{% endblock %}{% endblock %} + + {% if page.history is defined %} +

Version History

+ + + + + + + + + {% for changeDate, changeText in page.history|reverse(true) %} + + + + + {% endfor %} + +
DateChanges
{{ changeDate|date("Y-m-d") }}{{ changeText }}
+ {% endif %} +
+ +
+
+ + +
+ + +
+
+ +

Contribute & Resources

+ + + + + +
+
+ + + +
+
+ +
+
+ +
+
+
+
+
+
+
Niklas Dzösch
+

Shopware Evangelist and Core Developer

+
+ +
+
+ +
+

Develop your own unique theme

+

Here Shopware Evangelist and Core Developer, Niklas Dzösch, takes you step-by-step through the process of developing your own unique Shopware theme. From installation to uploading your own theme in our store, this video series provides you with first-hand experience so that you can easily start creating your own Shopware themes today.

+
+ + +
+
+ + +
+

Latest changes

+
+ + show more + show less + +
+ {% include 'latest_changes.twig' %} +
+
+
+ +
+ + + {% endblock %} + + + {% block footer_main %} + + + + {% endblock %} +
+ + + + + + + + + + + + +{% block scripts_after %} +{% endblock %} + +{% include 'usercentrics.twig' %} + + + diff --git a/source/_partials/api_badge.twig b/source/_partials/api_badge.twig new file mode 100644 index 0000000000..c0b0e7fd0a --- /dev/null +++ b/source/_partials/api_badge.twig @@ -0,0 +1,13 @@ +
+ + {{ method }} + + + {{ route|raw }} + + {% if body %} + + + + {% endif %} +
diff --git a/source/_partials/github_footer.twig b/source/_partials/github_footer.twig new file mode 100644 index 0000000000..77ce7dce6d --- /dev/null +++ b/source/_partials/github_footer.twig @@ -0,0 +1,27 @@ +
+

...and many more exciting features

+ +
+ +
+

+ Now it's your turn to check out the current development state. We published a branch on GitHub which contains all the changes regarding our research topics. +

+

+ + + + + Open on + + + + GitHub + + +

+
+
+
diff --git a/source/_partials/latest_changes.twig b/source/_partials/latest_changes.twig new file mode 100644 index 0000000000..5539ca7465 --- /dev/null +++ b/source/_partials/latest_changes.twig @@ -0,0 +1,13 @@ +

{% if title %}{{ title }}{% else %}Latest changes{% endif %}

+ +{% for historyDate, historyArticles in page.docHistory %} +
+

{{ historyDate|date("F jS Y") }}

+ +
    + {% for articleTitle, articleLink in historyArticles %} +
  • {{ articleTitle }}
  • + {% endfor %} +
+
+{% endfor %} \ No newline at end of file diff --git a/source/_partials/usercentrics.twig b/source/_partials/usercentrics.twig new file mode 100644 index 0000000000..6e365ca74d --- /dev/null +++ b/source/_partials/usercentrics.twig @@ -0,0 +1,285 @@ + + + + diff --git a/source/assets/css/_imports/alerts.less b/source/assets/css/_imports/alerts.less index aa99e14cf6..bb5c600748 100644 --- a/source/assets/css/_imports/alerts.less +++ b/source/assets/css/_imports/alerts.less @@ -1,7 +1,7 @@ .alert { padding: 10px 20px; margin-bottom: 20px; - border: 2px solid rgba(0, 0, 0, 0); + border: 1px solid rgba(0, 0, 0, 0); .border-radius(5px); line-height: 26px; @@ -11,26 +11,26 @@ } .alert-success { - color: darken(@success, 30%); + color: @success; border-color: fade(@success, 40%); - background: fade(@success, 20%); + background: fade(@success, 10%); } .alert-info { - color: darken(@blue01, 30%); + color: @blue01; border-color: fade(@blue01, 40%); - background: fade(@blue01, 20%); + background: fade(@blue01, 10%); } -.alert-warning, -.alert-error { - color: darken(@warning, 30%); +.alert-warning { + color: #d49000; border-color: fade(@warning, 40%); - background: fade(@warning, 20%); + background: fade(@warning, 10%); } -.alert-danger { - color: darken(@danger, 30%); +.alert-danger, +.alert-error { + color: @danger; border-color: fade(@danger, 40%); - background: fade(@danger, 20%); + background: fade(@danger, 10%); } \ No newline at end of file diff --git a/source/assets/css/_imports/api-badge.less b/source/assets/css/_imports/api-badge.less new file mode 100644 index 0000000000..a432f0358d --- /dev/null +++ b/source/assets/css/_imports/api-badge.less @@ -0,0 +1,109 @@ +.api-badge { + + display: flex; + position: relative; + width: 100%; + box-sizing: border-box; + + & + pre, & + .code-wrapper pre { + margin-top: 0; + + code.hljs { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + & + p { + margin-top: 1rem; + } + + .method, .route { + display: block; + padding: .25rem .5rem; + box-sizing: border-box; + } + + .method { + color: #fff; + font-weight: bold; + border-radius: 5px 0 0 5px; + } + + .route { + width: 100%; + color: @text-color; + border-radius: 0 5px 5px 0; + } + + .icon-copy { + display: inline-block; + position: absolute; + right: 1rem; + font-size: .8rem; + line-height: 1.5rem; + color: rgba(0, 0, 0, .33); + cursor: pointer; + + &:hover { + color: rgba(0, 0, 0, .66); + } + + span { + font: @main-font 'Inter', Arial, sans-serif; + font-weight: inherit; + font-size: inherit; + color: inherit; + } + } + + &.body { + .method { + border-radius: 5px 0 0 0; + } + + .route { + border-radius: 0 5px 0 0; + } + } + + &-get { + .method { + background-color: @highlight-color-blue; + } + + .route { + background-color: lighten(@highlight-color-blue, 40%); + } + } + + &-post { + .method { + background-color: @highlight-color-green; + } + + .route { + background-color: lighten(@highlight-color-green, 30%); + } + } + + &-put { + .method { + background-color: @highlight-color-yellow; + } + + .route { + background-color: lighten(@highlight-color-yellow, 30%); + } + } + + &-delete { + .method { + background-color: @highlight-color-red; + } + + .route { + background-color: lighten(@highlight-color-red, 40%); + } + } +} diff --git a/source/assets/css/_imports/buttons.less b/source/assets/css/_imports/buttons.less index 1cb2b57aa7..8769821e38 100644 --- a/source/assets/css/_imports/buttons.less +++ b/source/assets/css/_imports/buttons.less @@ -288,4 +288,71 @@ top: 50%; margin-top: -7px; } +} + +.btn--github { + border: 2px solid #000; + border-radius: 3px; + margin: 50px 0 0; + padding: 20px 20px 20px 65px; + color: #000; + position: relative; + display: inline-block; + text-align: left; + transition: all 0.2s ease-out; + + .github--logo-path { + transition: all 0.2s ease-out; + } + + &:hover { + color: #fff; + border-color: #fff; + + .github--logo-path { + fill: #fff; + } + } + + span { + font-weight: bold; + display: block; + font-size: 18px; + line-height: 21px; + } + + .github--logo { + display: block; + position: absolute; + height: 64px; + width: 64px; + left: 4px; + top: 12px; + } + + + .github--top-part { + text-transform: uppercase; + } + + .github--bottom-part { + font-size: 32px; + } +} + +.landingpage--btn { + border: 2px solid @blue02; + border-radius: 5px; + color: @blue02; + display: inline-block; + margin-top: 50px; + padding: 11px 20px; + font-weight: bold; + transition: all 0.2s ease-out; + font-size: 18px; + + &:hover { + color: #fff; + background: @blue02; + } } \ No newline at end of file diff --git a/source/assets/css/_imports/codeExpand.less b/source/assets/css/_imports/codeExpand.less index f718827bd2..199a71e393 100644 --- a/source/assets/css/_imports/codeExpand.less +++ b/source/assets/css/_imports/codeExpand.less @@ -5,26 +5,41 @@ pre code { } .code-expandButton { - background-color: #E8E8E8; + background-color: #607182; + border-radius: 30px; bottom: 0; color: #777777; cursor: pointer; display: block; font-family: 'Open Sans', sans-serif; - font-size: 12px; + font-size: 14px; + color: #FFF; font-weight: 600; - height: 30px; - padding-top: 5px; + padding: 13px 10px; position: absolute; right: 0; text-align: center; width: 15%; + min-width: 150px; z-index: 100; - border-bottom-right-radius: 3px; + transition: all .3s cubic-bezier(0.25, 0.2, 0.45, 1); + -moz-transform: translate(-50%, 50%); + -ms-transform: translate(-50%, 50%); + -o-transform: translate(-50%, 50%); + -webkit-transform: translate(-50%, 50%); + transform: translate(-50%, 50%); + margin-right: 35%; } + .code-expandButton:hover { - background-color: #d8d8d8; - color: #555; + background-color: #395467; + color: #FFF; + -moz-transform: translate(-50%, calc(50% - 3px)); + -ms-transform: translate(-50%, calc(50% - 3px)); + -o-transform: translate(-50%, calc(50% - 3px)); + -webkit-transform: translate(-50%, calc(50% - 3px)); + transform: translate(-50%, calc(50% - 3px)); + box-shadow: 0 0.5rem 1rem -0.25rem rgba(57, 84, 103, 0.33); } .code-wrapper { @@ -43,4 +58,5 @@ pre code { .code-collapsed { height: 370px; overflow: hidden; -} \ No newline at end of file + margin-bottom: 60px; +} diff --git a/source/assets/css/_imports/des-guide.less b/source/assets/css/_imports/des-guide.less new file mode 100644 index 0000000000..740d78b4c4 --- /dev/null +++ b/source/assets/css/_imports/des-guide.less @@ -0,0 +1,324 @@ +.start-guide { + .des-guide { + text-align: center; + + img { + margin-top: 0; + width: 100px; + } + } + + h1:first-of-type { + color: #142432; + font-size: 33px; + font-weight: 600; + margin-bottom: 0; + padding-bottom: 0; + } + + .info { + font-weight: 600; + } + + .anchorjs-icon:before { + content: ''; + } + + .row { + clear: both; + } +} + +/*------- START & END COLUMN -------*/ +.des-guide--container { + display: inline-block; + padding-top: 20px; + padding-bottom: 30px; + width: 98%; + + .col { + padding: 5% 1%; + text-align: center; + width: 98%; + } + + .say { + color: darken(@text-color, 20%); + display: block; + font-size: 18px; + font-weight: 500; + font-style: italic; + text-align: center; + margin-bottom: 10px; + padding: 8px 0 0 0; + + small { + color: lighten(@text-color, 20%); + font-size: 12px; + font-weight: 500; + font-style: normal; + } + } + + .designer-img { + background: url("../../img/des-img-one.jpg") center center no-repeat; + background-size: cover; + border-radius: 100%; + box-shadow: 0 9px 20px 0 rgba(0,0,0,0.14); + height: 110px; + margin: 0 auto; + width: 110px; + } + + h2 { + font-size: 22px; + margin: 2px 0; + } + + h3 { + font-size: 33px; + font-weight: 600; + } + + .styletile-headline { + padding: 2% 1% 0 1%; + } + + .udemy-teaser--screenshot { + background: url("../../img/screen-des-udemy.jpg") top center no-repeat; + background-size: contain; + height: 170px; + margin: 0 auto; + width: 278px; + } + + .styletile--screenshot { + background: url("../../img/screen-des-styletile.svg") top center no-repeat; + background-size: contain; + margin: 0 auto; + height: 170px; + width: auto; + } + + .styletile--dialog { + text-align: left; + padding: 0 0 20px 20px; + clear: both; + + .avatar-one { + background: url("../../img/styletile-preview.svg") center center no-repeat; + background-size: cover; + float: left; + height: 105px; + width: 80px; + } + + .avatar-two { + background: url("../../img/styletile-cheatsheet.svg") center center no-repeat; + background-size: cover; + margin-top: 10px; + float: left; + height: 60px; + width: 80px; + } + + h4 { + color: darken(@text-color, 20%); + font-size: 24px; + } + + .chat { + color: @text-color; + display: block; + font-weight: 500; + padding-left: 120px; + + } + } +} + +/*------- BUTTON STYLES -------*/ +.start-guide { + .button { + clear: both; + margin: 0 auto; + text-align: center; + padding-top: 10px; + + a { + background: @gray05; + border-radius: 30px; + box-shadow: 0 0.5rem 1rem -0.25rem rgba(0, 0, 0, 0.12); + color: #FFFFFF; + display: inline-block; + font-weight: 600; + font-size: 16px; + padding: 13px 25px; + transition: all .3s cubic-bezier(0.25, 0.2, 0.45, 1); + + &:hover { + background: darken(@gray05, 9%); + box-shadow: 0 0.5rem 1rem -0.25rem rgba(0, 0, 0, 0.33); + transform: translateY(-3px); + -webkit-transform: translateY(-3px); + -moz-transform: translateY(-3px); + -ms-transform: translateY(-3px); + -o-transform: translateY(-3px); + } + } + } + + .udemy-teaser .button a { + background: #FA6179; + box-shadow: 0 0.5rem 1rem -0.3rem rgba(250, 97, 121, 0.22); + color: #FFF; + + &:hover { + background: darken(#FA6179, 5%); + box-shadow: 0 0.5rem 1rem -0.3rem rgba(250, 97, 121, 0.44); + } + } +} + +/*------- MENUE BOXES -------*/ +.des-guide--menu { + display: inline-block; + width: 100%; + + a.col { + color: @text-color; + } + + .col { + background: #FFFFFF; + box-shadow: 0 5px 20px 0 rgba(12, 21, 75, 0.12); + border-radius: 8px; + font-size: 14px; + font-weight: 500; + float: left; + margin: 1%; + padding: 20px 15px; + text-align: center; + transition: all .3s cubic-bezier(0.25, 0.2, 0.45, 1); + width: 98%; + + &:hover { + box-shadow: 0 5px 25px 0 rgba(12, 21, 85, 0.23); + transform: translateY(-3px); + -webkit-transform: translateY(-2px); + -moz-transform: translateY(-2px); + -ms-transform: translateY(-2px); + -o-transform: translateY(-2px); + } + + h2 { + color: #142432; + font-size: 21px; + line-height: 115%; + margin: 0 0 5px 0; + padding: 0; + } + } +} + +@media screen and (min-width: 768px) { + .start-guide .des-guide img { + width: 180px; + } + + .start-guide h1:first-of-type { + font-size: 53px; + } + + .des-guide--container .col { + float: left; + width: 98%; + padding: 5% 1%; + + &.medium { + width: 60%; + } + + &.small { + width: 40%; + } + + .styletile--screenshot { + background-position: top left; + margin-top: 20px; + } + } + + .des-guide--container .styletile-headline { + padding: 5% 10% 0 10%; + } + + .des-guide--container .col .say { + font-size: 20px; + padding: 8px 25px 0px 25px; + } + + .des-guide--menu .col { + margin: 1%; + min-height: 140px; + padding: 40px 15px; + width: 31%; + } +} + +@media screen and (min-width: 1200px) { + .start-guide .des-guide img { + margin-top: -30px; + width: 180px; + } + + .start-guide h1:first-of-type { + font-size: 53px; + } + + .des-guide--menu .col { + min-height: auto; + } +} + +@media screen and (min-width: 1400px) { + .bubble { + overflow: hidden; + } + + .bubble--big { + background-color: rgba(0, 73, 255, 0.05); + border-radius: 100%; + left: 85%; + position: fixed; + top: 3%; + height: 660px; + width: 660px; + z-index: 0; + } + + .bubble--small { + background-color: rgba(24, 158, 255, 0.09); + border-radius: 100%; + height: 200px; + width: 200px; + bottom: 8%; + right: 15px; + position: fixed; + z-index: 0; + } +} + +@media screen and (min-width: 1700px) { + .bubble--big { + background-color: rgba(0, 73, 255, 0.03); + height: 900px; + width: 900px; + left: 78%; + } + + .bubble--small { + background-color: rgba(24, 158, 255, 0.06); + right: 10%; + } +} \ No newline at end of file diff --git a/source/assets/css/_imports/dev-guide.less b/source/assets/css/_imports/dev-guide.less new file mode 100644 index 0000000000..c21afb3c2a --- /dev/null +++ b/source/assets/css/_imports/dev-guide.less @@ -0,0 +1,321 @@ +.start-guide { + .dev-guide { + text-align: center; + + img { + margin-top: 0; + width: 120px; + } + } + + h1:first-of-type { + color: #142432; + font-size: 33px; + font-weight: 600; + margin-bottom: 0; + padding-bottom: 0; + } + + .info.guide { + font-weight: 600; + } + + .anchorjs-icon:before { + content: ''; + } + + .row { + clear: both; + } +} + +/*------- START & END COLUMN -------*/ +.dev-guide--container { + display: inline-block; + padding-top: 20px; + padding-bottom: 30px; + width: 98%; + + .col { + padding: 5% 1%; + text-align: center; + width: 98%; + } + + .say { + color: darken(@text-color, 20%); + display: block; + font-size: 18px; + font-weight: 500; + font-style: italic; + text-align: center; + margin-bottom: 10px; + padding: 8px 0 0 0; + + small { + color: lighten(@text-color, 20%); + font-size: 12px; + font-weight: 500; + font-style: normal; + } + } + + .developer-img { + background: url("../../img/img-one.jpg") center center no-repeat; + background-size: cover; + border-radius: 100%; + box-shadow: 0 9px 20px 0 rgba(0,0,0,0.14); + height: 110px; + margin: 0 auto; + width: 110px; + } + + h2 { + font-size: 22px; + margin: 2px 0; + } + + h3 { + font-size: 33px; + font-weight: 600; + } + + .gitter-headline { + padding: 2% 1% 0 1%; + } + + .udemy-teaser--screenshot { + background: url("../../img/screen-dev-udemy.jpg") top center no-repeat; + background-size: contain; + height: 170px; + margin: 0 auto; + width: 278px; + } + + .gitter--screenshot { + background: url("../../img/screen-dev-slack.jpg") top center no-repeat; + background-size: contain; + margin: 0 auto; + height: 250px; + width: auto; + } + + .gitter--dialog { + text-align: left; + padding: 0 0 20px 20px; + + .avatar-one { + background: url("../../img/img-one.jpg") center center no-repeat; + background-size: cover; + border-radius: 3px; + float: left; + margin-top: 10px; + height: 45px; + width: 45px; + } + + .avatar-two { + background: url("../../img/dnoegel.png") center center no-repeat; + background-size: cover; + margin-top: 10px; + border-radius: 3px; + float: left; + height: 45px; + width: 45px; + } + + .chat { + color: darken(@text-color, 20%); + display: block; + font-weight: 500; + padding-left: 60px; + + small { + color: lighten(@text-color, 20%); + font-size: 12px; + font-weight: 500; + } + } + } +} + +/*------- BUTTON STYLES -------*/ +.start-guide { + .button { + clear: both; + margin: 0 auto; + text-align: center; + padding-top: 10px; + + a { + background: @gray05; + border-radius: 30px; + box-shadow: 0 0.5rem 1rem -0.25rem rgba(0, 0, 0, 0.12); + color: #FFFFFF; + display: inline-block; + font-weight: 600; + font-size: 16px; + padding: 13px 25px; + transition: all .3s cubic-bezier(0.25, 0.2, 0.45, 1); + + &:hover { + background: darken(@gray05, 9%); + box-shadow: 0 0.5rem 1rem -0.25rem rgba(0, 0, 0, 0.33); + transform: translateY(-3px); + -webkit-transform: translateY(-3px); + -moz-transform: translateY(-3px); + -ms-transform: translateY(-3px); + -o-transform: translateY(-3px); + } + } + } + + .udemy-teaser .button a { + background: #FA6179; + box-shadow: 0 0.5rem 1rem -0.3rem rgba(250, 97, 121, 0.22); + color: #FFF; + + &:hover { + background: darken(#FA6179, 5%); + box-shadow: 0 0.5rem 1rem -0.3rem rgba(250, 97, 121, 0.44); + } + } +} + +/*------- MENUE BOXES -------*/ +.dev-guide--menu { + display: inline-block; + width: 100%; + + a.col { + color: @text-color; + } + + .col { + background: #FFFFFF; + box-shadow: 0 5px 20px 0 rgba(12, 21, 75, 0.12); + border-radius: 8px; + font-size: 14px; + font-weight: 500; + float: left; + margin: 1%; + padding: 20px 15px; + text-align: center; + transition: all .3s cubic-bezier(0.25, 0.2, 0.45, 1); + width: 98%; + + &:hover { + box-shadow: 0 5px 25px 0 rgba(12, 21, 85, 0.23); + transform: translateY(-3px); + -webkit-transform: translateY(-2px); + -moz-transform: translateY(-2px); + -ms-transform: translateY(-2px); + -o-transform: translateY(-2px); + } + + h2 { + color: #142432; + font-size: 21px; + line-height: 115%; + margin: 0 0 5px 0; + padding: 0; + } + } +} + +@media screen and (min-width: 768px) { + .start-guide .dev-guide img { + width: 180px; + } + + .start-guide h1:first-of-type { + font-size: 53px; + } + + .dev-guide--container .col { + padding: 5% 1%; + float: left; + width: 98%; + + &.medium { + width: 60%; + } + + &.small { + width: 40%; + } + + .gitter-headline { + padding: 5% 10% 0 10%; + } + + .say { + font-size: 20px; + padding: 8px 25px 0px 25px; + } + } + + .dev-guide--menu .col { + margin: 1%; + min-height: 140px; + padding: 40px 15px; + width: 31%; + } +} + +@media screen and (min-width: 1200px) { + .start-guide .dev-guide img { + margin-top: -30px; + width: 220px; + } + + .start-guide h1:first-of-type { + font-size: 53px; + } + + .dev-guide--menu .col { + min-height: auto; + } +} + +@media screen and (min-width: 1400px) { + .bubble { + overflow: hidden; + } + + .bubble--big { + background-color: rgba(0, 73, 255, 0.05); + border-radius: 100%; + left: 85%; + position: fixed; + top: 3%; + height: 660px; + width: 660px; + z-index: 0; + } + + .bubble--small { + background-color: rgba(24, 158, 255, 0.09); + border-radius: 100%; + height: 200px; + width: 200px; + bottom: 8%; + right: 15px; + position: fixed; + z-index: 0; + } +} + +@media screen and (min-width: 1700px) { + .bubble--big { + background-color: rgba(0, 73, 255, 0.03); + height: 900px; + width: 900px; + left: 78%; + } + + .bubble--small { + background-color: rgba(24, 158, 255, 0.06); + right: 10%; + } +} diff --git a/source/assets/css/_imports/font.less b/source/assets/css/_imports/font.less new file mode 100644 index 0000000000..a8809e2256 --- /dev/null +++ b/source/assets/css/_imports/font.less @@ -0,0 +1,20 @@ +// Inter +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: local('Inter'), + url('../../fonts/InterVariable.woff2') format('woff2'); + } + + @font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: local('Inter Italic'), + url('../../fonts/InterVariable-Italic.woff2') format('woff2'); + } + +@font-family-default: 'Inter', Arial, Helvetica, sans-serif; diff --git a/source/assets/css/_imports/icons-sw.less b/source/assets/css/_imports/icons-sw.less new file mode 100644 index 0000000000..7c330ce11c --- /dev/null +++ b/source/assets/css/_imports/icons-sw.less @@ -0,0 +1,1323 @@ +@font-face { + font-family: shopware_website; + src: url("../../fonts/shopware_website.eot?-g83jdd"); + src: url("../../fonts/shopware_website.eot?#iefix-g83jdd") format("embedded-opentype"), url("../../fonts/shopware_website.woff?-g83jdd") format("woff"), url("../../fonts/shopware_website.ttf?-g83jdd") format("truetype"), url("../../fonts/shopware_website.svg?-g83jdd#shopware") format("svg"); + font-weight: 400; + font-style: normal +} + +.iconfont { + font-family: shopware_website; + speak: none; + font-style: normal; + font-weight: 400; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.icon-block { + display: block +} + +.icon-block > img { + width: 36px; + max-height: 36px +} + +.icon-left { + display: block; + float: left; + margin-right: 26px +} + +.icon-left > img { + position: relative; + top: 14px; + width: 40px; + max-height: 40px +} + +.icon-left > img.one--height { + top: 0 +} + +[class*=" icon--"], [class^=icon--] { + font-family: shopware_website; + speak: none; + font-style: normal; + font-weight: 400; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +[class*=" icon--"].has--circle, [class^=icon--].has--circle { + border: 1px solid; + padding: 7px; + border-radius: 50% +} + +.icon--shopping-cart:before { + content: "\F07A" +} + +.icon--googleplus3:before { + content: "\E800" +} + +.icon--googleplus4:before { + content: "\E801" +} + +.icon--kununu:before { + content: "\E733" +} + +.icon--instagram2:before { + content: "\E734" +} + +.icon--xing2:before { + content: "\E802" +} + +.icon--xing:before { + content: "\E724" +} + +.icon--youtube:before { + content: "\E725" +} + +.icon--pp-importexport:before { + content: "\E726" +} + +.icon--pp-advancedcart:before { + content: "\E727" +} + +.icon--pp-bonus:before { + content: "\E728" +} + +.icon--pp-liveshopping:before { + content: "\E729" +} + +.icon--pp-intelligentnewsletter:before { + content: "\E72A" +} + +.icon--pp-businessessentials:before { + content: "\E72B" +} + +.icon--pp-subshop:before { + content: "\E72C" +} + +.icon--pp-intelligentsearch:before { + content: "\E72D" +} + +.icon--pp-bundle:before { + content: "\E72E" +} + +.icon--pp-advancedpromotionsuite:before { + content: ""; + background-image: url("/media/unknown/icon-aps.svg"); + height: 26px; + width: 26px; + display: inherit +} + +.icon--pp-abocommerce:before { + content: "\E72F" +} + +.icon--pp-ticketsystem:before { + content: "\E730" +} + +.icon--pp-customproducts:before { + content: "\E731" +} + +.icon--pp-productadvisor:before { + content: "\E732" +} + +.icon--coupon:before { + content: "\E71C" +} + +.icon--truck:before { + content: "\E71D" +} + +.icon--star-half:before { + content: "\E71E" +} + +.icon--logout:before { + content: "\E614" +} + +.icon--grid:before { + content: "\E615" +} + +.icon--filter:before { + content: "\E616" +} + +.icon--clock:before { + content: "\E617" +} + +.icon--arrow-up:before { + content: "\E610" +} + +.icon--arrow-right:before { + content: "\E60F" +} + +.icon--arrow-left:before { + content: "\E611" +} + +.icon--arrow-down:before { + content: "\E612" +} + +.icon--star:before { + content: "\E600" +} + +.icon--star-empty:before { + content: "\E601" +} + +.icon--shopware:before { + content: "\E602" +} + +.icon--service:before { + content: "\E603" +} + +.icon--search:before { + content: "\E604" +} + +.icon--numbered-list:before { + content: "\E605" +} + +.icon--menu:before { + content: "\E606" +} + +.icon--mail:before { + content: "\E607" +} + +.icon--list:before { + content: "\E608" +} + +.icon--layout:before { + content: "\E609" +} + +.icon--heart:before { + content: "\E60A" +} + +.icon--cross:before { + content: "\E60D" +} + +.icon--compare:before { + content: "\E60B" +} + +.icon--check:before { + content: "\E60C" +} + +.icon--basket:before { + content: "\E60E" +} + +.icon--account:before { + content: "\E613" +} + +.icon--delicious:before { + content: "\F1A5" +} + +.icon--digg:before { + content: "\F1A6" +} + +.icon--phone:before { + content: "\E619" +} + +.icon--mobile:before { + content: "\E61D" +} + +.icon--mouse:before { + content: "\E61E" +} + +.icon--directions:before { + content: "\E61F" +} + +.icon--paperplane:before { + content: "\E620" +} + +.icon--pencil:before { + content: "\E618" +} + +.icon--feather:before { + content: "\E621" +} + +.icon--paperclip:before { + content: "\E622" +} + +.icon--drawer:before { + content: "\E623" +} + +.icon--reply:before { + content: "\E624" +} + +.icon--reply-all:before { + content: "\E625" +} + +.icon--forward:before { + content: "\E626" +} + +.icon--users:before { + content: "\E627" +} + +.icon--user-add:before { + content: "\E628" +} + +.icon--vcard:before { + content: "\E629" +} + +.icon--export:before { + content: "\E62A" +} + +.icon--location:before { + content: "\E62B" +} + +.icon--map:before { + content: "\E62C" +} + +.icon--compass:before { + content: "\E62D" +} + +.icon--location2:before { + content: "\E62E" +} + +.icon--target:before { + content: "\E62F" +} + +.icon--share:before { + content: "\E630" +} + +.icon--sharable:before { + content: "\E631" +} + +.icon--thumbsup:before { + content: "\E632" +} + +.icon--thumbsdown:before { + content: "\E633" +} + +.icon--chat:before { + content: "\E634" +} + +.icon--comment:before { + content: "\E61A" +} + +.icon--quote:before { + content: "\E635" +} + +.icon--house:before { + content: "\E636" +} + +.icon--popup:before { + content: "\E637" +} + +.icon--flashlight:before { + content: "\E638" +} + +.icon--printer:before { + content: "\E639" +} + +.icon--bell:before { + content: "\E63A" +} + +.icon--link:before { + content: "\E63B" +} + +.icon--flag:before { + content: "\E63C" +} + +.icon--cog:before { + content: "\E63D" +} + +.icon--tools:before { + content: "\E63E" +} + +.icon--trophy:before { + content: "\E63F" +} + +.icon--tag:before { + content: "\E640" +} + +.icon--camera:before { + content: "\E641" +} + +.icon--megaphone:before { + content: "\E642" +} + +.icon--moon:before { + content: "\E643" +} + +.icon--palette:before { + content: "\E644" +} + +.icon--leaf:before { + content: "\E645" +} + +.icon--music:before { + content: "\E646" +} + +.icon--music2:before { + content: "\E647" +} + +.icon--new:before { + content: "\E648" +} + +.icon--graduation:before { + content: "\E649" +} + +.icon--book:before { + content: "\E64A" +} + +.icon--newspaper:before { + content: "\E64B" +} + +.icon--bag:before { + content: "\E64C" +} + +.icon--airplane:before { + content: "\E64D" +} + +.icon--lifebuoy:before { + content: "\E64E" +} + +.icon--eye:before { + content: "\E64F" +} + +.icon--clock2:before { + content: "\E650" +} + +.icon--microphone:before { + content: "\E651" +} + +.icon--calendar:before { + content: "\E652" +} + +.icon--bolt:before { + content: "\E653" +} + +.icon--thunder:before { + content: "\E654" +} + +.icon--droplet:before { + content: "\E655" +} + +.icon--cd:before { + content: "\E656" +} + +.icon--briefcase:before { + content: "\E657" +} + +.icon--air:before { + content: "\E658" +} + +.icon--hourglass:before { + content: "\E659" +} + +.icon--gauge:before { + content: "\E65A" +} + +.icon--language:before { + content: "\E65B" +} + +.icon--network:before { + content: "\E65C" +} + +.icon--key:before { + content: "\E65D" +} + +.icon--battery:before { + content: "\E65E" +} + +.icon--bucket:before { + content: "\E65F" +} + +.icon--magnet:before { + content: "\E660" +} + +.icon--drive:before { + content: "\E661" +} + +.icon--cup:before { + content: "\E662" +} + +.icon--rocket:before { + content: "\E663" +} + +.icon--brush:before { + content: "\E664" +} + +.icon--suitcase:before { + content: "\E665" +} + +.icon--cone:before { + content: "\E666" +} + +.icon--earth:before { + content: "\E667" +} + +.icon--keyboard:before { + content: "\E668" +} + +.icon--browser:before { + content: "\E669" +} + +.icon--publish:before { + content: "\E66A" +} + +.icon--progress-3:before { + content: "\E66B" +} + +.icon--progress-2:before { + content: "\E66C" +} + +.icon--brogress-1:before { + content: "\E66D" +} + +.icon--progress-0:before { + content: "\E66E" +} + +.icon--sun:before { + content: "\E66F" +} + +.icon--sun2:before { + content: "\E670" +} + +.icon--adjust:before { + content: "\E671" +} + +.icon--code:before { + content: "\E672" +} + +.icon--screen:before { + content: "\E673" +} + +.icon--infinity:before { + content: "\E674" +} + +.icon--light-bulb:before { + content: "\E675" +} + +.icon--creditcard:before { + content: "\E676" +} + +.icon--database:before { + content: "\E677" +} + +.icon--voicemail:before { + content: "\E678" +} + +.icon--clipboard:before { + content: "\E679" +} + +.icon--box:before { + content: "\E67A" +} + +.icon--ticket:before { + content: "\E67B" +} + +.icon--rss:before { + content: "\E67C" +} + +.icon--signal:before { + content: "\E67D" +} + +.icon--thermometer:before { + content: "\E67E" +} + +.icon--droplets:before { + content: "\E67F" +} + +.icon--uniE680:before { + content: "\E680" +} + +.icon--statistics:before { + content: "\E681" +} + +.icon--pie:before { + content: "\E682" +} + +.icon--bars:before { + content: "\E683" +} + +.icon--graph:before { + content: "\E684" +} + +.icon--lock:before { + content: "\E685" +} + +.icon--lock-open:before { + content: "\E686" +} + +.icon--login:before { + content: "\E687" +} + +.icon--minus:before { + content: "\E688" +} + +.icon--plus:before { + content: "\E689" +} + +.icon--cross2:before { + content: "\E68A" +} + +.icon--minus2:before { + content: "\E68B" +} + +.icon--plus2:before { + content: "\E68C" +} + +.icon--cross3:before { + content: "\E68D" +} + +.icon--minus3:before { + content: "\E68E" +} + +.icon--plus3:before { + content: "\E68F" +} + +.icon--erase:before { + content: "\E690" +} + +.icon--blocked:before { + content: "\E691" +} + +.icon--info:before { + content: "\E61B" +} + +.icon--info2:before { + content: "\E692" +} + +.icon--question:before { + content: "\E693" +} + +.icon--help:before { + content: "\E694" +} + +.icon--warning:before { + content: "\E695" +} + +.icon--cycle:before { + content: "\E696" +} + +.icon--cw:before { + content: "\E697" +} + +.icon--ccw:before { + content: "\E698" +} + +.icon--shuffle:before { + content: "\E699" +} + +.icon--arrow:before { + content: "\E69A" +} + +.icon--arrow2:before { + content: "\E69B" +} + +.icon--retweet:before { + content: "\E69C" +} + +.icon--loop:before { + content: "\E69D" +} + +.icon--history:before { + content: "\E69E" +} + +.icon--back:before { + content: "\E69F" +} + +.icon--list2:before { + content: "\E6A0" +} + +.icon--add-to-list:before { + content: "\E6A1" +} + +.icon--layout2:before { + content: "\E6A2" +} + +.icon--list3:before { + content: "\E6A3" +} + +.icon--text:before { + content: "\E6A4" +} + +.icon--text2:before { + content: "\E6A5" +} + +.icon--document:before { + content: "\E6A6" +} + +.icon--docs:before { + content: "\E6A7" +} + +.icon--landscape:before { + content: "\E6A8" +} + +.icon--pictures:before { + content: "\E6A9" +} + +.icon--video:before { + content: "\E6AA" +} + +.icon--music3:before { + content: "\E6AB" +} + +.icon--folder:before { + content: "\E6AC" +} + +.icon--archive:before { + content: "\E6AD" +} + +.icon--trash:before { + content: "\E6AE" +} + +.icon--upload:before { + content: "\E6AF" +} + +.icon--download:before { + content: "\E6B0" +} + +.icon--disk:before { + content: "\E6B1" +} + +.icon--install:before { + content: "\E61C" +} + +.icon--cloud:before { + content: "\E6B2" +} + +.icon--upload2:before { + content: "\E6B3" +} + +.icon--bookmark:before { + content: "\E6B4" +} + +.icon--bookmarks:before { + content: "\E6B5" +} + +.icon--book2:before { + content: "\E6B6" +} + +.icon--play:before { + content: "\E6B7" +} + +.icon--pause:before { + content: "\E6B8" +} + +.icon--record:before { + content: "\E6B9" +} + +.icon--stop:before { + content: "\E6BA" +} + +.icon--next:before { + content: "\E6BB" +} + +.icon--previous:before { + content: "\E6BC" +} + +.icon--first:before { + content: "\E6BD" +} + +.icon--last:before { + content: "\E6BE" +} + +.icon--resize-enlarge:before { + content: "\E6BF" +} + +.icon--resize-shrink:before { + content: "\E6C0" +} + +.icon--volume:before { + content: "\E6C1" +} + +.icon--sound:before { + content: "\E6C2" +} + +.icon--mute:before { + content: "\E6C3" +} + +.icon--flow-cascade:before { + content: "\E6C4" +} + +.icon--flow-branch:before { + content: "\E6C5" +} + +.icon--flow-tree:before { + content: "\E6C6" +} + +.icon--flow-line:before { + content: "\E6C7" +} + +.icon--flow-parallel:before { + content: "\E6C8" +} + +.icon--arrow-left2:before { + content: "\E6C9" +} + +.icon--arrow-down2:before { + content: "\E6CA" +} + +.icon--arrow-up-upload:before { + content: "\E6CB" +} + +.icon--arrow-right2:before { + content: "\E6CC" +} + +.icon--arrow-left3:before { + content: "\E6CD" +} + +.icon--arrow-down3:before { + content: "\E6CE" +} + +.icon--arrow-up2:before { + content: "\E6CF" +} + +.icon--arrow-right3:before { + content: "\E6D0" +} + +.icon--arrow-left4:before { + content: "\E6D1" +} + +.icon--arrow-down4:before { + content: "\E6D2" +} + +.icon--arrow-up3:before { + content: "\E6D3" +} + +.icon--arrow-right4:before { + content: "\E6D4" +} + +.icon--arrow-left5:before { + content: "\E6D5" +} + +.icon--arrow-down5:before { + content: "\E6D6" +} + +.icon--arrow-up4:before { + content: "\E6D7" +} + +.icon--arrow-right5:before { + content: "\E6D8" +} + +.icon--arrow-left6:before { + content: "\E6D9" +} + +.icon--arrow-down6:before { + content: "\E6DA" +} + +.icon--arrow-up5:before { + content: "\E6DB" +} + +.icon--arrow-right6:before { + content: "\E6DC" +} + +.icon--menu2:before { + content: "\E6DD" +} + +.icon--ellipsis:before { + content: "\E6DE" +} + +.icon--dots:before { + content: "\E6DF" +} + +.icon--dot:before { + content: "\E6E0" +} + +.icon--cc:before { + content: "\E6E1" +} + +.icon--cc-by:before { + content: "\E6E2" +} + +.icon--cc-nc:before { + content: "\E6E3" +} + +.icon--cc-nc-eu:before { + content: "\E6E4" +} + +.icon--cc-nc-jp:before { + content: "\E6E5" +} + +.icon--cc-sa:before { + content: "\E6E6" +} + +.icon--cc-nd:before { + content: "\E6E7" +} + +.icon--cc-pd:before { + content: "\E6E8" +} + +.icon--cc-zero:before { + content: "\E6E9" +} + +.icon--cc-share:before { + content: "\E6EA" +} + +.icon--cc-share2:before { + content: "\E6EB" +} + +.icon--danielbruce:before { + content: "\E6EC" +} + +.icon--danielbruce2:before { + content: "\E6ED" +} + +.icon--github:before { + content: "\E6EE" +} + +.icon--github2:before { + content: "\E6EF" +} + +.icon--flickr:before { + content: "\E6F0" +} + +.icon--flickr2:before { + content: "\E6F1" +} + +.icon--vimeo:before { + content: "\E6F2" +} + +.icon--vimeo2:before { + content: "\E6F3" +} + +.icon--twitter:before { + content: "\E6F4" +} + +.icon--twitter2:before { + content: "\E6F5" +} + +.icon--facebook:before { + content: "\E6F6" +} + +.icon--facebook2:before { + content: "\E6F7" +} + +.icon--facebook3:before { + content: "\E6F8" +} + +.icon--googleplus:before { + content: "\E6F9" +} + +.icon--googleplus2:before { + content: "\E6FA" +} + +.icon--pinterest:before { + content: "\E6FB" +} + +.icon--pinterest2:before { + content: "\E6FC" +} + +.icon--tumblr:before { + content: "\E6FD" +} + +.icon--tumblr2:before { + content: "\E6FE" +} + +.icon--linkedin:before { + content: "\E6FF" +} + +.icon--linkedin2:before { + content: "\E700" +} + +.icon--dribbble:before { + content: "\E701" +} + +.icon--dribbble2:before { + content: "\E702" +} + +.icon--stumbleupon:before { + content: "\E703" +} + +.icon--stumbleupon2:before { + content: "\E704" +} + +.icon--lastfm:before { + content: "\E705" +} + +.icon--lastfm2:before { + content: "\E706" +} + +.icon--rdio:before { + content: "\E707" +} + +.icon--rdio2:before { + content: "\E708" +} + +.icon--spotify:before { + content: "\E709" +} + +.icon--spotify2:before { + content: "\E70A" +} + +.icon--qq:before { + content: "\E70B" +} + +.icon--instagram:before { + content: "\E70C" +} + +.icon--dropbox:before { + content: "\E70D" +} + +.icon--evernote:before { + content: "\E70E" +} + +.icon--flattr:before { + content: "\E70F" +} + +.icon--skype:before { + content: "\E710" +} + +.icon--skype2:before { + content: "\E711" +} + +.icon--renren:before { + content: "\E712" +} + +.icon--sina-weibo:before { + content: "\E713" +} + +.icon--paypal:before { + content: "\E714" +} + +.icon--picasa:before { + content: "\E715" +} + +.icon--soundcloud:before { + content: "\E716" +} + +.icon--mixi:before { + content: "\E717" +} + +.icon--behance:before { + content: "\E718" +} + +.icon--circles:before { + content: "\E719" +} + +.icon--vk:before { + content: "\E71A" +} + +.icon--smashing:before { + content: "\E71B" +} + +.icon--feed:before { + content: "\E71F" +} + +.icon--feed2:before { + content: "\E720" +} + +.icon--delicious2:before { + content: "\E721" +} \ No newline at end of file diff --git a/source/assets/css/_imports/icons.less b/source/assets/css/_imports/icons.less index f51c3d3264..0a30b0d608 100755 --- a/source/assets/css/_imports/icons.less +++ b/source/assets/css/_imports/icons.less @@ -74,6 +74,7 @@ .icon-th-list:before { content: "\f00b"; } + .icon-check:before { content: "\f00c"; } @@ -203,9 +204,14 @@ .icon-align-justify:before { content: "\f039"; } -.icon-list:before { - content: "\f03a"; + +.offcanvas--trigger { + background: url("../../img/menu.png") center center no-repeat; + height: 16px; + width: 24px; + color: #fff; } + .icon-dedent:before { content: "\f03b"; } diff --git a/source/assets/css/_imports/labs.less b/source/assets/css/_imports/labs.less new file mode 100644 index 0000000000..d5e07d7f63 --- /dev/null +++ b/source/assets/css/_imports/labs.less @@ -0,0 +1,110 @@ +.topic--overview { + max-width: 1000px; + margin: 40px auto; + padding: 0 30px; + display: grid; + grid-template-columns: 1fr; + grid-gap: 30px 30px; + align-items: center; + align-content: center; + + .topic { + display: grid; + height: 120px; + padding: 15px; + align-content: center; + align-items: center; + background: @bg-color; + border-bottom: 4px solid; + text-align: center; + color: @text-color; + font-size: 18px; + font-weight: bold; + transition: all 0.2s ease-out; + + &:hover { + color: #fff; + + .topic--icon svg { + fill: #fff; + } + } + } + + .topic--label { + display: inline-block; + vertical-align: middle; + font-weight: 600; + } + + .topic--icon { + display: block; + max-width: 64px; + margin: 0 auto 15px; + + svg { + fill: @text-color; + } + } + + .topic--vision { + border-color: @text-color; + + &:hover { + background: @text-color; + } + } + + .topic--playground { + border-color: @highlight-color-red; + + &:hover { + background: @highlight-color-red; + } + } + + .topic--github { + border-color: @highlight-color-light-blue; + + &:hover { + background: @highlight-color-light-blue; + } + } + + .topic--docu { + border-color: @highlight-color-blue; + + &:hover { + background: @highlight-color-blue; + } + } +} + +@media screen and (min-width: @viewport-s) { + .topic--overview { + grid-template-columns: 1fr; + } +} + +@media screen and (min-width: @viewport-m) { + .landingpage .hero--text { + margin: 0 auto; + } + + .topic--overview { + grid-template-columns: 1fr 1fr; + } +} + +@media screen and (min-width: @viewport-l) { + .landingpage--headline { + font-size: 58px; + line-height: 61px; + } +} + +@media screen and (min-width: @viewport-xl) { + .topic--overview { + grid-template-columns: 1fr 1fr 1fr 1fr; + } +} diff --git a/source/assets/css/_imports/landing-page.less b/source/assets/css/_imports/landing-page.less new file mode 100644 index 0000000000..21d06b02ce --- /dev/null +++ b/source/assets/css/_imports/landing-page.less @@ -0,0 +1,135 @@ +.landingpage--content { + text-align: center; + padding-top: 50px; + + &.bg--github { + background: url('../../img/bg--github.jpg') no-repeat; + background-size: cover; + margin-bottom: 0; + } + + &.bg--github { + color: #000; + } + + &.content--dark { + background-color: #002e4c; + background-image: linear-gradient(to bottom, #002e4c 0%, #041a2c 100%); + color: #fff; + + .landingpage--headline { + color: #fff; + } + } + + &.bg--hello-human { + background: url('../../img/rd-hellohuman-bg.png'), linear-gradient(to bottom, #002e4c 0%, #041a2c 100%) no-repeat top center; + background-size: 100%; + } + + .inner-container { + max-width: 1220px; + margin: 0 auto; + } + + .flag-content--headline { + text-transform: uppercase; + font-size: 36px; + line-height: 31px; + margin-bottom: 20px; + margin-top: 10px; + border-bottom: 0 none; + } +} + +.landingpage { + .clearfix(); + margin-top: 90px; + + .hero--unit { + margin-bottom: 50px; + } + + .landingpage--headline { + margin-bottom: 20px; + text-transform: uppercase; + border-bottom: 0 none; + } + + .hero--text { + max-width: 900px; + margin: 0 20px; + } + + .content--history h2 { + margin-top: 0; + } + + .flag-content { + .clearfix(); + max-width: 1220px; + margin: 0 auto 50px; + text-align: center; + + .flag-content--content { + font-size: 18px; + line-height: 31px; + } + + .flag-content--media img { + max-width: 100%; + box-shadow: 0 6px 60px rgba(35,62,82,0.28); + } + } + + .flag-content--left { + .flag-content--content, .flag-content--media { + float: left; + text-align: left; + } + .flag-content--content { + width: 35%; + margin-right: 5%; + } + .flag-content--media { + width: 60%; + } + } + + .flag-content--right { + .flag-content--content, .flag-content--media { + float: left; + text-align: left; + } + .flag-content--content { + width: 35%; + margin-left: 5%; + } + .flag-content--media { + width: 60%; + } + } +} + +@media screen and (min-width: @viewport-m) { + .landingpage .hero--text { + margin: 0 auto; + } +} + +@media screen and (min-width: @viewport-l) { + .landingpage--headline { + font-size: 58px; + line-height: 61px; + } +} + +@media screen and (min-width: (@viewport-xl - 20)) { + .bg--github.github--labs-section { + margin-left: 350px; + } + .landingpage { + margin-left: 350px; + margin-top: 0; + } +} \ No newline at end of file diff --git a/source/assets/css/_imports/start.less b/source/assets/css/_imports/start.less new file mode 100644 index 0000000000..d05f92f17b --- /dev/null +++ b/source/assets/css/_imports/start.less @@ -0,0 +1,578 @@ + +.start.page-wrap { + padding-right: 0; + -webkit-transition-property: padding, opacity; + -webkit-transition-duration: .5s; + -webkit-transition-timing-function: cubic-bezier(0.8, 0, 0.5, 0.95); + transition-property: padding, opacity; + transition-duration: .5s; + transition-timing-function: cubic-bezier(0.8, 0, 0.5, 0.95); + will-change: padding, opacity; + + h1, h2, h3, h4 { + font-family: 'Inter', sansserif; + font-weight: 600; + line-height: 140%; + } + + h3 { + font-size: 33px; + } + + a { + font-family: 'Inter', sansserif; + } + + .wrapper { + z-index: 1; + padding-bottom: 0; + position: relative; + } + + .row { + clear: both; + } + + .col { + margin-top: 30px; + width: 100%; + padding: 2% 4%; + } +} + +.header-banner { + color: #fff; + background-color: #189eff; + margin-top: 140px; + position: relative; + height: 100%; + -webkit-transition-property: padding, margin; + -webkit-transition-duration: .5s; + -webkit-transition-timing-function: cubic-bezier(0.8, 0, 0.5, 0.95); + transition-property: padding, margin; + transition-duration: .5s; + transition-timing-function: cubic-bezier(0.8, 0, 0.5, 0.95); + will-change: padding, margin; + overflow: hidden; + padding-bottom: 100px; + text-align: center; + z-index: 0; + + .gradient { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + background-color: #2DA7FF; + background-image: linear-gradient(-180deg, #2DA7FF 12%, #0038E2 92%); + } + + .bridge { + content: ""; + display: block; + position: absolute; + bottom: 0; + width: 100%; + height: 50px; + background-color: #fff; + -webkit-clip-path: polygon(100% 50%, 0% 100%, 100% 100%); + clip-path: polygon(100% 50%, 0% 100%, 100% 100%); + } + + .headline { + display: none; + } + + .start-icon.install { + background: url("../../img/install.png") center center no-repeat; + min-height: 120px; + width: auto; + } + + .start-icon.design { + background: url("../../img/design.png") center center no-repeat; + min-height: 120px; + width: auto; + } + + .start-icon.plugin { + background: url("../../img/plugin.png") center center no-repeat; + min-height: 120px; + width: auto; + } + + h1 { + font-size: 58px; + color: #FFFFFF; + letter-spacing: 0.2px; + text-shadow: 5px 5px 0 rgba(20, 36, 50, 0.10); + padding-bottom: 0; + margin: 0; + } + + h5 { + font-size: 18px; + color: #FFFFFF; + letter-spacing: 0; + margin: 10px 0; + } +} + +@media screen and (min-width: 768px) { + .header-banner { + position: relative; + height: 650px; + padding-bottom: 0; + } + + @-webkit-keyframes typing { + from { + width: 0; + } + } + @-webkit-keyframes blink-caret { + 50% { + border-color: transparent; + } + } + + .header-banner .headline { + display: block; + font-size: 60px; + margin: 50px 30px 25px 55px; + color: #FFF; + font-weight: 600; + text-align: left; + font: 600 380% 'Inter', sansserif; + border-right: .1em solid rgba(255, 255, 255, 0.2); + width: 11em; + width: 11ch; + white-space: nowrap; + overflow: hidden; + -webkit-animation: typing 2s steps(21, end), blink-caret .5s step-end infinite alternate; + } + + .start.page-wrap .col { + margin-top: 0; + width: 33%; + text-align: center; + float: left; + } +} + +@media screen and (min-width: 1024px) { + .devdocs.content.start { + padding-top: 30px; + padding-bottom: 90px; + } +} + +@media screen and (min-width: 1200px) { + .start.page-wrap { + margin-top: 0; + } + + .start.page-wrap .wrapper { + margin-left: 25%; + max-width: 1200px; + } + + .header-banner { + margin-top: 0px; + } + + .header-banner .col { + width: 33%; + text-align: center; + float: left; + } +} + +/*------- BLOG POST -----------*/ + +.devdocs.content.start { + margin-top: 0; + padding: 10px 0 30px 20px; + + @media screen and (min-width: 1200px) { + margin-left: 25%; + max-width: 1200px; + } +} + +h2#recent-blog-posts { + text-align: center; + padding-bottom: 15px; +} + +/*------- BUTTON STYLES -------*/ +.start.page-wrap { + .button { + clear: both; + margin: 0 auto; + text-align: center; + padding-top: 10px; + + a { + display: inline-block; + color: #132533; + background: #FFDB47; + border-radius: 30px; + font-weight: 600; + font-size: 16px; + letter-spacing: 0.1px; + padding: 13px 25px; + transition: all .3s cubic-bezier(0.25, 0.2, 0.45, 1); + box-shadow: 0 0.5rem 1rem -0.25rem rgba(0, 0, 0, 0.12); + + &:hover { + color: #132533; + background: darken(#FFDB47, 9%); + box-shadow: 0 0.5rem 1rem -0.25rem rgba(0, 0, 0, 0.33); + transform: translateY(-3px); + -webkit-transform: translateY(-3px); + -moz-transform: translateY(-3px); + -ms-transform: translateY(-3px); + -o-transform: translateY(-3px); + } + } + } + + .udemy-teaser .button a { + background: #FA6179; + color: #FFF; + box-shadow: 0 0.5rem 1rem -0.3rem rgba(250, 97, 121, 0.22); + + &:hover { + background: darken(#FA6179, 5%); + box-shadow: 0 0.5rem 1rem -0.3rem rgba(250, 97, 121, 0.33); + } + } +} + +/*------- UDEMY CONTENT -------*/ +.start.page-wrap .udemy-teaser { + width: 100%; + + .wrapper { + padding-top: 140px; + padding-bottom: 50px; + } + + .col { + text-align: center; + width: 100%; + + h3 { + margin-top: 0px; + margin-bottom: 25px; + } + } + + .bubbles { + width: 100%; + padding-bottom: 20px; + } + + .bubble-img { + overflow-x: hidden; + } + + @media screen and (min-width: 568px) { + .bubble-img { + margin: 0 auto; + width: 480px; + } + } + + .bubble-img { + .big-one { + background: url("../../img/img-big-one.jpg") center center no-repeat; + background-size: cover; + height: 220px; + width: 220px; + border-radius: 100%; + position: relative; + z-index: 1; + margin-left: 135px; + } + + .bubble-name { + text-align: center; + + h6 { + font-size: 19px; + text-align: center; + margin-bottom: 0; + } + + p { + text-align: center; + font-size: 13px; + } + } + + .one, .two, .three, .four, .five { + height: 90px; + width: 90px; + border-radius: 100%; + } + + .one { + background: url("../../img/img-one.jpg") center center no-repeat; + background-size: cover; + margin-left: 65px; + margin-top: -180px; + } + + .two { + background: url("../../img/img-two.jpg") center center no-repeat; + background-size: cover; + margin-left: 0px; + margin-top: -25px; + } + + .three { + background: url("../../img/img-three.jpg") center center no-repeat; + background-size: cover; + margin-left: 80px; + margin-top: -43px; + } + + .four { + background: url("../../img/img-four.jpg") center center no-repeat; + background-size: cover; + margin-left: 330px; + margin-top: -105px; + } + } +} + +@media screen and (min-width: 1200px) { + .start.page-wrap .udemy-teaser .col, + .start.page-wrap .udemy-teaser .bubbles { + width: 50%; + text-align: left; + float: left; + } + + .start.page-wrap .udemy-teaser .col { + padding-bottom: 30px; + } +} + +/*-------- Contribute & Resources --------*/ +.start.page-wrap .icon-banner { + width: 100%; + background: #EBF6FF; + text-align: center; + padding: 60px 0 60px 0; + + .row { + clear: both; + } + + .col { + width: 33%; + float: left; + } + + h3 { + padding-top: 0; + margin-top: 0; + margin-bottom: 40px; + } + + a { + color: @blue06; + display: block; + background: #EBF6FF; + transition: background-color 0.5s ease; + + &:hover { + background: rgba(255, 255, 255, 0.69); + background: -moz-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.69) 0%, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, rgba(255, 255, 255, 0.69)), color-stop(40%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(255, 255, 255, 0))); + background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.69) 0%, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 100%); + background: -o-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.69) 0%, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 100%); + background: -ms-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.69) 0%, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 100%); + background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.69) 0%, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#ffffff', GradientType=1); + } + + h5 { + font-size: 16px; + font-weight: 600; + } + + .icon { + height: 72px; + width: auto; + } + + .icon.issuetracker, + .icon.labs, + .icon.styleguide, + .icon.github, + .icon.twitter, + .icon.forum { + zoom: 0.9; + } + + .icon.issuetracker { + background: url("../../img/issue-tracker.svg") bottom center no-repeat; + margin-bottom: -10px; + margin-top: 10px; + } + + .icon.labs { + background: url("../../img/labs.svg") bottom center no-repeat; + margin-left: -10px; + } + + .icon.styleguide { + background: url("../../img/styleguide.svg") bottom center no-repeat; + } + + .icon.github { + background: url("../../img/github.svg") bottom center no-repeat; + } + + .icon.twitter { + background: url("../../img/twitter.svg") bottom center no-repeat; + } + + .icon.forum { + background: url("../../img/forum.svg") bottom center no-repeat; + } + } +} + +@media screen and (min-width: 768px) { + .start.page-wrap .icon-banner a { + .icon.issuetracker, + .icon.labs, + .icon.styleguide, + .icon.github, + .icon.twitter, + .icon.forum { + zoom: 1; + } + } +} + +/* --------- Latest Changes ---------- */ +.start.page-wrap .latest-changes { + background: #f5f5f5; + padding: 50px 15px 0px 15px; + margin-top: 100px; + + .wrapper { + padding-bottom: 120px; + min-height: 300px; + } + + .content-list { + //display: none; + height: 200px; + overflow: hidden; + margin: 0 auto; + } + + .history-item { + width: 33%; + float: left; + padding: 0 3%; + min-height: 180px; + + a { + font-size: 13px; + } + } + + .is--center { + margin: 0 auto; + text-align: center; + } + + .collapse { + display: none; + } + + .expand:target + .collapse { + display: inline; + } + + .expand:target { + display: inline; + } + + .expand:target ~ .content-list { + display: inline; + } + + .wrapper ~ .expand:target { + height: auto; + } + + .expand, .collapse { + text-align: center; + color: #132533; + background: #607182; + border-radius: 30px; + font-weight: 600; + font-size: 16px; + color: #FFF; + letter-spacing: 0.1px; + padding: 12px 20px; + transition: all .3s cubic-bezier(0.25, 0.2, 0.45, 1); + position: absolute; + bottom: 40px; + left: 40%; + + &:hover { + box-shadow: 0 0.5rem 1rem -0.25rem rgba(57, 84, 103, 0.33); + } + } + + h3 { + font-size: 33px; + text-align: center; + margin-top: 0; + } + + h2 { + display: none; + } +} + +@media screen and (max-width: 767px) { + .history-item:nth-child(3n+2) { + clear: both; + } +} + +@media screen and (min-width: 768px) { + .start.page-wrap .latest-changes .history-item { + width: 25%; + } + + .start.page-wrap .latest-changes .history-item:nth-child(4n+2) { + clear: both; + } + + .start.page-wrap .latest-changes .expand, + .start.page-wrap .latest-changes .collapse { + left: 44%; + } +} + +@media screen and (min-width: 1200px) { + .start.page-wrap .latest-changes h3 { + margin-left: 25%; + max-width: 1200px; + } +} + diff --git a/source/assets/css/_imports/typography.less b/source/assets/css/_imports/typography.less index 36b7acddd6..93d27a4af2 100644 --- a/source/assets/css/_imports/typography.less +++ b/source/assets/css/_imports/typography.less @@ -1,6 +1,6 @@ h1, h2, h3, h4, h5, h6 { word-wrap: break-word; - color: @gray05; + color: @blue06; a { color: @blue05; @@ -12,9 +12,8 @@ h1, h2 { } h1 { - font-weight: 400; - font-size: 32px; - border-bottom: 2px solid @gray03; + font-weight: 600; + font-size: 36px; margin-top: 60px; margin-bottom: 20px; padding-bottom: 10px; @@ -24,36 +23,31 @@ h1:first-of-type { margin-top: 0; } -h1 + h2 { - margin-top: 30px; -} - h2 { - border-bottom: 1px solid @gray03; - font-size: 24px; - font-weight: 400; + font-size: 30px; + font-weight: 600; margin-bottom: 15px; - margin-top: 60px; + margin-top: 40px; padding-bottom: 5px; } h3 { - font-size: 22px; + font-size: 24px; font-weight: 400; margin-bottom: 10px; - margin-top: 40px; + margin-top: 30px; } h4 { font-size: 18px; - font-weight: 600; + font-weight: 500; margin-bottom: 5px; - margin-top: 40px; + margin-top: 20px; } h5 { font-size: 14px; - font-weight: 600; + font-weight: 500; margin-bottom: 5px; margin-top: 20px; } @@ -78,7 +72,7 @@ b, strong { } p { - line-height: 1.75; + line-height: 1.55; margin-bottom: 16px; margin-bottom: 1rem; diff --git a/source/assets/css/_imports/variables.less b/source/assets/css/_imports/variables.less index e7f6aba91d..2948bf5732 100644 --- a/source/assets/css/_imports/variables.less +++ b/source/assets/css/_imports/variables.less @@ -6,15 +6,31 @@ @gray04: #93a6b3; @gray05: #52606b; -@blue01: #1fb3fb; +@blue01: #4aa3df; @blue02: #1491e7; @blue03: #316c8b; @blue04: #26556f; @blue05: #0e384f; -@blue06: #eef4f7; +@blue06: #142432; @blue07: #4690b1; @blue08: #7FABBF; @success: #37d046; @warning: #ffb71c; -@danger: #c5425b; +@danger: #de294c; + +/** Viewport sizes */ +@viewport-s: 480px; +@viewport-m: 768px; +@viewport-l: 1024px; +@viewport-xl: 1220px; +@viewport-xxl: 1440px; + +@text-color: #607182; +@bg-color: #f1f5f8; +@highlight-color-green: #7DD181; +@highlight-color-turquoise: #F7A072; +@highlight-color-yellow: #FFD23F; +@highlight-color-blue: #2F97C1; +@highlight-color-red: #E32D76; +@highlight-color-light-blue: #54C6EB; diff --git a/source/assets/css/basic.less b/source/assets/css/basic.less index dd2e791024..cdb3f38337 100644 --- a/source/assets/css/basic.less +++ b/source/assets/css/basic.less @@ -1,6 +1,7 @@ @import "_imports/reset.less"; @import "_imports/anchor.less"; @import "_imports/icons.less"; +@import "_imports/icons-sw.less"; @import "_imports/variables.less"; @import "_imports/gradients.less"; @import "_imports/mixins.less"; @@ -8,20 +9,30 @@ @import "_imports/buttons.less"; @import "_imports/alerts.less"; @import "_imports/grid.less"; +@import "_imports/font.less"; @import "_imports/typography.less"; @import "_imports/pagination.less"; @import "_imports/codeExpand.less"; @import "_imports/video.less"; @import "_imports/overlay.less"; @import "_imports/icon-search.less"; +@import "_imports/landing-page.less"; +@import "_imports/labs.less"; +@import "_imports/start.less"; +@import "_imports/dev-guide.less"; +@import "_imports/des-guide.less"; +@import "_imports/api-badge.less"; /** * — Minor Section Heading — */ html, body { - font: @main-font 'Open Sans', Arial, sans-serif; - color: @gray05; + font: @main-font 'Inter', Arial, sans-serif; + color: @text-color; + font-weight: 300; + background: #FFF; } + a { &, &:hover, &:active, &:focus { text-decoration: none; @@ -33,6 +44,21 @@ a { } } +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + color: @blue06; +} + +b, strong { + font-weight: 600; +} + +.flag-new { + color: #FFF; + background-color:@blue02; + padding: 0 5px; + border-radius: 2px; +} + .full-width { width: 100% !important; } @@ -79,8 +105,7 @@ a { /* GRID */ .wrapper { .clearfix(); - position: relative; - padding-bottom: 160px; + height: 100%; } .container { @@ -93,16 +118,21 @@ a { .content { .clearfix(); - max-width: 1200px; - margin-top: 90px; + margin-top: 80px; background: #fff; - padding-top: 40px; + padding-top: 90px; padding-bottom: 0; + padding-left: 15px; + padding-right: 20px; position: relative; @media screen and (min-width: 1200px) { - margin-left: 350px; + max-width: 900px; + margin-left: 390px; margin-top: 0; + padding-top: 60px; + padding-left: 0; + padding-right: 50px; } } @@ -126,19 +156,18 @@ a { width: 100%; z-index: 9900; background: @blue06; - height: 46px; + height: 150px; @media screen and (min-width: 1200px) { - height: 100px; width: 350px; } .logo { - width: 133px; - height: 26px; + width: 90px; + height: auto; margin: auto; position: absolute; - top: 0; left: 0; + top: 13px; left: 0; bottom: 0; right: 0; img { @@ -148,8 +177,9 @@ a { } @media screen and (min-width: 1200px) { - width: 190px; - height: 37px; + width: 118px; + height: 118px; + top: 53px; } } } @@ -163,7 +193,7 @@ a { font-size: 28px; display: inline-block; position: absolute; - top: 50%; left: 10px; + top: 33%; left: 30px; padding: 0; margin: 0 0 0 5px; color: #26556f; @@ -176,6 +206,7 @@ a { .searchButton { right: 0; } + .navi--content { width: 100%; height: 100%; @@ -183,7 +214,23 @@ a { overflow-y: auto; overflow-x: hidden; } + +nav.navi--main:after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 40px; + height: 20px; + z-index: 10; + pointer-events: none; +} + .navi--main { + padding: 150px 0 0px 0; + @media screen and (min-width: 768px) { + padding: 150px 0 80px 0; + } .transform(translateX(-100%)); .transition(all 0.4s ease-out); width: 100%; @@ -193,7 +240,11 @@ a { position: fixed; top: 0; left: 0; z-index: 9800; - padding: 90px 0 0 0; + overflow: hidden; + ::-webkit-scrollbar { + display: none; + background: transparent; + } &.is--active { .transform(translateX(0)); @@ -203,40 +254,38 @@ a { .transform(translateX(0)); .transition(none); width: 350px; - padding-top: 152px; + padding-top: 240px; } ul.navi--tree { - border-bottom: 1px solid #d2dde4; li.navi--chapter { + + &.is--active { + padding-bottom: 10px; + + ul.navi--sub-1-tree { + height: auto; + border: none; + } + } + .navi--link { display: block; - padding: 10px 15px; - background: #ebf1f5; - border-top: 1px solid #d2dde4; - color: #47606d; - font-weight: bold; + padding: 10px 38px; + color: rgba(255, 255, 255, 0.95); + font-weight: 500; position: relative; - font-size: 16px; + font-size: 1.2rem; z-index: 10; - - &.has--children:after { - .transform(translateY(-50%)); - content: '\e6d0'; - font-family: 'scc'; - display: inline-block; - position: absolute; - width: 20px; - top: 50%; right: 12px; - text-align: center; - color: #b8d2e0; - } + border-left: 3px solid rgba(255, 255, 255, 0); + background-color: rgba(255, 255, 255, 0); &:hover { - color: #006ba3; - background: #e5ecf1; - border-color: #cbd6dd; + background-color: rgba(255, 255, 255, 0.05); + border-left: 3px solid rgba(255, 255, 255, 1); + color: rgba(255, 255, 255, 1); + transition: all 0.3s cubic-bezier(0.25, 0.2, 0.45, 1); &:after { color: #006ba3; @@ -245,34 +294,32 @@ a { &.is--active { color: #ffffff; - background: #26556f; - border-color: #223946; - - &.has--children:after { - content: '\e6ce'; - color: #ffffff; - } + border-left: 3px solid rgba(255, 255, 255, 1); + font-weight: 600; } - } - &.is--active { - ul.navi--sub-tree { - height: auto; + .icon-external-link { + margin: 0.2rem 0 0 0; + float: right; + opacity: 0.75; } } - ul.navi--sub-tree { + ul.navi--sub-1-tree { .transition(all 0.4s ease-out); height: 0; overflow: hidden; - li.navi--sub-chapter { + &.is-numbered { + margin-bottom: 5px; + } + + li.navi--sub-1-chapter { .navi--link { - font-size: 14px; - padding: 8px 15px 8px 30px; - font-weight: bold; - background: #d9e5ed; - border-color: #b6d1de; + font-size: 1rem; + color: rgba(255, 255, 255, 0.55); + padding: 5px 15px 5px 50px; + font-weight: 500; z-index: 5; &:after { @@ -280,45 +327,46 @@ a { } &.is--active { - background: #477d9b; - border-color: #477d9b; - color: #ffffff; + color: rgba(255, 255, 255, 1); + font-weight: 600; &:after { color: #ffffff; } + + &:hover { + color: rgba(255, 255, 255, 1); + background-color: rgba(255, 255, 255, 0); + } } &:not(.is--active):hover { - background: #cedce5; - border-color: #bbcedb; - color: #006ba3; + color: rgba(255, 255, 255, 0.9); + background-color: rgba(255, 255, 255, 0); &:after { - color: #006ba3; + color: #ffffff; } } } &.is--active { - ul.navi--sub-tree { + ul.navi--sub-2-tree { height: auto; } } - ul.navi--sub-tree { + ul.navi--sub-1-tree, ul.navi--sub-2-tree, ul.navi--sub-3-tree, ul.navi--sub-4-tree { .transition(all 0.4s ease-out); height: 0; overflow: hidden; - li.navi--sub-chapter { + li.navi--sub-1-chapter { .navi--link { - font-size: 13px; - padding: 6px 15px 6px 50px; - font-weight: 400; - background: #abc9d9; - border-color: #88b1c9; - color: #263943; + font-size: 15px; + color: rgba(255, 255, 255, 0.33); + padding: 3px 30px 3px 50px; + font-weight: 500; z-index: 1; &:after { @@ -327,74 +375,194 @@ a { } &.is--active { - background: #5795b8; - border-color: #5795b8; color: #ffffff; - font-weight: bold; + border-color: rgba(255, 255, 255, 0); + font-weight: 600; } &:not(.is--active):hover { - background: #9cbed1; - border-color: #84aac0; - color: #002d44; + color: #ffffff; + background-color: rgba(255, 255, 255, 0); + border-color: rgba(255, 255, 255, 0); &:before { - background: #53829b; } } } } - &.is--bulleted { - li.navi--sub-chapter { - .navi--link { - &:before { - .border-radius(3px); - content: ''; - display: inline-block; - width: 6px; - height: 6px; - margin-top: -3px; - background: #7698aa; - position: absolute; - top: 50%; - left: 35px; - } + &.is--numbered li.navi--sub-1-chapter .navi--link { + &:before { + display: block; + width: 20px; + font-size: 13px; + position: absolute; + font-weight: bold; + text-align: right; + top: 6px; + left: 20px; + } + + &:hover:before { + background: none; - &.is--active:before { - background: #ffffff; - } - } } } + } + } + } - &.is--numbered { - counter-reset: guide-counter; + ul.navi--sub-2-tree { + li.navi--sub-2-chapter { - li.navi--sub-chapter { - .navi--link { - &:before { - content: counter(guide-counter) + '.'; - counter-increment: guide-counter; - display: block; - width: 20px; - font-size: 13px; - position: absolute; - font-weight: bold; - text-align: right; - top: 6px; - left: 25px; - } + &.is--active { + ul.navi--sub-3-tree { + height: auto; + } + } - &:hover:before { - background: none; - } - } - } + .navi--link { + font-size: 1rem; + padding-left: 62px; + } + } + } + + ul.navi--sub-3-tree { + li.navi--sub-3-chapter { + &.is--active { + ul.navi--sub-4-tree { + height: auto; } } + + .navi--link { + font-size: 1rem; + padding-left: 74px; + } + } + } + + ul.navi--sub-4-tree { + li.navi--sub-4-chapter { + .navi--link { + font-size: 1rem; + padding-left: 86px; + } + } + } + } + } +} + +.navi--backlink { + display: none; +} + +.navi--backlink-mobile { + font-family: 'Inter', Arial, sans-serif; + font-weight: 400; + display: block; + height: 60px; + line-height: 60px; + text-align: center; + margin: 0; + color: #fff; + background-color: hsla(0, 0%, 100%, .05); + cursor: pointer; + transition: all .3s cubic-bezier(.25, .2, .45, 1); + position: absolute; + left: 0; + right: 0; + bottom: 0; + + a { + font-family: 'Inter', Arial, sans-serif; + display: block; + height: 60px; + line-height: 60px; + text-align: center; + margin: 0; + padding: 0 !important; + color: #fff; + background-color: hsla(0, 0%, 100%, .05); + cursor: pointer; + transition: all .3s cubic-bezier(.25, .2, .45, 1); + &:hover { + background-color: hsla(0,0%,100%,.1); + transition: all .3s cubic-bezier(.25,.2,.45,1); + i { + left: -24px; + } + } + i { + height: 60px; + font-size: 10px; + display: inline-block; + transform: translateY(-3px); + line-height: 10px; + text-align: center; + margin: 0; + position: relative; + left: -14px; + transition: left .3s cubic-bezier(.25, .2, .45, 1); + } + } +} + +@media screen and (min-width: 768px) { + + .navi--backlink-mobile { + display: none; + } + + .navi--backlink { + font-family: 'Inter', Arial, sans-serif; + font-weight: 400; + display: block; + height: 60px; + line-height: 68px; + text-align: center; + margin: 0; + color: #fff; + background-color: hsla(0, 0%, 100%, .05); + cursor: pointer; + transition: all .3s cubic-bezier(.25, .2, .45, 1); + position: absolute; + left: 0; + right: 0; + bottom: 0; + + a { + font-family: 'Inter', Arial, sans-serif; + display: block; + height: 60px; + line-height: 60px; + text-align: center; + margin: 0; + color: #fff; + background-color: hsla(0, 0%, 100%, .05); + cursor: pointer; + transition: all .3s cubic-bezier(.25, .2, .45, 1); + &:hover { + background-color: hsla(0,0%,100%,.1); + transition: all .3s cubic-bezier(.25,.2,.45,1); + i { + left: -24px; } } + i { + height: 60px; + font-size: 10px; + display: inline-block; + transform: translateY(-3px); + line-height: 10px; + text-align: center; + margin: 0; + position: relative; + left: -14px; + transition: left .3s cubic-bezier(.25, .2, .45, 1); + } } } } @@ -402,20 +570,25 @@ a { /* FOOTER */ .main-footer { .clearfix(); - background: #26556f; + background: #f9f9f9; + font-size: 14px; + font-weight: 600; padding: 20px 10px; overflow: hidden; - position: absolute; + position: static; bottom: 0; left: 0; right: 0; - color: #fff; + color: rgba(20,36,50,0.2); text-align: center; z-index: 9200; .footer--links { a { - color: #fff; + color: rgba(20,36,50,0.2); + font-size: 14px; + font-weight: 600; + padding: 0 20px; &:hover { text-decoration: underline; @@ -424,11 +597,14 @@ a { } @media screen and (min-width: 1200px) { - padding: 20px 0 20px 40px; + padding: 20px 0 20px 60px; margin-left: 350px; - text-align: left; + text-align: right; + + .copyright { + float: right; + } - .copyright, .footer--links { float: left; } @@ -451,21 +627,27 @@ a { #searchBox { display: block; width: 100%; - padding: 6px 10px 8px 10px; - background: @blue06; + padding: 0px 30px 8px 30px; position: fixed; - top: 46px; left: 0; + top: 100px; left: 0; z-index: 9990; @media screen and (min-width: 1200px) { - top: 100px; left: 0; - padding: 10px 12px 12px 12px; + top: 180px; + left: 0; + padding: 0px 18px 15px 18px; width: 350px; } } #search-form { position: relative; + +} + +form#search-form { + font-weight: bold; + color: rgba(255, 255, 255, 0.35); } #search-action { @@ -474,19 +656,20 @@ a { height: 16px; position: absolute; top: 50%; - right: 10px; + left: 20px; margin-top: -9px; background: none; border: none; font-family: 'scc'; font-size: 16px; - color: @blue03; + color: rgba(255, 255, 255, 0.35); text-align: center; &:hover, &:focus, &:active { outline: none; + color: rgba(255, 255, 255, 1); } &:before { @@ -496,6 +679,7 @@ a { height: 16px; position: absolute; top: 0; left: 0; + transition: all 0.3s cubic-bezier(0.25, 0.2, 0.45, 1); } .active & { @@ -509,25 +693,38 @@ a { } input#search-query { - .box-shadow(inset 0 2px 3px 0 #d3e5f1); - font-size: 14px; - height: 32px; - padding: 0 20px; + font-size: 16px; + height: 40px; + padding: 5px 50px; width: 100%; - border-radius: 12px; - border: 1px solid #8cadc4; - color: @blue03; - font-weight: bold; + border-radius: 30px; + color: rgba(255, 255, 255, 1); + font-weight: 400; + background: rgba(205, 221, 247, 0.15); + font-family: 'Inter'; &::-ms-clear { display: none; } - &:focus, - &:active { - .box-shadow(inset 0 2px 3px 0 #bfd7e7); - background: #f4fcff; - border-color: #6a98b8; + &::-webkit-input-placeholder { /* Chrome/Opera/Safari */ + color: rgba(255, 255, 255, 0.3); + font-weight: 600; + } + + &::-moz-placeholder { /* Firefox 19+ */ + color: rgba(255, 255, 255, 0.3); + font-weight: 600; + } + + &:-ms-input-placeholder { /* IE 10+ */ + color: rgba(255, 255, 255, 0.3); + font-weight: 600; + } + + &:-moz-placeholder { /* Firefox 18- */ + color: rgba(255, 255, 255, 0.3); + font-weight: 600; } } @@ -653,6 +850,12 @@ input#search-query { background-size: cover; } + .teaser-b2b { + float: left; + background: url("../img/b2b-suite/v2/mockup-contact-index-slim.jpg") no-repeat right center; + background-size: cover; + } + .teaser--headline { .transition(all 0.2s ease-out); display: block; @@ -680,9 +883,10 @@ input#search-query { color: #677c8a; @media screen and (min-width: 480px) { - width: 32%; + width: 23.5%; margin-right: 2%; margin-bottom: 0; + min-height: 195px; &:last-child { margin-right: 0; @@ -713,6 +917,14 @@ input#search-query { } } + &.teaser--labs { + border-color: @gray04; + + &:hover { + background: @gray04; + } + } + .icon--sys { background: url("../img/icon_sys.png") center center no-repeat; } @@ -725,6 +937,10 @@ input#search-query { background: url("../img/icon_design.png") center center no-repeat; } + .icon--labs { + background: url("../img/icon_labs.png") center center no-repeat; + } + &:hover { color: #ffffff; @@ -739,6 +955,10 @@ input#search-query { .icon--design { background: url("../img/icon_design_hover.png") center center no-repeat; } + + .icon--labs { + background: url("../img/icon_labs_hover.png") center center no-repeat; + } } } @@ -761,7 +981,9 @@ input#search-query { .clearfix(); .blog-post { + border-bottom: 0 none; padding-bottom: 0; + margin-bottom: 30px; .header-title { margin-top: 5px; @@ -778,8 +1000,6 @@ input#search-query { } @media screen and (min-width: 768px) { - border-bottom: 1px solid #dfe4e7; - .blog-post { width: 30%; margin-right: 5%; @@ -842,18 +1062,24 @@ input#search-query { .devdocs { .clearfix(); - font-size: 15px; - min-height: 900px; - min-height: 90vh; code.hljs, pre > code.language-nohighlight { - background: #fafafa; + background: #f9f9f9; border-radius: 3px; font-size: 14px; - padding: 16px; + padding: 30px; font-family: "Consolas", "Monaco", "DejaVu Sans Mono", "Courier New", sans-serif; + margin: 0px 0 40px 0; + border: 1px solid #f2f2f2; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -moz-background-clip: padding; + -webkit-background-clip: padding-box; + background-clip: padding-box; } + code:not(.hljs):not(.language-nohighlight) { &::before { @@ -870,7 +1096,8 @@ input#search-query { background-color: rgba(0,0,0,.04); border-radius: 3px; font-family: "Consolas", "Monaco", "DejaVu Sans Mono", "Courier New", sans-serif; - color: #333; + font-size: 13px; + color: #000; word-wrap: break-word; } @@ -896,7 +1123,7 @@ input#search-query { .edit-on-github { display: inline-block; - margin-bottom: 20px; + margin-bottom: 5px; .icon-github { font-size: 24px; @@ -920,9 +1147,7 @@ input#search-query { } @media screen and (min-width: 1200px) { - margin-left: 10px; - margin-top: 8px; - float: right; + margin-bottom: 5px; .edit-on-github { margin-bottom: 0; @@ -952,27 +1177,25 @@ input#search-query { li { position: relative; - margin: 4px 0 4px 40px; + margin: 5px 0 5px 30px; @media screen and (min-width: 768px) { - margin: 4px 0 4px 60px; + margin: 8px 0 8px 30px; } &:before { content: ''; position: absolute; - background: linear-gradient(to bottom, #1fb3fb 0, #1491e7 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#1491e7, endColorstr=#1fb3fb, GradientType=0); background-color: #1491e7; background-repeat: no-repeat; border-radius: 100%; -webkit-border-radius: 100%; -moz-border-radius: 100%; - height: 10px; - width: 10px; + height: 5px; + width: 5px; display: block; - margin-top: 5px; - left: -25px; + margin-top: 8px; + left: -15px; } li { @@ -1012,12 +1235,13 @@ input#search-query { padding: 10px 25px; font-size: 18px; font-weight: 400; - background: #f1f5f8; + background: #f4f7fa; + border-radius: 3px; color: #4e697a; &:hover { color: @blue02; - background: #e7f2f9; + background: lighten(@blue02, 46%); } } } @@ -1040,8 +1264,7 @@ input#search-query { font-weight: bold; text-align: center; border-radius: 28px; - background: #46a8d1; - border: 2px solid #ffffff; + background: @blue02; color: #ffffff; } } @@ -1146,6 +1369,11 @@ input#search-query { /** {#} Blog --------------------------------------------------------*/ + +h2#recent-blog-posts a { + font-size: 33px; +} + .blog-post { border-bottom: 1px solid #dfe4e7; margin-bottom: 20px; @@ -1294,46 +1522,147 @@ input#search-query { } } -// Scrollbar Styles +// -------- Table of Content -------------- -::-webkit-scrollbar { - width: 6px; - height: 6px; +.toc-wrapper { + background: #FFFFFF; + box-shadow: 0 0px 20px 0 rgba(12, 21, 75, 0.10); + border-radius: 5px; + padding: 10px 30px 30px 35px; } -::-webkit-scrollbar-button { - width: 0; - height: 0; +.toc-list { + h2 { + margin-top: 20px; + color: #142432; + } + + .devdocs ul.toc-list--element li { + margin: 8px 0 8px 20px; + } } -::-webkit-scrollbar-thumb { - background: #8FA6BA; - border: 0 none #ffffff; - border-radius: 50px; +.devdocs .toc-list { + ul li { + margin: 5px 0px 5px 20px; + font-weight: 500; + font-size: 15px; + display: block; + word-wrap: break-word; + } + + ul li ul { + margin-bottom: 16px; + margin-top: 10px; + } + + ul li li { + margin: 2px 0 2px 20px; + font-weight: 500; + word-wrap: break-word; + + a { + font-size: 14px; + } + } } -::-webkit-scrollbar-thumb:hover { - background: #8FA6BA; + +@media screen and (min-width: 1660px) { + .toc-wrapper { + overflow-y: auto; + background: #FFFFFF; + box-shadow: 0 -20px 30px 0 rgba(12,21,75, 0.10); + border-radius: 5px; + padding: 50px 50px 30px 45px; + position: fixed; + right: -20px; + top: 0; + z-index: 1; + width: 390px; + min-height: 43%; + max-height: 93%; + + } + + .toc-list { + font-size: 15px; + line-height: 130%; + + + &:hover { + opacity: 1; + } + + .devdocs ul li { + margin: 8px 0px 8px 20px; + } + + .devdocs ul li ul { + font-size: 14px; + } + } + + .toc-list--headline { + font-size: 22px; + color: #607182; + } } -::-webkit-scrollbar-thumb:active { - background: #8FA6BA; +@media screen and (min-width: 1900px) { + .toc-wrapper { + width: 450px; + } } -::-webkit-scrollbar-track { - background: @blue06; - border: 0 none #d2d2d2; +//Back to top button ---------------- + +.dev-top { + display: inline-block; + height: 50px; + width: 50px; border-radius: 50px; + position: fixed; + bottom: 70px; + right: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.05); + overflow: hidden; + text-indent: 100%; + white-space: nowrap; + background: rgba(96, 113, 130, 0.8) url("../img/arrow-up.svg") no-repeat center; + background-size: 23px 23px; + visibility: hidden; + opacity: 0; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + transition: all 0.3s; } -::-webkit-scrollbar-track:hover { - background: @blue06; +.dev-top.dev-is-visible { + visibility: visible; + opacity: 1; } -::-webkit-scrollbar-track:active { - background: #ffffff; +.dev-top.dev-fade-out { + opacity: .8; +} + +.no-touch .dev-top:hover, +.dev-top.dev-fade-out:hover { + background-color: #607182; + opacity: 1; } -::-webkit-scrollbar-corner { - background: transparent; +@media only screen and (min-width: 1600px) { + .dev-top { + right: auto; + bottom: 70px; + left: 1215px; + } +} + +@media screen and (min-width: 1900px) { + .dev-top { + left: 1230px; + } } diff --git a/source/assets/css/docsearch.css b/source/assets/css/docsearch.css new file mode 100644 index 0000000000..6f0bbf8d6c --- /dev/null +++ b/source/assets/css/docsearch.css @@ -0,0 +1,392 @@ +/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */ +.algolia-autocomplete { + width: 100%; +} + +.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu { + right: 0 !important; + left: inherit !important; +} + +.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before { + right: 48px; +} + +.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu { + left: 0 !important; + right: inherit !important; +} + +.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before { + left: 48px; +} + +@media screen and (min-width: 1200px) { + .algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu { + left: 350px !important; + min-width: 820px; + max-width: 1200px; + top: -18px !important; + } + .algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before { + transform: rotate(-135deg); + left: -7px; + top: 20px; + } +} + +@media screen and (min-width: 1550px) { + .algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu { + min-width: 1150px; + max-width: 1200px; + } +} + +.algolia-autocomplete .ds-dropdown-menu { + position: relative; + top: -6px; + border-radius: 4px; + margin: 6px 0 0; + padding: 0; + text-align: left; + height: auto; + position: relative; + background: transparent; + border: none; + z-index: 999; + overflow: visible; + max-width: 600px; + min-width: 320px; + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1); +} + +@media screen and (min-width: 500px) { + .algolia-autocomplete .ds-dropdown-menu { + min-width: 500px; + } +} + +.algolia-autocomplete .ds-dropdown-menu:before { + display: block; + position: absolute; + content: ''; + width: 14px; + height: 14px; + background: #fff; + z-index: 1000; + top: -7px; + border-top: 1px solid #cad9e0; + border-right: 1px solid #cad9e0; + transform: rotate(-45deg); + border-radius: 2px; +} + +.algolia-autocomplete .ds-dropdown-menu .ds-suggestions { + position: relative; + z-index: 1000; + margin-top: 8px; +} + +.algolia-autocomplete .ds-dropdown-menu .ds-suggestion { + cursor: pointer; +} + +.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple { + background-color: rgba(20, 145, 231, 0.05); +} + +.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content { + background-color: rgba(20, 145, 231, 0.05); +} + +.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text { + font-weight: 600; +} + +.algolia-autocomplete .ds-dropdown-menu [class^="ds-dataset-"] { + position: relative; + border: solid 1px #cad9e0; + background: #fff; + border-radius: 4px; + overflow: auto; + padding: 0 8px 8px; +} + +.algolia-autocomplete .ds-dropdown-menu * { + box-sizing: border-box; +} + +.algolia-autocomplete .algolia-docsearch-suggestion { + position: relative; + padding: 0 8px; + background: #fff; + color: #000; + overflow: hidden; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--highlight { + color: #0c4770; + background: rgba(114, 189, 241, 0.1); + padding: 0.1em 0.05em; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight, +.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight { + color: inherit; + background: inherit; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight { + padding: 0 0 1px; + background: inherit; + box-shadow: inset 0 -2px 0 0 rgba(20, 145, 231, 0.8); + color: inherit; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--content { + display: block; + float: right; + width: 70%; + position: relative; + padding: 5.33333px 0 5.33333px 10.66667px; + cursor: pointer; +} + +@media screen and (max-width: 500px) { + .algolia-autocomplete .algolia-docsearch-suggestion--content { + width: 100%; + float: none; + } +} + +.algolia-autocomplete .algolia-docsearch-suggestion--content:before { + content: ''; + position: absolute; + display: block; + top: 0; + height: 100%; + width: 1px; + background: #ddd; + left: -1px; +} + +@media screen and (max-width: 500px) { + .algolia-autocomplete .algolia-docsearch-suggestion--content:before { + display: none; + } +} + +.algolia-autocomplete .algolia-docsearch-suggestion--category-header { + position: relative; + border-bottom: 1px solid #ddd; + display: none; + margin-top: 8px; + padding: 4px 0; + font-size: 1em; + color: #33363D; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--wrapper { + width: 100%; + float: left; + padding: 8px 0 0 0; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column { + float: left; + width: 30%; + display: none; + padding-left: 0; + text-align: right; + position: relative; + padding: 5.33333px 10.66667px; + color: #677c8a; + font-size: 0.9em; + word-wrap: break-word; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before { + content: ''; + position: absolute; + display: block; + top: 0; + height: 100%; + width: 1px; + background: #ddd; + right: 0; +} + +@media screen and (max-width: 500px) { + .algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before { + display: none; + } +} + +.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight { + background-color: inherit; + color: inherit; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline { + display: none; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--title { + margin-bottom: 4px; + color: #000; + font-size: 0.9em; + font-weight: bold; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--text { + display: block; + line-height: 1.2em; + font-size: 0.85em; + color: #0e384f; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--no-results { + width: 100%; + padding: 8px 0; + text-align: center; + font-size: 1.2em; +} + +.algolia-autocomplete .algolia-docsearch-suggestion--no-results::before { + display: none; +} + +.algolia-autocomplete .algolia-docsearch-suggestion code { + padding: 1px 5px; + font-size: 90%; + border: none; + color: #222222; + background-color: #EBEBEB; + border-radius: 3px; + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; +} + +.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight { + background: none; +} + +.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header { + display: block; +} + +.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column { + display: block; +} + +@media screen and (max-width: 500px) { + .algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column { + display: none; + } +} + +.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion { + border-bottom: solid 1px #eee; + padding: 8px; + margin: 0; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content { + width: 100%; + padding: 0; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content::before { + display: none; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header { + margin: 0; + padding: 0; + display: block; + width: 100%; + border: none; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0 { + opacity: .6; + font-size: 0.85em; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1 { + opacity: .6; + font-size: 0.85em; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1::before { + background-image: url('data:image/svg+xml;utf8,'); + content: ''; + width: 10px; + height: 10px; + display: inline-block; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper { + width: 100%; + float: left; + margin: 0; + padding: 0; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column, .algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content, .algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline { + display: none !important; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title { + margin: 0; + color: #1491e7; + font-size: 0.9em; + font-weight: normal; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title::before { + content: "#"; + font-weight: bold; + color: #1491e7; + display: inline-block; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text { + margin: 4px 0 0; + display: block; + line-height: 1.4em; + padding: 5.33333px 8px; + background: #f8f8f8; + font-size: 0.85em; + opacity: .8; +} + +.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight { + color: #020a0e; + font-weight: bold; + box-shadow: none; +} + +.algolia-autocomplete .algolia-docsearch-footer { + width: 100px; + height: 20px; + z-index: 2000; + margin-top: 10.66667px; + float: right; + font-size: 0; + line-height: 0; +} + +.algolia-autocomplete .algolia-docsearch-footer--logo { + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: center; + background-size: 100%; + overflow: hidden; + text-indent: -9000px; + padding: 0 !important; + width: 100%; + height: 100%; + display: block; +} + +/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0ZGluIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO0VBQ0UsV0FBVztBQUNiOztBQUVBO0VBQ0UsbUJBQW1CO0VBQ25CLHdCQUF3QjtBQUMxQjs7QUFFQTtFQUNFLFdBQVc7QUFDYjs7QUFFQTtFQUNFLGtCQUFrQjtFQUNsQix5QkFBeUI7QUFDM0I7O0FBRUE7RUFDRSxVQUFVO0FBQ1o7O0FBRUE7RUFDRTtJQUNFLHNCQUFzQjtJQUN0QixnQkFBZ0I7SUFDaEIsaUJBQWlCO0lBQ2pCLHFCQUFxQjtFQUN2QjtFQUNBO0lBQ0UsMEJBQTBCO0lBQzFCLFVBQVU7SUFDVixTQUFTO0VBQ1g7QUFDRjs7QUFFQTtFQUNFO0lBQ0UsaUJBQWlCO0lBQ2pCLGlCQUFpQjtFQUNuQjtBQUNGOztBQUVBO0VBQ0Usa0JBQWtCO0VBQ2xCLFNBQVM7RUFDVCxrQkFBa0I7RUFDbEIsZUFBZTtFQUNmLFVBQVU7RUFDVixnQkFBZ0I7RUFDaEIsWUFBWTtFQUNaLGtCQUFrQjtFQUNsQix1QkFBdUI7RUFDdkIsWUFBWTtFQUNaLFlBQVk7RUFDWixpQkFBaUI7RUFDakIsZ0JBQWdCO0VBQ2hCLGdCQUFnQjtFQUNoQix3RUFBd0U7QUFDMUU7O0FBRUE7RUFDRTtJQUNFLGdCQUFnQjtFQUNsQjtBQUNGOztBQUVBO0VBQ0UsY0FBYztFQUNkLGtCQUFrQjtFQUNsQixXQUFXO0VBQ1gsV0FBVztFQUNYLFlBQVk7RUFDWixnQkFBZ0I7RUFDaEIsYUFBYTtFQUNiLFNBQVM7RUFDVCw2QkFBNkI7RUFDN0IsK0JBQStCO0VBQy9CLHlCQUF5QjtFQUN6QixrQkFBa0I7QUFDcEI7O0FBRUE7RUFDRSxrQkFBa0I7RUFDbEIsYUFBYTtFQUNiLGVBQWU7QUFDakI7O0FBRUE7RUFDRSxlQUFlO0FBQ2pCOztBQUVBO0VBQ0UsMENBQTBDO0FBQzVDOztBQUVBO0VBQ0UsMENBQTBDO0FBQzVDOztBQUVBO0VBQ0UsZ0JBQWdCO0FBQ2xCOztBQUVBO0VBQ0Usa0JBQWtCO0VBQ2xCLHlCQUF5QjtFQUN6QixnQkFBZ0I7RUFDaEIsa0JBQWtCO0VBQ2xCLGNBQWM7RUFDZCxrQkFBa0I7QUFDcEI7O0FBRUE7RUFDRSxzQkFBc0I7QUFDeEI7O0FBRUE7RUFDRSxrQkFBa0I7RUFDbEIsY0FBYztFQUNkLGdCQUFnQjtFQUNoQixXQUFXO0VBQ1gsZ0JBQWdCO0FBQ2xCOztBQUVBO0VBQ0UsY0FBYztFQUNkLG9DQUFvQztFQUNwQyxxQkFBcUI7QUFDdkI7O0FBRUE7O0VBRUUsY0FBYztFQUNkLG1CQUFtQjtBQUNyQjs7QUFFQTtFQUNFLGdCQUFnQjtFQUNoQixtQkFBbUI7RUFDbkIsb0RBQW9EO0VBQ3BELGNBQWM7QUFDaEI7O0FBRUE7RUFDRSxjQUFjO0VBQ2QsWUFBWTtFQUNaLFVBQVU7RUFDVixrQkFBa0I7RUFDbEIseUNBQXlDO0VBQ3pDLGVBQWU7QUFDakI7O0FBRUE7RUFDRTtJQUNFLFdBQVc7SUFDWCxXQUFXO0VBQ2I7QUFDRjs7QUFFQTtFQUNFLFdBQVc7RUFDWCxrQkFBa0I7RUFDbEIsY0FBYztFQUNkLE1BQU07RUFDTixZQUFZO0VBQ1osVUFBVTtFQUNWLGdCQUFnQjtFQUNoQixVQUFVO0FBQ1o7O0FBRUE7RUFDRTtJQUNFLGFBQWE7RUFDZjtBQUNGOztBQUVBO0VBQ0Usa0JBQWtCO0VBQ2xCLDZCQUE2QjtFQUM3QixhQUFhO0VBQ2IsZUFBZTtFQUNmLGNBQWM7RUFDZCxjQUFjO0VBQ2QsY0FBYztBQUNoQjs7QUFFQTtFQUNFLFdBQVc7RUFDWCxXQUFXO0VBQ1gsa0JBQWtCO0FBQ3BCOztBQUVBO0VBQ0UsV0FBVztFQUNYLFVBQVU7RUFDVixhQUFhO0VBQ2IsZUFBZTtFQUNmLGlCQUFpQjtFQUNqQixrQkFBa0I7RUFDbEIsNkJBQTZCO0VBQzdCLGNBQWM7RUFDZCxnQkFBZ0I7RUFDaEIscUJBQXFCO0FBQ3ZCOztBQUVBO0VBQ0UsV0FBVztFQUNYLGtCQUFrQjtFQUNsQixjQUFjO0VBQ2QsTUFBTTtFQUNOLFlBQVk7RUFDWixVQUFVO0VBQ1YsZ0JBQWdCO0VBQ2hCLFFBQVE7QUFDVjs7QUFFQTtFQUNFO0lBQ0UsYUFBYTtFQUNmO0FBQ0Y7O0FBRUE7RUFDRSx5QkFBeUI7RUFDekIsY0FBYztBQUNoQjs7QUFFQTtFQUNFLGFBQWE7QUFDZjs7QUFFQTtFQUNFLGtCQUFrQjtFQUNsQixXQUFXO0VBQ1gsZ0JBQWdCO0VBQ2hCLGlCQUFpQjtBQUNuQjs7QUFFQTtFQUNFLGNBQWM7RUFDZCxrQkFBa0I7RUFDbEIsaUJBQWlCO0VBQ2pCLGNBQWM7QUFDaEI7O0FBRUE7RUFDRSxXQUFXO0VBQ1gsY0FBYztFQUNkLGtCQUFrQjtFQUNsQixnQkFBZ0I7QUFDbEI7O0FBRUE7RUFDRSxhQUFhO0FBQ2Y7O0FBRUE7RUFDRSxnQkFBZ0I7RUFDaEIsY0FBYztFQUNkLFlBQVk7RUFDWixjQUFjO0VBQ2QseUJBQXlCO0VBQ3pCLGtCQUFrQjtFQUNsQiwwREFBMEQ7QUFDNUQ7O0FBRUE7RUFDRSxnQkFBZ0I7QUFDbEI7O0FBRUE7RUFDRSxjQUFjO0FBQ2hCOztBQUVBO0VBQ0UsY0FBYztBQUNoQjs7QUFFQTtFQUNFO0lBQ0UsYUFBYTtFQUNmO0FBQ0Y7O0FBRUE7RUFDRSw2QkFBNkI7RUFDN0IsWUFBWTtFQUNaLFNBQVM7QUFDWDs7QUFFQTtFQUNFLFdBQVc7RUFDWCxVQUFVO0FBQ1o7O0FBRUE7RUFDRSxhQUFhO0FBQ2Y7O0FBRUE7RUFDRSxTQUFTO0VBQ1QsVUFBVTtFQUNWLGNBQWM7RUFDZCxXQUFXO0VBQ1gsWUFBWTtBQUNkOztBQUVBO0VBQ0UsV0FBVztFQUNYLGlCQUFpQjtBQUNuQjs7QUFFQTtFQUNFLFdBQVc7RUFDWCxpQkFBaUI7QUFDbkI7O0FBRUE7RUFDRSwyVUFBMlU7RUFDM1UsV0FBVztFQUNYLFdBQVc7RUFDWCxZQUFZO0VBQ1oscUJBQXFCO0FBQ3ZCOztBQUVBO0VBQ0UsV0FBVztFQUNYLFdBQVc7RUFDWCxTQUFTO0VBQ1QsVUFBVTtBQUNaOztBQUVBO0VBQ0Usd0JBQXdCO0FBQzFCOztBQUVBO0VBQ0UsU0FBUztFQUNULGNBQWM7RUFDZCxnQkFBZ0I7RUFDaEIsbUJBQW1CO0FBQ3JCOztBQUVBO0VBQ0UsWUFBWTtFQUNaLGlCQUFpQjtFQUNqQixjQUFjO0VBQ2QscUJBQXFCO0FBQ3ZCOztBQUVBO0VBQ0UsZUFBZTtFQUNmLGNBQWM7RUFDZCxrQkFBa0I7RUFDbEIsc0JBQXNCO0VBQ3RCLG1CQUFtQjtFQUNuQixpQkFBaUI7RUFDakIsV0FBVztBQUNiOztBQUVBO0VBQ0UsY0FBYztFQUNkLGlCQUFpQjtFQUNqQixnQkFBZ0I7QUFDbEI7O0FBRUE7RUFDRSxZQUFZO0VBQ1osWUFBWTtFQUNaLGFBQWE7RUFDYixzQkFBc0I7RUFDdEIsWUFBWTtFQUNaLFlBQVk7RUFDWixjQUFjO0FBQ2hCOztBQUVBO0VBQ0Usa3JNQUFrck07RUFDbHJNLDRCQUE0QjtFQUM1QiwyQkFBMkI7RUFDM0IscUJBQXFCO0VBQ3JCLGdCQUFnQjtFQUNoQixvQkFBb0I7RUFDcEIscUJBQXFCO0VBQ3JCLFdBQVc7RUFDWCxZQUFZO0VBQ1osY0FBYztBQUNoQiIsImZpbGUiOiJzdGRpbiIsInNvdXJjZXNDb250ZW50IjpbIi5hbGdvbGlhLWF1dG9jb21wbGV0ZSB7XG4gIHdpZHRoOiAxMDAlO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtcmlnaHQgLmRzLWRyb3Bkb3duLW1lbnUge1xuICByaWdodDogMCAhaW1wb3J0YW50O1xuICBsZWZ0OiBpbmhlcml0ICFpbXBvcnRhbnQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZS5hbGdvbGlhLWF1dG9jb21wbGV0ZS1yaWdodCAuZHMtZHJvcGRvd24tbWVudTpiZWZvcmUge1xuICByaWdodDogNDhweDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlLmFsZ29saWEtYXV0b2NvbXBsZXRlLWxlZnQgLmRzLWRyb3Bkb3duLW1lbnUge1xuICBsZWZ0OiAwICFpbXBvcnRhbnQ7XG4gIHJpZ2h0OiBpbmhlcml0ICFpbXBvcnRhbnQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZS5hbGdvbGlhLWF1dG9jb21wbGV0ZS1sZWZ0IC5kcy1kcm9wZG93bi1tZW51OmJlZm9yZSB7XG4gIGxlZnQ6IDQ4cHg7XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtaW4td2lkdGg6IDEyMDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudSB7XG4gICAgbGVmdDogMzUwcHggIWltcG9ydGFudDtcbiAgICBtaW4td2lkdGg6IDgyMHB4O1xuICAgIG1heC13aWR0aDogMTIwMHB4O1xuICAgIHRvcDogLTE4cHggIWltcG9ydGFudDtcbiAgfVxuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudTpiZWZvcmUge1xuICAgIHRyYW5zZm9ybTogcm90YXRlKC0xMzVkZWcpO1xuICAgIGxlZnQ6IC03cHg7XG4gICAgdG9wOiAyMHB4O1xuICB9XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtaW4td2lkdGg6IDE1NTBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudSB7XG4gICAgbWluLXdpZHRoOiAxMTUwcHg7XG4gICAgbWF4LXdpZHRoOiAxMjAwcHg7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB0b3A6IC02cHg7XG4gIGJvcmRlci1yYWRpdXM6IDRweDtcbiAgbWFyZ2luOiA2cHggMCAwO1xuICBwYWRkaW5nOiAwO1xuICB0ZXh0LWFsaWduOiBsZWZ0O1xuICBoZWlnaHQ6IGF1dG87XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7XG4gIGJvcmRlcjogbm9uZTtcbiAgei1pbmRleDogOTk5O1xuICBvdmVyZmxvdzogdmlzaWJsZTtcbiAgbWF4LXdpZHRoOiA2MDBweDtcbiAgbWluLXdpZHRoOiAzMjBweDtcbiAgYm94LXNoYWRvdzogMCAxcHggMCAwIHJnYmEoMCwgMCwgMCwgMC4yKSwgMCAycHggM3B4IDAgcmdiYSgwLCAwLCAwLCAwLjEpO1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWluLXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUge1xuICAgIG1pbi13aWR0aDogNTAwcHg7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51OmJlZm9yZSB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGNvbnRlbnQ6ICcnO1xuICB3aWR0aDogMTRweDtcbiAgaGVpZ2h0OiAxNHB4O1xuICBiYWNrZ3JvdW5kOiAjZmZmO1xuICB6LWluZGV4OiAxMDAwO1xuICB0b3A6IC03cHg7XG4gIGJvcmRlci10b3A6IDFweCBzb2xpZCAjY2FkOWUwO1xuICBib3JkZXItcmlnaHQ6IDFweCBzb2xpZCAjY2FkOWUwO1xuICB0cmFuc2Zvcm06IHJvdGF0ZSgtNDVkZWcpO1xuICBib3JkZXItcmFkaXVzOiAycHg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAuZHMtc3VnZ2VzdGlvbnMge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHotaW5kZXg6IDEwMDA7XG4gIG1hcmdpbi10b3A6IDhweDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IC5kcy1zdWdnZXN0aW9uIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUgLmRzLXN1Z2dlc3Rpb24uZHMtY3Vyc29yIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSB7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjAsIDE0NSwgMjMxLCAwLjA1KTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IC5kcy1zdWdnZXN0aW9uLmRzLWN1cnNvciAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbjpub3QoLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSkgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDIwLCAxNDUsIDIzMSwgMC4wNSk7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tc3ViY2F0ZWdvcnktY29sdW1uLXRleHQge1xuICBmb250LXdlaWdodDogNjAwO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUgW2NsYXNzXj1cImRzLWRhdGFzZXQtXCJdIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBib3JkZXI6IHNvbGlkIDFweCAjY2FkOWUwO1xuICBiYWNrZ3JvdW5kOiAjZmZmO1xuICBib3JkZXItcmFkaXVzOiA0cHg7XG4gIG92ZXJmbG93OiBhdXRvO1xuICBwYWRkaW5nOiAwIDhweCA4cHg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAqIHtcbiAgYm94LXNpemluZzogYm9yZGVyLWJveDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBwYWRkaW5nOiAwIDhweDtcbiAgYmFja2dyb3VuZDogI2ZmZjtcbiAgY29sb3I6ICMwMDA7XG4gIG92ZXJmbG93OiBoaWRkZW47XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgY29sb3I6ICMwYzQ3NzA7XG4gIGJhY2tncm91bmQ6IHJnYmEoMTE0LCAxODksIDI0MSwgMC4xKTtcbiAgcGFkZGluZzogMC4xZW0gMC4wNWVtO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNhdGVnb3J5LWhlYWRlciAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyLWx2bDAgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWhpZ2hsaWdodCxcbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXItbHZsMSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgY29sb3I6IGluaGVyaXQ7XG4gIGJhY2tncm91bmQ6IGluaGVyaXQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tdGV4dCAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgcGFkZGluZzogMCAwIDFweDtcbiAgYmFja2dyb3VuZDogaW5oZXJpdDtcbiAgYm94LXNoYWRvdzogaW5zZXQgMCAtMnB4IDAgMCByZ2JhKDIwLCAxNDUsIDIzMSwgMC44KTtcbiAgY29sb3I6IGluaGVyaXQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY29udGVudCB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBmbG9hdDogcmlnaHQ7XG4gIHdpZHRoOiA3MCU7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgcGFkZGluZzogNS4zMzMzM3B4IDAgNS4zMzMzM3B4IDEwLjY2NjY3cHg7XG4gIGN1cnNvcjogcG9pbnRlcjtcbn1cblxuQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNTAwcHgpIHtcbiAgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jb250ZW50IHtcbiAgICB3aWR0aDogMTAwJTtcbiAgICBmbG9hdDogbm9uZTtcbiAgfVxufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6YmVmb3JlIHtcbiAgY29udGVudDogJyc7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHRvcDogMDtcbiAgaGVpZ2h0OiAxMDAlO1xuICB3aWR0aDogMXB4O1xuICBiYWNrZ3JvdW5kOiAjZGRkO1xuICBsZWZ0OiAtMXB4O1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6YmVmb3JlIHtcbiAgICBkaXNwbGF5OiBub25lO1xuICB9XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBib3JkZXItYm90dG9tOiAxcHggc29saWQgI2RkZDtcbiAgZGlzcGxheTogbm9uZTtcbiAgbWFyZ2luLXRvcDogOHB4O1xuICBwYWRkaW5nOiA0cHggMDtcbiAgZm9udC1zaXplOiAxZW07XG4gIGNvbG9yOiAjMzMzNjNEO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXdyYXBwZXIge1xuICB3aWR0aDogMTAwJTtcbiAgZmxvYXQ6IGxlZnQ7XG4gIHBhZGRpbmc6IDhweCAwIDAgMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4ge1xuICBmbG9hdDogbGVmdDtcbiAgd2lkdGg6IDMwJTtcbiAgZGlzcGxheTogbm9uZTtcbiAgcGFkZGluZy1sZWZ0OiAwO1xuICB0ZXh0LWFsaWduOiByaWdodDtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBwYWRkaW5nOiA1LjMzMzMzcHggMTAuNjY2NjdweDtcbiAgY29sb3I6ICM2NzdjOGE7XG4gIGZvbnQtc2l6ZTogMC45ZW07XG4gIHdvcmQtd3JhcDogYnJlYWstd29yZDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW46YmVmb3JlIHtcbiAgY29udGVudDogJyc7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHRvcDogMDtcbiAgaGVpZ2h0OiAxMDAlO1xuICB3aWR0aDogMXB4O1xuICBiYWNrZ3JvdW5kOiAjZGRkO1xuICByaWdodDogMDtcbn1cblxuQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNTAwcHgpIHtcbiAgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW46YmVmb3JlIHtcbiAgICBkaXNwbGF5OiBub25lO1xuICB9XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tc3ViY2F0ZWdvcnktY29sdW1uIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiBpbmhlcml0O1xuICBjb2xvcjogaW5oZXJpdDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1pbmxpbmUge1xuICBkaXNwbGF5OiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlIHtcbiAgbWFyZ2luLWJvdHRvbTogNHB4O1xuICBjb2xvcjogIzAwMDtcbiAgZm9udC1zaXplOiAwLjllbTtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tdGV4dCB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBsaW5lLWhlaWdodDogMS4yZW07XG4gIGZvbnQtc2l6ZTogMC44NWVtO1xuICBjb2xvcjogIzBlMzg0Zjtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1uby1yZXN1bHRzIHtcbiAgd2lkdGg6IDEwMCU7XG4gIHBhZGRpbmc6IDhweCAwO1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGZvbnQtc2l6ZTogMS4yZW07XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tbm8tcmVzdWx0czo6YmVmb3JlIHtcbiAgZGlzcGxheTogbm9uZTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uIGNvZGUge1xuICBwYWRkaW5nOiAxcHggNXB4O1xuICBmb250LXNpemU6IDkwJTtcbiAgYm9yZGVyOiBub25lO1xuICBjb2xvcjogIzIyMjIyMjtcbiAgYmFja2dyb3VuZC1jb2xvcjogI0VCRUJFQjtcbiAgYm9yZGVyLXJhZGl1czogM3B4O1xuICBmb250LWZhbWlseTogTWVubG8sTW9uYWNvLENvbnNvbGFzLFwiQ291cmllciBOZXdcIixtb25vc3BhY2U7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbiBjb2RlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBiYWNrZ3JvdW5kOiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24uYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbl9fbWFpbiAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIHtcbiAgZGlzcGxheTogYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uX19zZWNvbmRhcnkgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXN1YmNhdGVnb3J5LWNvbHVtbiB7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24uYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbl9fc2Vjb25kYXJ5IC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4ge1xuICAgIGRpc3BsYXk6IG5vbmU7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbiB7XG4gIGJvcmRlci1ib3R0b206IHNvbGlkIDFweCAjZWVlO1xuICBwYWRkaW5nOiA4cHg7XG4gIG1hcmdpbjogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQge1xuICB3aWR0aDogMTAwJTtcbiAgcGFkZGluZzogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6OmJlZm9yZSB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXIge1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyOiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyLWx2bDAge1xuICBvcGFjaXR5OiAuNjtcbiAgZm9udC1zaXplOiAwLjg1ZW07XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXItbHZsMSB7XG4gIG9wYWNpdHk6IC42O1xuICBmb250LXNpemU6IDAuODVlbTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNhdGVnb3J5LWhlYWRlci1sdmwxOjpiZWZvcmUge1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDt1dGY4LDxzdmcgd2lkdGg9XCIxMFwiIGhlaWdodD1cIjEwXCIgdmlld0JveD1cIjAgMCAyMCAzOFwiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIj48cGF0aCBkPVwiTTEuNDkgNC4zMWwxNCAxNi4xMjYuMDAyLTIuNjI0LTE0IDE2LjA3NC0xLjMxNCAxLjUxIDMuMDE3IDIuNjI2IDEuMzEzLTEuNTA4IDE0LTE2LjA3NSAxLjE0Mi0xLjMxMy0xLjE0LTEuMzEzLTE0LTE2LjEyNUwzLjIuMTguMTggMi44bDEuMzEgMS41MXpcIiBmaWxsLXJ1bGU9XCJldmVub2RkXCIgZmlsbD1cIiUyMzFEMzY1N1wiIC8+PC9zdmc+Jyk7XG4gIGNvbnRlbnQ6ICcnO1xuICB3aWR0aDogMTBweDtcbiAgaGVpZ2h0OiAxMHB4O1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS13cmFwcGVyIHtcbiAgd2lkdGg6IDEwMCU7XG4gIGZsb2F0OiBsZWZ0O1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4sIC5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1kdXBsaWNhdGUtY29udGVudCwgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXN1YmNhdGVnb3J5LWlubGluZSB7XG4gIGRpc3BsYXk6IG5vbmUgIWltcG9ydGFudDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlIHtcbiAgbWFyZ2luOiAwO1xuICBjb2xvcjogIzE0OTFlNztcbiAgZm9udC1zaXplOiAwLjllbTtcbiAgZm9udC13ZWlnaHQ6IG5vcm1hbDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlOjpiZWZvcmUge1xuICBjb250ZW50OiBcIiNcIjtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gIGNvbG9yOiAjMTQ5MWU3O1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS10ZXh0IHtcbiAgbWFyZ2luOiA0cHggMCAwO1xuICBkaXNwbGF5OiBibG9jaztcbiAgbGluZS1oZWlnaHQ6IDEuNGVtO1xuICBwYWRkaW5nOiA1LjMzMzMzcHggOHB4O1xuICBiYWNrZ3JvdW5kOiAjZjhmOGY4O1xuICBmb250LXNpemU6IDAuODVlbTtcbiAgb3BhY2l0eTogLjg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS10ZXh0IC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBjb2xvcjogIzAyMGEwZTtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gIGJveC1zaGFkb3c6IG5vbmU7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtZm9vdGVyIHtcbiAgd2lkdGg6IDEwMHB4O1xuICBoZWlnaHQ6IDIwcHg7XG4gIHotaW5kZXg6IDIwMDA7XG4gIG1hcmdpbi10b3A6IDEwLjY2NjY3cHg7XG4gIGZsb2F0OiByaWdodDtcbiAgZm9udC1zaXplOiAwO1xuICBsaW5lLWhlaWdodDogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1mb290ZXItLWxvZ28ge1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDt1dGY4LDxzdmcgd2lkdGg9XCI4NlwiIGhlaWdodD1cIjEzXCIgdmlld0JveD1cIjAgMCA4NiAxM1wiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIj48ZyBmaWxsPVwibm9uZVwiIGZpbGwtcnVsZT1cImV2ZW5vZGRcIj48cGF0aCBkPVwiTTUuMDEgOC4xYzAgLjYzLS4yMjggMS4xMi0uNjg0IDEuNDctLjQ1Ni4zNTItMS4wNzQuNTI4LTEuODU1LjUyOC0uODQgMC0xLjQ5LS4xMS0xLjk1LS4zMjd2LS44Yy4zLjEzLjYxLjIyLjk2LjMuMzUuMDcuNjkuMTEgMS4wMy4xMS41NiAwIC45Ny0uMSAxLjI1LS4zMXMuNDItLjUuNDItLjg3YzAtLjI1LS4wNS0uNDUtLjE1LS42MS0uMDktLjE1LS4yNi0uMy0uNDktLjQzLS4yNC0uMTQtLjU5LS4yOS0xLjA3LS40Ni0uNjYtLjIzLTEuMTMtLjUyLTEuNDItLjg0Qy43NiA1LjUyLjYxIDUuMS42MSA0LjU3YzAtLjU1LjIxLS45ODUuNjItMS4zMS40Mi0uMzI1Ljk2LS40ODggMS42NC0uNDg4LjcxIDAgMS4zNy4xMyAxLjk2LjM5bC0uMjYuNzJjLS41OS0uMjUtMS4xNi0uMzctMS43MS0uMzctLjQ0IDAtLjc4LjA5LTEuMDMuMjgtLjI1LjE5LS4zNy40NS0uMzcuNzggMCAuMjQuMDUuNDUuMTQuNi4wOS4xNi4yNDYuMy40Ni40My4yMi4xMy41NS4yNzcuOTk2LjQzNy43NS4yNiAxLjI2NC41NSAxLjU0NS44Ni4yOS4zLjQzLjcuNDMgMS4xOXptMy41OTggMmMtLjc5IDAtMS40MTUtLjI0LTEuODcyLS43MjMtLjQ1OC0uNDgyLS42ODYtMS4xNS0uNjg2LTIuMDA3IDAtLjg2Mi4yMTItMS41NDguNjM3LTIuMDU2LjQyNS0uNTA3Ljk5NS0uNzYgMS43MS0uNzYuNjcyIDAgMS4yMDMuMjIgMS41OTMuNjYuMzkuNDQuNTg2IDEuMDIzLjU4NiAxLjc0NnYuNTEzSDYuODljLjAxNi42My4xNzUgMS4xMDYuNDc2IDEuNDMuMy4zMjcuNzI1LjQ5IDEuMjcyLjQ5LjU3NiAwIDEuMTQ2LS4xMiAxLjcxLS4zNjJ2LjczYy0uMjg4LjEzLS41Ni4yMi0uODE0LjI3LS4yNTYuMDYtLjU2NC4wOC0uOTI2LjA4em0tLjIyLTQuODdjLS40MyAwLS43NzIuMTQtMS4wMjcuNDItLjI1LjI4LS40LjY3LS40NSAxLjE2NGgyLjhjMC0uNTEyLS4xMS0uOTAzLS4zNC0xLjE3NS0uMjMtLjI4LS41NS0uNDEtLjk3LS40MXpNMTUuMjUgMTBsLS4xNi0uNzYyaC0uMDRjLS4yNjYuMzM2LS41MzIuNTYzLS43OTguNjgtLjI2NS4xMi0uNTk2LjE4LS45OTMuMTgtLjUzIDAtLjk1LS4xMzctMS4yNS0uNDEtLjMtLjI3NC0uNDUtLjY2My0uNDUtMS4xNjcgMC0xLjA4Ljg2LTEuNjQgMi41OS0xLjdsLjkxLS4wMnYtLjM0YzAtLjQyLS4wOS0uNzMtLjI3LS45My0uMTgtLjItLjQ3LS4zLS44Ny0uMy0uNDUgMC0uOTUuMTQtMS41Mi40MWwtLjI1LS42MmMuMjYtLjE0LjU1LS4yNS44Ni0uMzMuMzEtLjA4LjYzLS4xMi45NC0uMTIuNjMgMCAxLjExLjE1IDEuNDEuNDMuMy4yOS40Ni43NC40NiAxLjM3VjEwaC0uNnptLTEuODMtLjU3Yy41MDUgMCAuOS0uMTQgMS4xOS0uNDE2LjI4Ny0uMjc3LjQzLS42NjQuNDMtMS4xNjJ2LS40ODRsLS44MS4wMzRjLS42NDQuMDIzLTEuMTEuMTIzLTEuMzk0LjMtLjI4NC4xNzgtLjQyNy40NTQtLjQyNy44MjggMCAuMjkzLjA5LjUxNi4yNi42Ny4xOC4xNTIuNDIuMjMuNzQuMjN6bTYuNTQzLTQuODhjLjI0IDAgLjQ1Mi4wMi42NC4wNmwtLjExMi43NWMtLjIyLS4wNDgtLjQxLS4wNzItLjU4LS4wNzItLjQzIDAtLjguMTc2LTEuMTEuNTI3LS4zMS4zNTItLjQ2Ljc5LS40NiAxLjMxNFYxMGgtLjgxVjQuNjQ4aC42N2wuMS45OTJoLjA0Yy4yLS4zNS40NC0uNjE3LjcyLS44MDYuMjgtLjE5LjU5LS4yODMuOTMtLjI4M3ptMy41OCA1LjU0OGMtLjc3NSAwLTEuMzc0LS4yNC0xLjgtLjcxNi0uNDI0LS40NzctLjYzNi0xLjE1LS42MzYtMi4wMjQgMC0uODk1LjIxNS0xLjU4Ny42NDYtMi4wNzUuNDMyLS40ODggMS4wNDYtLjczMiAxLjg0NC0uNzMyLjI1NyAwIC41MTQuMDMuNzcuMDkuMjU4LjA2LjQ2LjEyLjYwNy4ybC0uMjQ4LjY5Yy0uMTgtLjA3LS4zNzUtLjEzLS41ODYtLjE4LS4yMTItLjA0LS40LS4wNy0uNTYyLS4wNy0xLjA4NyAwLTEuNjMuNy0xLjYzIDIuMDggMCAuNjYuMTMyIDEuMTYuMzk3IDEuNTIuMjY3LjM1LjY2LjUzIDEuMTguNTMuNDQ2IDAgLjkwNC0uMDkgMS4zNzMtLjI4di43MmMtLjM1OC4xOS0uODEuMjgtMS4zNTMuMjh6bTYuMjgtLjA5OFY2LjUzOGMwLS40MzYtLjEtLjc2Mi0uMjk3LS45NzYtLjE5OC0uMjE1LS41MS0uMzIzLS45MzItLjMyMy0uNTYzIDAtLjk3NC4xNS0xLjIzMy40NS0uMjUuMy0uMzguODEtLjM4IDEuNVYxMGgtLjgxVjIuNDAyaC44MXYyLjNjMCAuMjc3LS4wMS41MDYtLjA0LjY5aC4wNWMuMTYtLjI2LjM5LS40Ni42OC0uNjEuMy0uMTQ3LjYzLS4yMiAxLjAxLS4yMi42NiAwIDEuMTUuMTU0IDEuNDcuNDY1LjMzLjMxLjQ5LjgwNS40OSAxLjQ4MlYxMGgtLjgxem03LjU2NS01LjQ0Yy43MDMgMCAxLjI1LjI0IDEuNjM4LjcyLjM5LjQ4LjU4MyAxLjE2LjU4MyAyLjA0IDAgLjg3OC0uMiAxLjU2LS41OSAyLjA0OC0uMzkuNDg2LS45NC43My0xLjY0LjczLS4zNSAwLS42Ny0uMDY1LS45Ni0uMTkzLS4yOS0uMTMtLjUzLS4zMjctLjczLS41OTNoLS4wNmwtLjE3LjY4OGgtLjU4VjIuNDAyaC44MXYxLjg0NmMwIC40MTMtLjAyLjc4NS0uMDQgMS4xMTNoLjA0Yy4zOC0uNTMuOTQtLjggMS42OC0uOHptLS4xMTcuNjhjLS41NSAwLS45NS4xNTgtMS4xOS40NzUtLjI0LjMxOC0uMzYuODUyLS4zNiAxLjYwNCAwIC43NS4xMyAxLjI5LjM4IDEuNjEuMjUuMzIuNjYuNDggMS4yMS40OC41IDAgLjg3LS4xOSAxLjEyLS41NS4yNS0uMzcuMzctLjg5LjM3LTEuNTcgMC0uNy0uMTItMS4yMi0uMzYtMS41Ni0uMjQtLjM1LS42Mi0uNTItMS4xMy0uNTJ6bTIuNzEtLjU5MmguODdMNDIuMDIgNy43Yy4yNTcuNjk3LjQxNiAxLjIuNDggMS41MWguMDM3Yy4wNDItLjE2Ny4xMy0uNDUuMjY2LS44NTMuMTM2LS40MDIuNTc4LTEuNjM4IDEuMzI3LTMuNzFINDVsLTIuMyA2LjA5NWMtLjIzLjYwMi0uNDk2IDEuMDMtLjggMS4yODItLjMwNC4yNTItLjY3OC4zNzgtMS4xMi4zNzgtLjI0OCAwLS40OS0uMDI3LS43MzMtLjA4M3YtLjY1Yy4xOC4wNC4zOC4wNi42LjA2LjU1NyAwIC45NTQtLjMyIDEuMTkyLS45NGwuMjktLjc2LTIuMTYtNS4zOXpcIiBmaWxsPVwiJTIzNzk3OTc5XCIvPjxwYXRoIGQ9XCJNNzAuODgzIDQuOGwtLjU1MyAyLjA2IDEuNzgyLTEuMDI1Yy0uMjYtLjUtLjctLjg3Ni0xLjIzLTEuMDM0ek02Ny43NiAzLjQxYy0uMjM3LS4yNS0uNjI0LS4yNS0uODYyIDBsLS4xMDguMTE0Yy0uMjM4LjI1Mi0uMjM4LjY2IDAgLjkxbC4xMTUuMTIyYy4yNDQtLjQxNC41NTYtLjc4LjkxOC0xLjA4bC0uMDYyLS4wNjZ6bTMuNzMtLjYzYzAtLjAxNC4wMDMtLjAyNy4wMDMtLjA0MnYtLjMyMmMwLS4zNTUtLjI3My0uNjQ0LS42MS0uNjQ0aC0xLjA2OGMtLjMzNyAwLS42MS4yODgtLjYxLjY0NHYuMzE3Yy4zNC0uMS42OTgtLjE1NiAxLjA2OC0uMTU2LjQyNCAwIC44MzMuMDcyIDEuMjE2LjIwM1wiIGZpbGw9XCIlMjM0NkFFREFcIi8+PHBhdGggZD1cIk03MC4zMTYgNC4yNDNjMS4zNCAwIDIuNDI4IDEuMTQ1IDIuNDI4IDIuNTUyIDAgMS40MDgtMS4wOSAyLjU1My0yLjQyOCAyLjU1My0xLjM0IDAtMi40MjgtMS4xNDUtMi40MjgtMi41NTMgMC0xLjQwNyAxLjA5LTIuNTUyIDIuNDI4LTIuNTUybS0zLjQgMi41NTJjMCAxLjk3NCAxLjUyMiAzLjU3NCAzLjQgMy41NzRzMy40LTEuNiAzLjQtMy41OC0xLjUyMi0zLjU4LTMuNC0zLjU4LTMuNCAxLjYtMy40IDMuNTd6XCIgZmlsbD1cIiUyMzQ2QUVEQVwiLz48cGF0aCBkPVwiTTU0Ljc1OCAxMC4xNzVjLS4xNC0uMzktLjI3LS43NzYtLjM5NS0xLjE1NS0uMTI0LS4zNzgtLjI1LS43NjMtLjM4My0xLjE1NEg1MC4xbC0uNzggMi4zMWgtMS4yNDdjLjMzLS45NTguNjQtMS44NDMuOTI3LTIuNjU2LjI4OC0uODEzLjU3LTEuNTg1Ljg0Ny0yLjMxNi4yNzUtLjczLjU1LTEuNDMuODItMi4wOTQuMjczLS42NjYuNTU3LTEuMzI0Ljg1NC0xLjk3N2gxLjFjLjMuNjUzLjU4IDEuMzEuODYgMS45NzcuMjcuNjY1LjU1IDEuMzYzLjgyIDIuMDk0LjI4LjczLjU2IDEuNTAzLjg1IDIuMzE2LjI5LjgxMy42IDEuNjk4LjkzIDIuNjU1aC0xLjMxem0tMS4xMjQtMy4zNTNjLS4yNjQtLjc1Ny0uNTI1LTEuNDktLjc4NS0yLjItLjI2LS43MDgtLjUzLTEuMzg4LS44MS0yLjA0LS4yOS42NTItLjU3IDEuMzMyLS44MyAyLjA0LS4yNi43MS0uNTIgMS40NDMtLjc3IDIuMmgzLjE5em01LjQ1IDMuNDgzYy0uNzEtLjAxNy0xLjIxLS4xNzgtMS41MDgtLjQ4Mi0uMjk3LS4zMDUtLjQ0NS0uNzgtLjQ0NS0xLjQyM1YuMjZsMS4xNS0uMjF2OC4xNTVjMCAuMi4wMi4zNjUuMDUuNDk2LjA0LjEzLjA5LjI0LjE2LjMyLjA4LjA4LjE4LjE0LjMuMTguMTMuMDQuMjguMDguNDYuMWwtLjE2IDEuMDJtNS40Ny0uODFjLS4xLjA3LS4yOS4xNi0uNTcuMjctLjI4LjExLS42MS4xNy0uOTkuMTdzLS43NS0uMDYtMS4wOS0uMTljLS4zNC0uMTMtLjY0LS4zMy0uODktLjYtLjI2LS4yNy0uNDYtLjYxLS42MS0xLjAyLS4xNS0uNC0uMjItLjg5LS4yMi0xLjQ2IDAtLjQ5LjA3LS45NS4yMS0xLjM2LjE0LS40MS4zNS0uNzcuNjEtMS4wNy4yNy0uMy42LS41My45OS0uNy4zOS0uMTcuODMtLjI1IDEuMzEtLjI1LjU0IDAgMS4wMS4wNCAxLjQxLjEyLjQuMDkuNzMuMTYgMS4wMS4yM1Y5LjdjMCAxLjA0NC0uMjUgMS44LS43NiAyLjI3cy0xLjI5LjcwNS0yLjMzLjcwNWMtLjQgMC0uNzgtLjAzNS0xLjE0LS4xMDQtLjM2LS4wNy0uNjctLjE1LS45My0uMjRsLjIxLTEuMDZjLjIzLjA5LjUxLjE4Ljg0LjI1LjM0LjA4LjY4LjExIDEuMDUuMTEuNjkgMCAxLjE4LS4xNCAxLjQ4LS40My4zLS4yOC40NS0uNzQuNDUtMS4zN3YtLjN6bS0uNDctNS4xM2MtLjE5LS4wMy0uNDUtLjA0LS43OC0uMDQtLjYxIDAtMS4wOS4yMi0xLjQyLjY0LS4zMy40My0uNS45OS0uNSAxLjcgMCAuMzkuMDUuNzMuMTQgMS4wMS4xLjI4LjIzLjUxLjM5LjY5LjE2LjE4LjM1LjMyLjU2LjQxLjIxLjA5LjQzLjEzLjY1LjEzLjMxIDAgLjU5LS4wNC44NC0uMTMuMjYtLjA5LjQ2LS4yLjYxLS4zMnYtNGMtLjExLS4wMy0uMjctLjA2LS40Ni0uMDl6bTEyLjc3IDUuOTVjLS43LS4wMi0xLjIxLS4xOC0xLjUtLjQ4LS4yOS0uMy0uNDQtLjc4LS40NC0xLjQyVi4yNmwxLjE1LS4yMXY4LjE1NWMwIC4yLjAyLjM2NS4wNS40OTYuMDQuMTMuMDkuMjQuMTYuMzIuMDguMDguMTguMTQuMy4xOC4xMy4wNC4yOC4wOC40Ni4xbC0uMTYgMS4wMm0yLjAxLTguMTRjLS4yIDAtLjM4LS4wNy0uNTItLjIxLS4xNC0uMTQtLjIxLS4zNC0uMjEtLjU4IDAtLjI0LjA3LS40NC4yMi0uNTguMTQtLjE0LjMyLS4yMS41Mi0uMjEuMjEgMCAuMzguMDguNTMuMjIuMTUuMTUuMjIuMzQuMjIuNTggMCAuMjUtLjA3LjQ0LS4yMS41OC0uMTUuMTUtLjMyLjIyLS41My4yMnptLS41NyAxLjIzaDEuMTV2Ni43OUg3OC4zVjMuNHptNS4yLS4xN2MuNDYgMCAuODUuMDcgMS4xNy4xOS4zMi4xMy41Ny4zMS43Ny41NC4yLjIzLjMzLjUuNDIuODIuMDguMzIuMTIuNjcuMTIgMS4wNXY0LjI0bC0uNDEuMDdjLS4xNy4wMy0uMzcuMDYtLjYuMDktLjIyLjAzLS40Ni4wNS0uNzIuMDgtLjI2LjAyLS41MS4wMy0uNzcuMDMtLjM2IDAtLjY5LS4wNC0xLS4xMi0uMy0uMDgtLjU2LS4yLS43OS0uMzctLjIyLS4xNy0uMzktLjM5LS41Mi0uNjdTODEgOC41NyA4MSA4LjE4YzAtLjM3LjA3LS42OTIuMjItLjk2MnMuMzQtLjQ5LjU5LS42NTNjLjI1LS4xNjQuNTMtLjI4Ni44Ni0uMzY0LjMzLS4wOC42OC0uMTIgMS4wNC0uMTIuMTIgMCAuMjQuMDEuMzYuMDIuMTMuMDIuMjQuMDMuMzUuMDUuMTIuMDMuMjEuMDQuMy4wNi4wODMuMDIuMTQuMDMuMTc0LjA0VjUuOWMwLS4yLS4wMi0uMzk1LS4wNy0uNTktLjA0LS4xOTgtLjExNC0uMzctLjIyLS41MjQtLjEwNy0uMTUtLjI1My0uMjczLS40NC0uMzY0LS4xODMtLjA5LS40MjQtLjEzLS43Mi0uMTMtLjM4IDAtLjcxLjAzLS45OTYuMDktLjI5LjA1NS0uNS4xMTQtLjY0LjE3NWwtLjE0LTEuMDA3Yy4xNDctLjA3LjM5NS0uMTMzLjc0LS4yLjM0Ni0uMDY0LjcyLS4xIDEuMTI1LS4xem0uMSA2LjA4Yy4yNyAwIC41MiAwIC43My0uMDIuMjEtLjAxLjM5LS4wMy41My0uMDdWNy4xOWMtLjA4LS4wNDQtLjIyLS4wOC0uNC0uMTEtLjE4LS4wMy0uNDEtLjA0Ny0uNjctLjA0Ny0uMTcgMC0uMzYuMDItLjU1LjA0LS4xOS4wMy0uMzcuMDgtLjUzLjE3LS4xNi4wODUtLjI5LjE5OC0uNC4zNC0uMTEuMTQ1LS4xNi4zMy0uMTYuNTcgMCAuNDM1LjEzLjczNy4zOS45MDYuMjYuMTcuNjIuMjUgMS4wNy4yNXpcIiBmaWxsPVwiJTIzMUQzNjU3XCIvPjwvZz48L3N2Zz4nKTtcbiAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDtcbiAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCU7XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIHRleHQtaW5kZW50OiAtOTAwMHB4O1xuICBwYWRkaW5nOiAwICFpbXBvcnRhbnQ7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuIl19 */ \ No newline at end of file diff --git a/source/assets/css/docsearch.min.css b/source/assets/css/docsearch.min.css index e6ab099f08..b0b45cd982 100644 --- a/source/assets/css/docsearch.min.css +++ b/source/assets/css/docsearch.min.css @@ -1 +1,2 @@ -/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;box-sizing:border-box;visibility:visible!important}.searchbox .algolia-autocomplete{display:block;width:100%;height:100%}.searchbox__wrapper{width:100%;height:100%;z-index:1;position:relative}.searchbox__input{display:inline-block;box-sizing:border-box;-webkit-transition:box-shadow .4s ease,background .4s ease;transition:box-shadow .4s ease,background .4s ease;border:0;border-radius:16px;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0;padding-right:26px;padding-left:32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none}.searchbox__input:hover{box-shadow:inset 0 0 0 1px #b3b3b3}.searchbox__input:active,.searchbox__input:focus{outline:0;box-shadow:inset 0 0 0 1px #aaa;background:#fff}.searchbox__input::-webkit-input-placeholder{color:#aaa}.searchbox__input::-moz-placeholder{color:#aaa}.searchbox__input:-ms-input-placeholder{color:#aaa}.searchbox__input::placeholder{color:#aaa}.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69,142,225,0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0}.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:''}.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer}.searchbox__submit:focus{outline:0}.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96}.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0,0,0,.5)}.searchbox__reset.hide{display:none}.searchbox__reset:focus{outline:0}.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px}.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:a;animation-name:a;-webkit-animation-duration:.15s;animation-duration:.15s}@-webkit-keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(69,142,225,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #d9d9d9;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#02060c;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#174d8c;background:rgba(143,187,237,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(69,142,225,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#02060c;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#63676d}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#458ee1;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#458ee1;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#3f4145;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;box-sizing:border-box;visibility:visible!important}.searchbox .algolia-autocomplete{display:block;width:100%;height:100%}.searchbox__wrapper{width:100%;height:100%;z-index:1;position:relative}.searchbox__input{display:inline-block;box-sizing:border-box;-webkit-transition:box-shadow .4s ease,background .4s ease;transition:box-shadow .4s ease,background .4s ease;border:0;border-radius:16px;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0;padding-right:26px;padding-left:32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none}.searchbox__input:hover{box-shadow:inset 0 0 0 1px #b3b3b3}.searchbox__input:active,.searchbox__input:focus{outline:0;box-shadow:inset 0 0 0 1px #aaa;background:#fff}.searchbox__input::-webkit-input-placeholder{color:#aaa}.searchbox__input::-moz-placeholder{color:#aaa}.searchbox__input:-ms-input-placeholder{color:#aaa}.searchbox__input::placeholder{color:#aaa}.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69,142,225,0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0}.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:''}.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer}.searchbox__submit:focus{outline:0}.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96}.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0,0,0,.5)}.searchbox__reset.hide{display:none}.searchbox__reset:focus{outline:0}.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px}.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:a;animation-name:a;-webkit-animation-duration:.15s;animation-duration:.15s}@-webkit-keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(69,142,225,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #d9d9d9;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#02060c;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#174d8c;background:rgba(143,187,237,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(69,142,225,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#02060c;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#63676d}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#458ee1;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#458ee1;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#3f4145;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;box-sizing:border-box;visibility:visible!important}.searchbox .algolia-autocomplete{display:block;width:100%;height:100%}.searchbox__wrapper{width:100%;height:100%;z-index:1;position:relative}.searchbox__input{display:inline-block;box-sizing:border-box;-webkit-transition:box-shadow .4s ease,background .4s ease;transition:box-shadow .4s ease,background .4s ease;border:0;border-radius:16px;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0;padding-right:26px;padding-left:32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none}.searchbox__input:hover{box-shadow:inset 0 0 0 1px #b3b3b3}.searchbox__input:active,.searchbox__input:focus{outline:0;box-shadow:inset 0 0 0 1px #aaa;background:#fff}.searchbox__input::-webkit-input-placeholder{color:#aaa}.searchbox__input::-moz-placeholder{color:#aaa}.searchbox__input:-ms-input-placeholder{color:#aaa}.searchbox__input::placeholder{color:#aaa}.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69,142,225,0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0}.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:''}.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer}.searchbox__submit:focus{outline:0}.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96}.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0,0,0,.5)}.searchbox__reset.hide{display:none}.searchbox__reset:focus{outline:0}.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px}.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:a;animation-name:a;-webkit-animation-duration:.15s;animation-duration:.15s}@-webkit-keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(69,142,225,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #d9d9d9;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#02060c;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#174d8c;background:rgba(143,187,237,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(69,142,225,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#02060c;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#63676d}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#458ee1;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#458ee1;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#3f4145;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;box-sizing:border-box;visibility:visible!important}.searchbox .algolia-autocomplete{display:block;width:100%;height:100%}.searchbox__wrapper{width:100%;height:100%;z-index:1;position:relative}.searchbox__input{display:inline-block;box-sizing:border-box;-webkit-transition:box-shadow .4s ease,background .4s ease;transition:box-shadow .4s ease,background .4s ease;border:0;border-radius:16px;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0;padding-right:26px;padding-left:32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none}.searchbox__input:hover{box-shadow:inset 0 0 0 1px #b3b3b3}.searchbox__input:active,.searchbox__input:focus{outline:0;box-shadow:inset 0 0 0 1px #aaa;background:#fff}.searchbox__input::-webkit-input-placeholder{color:#aaa}.searchbox__input::-moz-placeholder{color:#aaa}.searchbox__input:-ms-input-placeholder{color:#aaa}.searchbox__input::placeholder{color:#aaa}.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69,142,225,0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0}.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:''}.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer}.searchbox__submit:focus{outline:0}.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96}.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0,0,0,.5)}.searchbox__reset.hide{display:none}.searchbox__reset:focus{outline:0}.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px}.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:a;animation-name:a;-webkit-animation-duration:.15s;animation-duration:.15s}@-webkit-keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(69,142,225,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #d9d9d9;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#02060c;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#174d8c;background:rgba(143,187,237,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(69,142,225,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#02060c;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#63676d}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#458ee1;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#458ee1;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#3f4145;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;box-sizing:border-box;visibility:visible!important}.searchbox .algolia-autocomplete{display:block;width:100%;height:100%}.searchbox__wrapper{width:100%;height:100%;z-index:1;position:relative}.searchbox__input{display:inline-block;box-sizing:border-box;-webkit-transition:box-shadow .4s ease,background .4s ease;transition:box-shadow .4s ease,background .4s ease;border:0;border-radius:16px;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0;padding-right:26px;padding-left:32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none}.searchbox__input:hover{box-shadow:inset 0 0 0 1px #b3b3b3}.searchbox__input:active,.searchbox__input:focus{outline:0;box-shadow:inset 0 0 0 1px #aaa;background:#fff}.searchbox__input::-webkit-input-placeholder{color:#aaa}.searchbox__input::-moz-placeholder{color:#aaa}.searchbox__input:-ms-input-placeholder{color:#aaa}.searchbox__input::placeholder{color:#aaa}.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69,142,225,0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0}.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:''}.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer}.searchbox__submit:focus{outline:0}.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96}.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0,0,0,.5)}.searchbox__reset.hide{display:none}.searchbox__reset:focus{outline:0}.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px}.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:a;animation-name:a;-webkit-animation-duration:.15s;animation-duration:.15s}@-webkit-keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(69,142,225,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #d9d9d9;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#02060c;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#174d8c;background:rgba(143,187,237,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(69,142,225,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#02060c;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#63676d}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#458ee1;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#458ee1;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#3f4145;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;box-sizing:border-box;visibility:visible!important}.searchbox .algolia-autocomplete{display:block;width:100%;height:100%}.searchbox__wrapper{width:100%;height:100%;z-index:1;position:relative}.searchbox__input{display:inline-block;box-sizing:border-box;-webkit-transition:box-shadow .4s ease,background .4s ease;transition:box-shadow .4s ease,background .4s ease;border:0;border-radius:16px;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0;padding-right:26px;padding-left:32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none}.searchbox__input:hover{box-shadow:inset 0 0 0 1px #b3b3b3}.searchbox__input:active,.searchbox__input:focus{outline:0;box-shadow:inset 0 0 0 1px #aaa;background:#fff}.searchbox__input::-webkit-input-placeholder{color:#aaa}.searchbox__input::-moz-placeholder{color:#aaa}.searchbox__input:-ms-input-placeholder{color:#aaa}.searchbox__input::placeholder{color:#aaa}.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69,142,225,0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0}.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:''}.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer}.searchbox__submit:focus{outline:0}.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96}.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0,0,0,.5)}.searchbox__reset.hide{display:none}.searchbox__reset:focus{outline:0}.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px}.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:a;animation-name:a;-webkit-animation-duration:.15s;animation-duration:.15s}@-webkit-keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes a{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#677c8a;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete{width:100%}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}@media screen and (min-width:1200px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:350px!important;min-width:820px;max-width:1200px;top:-18px!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{-webkit-transform:rotate(-135deg);transform:rotate(-135deg);left:-7px;top:20px}}@media screen and (min-width:1550px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{min-width:1150px;max-width:1200px}}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:320px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}@media screen and (min-width:500px){.algolia-autocomplete .ds-dropdown-menu{min-width:500px}}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text{font-weight:600}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content{width:100%;float:none}}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#677c8a;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete{width:100%}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}@media screen and (min-width:1200px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:350px!important;min-width:820px;max-width:1200px;top:-18px!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{-webkit-transform:rotate(-135deg);transform:rotate(-135deg);left:-7px;top:20px}}@media screen and (min-width:1550px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{min-width:1150px;max-width:1200px}}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:320px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}@media screen and (min-width:500px){.algolia-autocomplete .ds-dropdown-menu{min-width:500px}}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text{font-weight:600}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content{width:100%;float:none}}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#677c8a;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete{width:100%}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}@media screen and (min-width:1200px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:350px!important;min-width:820px;max-width:1200px;top:-18px!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{-webkit-transform:rotate(-135deg);transform:rotate(-135deg);left:-7px;top:20px}}@media screen and (min-width:1550px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{min-width:1150px;max-width:1200px}}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;overflow-y:scroll;max-width:600px;min-width:320px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}@media screen and (min-width:500px){.algolia-autocomplete .ds-dropdown-menu{min-width:500px}}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text{font-weight:600}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content{width:100%;float:none}}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#677c8a;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete{width:100%}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}@media screen and (min-width:1200px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:350px!important;min-width:820px;max-width:1200px;top:-18px!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{-webkit-transform:rotate(-135deg);transform:rotate(-135deg);left:-7px;top:20px}}@media screen and (min-width:1550px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{min-width:1150px;max-width:1200px}}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;overflow-y:scroll;max-width:600px;min-width:320px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}@media screen and (min-width:500px){.algolia-autocomplete .ds-dropdown-menu{min-width:500px}}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text{font-weight:600}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content{width:100%;float:none}}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#677c8a;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete{width:100%}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}@media screen and (min-width:1200px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:350px!important;min-width:820px;max-width:1200px;top:-18px!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{-webkit-transform:rotate(-135deg);transform:rotate(-135deg);left:-7px;top:20px}}@media screen and (min-width:1550px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{min-width:1150px;max-width:1200px}}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;max-width:600px;min-width:320px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}@media screen and (min-width:500px){.algolia-autocomplete .ds-dropdown-menu{min-width:500px}}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text{font-weight:600}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content{width:100%;float:none}}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#677c8a;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block}/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete{width:100%}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}@media screen and (min-width:1200px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:350px!important;min-width:820px;max-width:1200px;top:-18px!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{-webkit-transform:rotate(-135deg);transform:rotate(-135deg);left:-7px;top:20px}}@media screen and (min-width:1550px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{min-width:1150px;max-width:1200px}}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:1;overflow:visible;max-width:600px;min-width:320px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}@media screen and (min-width:500px){.algolia-autocomplete .ds-dropdown-menu{min-width:500px}}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:'';width:14px;height:14px;background:#fff;z-index:2;top:-7px;border-top:1px solid #cad9e0;border-right:1px solid #cad9e0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:2;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text{font-weight:600}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #cad9e0;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{position:relative;padding:0 8px;background:#fff;color:#000;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#0c4770;background:rgba(114,189,241,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{color:inherit;background:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content{width:100%;float:none}}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;display:none;padding-left:0;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#677c8a;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:'';position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#000;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#0e384f}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:'';width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#1491e7;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#1491e7;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#020a0e;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:100px;height:20px;z-index:3;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block} \ No newline at end of file +/*! docsearch UNRELEASED | © Algolia | github.com/algolia/docsearch */.algolia-autocomplete{width:100%}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{left:inherit!important;right:0!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}@media screen and (min-width:1200px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:350px!important;max-width:1200px;min-width:820px;top:-18px!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:-7px;top:20px;transform:rotate(-135deg)}}@media screen and (min-width:1550px){.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{max-width:1200px;min-width:1150px}}.algolia-autocomplete .ds-dropdown-menu{background:transparent;border:none;border-radius:4px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1);height:auto;margin:6px 0 0;max-width:600px;min-width:320px;overflow:visible;padding:0;position:relative;text-align:left;top:-6px;z-index:999}@media screen and (min-width:500px){.algolia-autocomplete .ds-dropdown-menu{min-width:500px}}.algolia-autocomplete .ds-dropdown-menu:before{background:#fff;border-radius:2px;border-right:1px solid #cad9e0;border-top:1px solid #cad9e0;content:"";display:block;height:14px;position:absolute;top:-7px;transform:rotate(-45deg);width:14px;z-index:1000}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{margin-top:8px;position:relative;z-index:1000}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(20,145,231,.05)}.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion--subcategory-column-text{font-weight:600}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{background:#fff;border:1px solid #cad9e0;border-radius:4px;overflow:auto;padding:0 8px 8px;position:relative}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{background:#fff;color:#000;overflow:hidden;padding:0 8px;position:relative}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{background:rgba(114,189,241,.1);color:#0c4770;padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight{background:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{background:inherit;box-shadow:inset 0 -2px 0 0 rgba(20,145,231,.8);color:inherit;padding:0 0 1px}.algolia-autocomplete .algolia-docsearch-suggestion--content{cursor:pointer;display:block;float:right;padding:5.33333px 0 5.33333px 10.66667px;position:relative;width:70%}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content{float:none;width:100%}}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{background:#ddd;content:"";display:block;height:100%;left:-1px;position:absolute;top:0;width:1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{border-bottom:1px solid #ddd;color:#33363d;display:none;font-size:1em;margin-top:8px;padding:4px 0;position:relative}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{float:left;padding:8px 0 0;width:100%}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{color:#677c8a;display:none;float:left;font-size:.9em;padding:5.33333px 10.66667px;position:relative;text-align:right;width:30%;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{background:#ddd;content:"";display:block;height:100%;position:absolute;right:0;top:0;width:1px}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{display:none}}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight{background-color:inherit;color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{color:#000;font-size:.9em;font-weight:700;margin-bottom:4px}.algolia-autocomplete .algolia-docsearch-suggestion--text{color:#0e384f;display:block;font-size:.85em;line-height:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{font-size:1.2em;padding:8px 0;text-align:center;width:100%}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{background-color:#ebebeb;border:none;border-radius:3px;color:#222;font-family:Menlo,Monaco,Consolas,Courier New,monospace;font-size:90%;padding:1px 5px}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:block}@media screen and (max-width:500px){.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary .algolia-docsearch-suggestion--subcategory-column{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;margin:0;padding:8px}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{padding:0;width:100%}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{border:none;display:block;margin:0;padding:0;width:100%}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{font-size:.85em;opacity:.6}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:"";display:inline-block;height:10px;width:10px}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{float:left;margin:0;padding:0;width:100%}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-column,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{color:#1491e7;font-size:.9em;font-weight:400;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{color:#1491e7;content:"#";display:inline-block;font-weight:700}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{background:#f8f8f8;display:block;font-size:.85em;line-height:1.4em;margin:4px 0 0;opacity:.8;padding:5.33333px 8px}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{box-shadow:none;color:#020a0e;font-weight:700}.algolia-autocomplete .algolia-docsearch-footer{float:right;font-size:0;height:20px;line-height:0;margin-top:10.66667px;width:100px;z-index:2000}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url('data:image/svg+xml;utf8,');background-position:50%;background-repeat:no-repeat;background-size:100%;display:block;height:100%;overflow:hidden;padding:0!important;text-indent:-9000px;width:100%} +/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0ZGluIiwiZG9jc2VhcmNoLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxxRUFBQSxDQ0NBLHNCRENBLFVDQ0EsQ0FFQSxtRURFQSxzQkFBQSxDQURFLGlCQ0VGLENBRUEsMEVEQ0EsVUNDQSxDQUVBLGtFRENFLGdCQUFBLENBQ0YsdUJDQ0EsQ0FFQSx5RURDQSxTQ0NBLENBRUEscUNBQ0Usa0VEQ0Usb0JBQWdCLENBRWhCLGdCQUFBLENBREEsZUFBQSxDQUVGLG1CQUNBLENDQ0EseUVERUUsU0FBUyxDQUNYLFFBQUEsQ0FGRSx5QkFHSixDQ0NBLENBRUEscUNBQ0Usa0VERUEsZ0JBQUEsQ0FERSxnQkFFSixDQ0NBLENBRUEsd0NEU0Usc0JBQVksQ0FDWixXQUFZLENBUFosaUJBQWUsQ0FZakIsOERBQUEsQ0FSRSxXQUFBLENBSEEsY0FBVSxDQVNWLGVBQWdCLENBQ2hCLGVBQUEsQ0FGQSxnQkFBZ0IsQ0FQaEIsU0FBQSxDQUdBLGlCQUFBLENBRkEsZUFBWSxDQUpaLFFBQUEsQ0FTQSxXQ0tGLENBRUEsb0NBQ0Usd0NEQ0EsZUFDRixDQ0NBLENBRUEsK0NETUUsZUFBYSxDQU1mLGlCQUFBLENBRkUsOEJBQXlCLENBRHpCLDRCQUFBLENBTkEsVUFBVyxDQUZYLGFBQUEsQ0FJQSxXQUFBLENBSEEsaUJBQVcsQ0FNWCxRQUFBLENBR0Esd0JBQWtCLENBUGxCLFVBQUEsQ0FHQSxZQ01GLENBRUEsd0RER0EsY0FBQSxDQUZFLGlCQUFhLENBQ2IsWUNFRixDQUVBLHVERENBLGNDQ0EsQ0FNQSw2UkRDQSxxQ0NDQSxDQUVBLCtGRENBLGVDQ0EsQ0FFQSw2RERHRSxlQUFBLENBREEsd0JBQWdCLENBRWhCLGlCQUFjLENBQ2QsYUFBQSxDQUNGLGlCQUFBLENBTEUsaUJDTUYsQ0FFQSwwQ0RDQSxxQkNDQSxDQUVBLG9EREdFLGVBQVcsQ0FDWCxVQUFBLENBQ0YsZUFBQSxDQUhFLGFBQUEsQ0FEQSxpQkNLRixDQUVBLCtEREVFLCtCQUFxQixDQURyQixhQUFBLENBRUYsa0JDQ0EsQ0FFQSxvVURHQSxrQkFBQSxDQURFLGFDRUYsQ0FFQSxtR0RFRSxrQkFBQSxDQUNBLCtDQUFjLENBQ2hCLGFBQUEsQ0FIRSxlQ0lGLENBRUEsNkRETUEsY0FBQSxDQUxFLGFBQVksQ0FDWixXQUFVLENBR1Ysd0NBQWUsQ0FEZixpQkFBQSxDQURBLFNDSUYsQ0FFQSxvQ0FDRSw2RERFQSxVQUFBLENBREUsVUFFSixDQ0NBLENBRUEsb0VET0UsZUFBVSxDQU5WLFVBQUEsQ0FFQSxhQUFNLENBRU4sV0FBVSxDQUdaLFNBQUEsQ0FORSxpQkFBYyxDQUVkLEtBQUEsQ0FFQSxTQ0dGLENBRUEsb0NBQ0Usb0VEQ0EsWUFDRixDQ0NBLENBRUEscUVERUUsNEJBQWEsQ0FLZixhQUFBLENBSkUsWUFBQSxDQUdBLGFBQWMsQ0FGZCxjQUFjLENBQ2QsYUFBYyxDQUpkLGlCQ09GLENBRUEsNkRERUUsVUFBQSxDQUNGLGVBQUEsQ0FGRSxVQ0dGLENBRUEsd0VEUUUsYUFBQSxDQUxBLFlBQUEsQ0FGQSxVQUFVLENBUVYsY0FBQSxDQUZBLDRCQUFjLENBRGQsaUJBQUEsQ0FEQSxnQkFBQSxDQUhBLFNBQUEsQ0FRRixvQkNDQSxDQUVBLCtFRE9FLGVBQVEsQ0FOUixVQUFBLENBRUEsYUFBTSxDQUVOLFdBQVUsQ0FIVixpQkFBYyxDQU1oQixPQUFBLENBSkUsS0FBQSxDQUVBLFNDR0YsQ0FFQSxvQ0FDRSwrRURDQSxZQUNGLENDQ0EsQ0FFQSxpSERDRSx3QkFBYyxDQUNoQixhQ0NBLENBRUEsd0VEQ0EsWUNDQSxDQUVBLDJEREVFLFVBQUEsQ0FDQSxjQUFBLENBQ0YsZUFBQSxDQUhFLGlCQ0lGLENBRUEsMERESUEsYUFBQSxDQUhFLGFBQUEsQ0FFQSxlQUFjLENBRGQsaUJDR0YsQ0FFQSxnRURJQSxlQUFBLENBRkUsYUFBQSxDQUNBLGlCQUFnQixDQUZoQixVQ0lGLENBRUEsdUVEQ0EsWUNDQSxDQUVBLHlEREtFLHdCQUFrQixDQUZsQixXQUFBLENBR0EsaUJBQUEsQ0FGQSxVQUFBLENBR0YsdURBQUEsQ0FMRSxhQUFZLENBRFosZUNPRixDQUVBLGtHRENBLGVDQ0EsQ0FNQSxvUkRDQSxhQ0NBLENBRUEsb0NBQ0UsOElEQ0EsWUFDRixDQ0NBLENBRUEsNkVEQ0UsNEJBQVksQ0FFZCxRQUFBLENBREUsV0NFRixDQUVBLHVGREVBLFNBQUEsQ0FERSxVQ0VGLENBRUEsOEZEQ0EsWUNDQSxDQUVBLCtGREtBLFdBQUEsQ0FGRSxhQUFXLENBRlgsUUFBQSxDQUNBLFNBQUEsQ0FFQSxVQ0VGLENBT0Esd01ERUEsZUFBQSxDQURFLFVDRUYsQ0FFQSwyR0RDRSwwVUFBVyxDQUNYLFVBQVcsQ0FHYixvQkFBQSxDQURFLFdBQUEsQ0FEQSxVQ0dGLENBRUEsdUZERUUsVUFBUyxDQUNULFFBQUEsQ0FDRixTQUFBLENBSEUsVUNJRixDQUVBLHFTRENBLHNCQ0NBLENBRUEscUZERUUsYUFBQSxDQUNBLGNBQUEsQ0FDRixlQUFBLENBSEUsUUNJRixDQUVBLDRGREdFLGFBQUEsQ0FGQSxXQUFBLENBR0Ysb0JBQUEsQ0FGRSxlQ0dGLENBRUEsb0ZES0Usa0JBQWlCLENBSGpCLGFBQUEsQ0FJQSxlQUFXLENBSFgsaUJBQUEsQ0FGQSxjQUFjLENBTWhCLFVBQUEsQ0FIRSxxQkNJRixDQUVBLDZIREdBLGVBQUEsQ0FGRSxhQUFBLENBQ0EsZUNFRixDQUVBLGdEREtFLFdBQVksQ0FDWixXQUFBLENBSkEsV0FBQSxDQUtGLGFBQUEsQ0FIRSxxQkFBWSxDQUhaLFdBQVksQ0FFWixZQ0tGLENBRUEsc0REQ0UsaXJNQUE0QixDQUU1Qix1QkFBcUIsQ0FEckIsMkJBQTJCLENBRTNCLG9CQUFnQixDQU1sQixhQUFBLENBREUsV0FBQSxDQUpBLGVBQUEsQ0FFQSxtQkFBVyxDQURYLG1CQUFBLENBRUEsVUNHRiIsImZpbGUiOiJkb2NzZWFyY2gubWluLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIi5hbGdvbGlhLWF1dG9jb21wbGV0ZSB7XG4gIHdpZHRoOiAxMDAlO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtcmlnaHQgLmRzLWRyb3Bkb3duLW1lbnUge1xuICByaWdodDogMCAhaW1wb3J0YW50O1xuICBsZWZ0OiBpbmhlcml0ICFpbXBvcnRhbnQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZS5hbGdvbGlhLWF1dG9jb21wbGV0ZS1yaWdodCAuZHMtZHJvcGRvd24tbWVudTpiZWZvcmUge1xuICByaWdodDogNDhweDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlLmFsZ29saWEtYXV0b2NvbXBsZXRlLWxlZnQgLmRzLWRyb3Bkb3duLW1lbnUge1xuICBsZWZ0OiAwICFpbXBvcnRhbnQ7XG4gIHJpZ2h0OiBpbmhlcml0ICFpbXBvcnRhbnQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZS5hbGdvbGlhLWF1dG9jb21wbGV0ZS1sZWZ0IC5kcy1kcm9wZG93bi1tZW51OmJlZm9yZSB7XG4gIGxlZnQ6IDQ4cHg7XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtaW4td2lkdGg6IDEyMDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudSB7XG4gICAgbGVmdDogMzUwcHggIWltcG9ydGFudDtcbiAgICBtaW4td2lkdGg6IDgyMHB4O1xuICAgIG1heC13aWR0aDogMTIwMHB4O1xuICAgIHRvcDogLTE4cHggIWltcG9ydGFudDtcbiAgfVxuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudTpiZWZvcmUge1xuICAgIHRyYW5zZm9ybTogcm90YXRlKC0xMzVkZWcpO1xuICAgIGxlZnQ6IC03cHg7XG4gICAgdG9wOiAyMHB4O1xuICB9XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtaW4td2lkdGg6IDE1NTBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudSB7XG4gICAgbWluLXdpZHRoOiAxMTUwcHg7XG4gICAgbWF4LXdpZHRoOiAxMjAwcHg7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB0b3A6IC02cHg7XG4gIGJvcmRlci1yYWRpdXM6IDRweDtcbiAgbWFyZ2luOiA2cHggMCAwO1xuICBwYWRkaW5nOiAwO1xuICB0ZXh0LWFsaWduOiBsZWZ0O1xuICBoZWlnaHQ6IGF1dG87XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7XG4gIGJvcmRlcjogbm9uZTtcbiAgei1pbmRleDogOTk5O1xuICBvdmVyZmxvdzogdmlzaWJsZTtcbiAgbWF4LXdpZHRoOiA2MDBweDtcbiAgbWluLXdpZHRoOiAzMjBweDtcbiAgYm94LXNoYWRvdzogMCAxcHggMCAwIHJnYmEoMCwgMCwgMCwgMC4yKSwgMCAycHggM3B4IDAgcmdiYSgwLCAwLCAwLCAwLjEpO1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWluLXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUge1xuICAgIG1pbi13aWR0aDogNTAwcHg7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51OmJlZm9yZSB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGNvbnRlbnQ6ICcnO1xuICB3aWR0aDogMTRweDtcbiAgaGVpZ2h0OiAxNHB4O1xuICBiYWNrZ3JvdW5kOiAjZmZmO1xuICB6LWluZGV4OiAxMDAwO1xuICB0b3A6IC03cHg7XG4gIGJvcmRlci10b3A6IDFweCBzb2xpZCAjY2FkOWUwO1xuICBib3JkZXItcmlnaHQ6IDFweCBzb2xpZCAjY2FkOWUwO1xuICB0cmFuc2Zvcm06IHJvdGF0ZSgtNDVkZWcpO1xuICBib3JkZXItcmFkaXVzOiAycHg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAuZHMtc3VnZ2VzdGlvbnMge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHotaW5kZXg6IDEwMDA7XG4gIG1hcmdpbi10b3A6IDhweDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IC5kcy1zdWdnZXN0aW9uIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUgLmRzLXN1Z2dlc3Rpb24uZHMtY3Vyc29yIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSB7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjAsIDE0NSwgMjMxLCAwLjA1KTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IC5kcy1zdWdnZXN0aW9uLmRzLWN1cnNvciAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbjpub3QoLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSkgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDIwLCAxNDUsIDIzMSwgMC4wNSk7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tc3ViY2F0ZWdvcnktY29sdW1uLXRleHQge1xuICBmb250LXdlaWdodDogNjAwO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUgW2NsYXNzXj1cImRzLWRhdGFzZXQtXCJdIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBib3JkZXI6IHNvbGlkIDFweCAjY2FkOWUwO1xuICBiYWNrZ3JvdW5kOiAjZmZmO1xuICBib3JkZXItcmFkaXVzOiA0cHg7XG4gIG92ZXJmbG93OiBhdXRvO1xuICBwYWRkaW5nOiAwIDhweCA4cHg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAqIHtcbiAgYm94LXNpemluZzogYm9yZGVyLWJveDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBwYWRkaW5nOiAwIDhweDtcbiAgYmFja2dyb3VuZDogI2ZmZjtcbiAgY29sb3I6ICMwMDA7XG4gIG92ZXJmbG93OiBoaWRkZW47XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgY29sb3I6ICMwYzQ3NzA7XG4gIGJhY2tncm91bmQ6IHJnYmEoMTE0LCAxODksIDI0MSwgMC4xKTtcbiAgcGFkZGluZzogMC4xZW0gMC4wNWVtO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNhdGVnb3J5LWhlYWRlciAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyLWx2bDAgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWhpZ2hsaWdodCxcbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXItbHZsMSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgY29sb3I6IGluaGVyaXQ7XG4gIGJhY2tncm91bmQ6IGluaGVyaXQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tdGV4dCAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgcGFkZGluZzogMCAwIDFweDtcbiAgYmFja2dyb3VuZDogaW5oZXJpdDtcbiAgYm94LXNoYWRvdzogaW5zZXQgMCAtMnB4IDAgMCByZ2JhKDIwLCAxNDUsIDIzMSwgMC44KTtcbiAgY29sb3I6IGluaGVyaXQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY29udGVudCB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBmbG9hdDogcmlnaHQ7XG4gIHdpZHRoOiA3MCU7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgcGFkZGluZzogNS4zMzMzM3B4IDAgNS4zMzMzM3B4IDEwLjY2NjY3cHg7XG4gIGN1cnNvcjogcG9pbnRlcjtcbn1cblxuQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNTAwcHgpIHtcbiAgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jb250ZW50IHtcbiAgICB3aWR0aDogMTAwJTtcbiAgICBmbG9hdDogbm9uZTtcbiAgfVxufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6YmVmb3JlIHtcbiAgY29udGVudDogJyc7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHRvcDogMDtcbiAgaGVpZ2h0OiAxMDAlO1xuICB3aWR0aDogMXB4O1xuICBiYWNrZ3JvdW5kOiAjZGRkO1xuICBsZWZ0OiAtMXB4O1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6YmVmb3JlIHtcbiAgICBkaXNwbGF5OiBub25lO1xuICB9XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBib3JkZXItYm90dG9tOiAxcHggc29saWQgI2RkZDtcbiAgZGlzcGxheTogbm9uZTtcbiAgbWFyZ2luLXRvcDogOHB4O1xuICBwYWRkaW5nOiA0cHggMDtcbiAgZm9udC1zaXplOiAxZW07XG4gIGNvbG9yOiAjMzMzNjNEO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXdyYXBwZXIge1xuICB3aWR0aDogMTAwJTtcbiAgZmxvYXQ6IGxlZnQ7XG4gIHBhZGRpbmc6IDhweCAwIDAgMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4ge1xuICBmbG9hdDogbGVmdDtcbiAgd2lkdGg6IDMwJTtcbiAgZGlzcGxheTogbm9uZTtcbiAgcGFkZGluZy1sZWZ0OiAwO1xuICB0ZXh0LWFsaWduOiByaWdodDtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBwYWRkaW5nOiA1LjMzMzMzcHggMTAuNjY2NjdweDtcbiAgY29sb3I6ICM2NzdjOGE7XG4gIGZvbnQtc2l6ZTogMC45ZW07XG4gIHdvcmQtd3JhcDogYnJlYWstd29yZDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW46YmVmb3JlIHtcbiAgY29udGVudDogJyc7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHRvcDogMDtcbiAgaGVpZ2h0OiAxMDAlO1xuICB3aWR0aDogMXB4O1xuICBiYWNrZ3JvdW5kOiAjZGRkO1xuICByaWdodDogMDtcbn1cblxuQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNTAwcHgpIHtcbiAgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW46YmVmb3JlIHtcbiAgICBkaXNwbGF5OiBub25lO1xuICB9XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tc3ViY2F0ZWdvcnktY29sdW1uIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiBpbmhlcml0O1xuICBjb2xvcjogaW5oZXJpdDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1pbmxpbmUge1xuICBkaXNwbGF5OiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlIHtcbiAgbWFyZ2luLWJvdHRvbTogNHB4O1xuICBjb2xvcjogIzAwMDtcbiAgZm9udC1zaXplOiAwLjllbTtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tdGV4dCB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBsaW5lLWhlaWdodDogMS4yZW07XG4gIGZvbnQtc2l6ZTogMC44NWVtO1xuICBjb2xvcjogIzBlMzg0Zjtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1uby1yZXN1bHRzIHtcbiAgd2lkdGg6IDEwMCU7XG4gIHBhZGRpbmc6IDhweCAwO1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGZvbnQtc2l6ZTogMS4yZW07XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tbm8tcmVzdWx0czo6YmVmb3JlIHtcbiAgZGlzcGxheTogbm9uZTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uIGNvZGUge1xuICBwYWRkaW5nOiAxcHggNXB4O1xuICBmb250LXNpemU6IDkwJTtcbiAgYm9yZGVyOiBub25lO1xuICBjb2xvcjogIzIyMjIyMjtcbiAgYmFja2dyb3VuZC1jb2xvcjogI0VCRUJFQjtcbiAgYm9yZGVyLXJhZGl1czogM3B4O1xuICBmb250LWZhbWlseTogTWVubG8sTW9uYWNvLENvbnNvbGFzLFwiQ291cmllciBOZXdcIixtb25vc3BhY2U7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbiBjb2RlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBiYWNrZ3JvdW5kOiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24uYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbl9fbWFpbiAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIHtcbiAgZGlzcGxheTogYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uX19zZWNvbmRhcnkgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXN1YmNhdGVnb3J5LWNvbHVtbiB7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24uYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbl9fc2Vjb25kYXJ5IC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4ge1xuICAgIGRpc3BsYXk6IG5vbmU7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbiB7XG4gIGJvcmRlci1ib3R0b206IHNvbGlkIDFweCAjZWVlO1xuICBwYWRkaW5nOiA4cHg7XG4gIG1hcmdpbjogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQge1xuICB3aWR0aDogMTAwJTtcbiAgcGFkZGluZzogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6OmJlZm9yZSB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXIge1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyOiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyLWx2bDAge1xuICBvcGFjaXR5OiAuNjtcbiAgZm9udC1zaXplOiAwLjg1ZW07XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXItbHZsMSB7XG4gIG9wYWNpdHk6IC42O1xuICBmb250LXNpemU6IDAuODVlbTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNhdGVnb3J5LWhlYWRlci1sdmwxOjpiZWZvcmUge1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDt1dGY4LDxzdmcgd2lkdGg9XCIxMFwiIGhlaWdodD1cIjEwXCIgdmlld0JveD1cIjAgMCAyMCAzOFwiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIj48cGF0aCBkPVwiTTEuNDkgNC4zMWwxNCAxNi4xMjYuMDAyLTIuNjI0LTE0IDE2LjA3NC0xLjMxNCAxLjUxIDMuMDE3IDIuNjI2IDEuMzEzLTEuNTA4IDE0LTE2LjA3NSAxLjE0Mi0xLjMxMy0xLjE0LTEuMzEzLTE0LTE2LjEyNUwzLjIuMTguMTggMi44bDEuMzEgMS41MXpcIiBmaWxsLXJ1bGU9XCJldmVub2RkXCIgZmlsbD1cIiUyMzFEMzY1N1wiIC8+PC9zdmc+Jyk7XG4gIGNvbnRlbnQ6ICcnO1xuICB3aWR0aDogMTBweDtcbiAgaGVpZ2h0OiAxMHB4O1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS13cmFwcGVyIHtcbiAgd2lkdGg6IDEwMCU7XG4gIGZsb2F0OiBsZWZ0O1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4sIC5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1kdXBsaWNhdGUtY29udGVudCwgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXN1YmNhdGVnb3J5LWlubGluZSB7XG4gIGRpc3BsYXk6IG5vbmUgIWltcG9ydGFudDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlIHtcbiAgbWFyZ2luOiAwO1xuICBjb2xvcjogIzE0OTFlNztcbiAgZm9udC1zaXplOiAwLjllbTtcbiAgZm9udC13ZWlnaHQ6IG5vcm1hbDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlOjpiZWZvcmUge1xuICBjb250ZW50OiBcIiNcIjtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gIGNvbG9yOiAjMTQ5MWU3O1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS10ZXh0IHtcbiAgbWFyZ2luOiA0cHggMCAwO1xuICBkaXNwbGF5OiBibG9jaztcbiAgbGluZS1oZWlnaHQ6IDEuNGVtO1xuICBwYWRkaW5nOiA1LjMzMzMzcHggOHB4O1xuICBiYWNrZ3JvdW5kOiAjZjhmOGY4O1xuICBmb250LXNpemU6IDAuODVlbTtcbiAgb3BhY2l0eTogLjg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS10ZXh0IC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBjb2xvcjogIzAyMGEwZTtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gIGJveC1zaGFkb3c6IG5vbmU7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtZm9vdGVyIHtcbiAgd2lkdGg6IDEwMHB4O1xuICBoZWlnaHQ6IDIwcHg7XG4gIHotaW5kZXg6IDIwMDA7XG4gIG1hcmdpbi10b3A6IDEwLjY2NjY3cHg7XG4gIGZsb2F0OiByaWdodDtcbiAgZm9udC1zaXplOiAwO1xuICBsaW5lLWhlaWdodDogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1mb290ZXItLWxvZ28ge1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDt1dGY4LDxzdmcgd2lkdGg9XCI4NlwiIGhlaWdodD1cIjEzXCIgdmlld0JveD1cIjAgMCA4NiAxM1wiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIj48ZyBmaWxsPVwibm9uZVwiIGZpbGwtcnVsZT1cImV2ZW5vZGRcIj48cGF0aCBkPVwiTTUuMDEgOC4xYzAgLjYzLS4yMjggMS4xMi0uNjg0IDEuNDctLjQ1Ni4zNTItMS4wNzQuNTI4LTEuODU1LjUyOC0uODQgMC0xLjQ5LS4xMS0xLjk1LS4zMjd2LS44Yy4zLjEzLjYxLjIyLjk2LjMuMzUuMDcuNjkuMTEgMS4wMy4xMS41NiAwIC45Ny0uMSAxLjI1LS4zMXMuNDItLjUuNDItLjg3YzAtLjI1LS4wNS0uNDUtLjE1LS42MS0uMDktLjE1LS4yNi0uMy0uNDktLjQzLS4yNC0uMTQtLjU5LS4yOS0xLjA3LS40Ni0uNjYtLjIzLTEuMTMtLjUyLTEuNDItLjg0Qy43NiA1LjUyLjYxIDUuMS42MSA0LjU3YzAtLjU1LjIxLS45ODUuNjItMS4zMS40Mi0uMzI1Ljk2LS40ODggMS42NC0uNDg4LjcxIDAgMS4zNy4xMyAxLjk2LjM5bC0uMjYuNzJjLS41OS0uMjUtMS4xNi0uMzctMS43MS0uMzctLjQ0IDAtLjc4LjA5LTEuMDMuMjgtLjI1LjE5LS4zNy40NS0uMzcuNzggMCAuMjQuMDUuNDUuMTQuNi4wOS4xNi4yNDYuMy40Ni40My4yMi4xMy41NS4yNzcuOTk2LjQzNy43NS4yNiAxLjI2NC41NSAxLjU0NS44Ni4yOS4zLjQzLjcuNDMgMS4xOXptMy41OTggMmMtLjc5IDAtMS40MTUtLjI0LTEuODcyLS43MjMtLjQ1OC0uNDgyLS42ODYtMS4xNS0uNjg2LTIuMDA3IDAtLjg2Mi4yMTItMS41NDguNjM3LTIuMDU2LjQyNS0uNTA3Ljk5NS0uNzYgMS43MS0uNzYuNjcyIDAgMS4yMDMuMjIgMS41OTMuNjYuMzkuNDQuNTg2IDEuMDIzLjU4NiAxLjc0NnYuNTEzSDYuODljLjAxNi42My4xNzUgMS4xMDYuNDc2IDEuNDMuMy4zMjcuNzI1LjQ5IDEuMjcyLjQ5LjU3NiAwIDEuMTQ2LS4xMiAxLjcxLS4zNjJ2LjczYy0uMjg4LjEzLS41Ni4yMi0uODE0LjI3LS4yNTYuMDYtLjU2NC4wOC0uOTI2LjA4em0tLjIyLTQuODdjLS40MyAwLS43NzIuMTQtMS4wMjcuNDItLjI1LjI4LS40LjY3LS40NSAxLjE2NGgyLjhjMC0uNTEyLS4xMS0uOTAzLS4zNC0xLjE3NS0uMjMtLjI4LS41NS0uNDEtLjk3LS40MXpNMTUuMjUgMTBsLS4xNi0uNzYyaC0uMDRjLS4yNjYuMzM2LS41MzIuNTYzLS43OTguNjgtLjI2NS4xMi0uNTk2LjE4LS45OTMuMTgtLjUzIDAtLjk1LS4xMzctMS4yNS0uNDEtLjMtLjI3NC0uNDUtLjY2My0uNDUtMS4xNjcgMC0xLjA4Ljg2LTEuNjQgMi41OS0xLjdsLjkxLS4wMnYtLjM0YzAtLjQyLS4wOS0uNzMtLjI3LS45My0uMTgtLjItLjQ3LS4zLS44Ny0uMy0uNDUgMC0uOTUuMTQtMS41Mi40MWwtLjI1LS42MmMuMjYtLjE0LjU1LS4yNS44Ni0uMzMuMzEtLjA4LjYzLS4xMi45NC0uMTIuNjMgMCAxLjExLjE1IDEuNDEuNDMuMy4yOS40Ni43NC40NiAxLjM3VjEwaC0uNnptLTEuODMtLjU3Yy41MDUgMCAuOS0uMTQgMS4xOS0uNDE2LjI4Ny0uMjc3LjQzLS42NjQuNDMtMS4xNjJ2LS40ODRsLS44MS4wMzRjLS42NDQuMDIzLTEuMTEuMTIzLTEuMzk0LjMtLjI4NC4xNzgtLjQyNy40NTQtLjQyNy44MjggMCAuMjkzLjA5LjUxNi4yNi42Ny4xOC4xNTIuNDIuMjMuNzQuMjN6bTYuNTQzLTQuODhjLjI0IDAgLjQ1Mi4wMi42NC4wNmwtLjExMi43NWMtLjIyLS4wNDgtLjQxLS4wNzItLjU4LS4wNzItLjQzIDAtLjguMTc2LTEuMTEuNTI3LS4zMS4zNTItLjQ2Ljc5LS40NiAxLjMxNFYxMGgtLjgxVjQuNjQ4aC42N2wuMS45OTJoLjA0Yy4yLS4zNS40NC0uNjE3LjcyLS44MDYuMjgtLjE5LjU5LS4yODMuOTMtLjI4M3ptMy41OCA1LjU0OGMtLjc3NSAwLTEuMzc0LS4yNC0xLjgtLjcxNi0uNDI0LS40NzctLjYzNi0xLjE1LS42MzYtMi4wMjQgMC0uODk1LjIxNS0xLjU4Ny42NDYtMi4wNzUuNDMyLS40ODggMS4wNDYtLjczMiAxLjg0NC0uNzMyLjI1NyAwIC41MTQuMDMuNzcuMDkuMjU4LjA2LjQ2LjEyLjYwNy4ybC0uMjQ4LjY5Yy0uMTgtLjA3LS4zNzUtLjEzLS41ODYtLjE4LS4yMTItLjA0LS40LS4wNy0uNTYyLS4wNy0xLjA4NyAwLTEuNjMuNy0xLjYzIDIuMDggMCAuNjYuMTMyIDEuMTYuMzk3IDEuNTIuMjY3LjM1LjY2LjUzIDEuMTguNTMuNDQ2IDAgLjkwNC0uMDkgMS4zNzMtLjI4di43MmMtLjM1OC4xOS0uODEuMjgtMS4zNTMuMjh6bTYuMjgtLjA5OFY2LjUzOGMwLS40MzYtLjEtLjc2Mi0uMjk3LS45NzYtLjE5OC0uMjE1LS41MS0uMzIzLS45MzItLjMyMy0uNTYzIDAtLjk3NC4xNS0xLjIzMy40NS0uMjUuMy0uMzguODEtLjM4IDEuNVYxMGgtLjgxVjIuNDAyaC44MXYyLjNjMCAuMjc3LS4wMS41MDYtLjA0LjY5aC4wNWMuMTYtLjI2LjM5LS40Ni42OC0uNjEuMy0uMTQ3LjYzLS4yMiAxLjAxLS4yMi42NiAwIDEuMTUuMTU0IDEuNDcuNDY1LjMzLjMxLjQ5LjgwNS40OSAxLjQ4MlYxMGgtLjgxem03LjU2NS01LjQ0Yy43MDMgMCAxLjI1LjI0IDEuNjM4LjcyLjM5LjQ4LjU4MyAxLjE2LjU4MyAyLjA0IDAgLjg3OC0uMiAxLjU2LS41OSAyLjA0OC0uMzkuNDg2LS45NC43My0xLjY0LjczLS4zNSAwLS42Ny0uMDY1LS45Ni0uMTkzLS4yOS0uMTMtLjUzLS4zMjctLjczLS41OTNoLS4wNmwtLjE3LjY4OGgtLjU4VjIuNDAyaC44MXYxLjg0NmMwIC40MTMtLjAyLjc4NS0uMDQgMS4xMTNoLjA0Yy4zOC0uNTMuOTQtLjggMS42OC0uOHptLS4xMTcuNjhjLS41NSAwLS45NS4xNTgtMS4xOS40NzUtLjI0LjMxOC0uMzYuODUyLS4zNiAxLjYwNCAwIC43NS4xMyAxLjI5LjM4IDEuNjEuMjUuMzIuNjYuNDggMS4yMS40OC41IDAgLjg3LS4xOSAxLjEyLS41NS4yNS0uMzcuMzctLjg5LjM3LTEuNTcgMC0uNy0uMTItMS4yMi0uMzYtMS41Ni0uMjQtLjM1LS42Mi0uNTItMS4xMy0uNTJ6bTIuNzEtLjU5MmguODdMNDIuMDIgNy43Yy4yNTcuNjk3LjQxNiAxLjIuNDggMS41MWguMDM3Yy4wNDItLjE2Ny4xMy0uNDUuMjY2LS44NTMuMTM2LS40MDIuNTc4LTEuNjM4IDEuMzI3LTMuNzFINDVsLTIuMyA2LjA5NWMtLjIzLjYwMi0uNDk2IDEuMDMtLjggMS4yODItLjMwNC4yNTItLjY3OC4zNzgtMS4xMi4zNzgtLjI0OCAwLS40OS0uMDI3LS43MzMtLjA4M3YtLjY1Yy4xOC4wNC4zOC4wNi42LjA2LjU1NyAwIC45NTQtLjMyIDEuMTkyLS45NGwuMjktLjc2LTIuMTYtNS4zOXpcIiBmaWxsPVwiJTIzNzk3OTc5XCIvPjxwYXRoIGQ9XCJNNzAuODgzIDQuOGwtLjU1MyAyLjA2IDEuNzgyLTEuMDI1Yy0uMjYtLjUtLjctLjg3Ni0xLjIzLTEuMDM0ek02Ny43NiAzLjQxYy0uMjM3LS4yNS0uNjI0LS4yNS0uODYyIDBsLS4xMDguMTE0Yy0uMjM4LjI1Mi0uMjM4LjY2IDAgLjkxbC4xMTUuMTIyYy4yNDQtLjQxNC41NTYtLjc4LjkxOC0xLjA4bC0uMDYyLS4wNjZ6bTMuNzMtLjYzYzAtLjAxNC4wMDMtLjAyNy4wMDMtLjA0MnYtLjMyMmMwLS4zNTUtLjI3My0uNjQ0LS42MS0uNjQ0aC0xLjA2OGMtLjMzNyAwLS42MS4yODgtLjYxLjY0NHYuMzE3Yy4zNC0uMS42OTgtLjE1NiAxLjA2OC0uMTU2LjQyNCAwIC44MzMuMDcyIDEuMjE2LjIwM1wiIGZpbGw9XCIlMjM0NkFFREFcIi8+PHBhdGggZD1cIk03MC4zMTYgNC4yNDNjMS4zNCAwIDIuNDI4IDEuMTQ1IDIuNDI4IDIuNTUyIDAgMS40MDgtMS4wOSAyLjU1My0yLjQyOCAyLjU1My0xLjM0IDAtMi40MjgtMS4xNDUtMi40MjgtMi41NTMgMC0xLjQwNyAxLjA5LTIuNTUyIDIuNDI4LTIuNTUybS0zLjQgMi41NTJjMCAxLjk3NCAxLjUyMiAzLjU3NCAzLjQgMy41NzRzMy40LTEuNiAzLjQtMy41OC0xLjUyMi0zLjU4LTMuNC0zLjU4LTMuNCAxLjYtMy40IDMuNTd6XCIgZmlsbD1cIiUyMzQ2QUVEQVwiLz48cGF0aCBkPVwiTTU0Ljc1OCAxMC4xNzVjLS4xNC0uMzktLjI3LS43NzYtLjM5NS0xLjE1NS0uMTI0LS4zNzgtLjI1LS43NjMtLjM4My0xLjE1NEg1MC4xbC0uNzggMi4zMWgtMS4yNDdjLjMzLS45NTguNjQtMS44NDMuOTI3LTIuNjU2LjI4OC0uODEzLjU3LTEuNTg1Ljg0Ny0yLjMxNi4yNzUtLjczLjU1LTEuNDMuODItMi4wOTQuMjczLS42NjYuNTU3LTEuMzI0Ljg1NC0xLjk3N2gxLjFjLjMuNjUzLjU4IDEuMzEuODYgMS45NzcuMjcuNjY1LjU1IDEuMzYzLjgyIDIuMDk0LjI4LjczLjU2IDEuNTAzLjg1IDIuMzE2LjI5LjgxMy42IDEuNjk4LjkzIDIuNjU1aC0xLjMxem0tMS4xMjQtMy4zNTNjLS4yNjQtLjc1Ny0uNTI1LTEuNDktLjc4NS0yLjItLjI2LS43MDgtLjUzLTEuMzg4LS44MS0yLjA0LS4yOS42NTItLjU3IDEuMzMyLS44MyAyLjA0LS4yNi43MS0uNTIgMS40NDMtLjc3IDIuMmgzLjE5em01LjQ1IDMuNDgzYy0uNzEtLjAxNy0xLjIxLS4xNzgtMS41MDgtLjQ4Mi0uMjk3LS4zMDUtLjQ0NS0uNzgtLjQ0NS0xLjQyM1YuMjZsMS4xNS0uMjF2OC4xNTVjMCAuMi4wMi4zNjUuMDUuNDk2LjA0LjEzLjA5LjI0LjE2LjMyLjA4LjA4LjE4LjE0LjMuMTguMTMuMDQuMjguMDguNDYuMWwtLjE2IDEuMDJtNS40Ny0uODFjLS4xLjA3LS4yOS4xNi0uNTcuMjctLjI4LjExLS42MS4xNy0uOTkuMTdzLS43NS0uMDYtMS4wOS0uMTljLS4zNC0uMTMtLjY0LS4zMy0uODktLjYtLjI2LS4yNy0uNDYtLjYxLS42MS0xLjAyLS4xNS0uNC0uMjItLjg5LS4yMi0xLjQ2IDAtLjQ5LjA3LS45NS4yMS0xLjM2LjE0LS40MS4zNS0uNzcuNjEtMS4wNy4yNy0uMy42LS41My45OS0uNy4zOS0uMTcuODMtLjI1IDEuMzEtLjI1LjU0IDAgMS4wMS4wNCAxLjQxLjEyLjQuMDkuNzMuMTYgMS4wMS4yM1Y5LjdjMCAxLjA0NC0uMjUgMS44LS43NiAyLjI3cy0xLjI5LjcwNS0yLjMzLjcwNWMtLjQgMC0uNzgtLjAzNS0xLjE0LS4xMDQtLjM2LS4wNy0uNjctLjE1LS45My0uMjRsLjIxLTEuMDZjLjIzLjA5LjUxLjE4Ljg0LjI1LjM0LjA4LjY4LjExIDEuMDUuMTEuNjkgMCAxLjE4LS4xNCAxLjQ4LS40My4zLS4yOC40NS0uNzQuNDUtMS4zN3YtLjN6bS0uNDctNS4xM2MtLjE5LS4wMy0uNDUtLjA0LS43OC0uMDQtLjYxIDAtMS4wOS4yMi0xLjQyLjY0LS4zMy40My0uNS45OS0uNSAxLjcgMCAuMzkuMDUuNzMuMTQgMS4wMS4xLjI4LjIzLjUxLjM5LjY5LjE2LjE4LjM1LjMyLjU2LjQxLjIxLjA5LjQzLjEzLjY1LjEzLjMxIDAgLjU5LS4wNC44NC0uMTMuMjYtLjA5LjQ2LS4yLjYxLS4zMnYtNGMtLjExLS4wMy0uMjctLjA2LS40Ni0uMDl6bTEyLjc3IDUuOTVjLS43LS4wMi0xLjIxLS4xOC0xLjUtLjQ4LS4yOS0uMy0uNDQtLjc4LS40NC0xLjQyVi4yNmwxLjE1LS4yMXY4LjE1NWMwIC4yLjAyLjM2NS4wNS40OTYuMDQuMTMuMDkuMjQuMTYuMzIuMDguMDguMTguMTQuMy4xOC4xMy4wNC4yOC4wOC40Ni4xbC0uMTYgMS4wMm0yLjAxLTguMTRjLS4yIDAtLjM4LS4wNy0uNTItLjIxLS4xNC0uMTQtLjIxLS4zNC0uMjEtLjU4IDAtLjI0LjA3LS40NC4yMi0uNTguMTQtLjE0LjMyLS4yMS41Mi0uMjEuMjEgMCAuMzguMDguNTMuMjIuMTUuMTUuMjIuMzQuMjIuNTggMCAuMjUtLjA3LjQ0LS4yMS41OC0uMTUuMTUtLjMyLjIyLS41My4yMnptLS41NyAxLjIzaDEuMTV2Ni43OUg3OC4zVjMuNHptNS4yLS4xN2MuNDYgMCAuODUuMDcgMS4xNy4xOS4zMi4xMy41Ny4zMS43Ny41NC4yLjIzLjMzLjUuNDIuODIuMDguMzIuMTIuNjcuMTIgMS4wNXY0LjI0bC0uNDEuMDdjLS4xNy4wMy0uMzcuMDYtLjYuMDktLjIyLjAzLS40Ni4wNS0uNzIuMDgtLjI2LjAyLS41MS4wMy0uNzcuMDMtLjM2IDAtLjY5LS4wNC0xLS4xMi0uMy0uMDgtLjU2LS4yLS43OS0uMzctLjIyLS4xNy0uMzktLjM5LS41Mi0uNjdTODEgOC41NyA4MSA4LjE4YzAtLjM3LjA3LS42OTIuMjItLjk2MnMuMzQtLjQ5LjU5LS42NTNjLjI1LS4xNjQuNTMtLjI4Ni44Ni0uMzY0LjMzLS4wOC42OC0uMTIgMS4wNC0uMTIuMTIgMCAuMjQuMDEuMzYuMDIuMTMuMDIuMjQuMDMuMzUuMDUuMTIuMDMuMjEuMDQuMy4wNi4wODMuMDIuMTQuMDMuMTc0LjA0VjUuOWMwLS4yLS4wMi0uMzk1LS4wNy0uNTktLjA0LS4xOTgtLjExNC0uMzctLjIyLS41MjQtLjEwNy0uMTUtLjI1My0uMjczLS40NC0uMzY0LS4xODMtLjA5LS40MjQtLjEzLS43Mi0uMTMtLjM4IDAtLjcxLjAzLS45OTYuMDktLjI5LjA1NS0uNS4xMTQtLjY0LjE3NWwtLjE0LTEuMDA3Yy4xNDctLjA3LjM5NS0uMTMzLjc0LS4yLjM0Ni0uMDY0LjcyLS4xIDEuMTI1LS4xem0uMSA2LjA4Yy4yNyAwIC41MiAwIC43My0uMDIuMjEtLjAxLjM5LS4wMy41My0uMDdWNy4xOWMtLjA4LS4wNDQtLjIyLS4wOC0uNC0uMTEtLjE4LS4wMy0uNDEtLjA0Ny0uNjctLjA0Ny0uMTcgMC0uMzYuMDItLjU1LjA0LS4xOS4wMy0uMzcuMDgtLjUzLjE3LS4xNi4wODUtLjI5LjE5OC0uNC4zNC0uMTEuMTQ1LS4xNi4zMy0uMTYuNTcgMCAuNDM1LjEzLjczNy4zOS45MDYuMjYuMTcuNjIuMjUgMS4wNy4yNXpcIiBmaWxsPVwiJTIzMUQzNjU3XCIvPjwvZz48L3N2Zz4nKTtcbiAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDtcbiAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCU7XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIHRleHQtaW5kZW50OiAtOTAwMHB4O1xuICBwYWRkaW5nOiAwICFpbXBvcnRhbnQ7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuIiwiLyohIGRvY3NlYXJjaCBVTlJFTEVBU0VEIHwgwqkgQWxnb2xpYSB8IGdpdGh1Yi5jb20vYWxnb2xpYS9kb2NzZWFyY2ggKi9cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSB7XG4gIHdpZHRoOiAxMDAlO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtcmlnaHQgLmRzLWRyb3Bkb3duLW1lbnUge1xuICByaWdodDogMCAhaW1wb3J0YW50O1xuICBsZWZ0OiBpbmhlcml0ICFpbXBvcnRhbnQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZS5hbGdvbGlhLWF1dG9jb21wbGV0ZS1yaWdodCAuZHMtZHJvcGRvd24tbWVudTpiZWZvcmUge1xuICByaWdodDogNDhweDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlLmFsZ29saWEtYXV0b2NvbXBsZXRlLWxlZnQgLmRzLWRyb3Bkb3duLW1lbnUge1xuICBsZWZ0OiAwICFpbXBvcnRhbnQ7XG4gIHJpZ2h0OiBpbmhlcml0ICFpbXBvcnRhbnQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZS5hbGdvbGlhLWF1dG9jb21wbGV0ZS1sZWZ0IC5kcy1kcm9wZG93bi1tZW51OmJlZm9yZSB7XG4gIGxlZnQ6IDQ4cHg7XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtaW4td2lkdGg6IDEyMDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudSB7XG4gICAgbGVmdDogMzUwcHggIWltcG9ydGFudDtcbiAgICBtaW4td2lkdGg6IDgyMHB4O1xuICAgIG1heC13aWR0aDogMTIwMHB4O1xuICAgIHRvcDogLTE4cHggIWltcG9ydGFudDtcbiAgfVxuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudTpiZWZvcmUge1xuICAgIHRyYW5zZm9ybTogcm90YXRlKC0xMzVkZWcpO1xuICAgIGxlZnQ6IC03cHg7XG4gICAgdG9wOiAyMHB4O1xuICB9XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtaW4td2lkdGg6IDE1NTBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUuYWxnb2xpYS1hdXRvY29tcGxldGUtbGVmdCAuZHMtZHJvcGRvd24tbWVudSB7XG4gICAgbWluLXdpZHRoOiAxMTUwcHg7XG4gICAgbWF4LXdpZHRoOiAxMjAwcHg7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB0b3A6IC02cHg7XG4gIGJvcmRlci1yYWRpdXM6IDRweDtcbiAgbWFyZ2luOiA2cHggMCAwO1xuICBwYWRkaW5nOiAwO1xuICB0ZXh0LWFsaWduOiBsZWZ0O1xuICBoZWlnaHQ6IGF1dG87XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQ7XG4gIGJvcmRlcjogbm9uZTtcbiAgei1pbmRleDogOTk5O1xuICBvdmVyZmxvdzogdmlzaWJsZTtcbiAgbWF4LXdpZHRoOiA2MDBweDtcbiAgbWluLXdpZHRoOiAzMjBweDtcbiAgYm94LXNoYWRvdzogMCAxcHggMCAwIHJnYmEoMCwgMCwgMCwgMC4yKSwgMCAycHggM3B4IDAgcmdiYSgwLCAwLCAwLCAwLjEpO1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWluLXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUge1xuICAgIG1pbi13aWR0aDogNTAwcHg7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51OmJlZm9yZSB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGNvbnRlbnQ6ICcnO1xuICB3aWR0aDogMTRweDtcbiAgaGVpZ2h0OiAxNHB4O1xuICBiYWNrZ3JvdW5kOiAjZmZmO1xuICB6LWluZGV4OiAxMDAwO1xuICB0b3A6IC03cHg7XG4gIGJvcmRlci10b3A6IDFweCBzb2xpZCAjY2FkOWUwO1xuICBib3JkZXItcmlnaHQ6IDFweCBzb2xpZCAjY2FkOWUwO1xuICB0cmFuc2Zvcm06IHJvdGF0ZSgtNDVkZWcpO1xuICBib3JkZXItcmFkaXVzOiAycHg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAuZHMtc3VnZ2VzdGlvbnMge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHotaW5kZXg6IDEwMDA7XG4gIG1hcmdpbi10b3A6IDhweDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IC5kcy1zdWdnZXN0aW9uIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUgLmRzLXN1Z2dlc3Rpb24uZHMtY3Vyc29yIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSB7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjAsIDE0NSwgMjMxLCAwLjA1KTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5kcy1kcm9wZG93bi1tZW51IC5kcy1zdWdnZXN0aW9uLmRzLWN1cnNvciAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbjpub3QoLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSkgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDIwLCAxNDUsIDIzMSwgMC4wNSk7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tc3ViY2F0ZWdvcnktY29sdW1uLXRleHQge1xuICBmb250LXdlaWdodDogNjAwO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmRzLWRyb3Bkb3duLW1lbnUgW2NsYXNzXj1cImRzLWRhdGFzZXQtXCJdIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBib3JkZXI6IHNvbGlkIDFweCAjY2FkOWUwO1xuICBiYWNrZ3JvdW5kOiAjZmZmO1xuICBib3JkZXItcmFkaXVzOiA0cHg7XG4gIG92ZXJmbG93OiBhdXRvO1xuICBwYWRkaW5nOiAwIDhweCA4cHg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuZHMtZHJvcGRvd24tbWVudSAqIHtcbiAgYm94LXNpemluZzogYm9yZGVyLWJveDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBwYWRkaW5nOiAwIDhweDtcbiAgYmFja2dyb3VuZDogI2ZmZjtcbiAgY29sb3I6ICMwMDA7XG4gIG92ZXJmbG93OiBoaWRkZW47XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgY29sb3I6ICMwYzQ3NzA7XG4gIGJhY2tncm91bmQ6IHJnYmEoMTE0LCAxODksIDI0MSwgMC4xKTtcbiAgcGFkZGluZzogMC4xZW0gMC4wNWVtO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNhdGVnb3J5LWhlYWRlciAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyLWx2bDAgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWhpZ2hsaWdodCxcbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXItbHZsMSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgY29sb3I6IGluaGVyaXQ7XG4gIGJhY2tncm91bmQ6IGluaGVyaXQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tdGV4dCAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0taGlnaGxpZ2h0IHtcbiAgcGFkZGluZzogMCAwIDFweDtcbiAgYmFja2dyb3VuZDogaW5oZXJpdDtcbiAgYm94LXNoYWRvdzogaW5zZXQgMCAtMnB4IDAgMCByZ2JhKDIwLCAxNDUsIDIzMSwgMC44KTtcbiAgY29sb3I6IGluaGVyaXQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY29udGVudCB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBmbG9hdDogcmlnaHQ7XG4gIHdpZHRoOiA3MCU7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgcGFkZGluZzogNS4zMzMzM3B4IDAgNS4zMzMzM3B4IDEwLjY2NjY3cHg7XG4gIGN1cnNvcjogcG9pbnRlcjtcbn1cblxuQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNTAwcHgpIHtcbiAgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jb250ZW50IHtcbiAgICB3aWR0aDogMTAwJTtcbiAgICBmbG9hdDogbm9uZTtcbiAgfVxufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6YmVmb3JlIHtcbiAgY29udGVudDogJyc7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHRvcDogMDtcbiAgaGVpZ2h0OiAxMDAlO1xuICB3aWR0aDogMXB4O1xuICBiYWNrZ3JvdW5kOiAjZGRkO1xuICBsZWZ0OiAtMXB4O1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6YmVmb3JlIHtcbiAgICBkaXNwbGF5OiBub25lO1xuICB9XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBib3JkZXItYm90dG9tOiAxcHggc29saWQgI2RkZDtcbiAgZGlzcGxheTogbm9uZTtcbiAgbWFyZ2luLXRvcDogOHB4O1xuICBwYWRkaW5nOiA0cHggMDtcbiAgZm9udC1zaXplOiAxZW07XG4gIGNvbG9yOiAjMzMzNjNEO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXdyYXBwZXIge1xuICB3aWR0aDogMTAwJTtcbiAgZmxvYXQ6IGxlZnQ7XG4gIHBhZGRpbmc6IDhweCAwIDAgMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4ge1xuICBmbG9hdDogbGVmdDtcbiAgd2lkdGg6IDMwJTtcbiAgZGlzcGxheTogbm9uZTtcbiAgcGFkZGluZy1sZWZ0OiAwO1xuICB0ZXh0LWFsaWduOiByaWdodDtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBwYWRkaW5nOiA1LjMzMzMzcHggMTAuNjY2NjdweDtcbiAgY29sb3I6ICM2NzdjOGE7XG4gIGZvbnQtc2l6ZTogMC45ZW07XG4gIHdvcmQtd3JhcDogYnJlYWstd29yZDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW46YmVmb3JlIHtcbiAgY29udGVudDogJyc7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHRvcDogMDtcbiAgaGVpZ2h0OiAxMDAlO1xuICB3aWR0aDogMXB4O1xuICBiYWNrZ3JvdW5kOiAjZGRkO1xuICByaWdodDogMDtcbn1cblxuQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNTAwcHgpIHtcbiAgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW46YmVmb3JlIHtcbiAgICBkaXNwbGF5OiBub25lO1xuICB9XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tc3ViY2F0ZWdvcnktY29sdW1uIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiBpbmhlcml0O1xuICBjb2xvcjogaW5oZXJpdDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1pbmxpbmUge1xuICBkaXNwbGF5OiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlIHtcbiAgbWFyZ2luLWJvdHRvbTogNHB4O1xuICBjb2xvcjogIzAwMDtcbiAgZm9udC1zaXplOiAwLjllbTtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tdGV4dCB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBsaW5lLWhlaWdodDogMS4yZW07XG4gIGZvbnQtc2l6ZTogMC44NWVtO1xuICBjb2xvcjogIzBlMzg0Zjtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1uby1yZXN1bHRzIHtcbiAgd2lkdGg6IDEwMCU7XG4gIHBhZGRpbmc6IDhweCAwO1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGZvbnQtc2l6ZTogMS4yZW07XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tbm8tcmVzdWx0czo6YmVmb3JlIHtcbiAgZGlzcGxheTogbm9uZTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uIGNvZGUge1xuICBwYWRkaW5nOiAxcHggNXB4O1xuICBmb250LXNpemU6IDkwJTtcbiAgYm9yZGVyOiBub25lO1xuICBjb2xvcjogIzIyMjIyMjtcbiAgYmFja2dyb3VuZC1jb2xvcjogI0VCRUJFQjtcbiAgYm9yZGVyLXJhZGl1czogM3B4O1xuICBmb250LWZhbWlseTogTWVubG8sTW9uYWNvLENvbnNvbGFzLFwiQ291cmllciBOZXdcIixtb25vc3BhY2U7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbiBjb2RlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBiYWNrZ3JvdW5kOiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24uYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbl9fbWFpbiAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyIHtcbiAgZGlzcGxheTogYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uX19zZWNvbmRhcnkgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXN1YmNhdGVnb3J5LWNvbHVtbiB7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA1MDBweCkge1xuICAuYWxnb2xpYS1hdXRvY29tcGxldGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24uYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbl9fc2Vjb25kYXJ5IC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4ge1xuICAgIGRpc3BsYXk6IG5vbmU7XG4gIH1cbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbiB7XG4gIGJvcmRlci1ib3R0b206IHNvbGlkIDFweCAjZWVlO1xuICBwYWRkaW5nOiA4cHg7XG4gIG1hcmdpbjogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQge1xuICB3aWR0aDogMTAwJTtcbiAgcGFkZGluZzogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNvbnRlbnQ6OmJlZm9yZSB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXIge1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyOiBub25lO1xufVxuXG4uYWxnb2xpYS1hdXRvY29tcGxldGUgLnN1Z2dlc3Rpb24tbGF5b3V0LXNpbXBsZSAuYWxnb2xpYS1kb2NzZWFyY2gtc3VnZ2VzdGlvbi0tY2F0ZWdvcnktaGVhZGVyLWx2bDAge1xuICBvcGFjaXR5OiAuNjtcbiAgZm9udC1zaXplOiAwLjg1ZW07XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1jYXRlZ29yeS1oZWFkZXItbHZsMSB7XG4gIG9wYWNpdHk6IC42O1xuICBmb250LXNpemU6IDAuODVlbTtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLWNhdGVnb3J5LWhlYWRlci1sdmwxOjpiZWZvcmUge1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDt1dGY4LDxzdmcgd2lkdGg9XCIxMFwiIGhlaWdodD1cIjEwXCIgdmlld0JveD1cIjAgMCAyMCAzOFwiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIj48cGF0aCBkPVwiTTEuNDkgNC4zMWwxNCAxNi4xMjYuMDAyLTIuNjI0LTE0IDE2LjA3NC0xLjMxNCAxLjUxIDMuMDE3IDIuNjI2IDEuMzEzLTEuNTA4IDE0LTE2LjA3NSAxLjE0Mi0xLjMxMy0xLjE0LTEuMzEzLTE0LTE2LjEyNUwzLjIuMTguMTggMi44bDEuMzEgMS41MXpcIiBmaWxsLXJ1bGU9XCJldmVub2RkXCIgZmlsbD1cIiUyMzFEMzY1N1wiIC8+PC9zdmc+Jyk7XG4gIGNvbnRlbnQ6ICcnO1xuICB3aWR0aDogMTBweDtcbiAgaGVpZ2h0OiAxMHB4O1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS13cmFwcGVyIHtcbiAgd2lkdGg6IDEwMCU7XG4gIGZsb2F0OiBsZWZ0O1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1zdWJjYXRlZ29yeS1jb2x1bW4sIC5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1kdXBsaWNhdGUtY29udGVudCwgLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXN1YmNhdGVnb3J5LWlubGluZSB7XG4gIGRpc3BsYXk6IG5vbmUgIWltcG9ydGFudDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlIHtcbiAgbWFyZ2luOiAwO1xuICBjb2xvcjogIzE0OTFlNztcbiAgZm9udC1zaXplOiAwLjllbTtcbiAgZm9udC13ZWlnaHQ6IG5vcm1hbDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5zdWdnZXN0aW9uLWxheW91dC1zaW1wbGUgLmFsZ29saWEtZG9jc2VhcmNoLXN1Z2dlc3Rpb24tLXRpdGxlOjpiZWZvcmUge1xuICBjb250ZW50OiBcIiNcIjtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gIGNvbG9yOiAjMTQ5MWU3O1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS10ZXh0IHtcbiAgbWFyZ2luOiA0cHggMCAwO1xuICBkaXNwbGF5OiBibG9jaztcbiAgbGluZS1oZWlnaHQ6IDEuNGVtO1xuICBwYWRkaW5nOiA1LjMzMzMzcHggOHB4O1xuICBiYWNrZ3JvdW5kOiAjZjhmOGY4O1xuICBmb250LXNpemU6IDAuODVlbTtcbiAgb3BhY2l0eTogLjg7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuc3VnZ2VzdGlvbi1sYXlvdXQtc2ltcGxlIC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS10ZXh0IC5hbGdvbGlhLWRvY3NlYXJjaC1zdWdnZXN0aW9uLS1oaWdobGlnaHQge1xuICBjb2xvcjogIzAyMGEwZTtcbiAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gIGJveC1zaGFkb3c6IG5vbmU7XG59XG5cbi5hbGdvbGlhLWF1dG9jb21wbGV0ZSAuYWxnb2xpYS1kb2NzZWFyY2gtZm9vdGVyIHtcbiAgd2lkdGg6IDEwMHB4O1xuICBoZWlnaHQ6IDIwcHg7XG4gIHotaW5kZXg6IDIwMDA7XG4gIG1hcmdpbi10b3A6IDEwLjY2NjY3cHg7XG4gIGZsb2F0OiByaWdodDtcbiAgZm9udC1zaXplOiAwO1xuICBsaW5lLWhlaWdodDogMDtcbn1cblxuLmFsZ29saWEtYXV0b2NvbXBsZXRlIC5hbGdvbGlhLWRvY3NlYXJjaC1mb290ZXItLWxvZ28ge1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDt1dGY4LDxzdmcgd2lkdGg9XCI4NlwiIGhlaWdodD1cIjEzXCIgdmlld0JveD1cIjAgMCA4NiAxM1wiIHhtbG5zPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmdcIj48ZyBmaWxsPVwibm9uZVwiIGZpbGwtcnVsZT1cImV2ZW5vZGRcIj48cGF0aCBkPVwiTTUuMDEgOC4xYzAgLjYzLS4yMjggMS4xMi0uNjg0IDEuNDctLjQ1Ni4zNTItMS4wNzQuNTI4LTEuODU1LjUyOC0uODQgMC0xLjQ5LS4xMS0xLjk1LS4zMjd2LS44Yy4zLjEzLjYxLjIyLjk2LjMuMzUuMDcuNjkuMTEgMS4wMy4xMS41NiAwIC45Ny0uMSAxLjI1LS4zMXMuNDItLjUuNDItLjg3YzAtLjI1LS4wNS0uNDUtLjE1LS42MS0uMDktLjE1LS4yNi0uMy0uNDktLjQzLS4yNC0uMTQtLjU5LS4yOS0xLjA3LS40Ni0uNjYtLjIzLTEuMTMtLjUyLTEuNDItLjg0Qy43NiA1LjUyLjYxIDUuMS42MSA0LjU3YzAtLjU1LjIxLS45ODUuNjItMS4zMS40Mi0uMzI1Ljk2LS40ODggMS42NC0uNDg4LjcxIDAgMS4zNy4xMyAxLjk2LjM5bC0uMjYuNzJjLS41OS0uMjUtMS4xNi0uMzctMS43MS0uMzctLjQ0IDAtLjc4LjA5LTEuMDMuMjgtLjI1LjE5LS4zNy40NS0uMzcuNzggMCAuMjQuMDUuNDUuMTQuNi4wOS4xNi4yNDYuMy40Ni40My4yMi4xMy41NS4yNzcuOTk2LjQzNy43NS4yNiAxLjI2NC41NSAxLjU0NS44Ni4yOS4zLjQzLjcuNDMgMS4xOXptMy41OTggMmMtLjc5IDAtMS40MTUtLjI0LTEuODcyLS43MjMtLjQ1OC0uNDgyLS42ODYtMS4xNS0uNjg2LTIuMDA3IDAtLjg2Mi4yMTItMS41NDguNjM3LTIuMDU2LjQyNS0uNTA3Ljk5NS0uNzYgMS43MS0uNzYuNjcyIDAgMS4yMDMuMjIgMS41OTMuNjYuMzkuNDQuNTg2IDEuMDIzLjU4NiAxLjc0NnYuNTEzSDYuODljLjAxNi42My4xNzUgMS4xMDYuNDc2IDEuNDMuMy4zMjcuNzI1LjQ5IDEuMjcyLjQ5LjU3NiAwIDEuMTQ2LS4xMiAxLjcxLS4zNjJ2LjczYy0uMjg4LjEzLS41Ni4yMi0uODE0LjI3LS4yNTYuMDYtLjU2NC4wOC0uOTI2LjA4em0tLjIyLTQuODdjLS40MyAwLS43NzIuMTQtMS4wMjcuNDItLjI1LjI4LS40LjY3LS40NSAxLjE2NGgyLjhjMC0uNTEyLS4xMS0uOTAzLS4zNC0xLjE3NS0uMjMtLjI4LS41NS0uNDEtLjk3LS40MXpNMTUuMjUgMTBsLS4xNi0uNzYyaC0uMDRjLS4yNjYuMzM2LS41MzIuNTYzLS43OTguNjgtLjI2NS4xMi0uNTk2LjE4LS45OTMuMTgtLjUzIDAtLjk1LS4xMzctMS4yNS0uNDEtLjMtLjI3NC0uNDUtLjY2My0uNDUtMS4xNjcgMC0xLjA4Ljg2LTEuNjQgMi41OS0xLjdsLjkxLS4wMnYtLjM0YzAtLjQyLS4wOS0uNzMtLjI3LS45My0uMTgtLjItLjQ3LS4zLS44Ny0uMy0uNDUgMC0uOTUuMTQtMS41Mi40MWwtLjI1LS42MmMuMjYtLjE0LjU1LS4yNS44Ni0uMzMuMzEtLjA4LjYzLS4xMi45NC0uMTIuNjMgMCAxLjExLjE1IDEuNDEuNDMuMy4yOS40Ni43NC40NiAxLjM3VjEwaC0uNnptLTEuODMtLjU3Yy41MDUgMCAuOS0uMTQgMS4xOS0uNDE2LjI4Ny0uMjc3LjQzLS42NjQuNDMtMS4xNjJ2LS40ODRsLS44MS4wMzRjLS42NDQuMDIzLTEuMTEuMTIzLTEuMzk0LjMtLjI4NC4xNzgtLjQyNy40NTQtLjQyNy44MjggMCAuMjkzLjA5LjUxNi4yNi42Ny4xOC4xNTIuNDIuMjMuNzQuMjN6bTYuNTQzLTQuODhjLjI0IDAgLjQ1Mi4wMi42NC4wNmwtLjExMi43NWMtLjIyLS4wNDgtLjQxLS4wNzItLjU4LS4wNzItLjQzIDAtLjguMTc2LTEuMTEuNTI3LS4zMS4zNTItLjQ2Ljc5LS40NiAxLjMxNFYxMGgtLjgxVjQuNjQ4aC42N2wuMS45OTJoLjA0Yy4yLS4zNS40NC0uNjE3LjcyLS44MDYuMjgtLjE5LjU5LS4yODMuOTMtLjI4M3ptMy41OCA1LjU0OGMtLjc3NSAwLTEuMzc0LS4yNC0xLjgtLjcxNi0uNDI0LS40NzctLjYzNi0xLjE1LS42MzYtMi4wMjQgMC0uODk1LjIxNS0xLjU4Ny42NDYtMi4wNzUuNDMyLS40ODggMS4wNDYtLjczMiAxLjg0NC0uNzMyLjI1NyAwIC41MTQuMDMuNzcuMDkuMjU4LjA2LjQ2LjEyLjYwNy4ybC0uMjQ4LjY5Yy0uMTgtLjA3LS4zNzUtLjEzLS41ODYtLjE4LS4yMTItLjA0LS40LS4wNy0uNTYyLS4wNy0xLjA4NyAwLTEuNjMuNy0xLjYzIDIuMDggMCAuNjYuMTMyIDEuMTYuMzk3IDEuNTIuMjY3LjM1LjY2LjUzIDEuMTguNTMuNDQ2IDAgLjkwNC0uMDkgMS4zNzMtLjI4di43MmMtLjM1OC4xOS0uODEuMjgtMS4zNTMuMjh6bTYuMjgtLjA5OFY2LjUzOGMwLS40MzYtLjEtLjc2Mi0uMjk3LS45NzYtLjE5OC0uMjE1LS41MS0uMzIzLS45MzItLjMyMy0uNTYzIDAtLjk3NC4xNS0xLjIzMy40NS0uMjUuMy0uMzguODEtLjM4IDEuNVYxMGgtLjgxVjIuNDAyaC44MXYyLjNjMCAuMjc3LS4wMS41MDYtLjA0LjY5aC4wNWMuMTYtLjI2LjM5LS40Ni42OC0uNjEuMy0uMTQ3LjYzLS4yMiAxLjAxLS4yMi42NiAwIDEuMTUuMTU0IDEuNDcuNDY1LjMzLjMxLjQ5LjgwNS40OSAxLjQ4MlYxMGgtLjgxem03LjU2NS01LjQ0Yy43MDMgMCAxLjI1LjI0IDEuNjM4LjcyLjM5LjQ4LjU4MyAxLjE2LjU4MyAyLjA0IDAgLjg3OC0uMiAxLjU2LS41OSAyLjA0OC0uMzkuNDg2LS45NC43My0xLjY0LjczLS4zNSAwLS42Ny0uMDY1LS45Ni0uMTkzLS4yOS0uMTMtLjUzLS4zMjctLjczLS41OTNoLS4wNmwtLjE3LjY4OGgtLjU4VjIuNDAyaC44MXYxLjg0NmMwIC40MTMtLjAyLjc4NS0uMDQgMS4xMTNoLjA0Yy4zOC0uNTMuOTQtLjggMS42OC0uOHptLS4xMTcuNjhjLS41NSAwLS45NS4xNTgtMS4xOS40NzUtLjI0LjMxOC0uMzYuODUyLS4zNiAxLjYwNCAwIC43NS4xMyAxLjI5LjM4IDEuNjEuMjUuMzIuNjYuNDggMS4yMS40OC41IDAgLjg3LS4xOSAxLjEyLS41NS4yNS0uMzcuMzctLjg5LjM3LTEuNTcgMC0uNy0uMTItMS4yMi0uMzYtMS41Ni0uMjQtLjM1LS42Mi0uNTItMS4xMy0uNTJ6bTIuNzEtLjU5MmguODdMNDIuMDIgNy43Yy4yNTcuNjk3LjQxNiAxLjIuNDggMS41MWguMDM3Yy4wNDItLjE2Ny4xMy0uNDUuMjY2LS44NTMuMTM2LS40MDIuNTc4LTEuNjM4IDEuMzI3LTMuNzFINDVsLTIuMyA2LjA5NWMtLjIzLjYwMi0uNDk2IDEuMDMtLjggMS4yODItLjMwNC4yNTItLjY3OC4zNzgtMS4xMi4zNzgtLjI0OCAwLS40OS0uMDI3LS43MzMtLjA4M3YtLjY1Yy4xOC4wNC4zOC4wNi42LjA2LjU1NyAwIC45NTQtLjMyIDEuMTkyLS45NGwuMjktLjc2LTIuMTYtNS4zOXpcIiBmaWxsPVwiJTIzNzk3OTc5XCIvPjxwYXRoIGQ9XCJNNzAuODgzIDQuOGwtLjU1MyAyLjA2IDEuNzgyLTEuMDI1Yy0uMjYtLjUtLjctLjg3Ni0xLjIzLTEuMDM0ek02Ny43NiAzLjQxYy0uMjM3LS4yNS0uNjI0LS4yNS0uODYyIDBsLS4xMDguMTE0Yy0uMjM4LjI1Mi0uMjM4LjY2IDAgLjkxbC4xMTUuMTIyYy4yNDQtLjQxNC41NTYtLjc4LjkxOC0xLjA4bC0uMDYyLS4wNjZ6bTMuNzMtLjYzYzAtLjAxNC4wMDMtLjAyNy4wMDMtLjA0MnYtLjMyMmMwLS4zNTUtLjI3My0uNjQ0LS42MS0uNjQ0aC0xLjA2OGMtLjMzNyAwLS42MS4yODgtLjYxLjY0NHYuMzE3Yy4zNC0uMS42OTgtLjE1NiAxLjA2OC0uMTU2LjQyNCAwIC44MzMuMDcyIDEuMjE2LjIwM1wiIGZpbGw9XCIlMjM0NkFFREFcIi8+PHBhdGggZD1cIk03MC4zMTYgNC4yNDNjMS4zNCAwIDIuNDI4IDEuMTQ1IDIuNDI4IDIuNTUyIDAgMS40MDgtMS4wOSAyLjU1My0yLjQyOCAyLjU1My0xLjM0IDAtMi40MjgtMS4xNDUtMi40MjgtMi41NTMgMC0xLjQwNyAxLjA5LTIuNTUyIDIuNDI4LTIuNTUybS0zLjQgMi41NTJjMCAxLjk3NCAxLjUyMiAzLjU3NCAzLjQgMy41NzRzMy40LTEuNiAzLjQtMy41OC0xLjUyMi0zLjU4LTMuNC0zLjU4LTMuNCAxLjYtMy40IDMuNTd6XCIgZmlsbD1cIiUyMzQ2QUVEQVwiLz48cGF0aCBkPVwiTTU0Ljc1OCAxMC4xNzVjLS4xNC0uMzktLjI3LS43NzYtLjM5NS0xLjE1NS0uMTI0LS4zNzgtLjI1LS43NjMtLjM4My0xLjE1NEg1MC4xbC0uNzggMi4zMWgtMS4yNDdjLjMzLS45NTguNjQtMS44NDMuOTI3LTIuNjU2LjI4OC0uODEzLjU3LTEuNTg1Ljg0Ny0yLjMxNi4yNzUtLjczLjU1LTEuNDMuODItMi4wOTQuMjczLS42NjYuNTU3LTEuMzI0Ljg1NC0xLjk3N2gxLjFjLjMuNjUzLjU4IDEuMzEuODYgMS45NzcuMjcuNjY1LjU1IDEuMzYzLjgyIDIuMDk0LjI4LjczLjU2IDEuNTAzLjg1IDIuMzE2LjI5LjgxMy42IDEuNjk4LjkzIDIuNjU1aC0xLjMxem0tMS4xMjQtMy4zNTNjLS4yNjQtLjc1Ny0uNTI1LTEuNDktLjc4NS0yLjItLjI2LS43MDgtLjUzLTEuMzg4LS44MS0yLjA0LS4yOS42NTItLjU3IDEuMzMyLS44MyAyLjA0LS4yNi43MS0uNTIgMS40NDMtLjc3IDIuMmgzLjE5em01LjQ1IDMuNDgzYy0uNzEtLjAxNy0xLjIxLS4xNzgtMS41MDgtLjQ4Mi0uMjk3LS4zMDUtLjQ0NS0uNzgtLjQ0NS0xLjQyM1YuMjZsMS4xNS0uMjF2OC4xNTVjMCAuMi4wMi4zNjUuMDUuNDk2LjA0LjEzLjA5LjI0LjE2LjMyLjA4LjA4LjE4LjE0LjMuMTguMTMuMDQuMjguMDguNDYuMWwtLjE2IDEuMDJtNS40Ny0uODFjLS4xLjA3LS4yOS4xNi0uNTcuMjctLjI4LjExLS42MS4xNy0uOTkuMTdzLS43NS0uMDYtMS4wOS0uMTljLS4zNC0uMTMtLjY0LS4zMy0uODktLjYtLjI2LS4yNy0uNDYtLjYxLS42MS0xLjAyLS4xNS0uNC0uMjItLjg5LS4yMi0xLjQ2IDAtLjQ5LjA3LS45NS4yMS0xLjM2LjE0LS40MS4zNS0uNzcuNjEtMS4wNy4yNy0uMy42LS41My45OS0uNy4zOS0uMTcuODMtLjI1IDEuMzEtLjI1LjU0IDAgMS4wMS4wNCAxLjQxLjEyLjQuMDkuNzMuMTYgMS4wMS4yM1Y5LjdjMCAxLjA0NC0uMjUgMS44LS43NiAyLjI3cy0xLjI5LjcwNS0yLjMzLjcwNWMtLjQgMC0uNzgtLjAzNS0xLjE0LS4xMDQtLjM2LS4wNy0uNjctLjE1LS45My0uMjRsLjIxLTEuMDZjLjIzLjA5LjUxLjE4Ljg0LjI1LjM0LjA4LjY4LjExIDEuMDUuMTEuNjkgMCAxLjE4LS4xNCAxLjQ4LS40My4zLS4yOC40NS0uNzQuNDUtMS4zN3YtLjN6bS0uNDctNS4xM2MtLjE5LS4wMy0uNDUtLjA0LS43OC0uMDQtLjYxIDAtMS4wOS4yMi0xLjQyLjY0LS4zMy40My0uNS45OS0uNSAxLjcgMCAuMzkuMDUuNzMuMTQgMS4wMS4xLjI4LjIzLjUxLjM5LjY5LjE2LjE4LjM1LjMyLjU2LjQxLjIxLjA5LjQzLjEzLjY1LjEzLjMxIDAgLjU5LS4wNC44NC0uMTMuMjYtLjA5LjQ2LS4yLjYxLS4zMnYtNGMtLjExLS4wMy0uMjctLjA2LS40Ni0uMDl6bTEyLjc3IDUuOTVjLS43LS4wMi0xLjIxLS4xOC0xLjUtLjQ4LS4yOS0uMy0uNDQtLjc4LS40NC0xLjQyVi4yNmwxLjE1LS4yMXY4LjE1NWMwIC4yLjAyLjM2NS4wNS40OTYuMDQuMTMuMDkuMjQuMTYuMzIuMDguMDguMTguMTQuMy4xOC4xMy4wNC4yOC4wOC40Ni4xbC0uMTYgMS4wMm0yLjAxLTguMTRjLS4yIDAtLjM4LS4wNy0uNTItLjIxLS4xNC0uMTQtLjIxLS4zNC0uMjEtLjU4IDAtLjI0LjA3LS40NC4yMi0uNTguMTQtLjE0LjMyLS4yMS41Mi0uMjEuMjEgMCAuMzguMDguNTMuMjIuMTUuMTUuMjIuMzQuMjIuNTggMCAuMjUtLjA3LjQ0LS4yMS41OC0uMTUuMTUtLjMyLjIyLS41My4yMnptLS41NyAxLjIzaDEuMTV2Ni43OUg3OC4zVjMuNHptNS4yLS4xN2MuNDYgMCAuODUuMDcgMS4xNy4xOS4zMi4xMy41Ny4zMS43Ny41NC4yLjIzLjMzLjUuNDIuODIuMDguMzIuMTIuNjcuMTIgMS4wNXY0LjI0bC0uNDEuMDdjLS4xNy4wMy0uMzcuMDYtLjYuMDktLjIyLjAzLS40Ni4wNS0uNzIuMDgtLjI2LjAyLS41MS4wMy0uNzcuMDMtLjM2IDAtLjY5LS4wNC0xLS4xMi0uMy0uMDgtLjU2LS4yLS43OS0uMzctLjIyLS4xNy0uMzktLjM5LS41Mi0uNjdTODEgOC41NyA4MSA4LjE4YzAtLjM3LjA3LS42OTIuMjItLjk2MnMuMzQtLjQ5LjU5LS42NTNjLjI1LS4xNjQuNTMtLjI4Ni44Ni0uMzY0LjMzLS4wOC42OC0uMTIgMS4wNC0uMTIuMTIgMCAuMjQuMDEuMzYuMDIuMTMuMDIuMjQuMDMuMzUuMDUuMTIuMDMuMjEuMDQuMy4wNi4wODMuMDIuMTQuMDMuMTc0LjA0VjUuOWMwLS4yLS4wMi0uMzk1LS4wNy0uNTktLjA0LS4xOTgtLjExNC0uMzctLjIyLS41MjQtLjEwNy0uMTUtLjI1My0uMjczLS40NC0uMzY0LS4xODMtLjA5LS40MjQtLjEzLS43Mi0uMTMtLjM4IDAtLjcxLjAzLS45OTYuMDktLjI5LjA1NS0uNS4xMTQtLjY0LjE3NWwtLjE0LTEuMDA3Yy4xNDctLjA3LjM5NS0uMTMzLjc0LS4yLjM0Ni0uMDY0LjcyLS4xIDEuMTI1LS4xem0uMSA2LjA4Yy4yNyAwIC41MiAwIC43My0uMDIuMjEtLjAxLjM5LS4wMy41My0uMDdWNy4xOWMtLjA4LS4wNDQtLjIyLS4wOC0uNC0uMTEtLjE4LS4wMy0uNDEtLjA0Ny0uNjctLjA0Ny0uMTcgMC0uMzYuMDItLjU1LjA0LS4xOS4wMy0uMzcuMDgtLjUzLjE3LS4xNi4wODUtLjI5LjE5OC0uNC4zNC0uMTEuMTQ1LS4xNi4zMy0uMTYuNTcgMCAuNDM1LjEzLjczNy4zOS45MDYuMjYuMTcuNjIuMjUgMS4wNy4yNXpcIiBmaWxsPVwiJTIzMUQzNjU3XCIvPjwvZz48L3N2Zz4nKTtcbiAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDtcbiAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCU7XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIHRleHQtaW5kZW50OiAtOTAwMHB4O1xuICBwYWRkaW5nOiAwICFpbXBvcnRhbnQ7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuXG4vKiMgc291cmNlTWFwcGluZ1VSTD1kYXRhOmFwcGxpY2F0aW9uL2pzb247YmFzZTY0LGV5SjJaWEp6YVc5dUlqb3pMQ0p6YjNWeVkyVnpJanBiSW5OMFpHbHVJbDBzSW01aGJXVnpJanBiWFN3aWJXRndjR2x1WjNNaU9pSkJRVUZCTzBWQlEwVXNWMEZCVnp0QlFVTmlPenRCUVVWQk8wVkJRMFVzYlVKQlFXMUNPMFZCUTI1Q0xIZENRVUYzUWp0QlFVTXhRanM3UVVGRlFUdEZRVU5GTEZkQlFWYzdRVUZEWWpzN1FVRkZRVHRGUVVORkxHdENRVUZyUWp0RlFVTnNRaXg1UWtGQmVVSTdRVUZETTBJN08wRkJSVUU3UlVGRFJTeFZRVUZWTzBGQlExbzdPMEZCUlVFN1JVRkRSVHRKUVVORkxITkNRVUZ6UWp0SlFVTjBRaXhuUWtGQlowSTdTVUZEYUVJc2FVSkJRV2xDTzBsQlEycENMSEZDUVVGeFFqdEZRVU4yUWp0RlFVTkJPMGxCUTBVc01FSkJRVEJDTzBsQlF6RkNMRlZCUVZVN1NVRkRWaXhUUVVGVE8wVkJRMWc3UVVGRFJqczdRVUZGUVR0RlFVTkZPMGxCUTBVc2FVSkJRV2xDTzBsQlEycENMR2xDUVVGcFFqdEZRVU51UWp0QlFVTkdPenRCUVVWQk8wVkJRMFVzYTBKQlFXdENPMFZCUTJ4Q0xGTkJRVk03UlVGRFZDeHJRa0ZCYTBJN1JVRkRiRUlzWlVGQlpUdEZRVU5tTEZWQlFWVTdSVUZEVml4blFrRkJaMEk3UlVGRGFFSXNXVUZCV1R0RlFVTmFMR3RDUVVGclFqdEZRVU5zUWl4MVFrRkJkVUk3UlVGRGRrSXNXVUZCV1R0RlFVTmFMRmxCUVZrN1JVRkRXaXhwUWtGQmFVSTdSVUZEYWtJc1owSkJRV2RDTzBWQlEyaENMR2RDUVVGblFqdEZRVU5vUWl4M1JVRkJkMFU3UVVGRE1VVTdPMEZCUlVFN1JVRkRSVHRKUVVORkxHZENRVUZuUWp0RlFVTnNRanRCUVVOR096dEJRVVZCTzBWQlEwVXNZMEZCWXp0RlFVTmtMR3RDUVVGclFqdEZRVU5zUWl4WFFVRlhPMFZCUTFnc1YwRkJWenRGUVVOWUxGbEJRVms3UlVGRFdpeG5Ra0ZCWjBJN1JVRkRhRUlzWVVGQllUdEZRVU5pTEZOQlFWTTdSVUZEVkN3MlFrRkJOa0k3UlVGRE4wSXNLMEpCUVN0Q08wVkJReTlDTEhsQ1FVRjVRanRGUVVONlFpeHJRa0ZCYTBJN1FVRkRjRUk3TzBGQlJVRTdSVUZEUlN4clFrRkJhMEk3UlVGRGJFSXNZVUZCWVR0RlFVTmlMR1ZCUVdVN1FVRkRha0k3TzBGQlJVRTdSVUZEUlN4bFFVRmxPMEZCUTJwQ096dEJRVVZCTzBWQlEwVXNNRU5CUVRCRE8wRkJRelZET3p0QlFVVkJPMFZCUTBVc01FTkJRVEJETzBGQlF6VkRPenRCUVVWQk8wVkJRMFVzWjBKQlFXZENPMEZCUTJ4Q096dEJRVVZCTzBWQlEwVXNhMEpCUVd0Q08wVkJRMnhDTEhsQ1FVRjVRanRGUVVONlFpeG5Ra0ZCWjBJN1JVRkRhRUlzYTBKQlFXdENPMFZCUTJ4Q0xHTkJRV003UlVGRFpDeHJRa0ZCYTBJN1FVRkRjRUk3TzBGQlJVRTdSVUZEUlN4elFrRkJjMEk3UVVGRGVFSTdPMEZCUlVFN1JVRkRSU3hyUWtGQmEwSTdSVUZEYkVJc1kwRkJZenRGUVVOa0xHZENRVUZuUWp0RlFVTm9RaXhYUVVGWE8wVkJRMWdzWjBKQlFXZENPMEZCUTJ4Q096dEJRVVZCTzBWQlEwVXNZMEZCWXp0RlFVTmtMRzlEUVVGdlF6dEZRVU53UXl4eFFrRkJjVUk3UVVGRGRrSTdPMEZCUlVFN08wVkJSVVVzWTBGQll6dEZRVU5rTEcxQ1FVRnRRanRCUVVOeVFqczdRVUZGUVR0RlFVTkZMR2RDUVVGblFqdEZRVU5vUWl4dFFrRkJiVUk3UlVGRGJrSXNiMFJCUVc5RU8wVkJRM0JFTEdOQlFXTTdRVUZEYUVJN08wRkJSVUU3UlVGRFJTeGpRVUZqTzBWQlEyUXNXVUZCV1R0RlFVTmFMRlZCUVZVN1JVRkRWaXhyUWtGQmEwSTdSVUZEYkVJc2VVTkJRWGxETzBWQlEzcERMR1ZCUVdVN1FVRkRha0k3TzBGQlJVRTdSVUZEUlR0SlFVTkZMRmRCUVZjN1NVRkRXQ3hYUVVGWE8wVkJRMkk3UVVGRFJqczdRVUZGUVR0RlFVTkZMRmRCUVZjN1JVRkRXQ3hyUWtGQmEwSTdSVUZEYkVJc1kwRkJZenRGUVVOa0xFMUJRVTA3UlVGRFRpeFpRVUZaTzBWQlExb3NWVUZCVlR0RlFVTldMR2RDUVVGblFqdEZRVU5vUWl4VlFVRlZPMEZCUTFvN08wRkJSVUU3UlVGRFJUdEpRVU5GTEdGQlFXRTdSVUZEWmp0QlFVTkdPenRCUVVWQk8wVkJRMFVzYTBKQlFXdENPMFZCUTJ4Q0xEWkNRVUUyUWp0RlFVTTNRaXhoUVVGaE8wVkJRMklzWlVGQlpUdEZRVU5tTEdOQlFXTTdSVUZEWkN4alFVRmpPMFZCUTJRc1kwRkJZenRCUVVOb1FqczdRVUZGUVR0RlFVTkZMRmRCUVZjN1JVRkRXQ3hYUVVGWE8wVkJRMWdzYTBKQlFXdENPMEZCUTNCQ096dEJRVVZCTzBWQlEwVXNWMEZCVnp0RlFVTllMRlZCUVZVN1JVRkRWaXhoUVVGaE8wVkJRMklzWlVGQlpUdEZRVU5tTEdsQ1FVRnBRanRGUVVOcVFpeHJRa0ZCYTBJN1JVRkRiRUlzTmtKQlFUWkNPMFZCUXpkQ0xHTkJRV003UlVGRFpDeG5Ra0ZCWjBJN1JVRkRhRUlzY1VKQlFYRkNPMEZCUTNaQ096dEJRVVZCTzBWQlEwVXNWMEZCVnp0RlFVTllMR3RDUVVGclFqdEZRVU5zUWl4alFVRmpPMFZCUTJRc1RVRkJUVHRGUVVOT0xGbEJRVms3UlVGRFdpeFZRVUZWTzBWQlExWXNaMEpCUVdkQ08wVkJRMmhDTEZGQlFWRTdRVUZEVmpzN1FVRkZRVHRGUVVORk8wbEJRMFVzWVVGQllUdEZRVU5tTzBGQlEwWTdPMEZCUlVFN1JVRkRSU3g1UWtGQmVVSTdSVUZEZWtJc1kwRkJZenRCUVVOb1FqczdRVUZGUVR0RlFVTkZMR0ZCUVdFN1FVRkRaanM3UVVGRlFUdEZRVU5GTEd0Q1FVRnJRanRGUVVOc1FpeFhRVUZYTzBWQlExZ3NaMEpCUVdkQ08wVkJRMmhDTEdsQ1FVRnBRanRCUVVOdVFqczdRVUZGUVR0RlFVTkZMR05CUVdNN1JVRkRaQ3hyUWtGQmEwSTdSVUZEYkVJc2FVSkJRV2xDTzBWQlEycENMR05CUVdNN1FVRkRhRUk3TzBGQlJVRTdSVUZEUlN4WFFVRlhPMFZCUTFnc1kwRkJZenRGUVVOa0xHdENRVUZyUWp0RlFVTnNRaXhuUWtGQlowSTdRVUZEYkVJN08wRkJSVUU3UlVGRFJTeGhRVUZoTzBGQlEyWTdPMEZCUlVFN1JVRkRSU3huUWtGQlowSTdSVUZEYUVJc1kwRkJZenRGUVVOa0xGbEJRVms3UlVGRFdpeGpRVUZqTzBWQlEyUXNlVUpCUVhsQ08wVkJRM3BDTEd0Q1FVRnJRanRGUVVOc1Fpd3dSRUZCTUVRN1FVRkROVVE3TzBGQlJVRTdSVUZEUlN4blFrRkJaMEk3UVVGRGJFSTdPMEZCUlVFN1JVRkRSU3hqUVVGak8wRkJRMmhDT3p0QlFVVkJPMFZCUTBVc1kwRkJZenRCUVVOb1FqczdRVUZGUVR0RlFVTkZPMGxCUTBVc1lVRkJZVHRGUVVObU8wRkJRMFk3TzBGQlJVRTdSVUZEUlN3MlFrRkJOa0k3UlVGRE4wSXNXVUZCV1R0RlFVTmFMRk5CUVZNN1FVRkRXRHM3UVVGRlFUdEZRVU5GTEZkQlFWYzdSVUZEV0N4VlFVRlZPMEZCUTFvN08wRkJSVUU3UlVGRFJTeGhRVUZoTzBGQlEyWTdPMEZCUlVFN1JVRkRSU3hUUVVGVE8wVkJRMVFzVlVGQlZUdEZRVU5XTEdOQlFXTTdSVUZEWkN4WFFVRlhPMFZCUTFnc1dVRkJXVHRCUVVOa096dEJRVVZCTzBWQlEwVXNWMEZCVnp0RlFVTllMR2xDUVVGcFFqdEJRVU51UWpzN1FVRkZRVHRGUVVORkxGZEJRVmM3UlVGRFdDeHBRa0ZCYVVJN1FVRkRia0k3TzBGQlJVRTdSVUZEUlN3eVZVRkJNbFU3UlVGRE0xVXNWMEZCVnp0RlFVTllMRmRCUVZjN1JVRkRXQ3haUVVGWk8wVkJRMW9zY1VKQlFYRkNPMEZCUTNaQ096dEJRVVZCTzBWQlEwVXNWMEZCVnp0RlFVTllMRmRCUVZjN1JVRkRXQ3hUUVVGVE8wVkJRMVFzVlVGQlZUdEJRVU5hT3p0QlFVVkJPMFZCUTBVc2QwSkJRWGRDTzBGQlF6RkNPenRCUVVWQk8wVkJRMFVzVTBGQlV6dEZRVU5VTEdOQlFXTTdSVUZEWkN4blFrRkJaMEk3UlVGRGFFSXNiVUpCUVcxQ08wRkJRM0pDT3p0QlFVVkJPMFZCUTBVc1dVRkJXVHRGUVVOYUxHbENRVUZwUWp0RlFVTnFRaXhqUVVGak8wVkJRMlFzY1VKQlFYRkNPMEZCUTNaQ096dEJRVVZCTzBWQlEwVXNaVUZCWlR0RlFVTm1MR05CUVdNN1JVRkRaQ3hyUWtGQmEwSTdSVUZEYkVJc2MwSkJRWE5DTzBWQlEzUkNMRzFDUVVGdFFqdEZRVU51UWl4cFFrRkJhVUk3UlVGRGFrSXNWMEZCVnp0QlFVTmlPenRCUVVWQk8wVkJRMFVzWTBGQll6dEZRVU5rTEdsQ1FVRnBRanRGUVVOcVFpeG5Ra0ZCWjBJN1FVRkRiRUk3TzBGQlJVRTdSVUZEUlN4WlFVRlpPMFZCUTFvc1dVRkJXVHRGUVVOYUxHRkJRV0U3UlVGRFlpeHpRa0ZCYzBJN1JVRkRkRUlzV1VGQldUdEZRVU5hTEZsQlFWazdSVUZEV2l4alFVRmpPMEZCUTJoQ096dEJRVVZCTzBWQlEwVXNhM0pOUVVGcmNrMDdSVUZEYkhKTkxEUkNRVUUwUWp0RlFVTTFRaXd5UWtGQk1rSTdSVUZETTBJc2NVSkJRWEZDTzBWQlEzSkNMR2RDUVVGblFqdEZRVU5vUWl4dlFrRkJiMEk3UlVGRGNFSXNjVUpCUVhGQ08wVkJRM0pDTEZkQlFWYzdSVUZEV0N4WlFVRlpPMFZCUTFvc1kwRkJZenRCUVVOb1FpSXNJbVpwYkdVaU9pSnpkR1JwYmlJc0luTnZkWEpqWlhORGIyNTBaVzUwSWpwYklpNWhiR2R2YkdsaExXRjFkRzlqYjIxd2JHVjBaU0I3WEc0Z0lIZHBaSFJvT2lBeE1EQWxPMXh1ZlZ4dVhHNHVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1V1WVd4bmIyeHBZUzFoZFhSdlkyOXRjR3hsZEdVdGNtbG5hSFFnTG1SekxXUnliM0JrYjNkdUxXMWxiblVnZTF4dUlDQnlhV2RvZERvZ01DQWhhVzF3YjNKMFlXNTBPMXh1SUNCc1pXWjBPaUJwYm1obGNtbDBJQ0ZwYlhCdmNuUmhiblE3WEc1OVhHNWNiaTVoYkdkdmJHbGhMV0YxZEc5amIyMXdiR1YwWlM1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTMXlhV2RvZENBdVpITXRaSEp2Y0dSdmQyNHRiV1Z1ZFRwaVpXWnZjbVVnZTF4dUlDQnlhV2RvZERvZ05EaHdlRHRjYm4xY2JseHVMbUZzWjI5c2FXRXRZWFYwYjJOdmJYQnNaWFJsTG1Gc1oyOXNhV0V0WVhWMGIyTnZiWEJzWlhSbExXeGxablFnTG1SekxXUnliM0JrYjNkdUxXMWxiblVnZTF4dUlDQnNaV1owT2lBd0lDRnBiWEJ2Y25SaGJuUTdYRzRnSUhKcFoyaDBPaUJwYm1obGNtbDBJQ0ZwYlhCdmNuUmhiblE3WEc1OVhHNWNiaTVoYkdkdmJHbGhMV0YxZEc5amIyMXdiR1YwWlM1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTMXNaV1owSUM1a2N5MWtjbTl3Wkc5M2JpMXRaVzUxT21KbFptOXlaU0I3WEc0Z0lHeGxablE2SURRNGNIZzdYRzU5WEc1Y2JrQnRaV1JwWVNCelkzSmxaVzRnWVc1a0lDaHRhVzR0ZDJsa2RHZzZJREV5TURCd2VDa2dlMXh1SUNBdVlXeG5iMnhwWVMxaGRYUnZZMjl0Y0d4bGRHVXVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1V0YkdWbWRDQXVaSE10WkhKdmNHUnZkMjR0YldWdWRTQjdYRzRnSUNBZ2JHVm1kRG9nTXpVd2NIZ2dJV2x0Y0c5eWRHRnVkRHRjYmlBZ0lDQnRhVzR0ZDJsa2RHZzZJRGd5TUhCNE8xeHVJQ0FnSUcxaGVDMTNhV1IwYURvZ01USXdNSEI0TzF4dUlDQWdJSFJ2Y0RvZ0xURTRjSGdnSVdsdGNHOXlkR0Z1ZER0Y2JpQWdmVnh1SUNBdVlXeG5iMnhwWVMxaGRYUnZZMjl0Y0d4bGRHVXVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1V0YkdWbWRDQXVaSE10WkhKdmNHUnZkMjR0YldWdWRUcGlaV1p2Y21VZ2UxeHVJQ0FnSUhSeVlXNXpabTl5YlRvZ2NtOTBZWFJsS0MweE16VmtaV2NwTzF4dUlDQWdJR3hsWm5RNklDMDNjSGc3WEc0Z0lDQWdkRzl3T2lBeU1IQjRPMXh1SUNCOVhHNTlYRzVjYmtCdFpXUnBZU0J6WTNKbFpXNGdZVzVrSUNodGFXNHRkMmxrZEdnNklERTFOVEJ3ZUNrZ2UxeHVJQ0F1WVd4bmIyeHBZUzFoZFhSdlkyOXRjR3hsZEdVdVlXeG5iMnhwWVMxaGRYUnZZMjl0Y0d4bGRHVXRiR1ZtZENBdVpITXRaSEp2Y0dSdmQyNHRiV1Z1ZFNCN1hHNGdJQ0FnYldsdUxYZHBaSFJvT2lBeE1UVXdjSGc3WEc0Z0lDQWdiV0Y0TFhkcFpIUm9PaUF4TWpBd2NIZzdYRzRnSUgxY2JuMWNibHh1TG1Gc1oyOXNhV0V0WVhWMGIyTnZiWEJzWlhSbElDNWtjeTFrY205d1pHOTNiaTF0Wlc1MUlIdGNiaUFnY0c5emFYUnBiMjQ2SUhKbGJHRjBhWFpsTzF4dUlDQjBiM0E2SUMwMmNIZzdYRzRnSUdKdmNtUmxjaTF5WVdScGRYTTZJRFJ3ZUR0Y2JpQWdiV0Z5WjJsdU9pQTJjSGdnTUNBd08xeHVJQ0J3WVdSa2FXNW5PaUF3TzF4dUlDQjBaWGgwTFdGc2FXZHVPaUJzWldaME8xeHVJQ0JvWldsbmFIUTZJR0YxZEc4N1hHNGdJSEJ2YzJsMGFXOXVPaUJ5Wld4aGRHbDJaVHRjYmlBZ1ltRmphMmR5YjNWdVpEb2dkSEpoYm5Od1lYSmxiblE3WEc0Z0lHSnZjbVJsY2pvZ2JtOXVaVHRjYmlBZ2VpMXBibVJsZURvZ09UazVPMXh1SUNCdmRtVnlabXh2ZHpvZ2RtbHphV0pzWlR0Y2JpQWdiV0Y0TFhkcFpIUm9PaUEyTURCd2VEdGNiaUFnYldsdUxYZHBaSFJvT2lBek1qQndlRHRjYmlBZ1ltOTRMWE5vWVdSdmR6b2dNQ0F4Y0hnZ01DQXdJSEpuWW1Fb01Dd2dNQ3dnTUN3Z01DNHlLU3dnTUNBeWNIZ2dNM0I0SURBZ2NtZGlZU2d3TENBd0xDQXdMQ0F3TGpFcE8xeHVmVnh1WEc1QWJXVmthV0VnYzJOeVpXVnVJR0Z1WkNBb2JXbHVMWGRwWkhSb09pQTFNREJ3ZUNrZ2UxeHVJQ0F1WVd4bmIyeHBZUzFoZFhSdlkyOXRjR3hsZEdVZ0xtUnpMV1J5YjNCa2IzZHVMVzFsYm5VZ2UxeHVJQ0FnSUcxcGJpMTNhV1IwYURvZ05UQXdjSGc3WEc0Z0lIMWNibjFjYmx4dUxtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzVrY3kxa2NtOXdaRzkzYmkxdFpXNTFPbUpsWm05eVpTQjdYRzRnSUdScGMzQnNZWGs2SUdKc2IyTnJPMXh1SUNCd2IzTnBkR2x2YmpvZ1lXSnpiMngxZEdVN1hHNGdJR052Ym5SbGJuUTZJQ2NuTzF4dUlDQjNhV1IwYURvZ01UUndlRHRjYmlBZ2FHVnBaMmgwT2lBeE5IQjRPMXh1SUNCaVlXTnJaM0p2ZFc1a09pQWpabVptTzF4dUlDQjZMV2x1WkdWNE9pQXhNREF3TzF4dUlDQjBiM0E2SUMwM2NIZzdYRzRnSUdKdmNtUmxjaTEwYjNBNklERndlQ0J6YjJ4cFpDQWpZMkZrT1dVd08xeHVJQ0JpYjNKa1pYSXRjbWxuYUhRNklERndlQ0J6YjJ4cFpDQWpZMkZrT1dVd08xeHVJQ0IwY21GdWMyWnZjbTA2SUhKdmRHRjBaU2d0TkRWa1pXY3BPMXh1SUNCaWIzSmtaWEl0Y21Ga2FYVnpPaUF5Y0hnN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVaSE10WkhKdmNHUnZkMjR0YldWdWRTQXVaSE10YzNWbloyVnpkR2x2Ym5NZ2UxeHVJQ0J3YjNOcGRHbHZiam9nY21Wc1lYUnBkbVU3WEc0Z0lIb3RhVzVrWlhnNklERXdNREE3WEc0Z0lHMWhjbWRwYmkxMGIzQTZJRGh3ZUR0Y2JuMWNibHh1TG1Gc1oyOXNhV0V0WVhWMGIyTnZiWEJzWlhSbElDNWtjeTFrY205d1pHOTNiaTF0Wlc1MUlDNWtjeTF6ZFdkblpYTjBhVzl1SUh0Y2JpQWdZM1Z5YzI5eU9pQndiMmx1ZEdWeU8xeHVmVnh1WEc0dVlXeG5iMnhwWVMxaGRYUnZZMjl0Y0d4bGRHVWdMbVJ6TFdSeWIzQmtiM2R1TFcxbGJuVWdMbVJ6TFhOMVoyZGxjM1JwYjI0dVpITXRZM1Z5YzI5eUlDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMbk4xWjJkbGMzUnBiMjR0YkdGNWIzVjBMWE5wYlhCc1pTQjdYRzRnSUdKaFkydG5jbTkxYm1RdFkyOXNiM0k2SUhKblltRW9NakFzSURFME5Td2dNak14TENBd0xqQTFLVHRjYm4xY2JseHVMbUZzWjI5c2FXRXRZWFYwYjJOdmJYQnNaWFJsSUM1a2N5MWtjbTl3Wkc5M2JpMXRaVzUxSUM1a2N5MXpkV2RuWlhOMGFXOXVMbVJ6TFdOMWNuTnZjaUF1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmpwdWIzUW9Mbk4xWjJkbGMzUnBiMjR0YkdGNWIzVjBMWE5wYlhCc1pTa2dMbUZzWjI5c2FXRXRaRzlqYzJWaGNtTm9MWE4xWjJkbGMzUnBiMjR0TFdOdmJuUmxiblFnZTF4dUlDQmlZV05yWjNKdmRXNWtMV052Ykc5eU9pQnlaMkpoS0RJd0xDQXhORFVzSURJek1Td2dNQzR3TlNrN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVaSE10WkhKdmNHUnZkMjR0YldWdWRTQXVZV3huYjJ4cFlTMWtiMk56WldGeVkyZ3RjM1ZuWjJWemRHbHZiaTB0YzNWaVkyRjBaV2R2Y25rdFkyOXNkVzF1TFhSbGVIUWdlMXh1SUNCbWIyNTBMWGRsYVdkb2REb2dOakF3TzF4dWZWeHVYRzR1WVd4bmIyeHBZUzFoZFhSdlkyOXRjR3hsZEdVZ0xtUnpMV1J5YjNCa2IzZHVMVzFsYm5VZ1cyTnNZWE56WGoxY0ltUnpMV1JoZEdGelpYUXRYQ0pkSUh0Y2JpQWdjRzl6YVhScGIyNDZJSEpsYkdGMGFYWmxPMXh1SUNCaWIzSmtaWEk2SUhOdmJHbGtJREZ3ZUNBalkyRmtPV1V3TzF4dUlDQmlZV05yWjNKdmRXNWtPaUFqWm1abU8xeHVJQ0JpYjNKa1pYSXRjbUZrYVhWek9pQTBjSGc3WEc0Z0lHOTJaWEptYkc5M09pQmhkWFJ2TzF4dUlDQndZV1JrYVc1bk9pQXdJRGh3ZUNBNGNIZzdYRzU5WEc1Y2JpNWhiR2R2YkdsaExXRjFkRzlqYjIxd2JHVjBaU0F1WkhNdFpISnZjR1J2ZDI0dGJXVnVkU0FxSUh0Y2JpQWdZbTk0TFhOcGVtbHVaem9nWW05eVpHVnlMV0p2ZUR0Y2JuMWNibHh1TG1Gc1oyOXNhV0V0WVhWMGIyTnZiWEJzWlhSbElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVJSHRjYmlBZ2NHOXphWFJwYjI0NklISmxiR0YwYVhabE8xeHVJQ0J3WVdSa2FXNW5PaUF3SURod2VEdGNiaUFnWW1GamEyZHliM1Z1WkRvZ0kyWm1aanRjYmlBZ1kyOXNiM0k2SUNNd01EQTdYRzRnSUc5MlpYSm1iRzkzT2lCb2FXUmtaVzQ3WEc1OVhHNWNiaTVoYkdkdmJHbGhMV0YxZEc5amIyMXdiR1YwWlNBdVlXeG5iMnhwWVMxa2IyTnpaV0Z5WTJndGMzVm5aMlZ6ZEdsdmJpMHRhR2xuYUd4cFoyaDBJSHRjYmlBZ1kyOXNiM0k2SUNNd1l6UTNOekE3WEc0Z0lHSmhZMnRuY205MWJtUTZJSEpuWW1Fb01URTBMQ0F4T0Rrc0lESTBNU3dnTUM0eEtUdGNiaUFnY0dGa1pHbHVaem9nTUM0eFpXMGdNQzR3TldWdE8xeHVmVnh1WEc0dVlXeG5iMnhwWVMxaGRYUnZZMjl0Y0d4bGRHVWdMbUZzWjI5c2FXRXRaRzlqYzJWaGNtTm9MWE4xWjJkbGMzUnBiMjR0TFdOaGRHVm5iM0o1TFdobFlXUmxjaUF1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmkwdFkyRjBaV2R2Y25rdGFHVmhaR1Z5TFd4MmJEQWdMbUZzWjI5c2FXRXRaRzlqYzJWaGNtTm9MWE4xWjJkbGMzUnBiMjR0TFdocFoyaHNhV2RvZEN4Y2JpNWhiR2R2YkdsaExXRjFkRzlqYjIxd2JHVjBaU0F1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmkwdFkyRjBaV2R2Y25rdGFHVmhaR1Z5SUM1aGJHZHZiR2xoTFdSdlkzTmxZWEpqYUMxemRXZG5aWE4wYVc5dUxTMWpZWFJsWjI5eWVTMW9aV0ZrWlhJdGJIWnNNU0F1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmkwdGFHbG5hR3hwWjJoMElIdGNiaUFnWTI5c2IzSTZJR2x1YUdWeWFYUTdYRzRnSUdKaFkydG5jbTkxYm1RNklHbHVhR1Z5YVhRN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVZV3huYjJ4cFlTMWtiMk56WldGeVkyZ3RjM1ZuWjJWemRHbHZiaTB0ZEdWNGRDQXVZV3huYjJ4cFlTMWtiMk56WldGeVkyZ3RjM1ZuWjJWemRHbHZiaTB0YUdsbmFHeHBaMmgwSUh0Y2JpQWdjR0ZrWkdsdVp6b2dNQ0F3SURGd2VEdGNiaUFnWW1GamEyZHliM1Z1WkRvZ2FXNW9aWEpwZER0Y2JpQWdZbTk0TFhOb1lXUnZkem9nYVc1elpYUWdNQ0F0TW5CNElEQWdNQ0J5WjJKaEtESXdMQ0F4TkRVc0lESXpNU3dnTUM0NEtUdGNiaUFnWTI5c2IzSTZJR2x1YUdWeWFYUTdYRzU5WEc1Y2JpNWhiR2R2YkdsaExXRjFkRzlqYjIxd2JHVjBaU0F1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmkwdFkyOXVkR1Z1ZENCN1hHNGdJR1JwYzNCc1lYazZJR0pzYjJOck8xeHVJQ0JtYkc5aGREb2djbWxuYUhRN1hHNGdJSGRwWkhSb09pQTNNQ1U3WEc0Z0lIQnZjMmwwYVc5dU9pQnlaV3hoZEdsMlpUdGNiaUFnY0dGa1pHbHVaem9nTlM0ek16TXpNM0I0SURBZ05TNHpNek16TTNCNElERXdMalkyTmpZM2NIZzdYRzRnSUdOMWNuTnZjam9nY0c5cGJuUmxjanRjYm4xY2JseHVRRzFsWkdsaElITmpjbVZsYmlCaGJtUWdLRzFoZUMxM2FXUjBhRG9nTlRBd2NIZ3BJSHRjYmlBZ0xtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1TFMxamIyNTBaVzUwSUh0Y2JpQWdJQ0IzYVdSMGFEb2dNVEF3SlR0Y2JpQWdJQ0JtYkc5aGREb2dibTl1WlR0Y2JpQWdmVnh1ZlZ4dVhHNHVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1VnTG1Gc1oyOXNhV0V0Wkc5amMyVmhjbU5vTFhOMVoyZGxjM1JwYjI0dExXTnZiblJsYm5RNlltVm1iM0psSUh0Y2JpQWdZMjl1ZEdWdWREb2dKeWM3WEc0Z0lIQnZjMmwwYVc5dU9pQmhZbk52YkhWMFpUdGNiaUFnWkdsemNHeGhlVG9nWW14dlkyczdYRzRnSUhSdmNEb2dNRHRjYmlBZ2FHVnBaMmgwT2lBeE1EQWxPMXh1SUNCM2FXUjBhRG9nTVhCNE8xeHVJQ0JpWVdOclozSnZkVzVrT2lBalpHUmtPMXh1SUNCc1pXWjBPaUF0TVhCNE8xeHVmVnh1WEc1QWJXVmthV0VnYzJOeVpXVnVJR0Z1WkNBb2JXRjRMWGRwWkhSb09pQTFNREJ3ZUNrZ2UxeHVJQ0F1WVd4bmIyeHBZUzFoZFhSdlkyOXRjR3hsZEdVZ0xtRnNaMjlzYVdFdFpHOWpjMlZoY21Ob0xYTjFaMmRsYzNScGIyNHRMV052Ym5SbGJuUTZZbVZtYjNKbElIdGNiaUFnSUNCa2FYTndiR0Y1T2lCdWIyNWxPMXh1SUNCOVhHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVZV3huYjJ4cFlTMWtiMk56WldGeVkyZ3RjM1ZuWjJWemRHbHZiaTB0WTJGMFpXZHZjbmt0YUdWaFpHVnlJSHRjYmlBZ2NHOXphWFJwYjI0NklISmxiR0YwYVhabE8xeHVJQ0JpYjNKa1pYSXRZbTkwZEc5dE9pQXhjSGdnYzI5c2FXUWdJMlJrWkR0Y2JpQWdaR2x6Y0d4aGVUb2dibTl1WlR0Y2JpQWdiV0Z5WjJsdUxYUnZjRG9nT0hCNE8xeHVJQ0J3WVdSa2FXNW5PaUEwY0hnZ01EdGNiaUFnWm05dWRDMXphWHBsT2lBeFpXMDdYRzRnSUdOdmJHOXlPaUFqTXpNek5qTkVPMXh1ZlZ4dVhHNHVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1VnTG1Gc1oyOXNhV0V0Wkc5amMyVmhjbU5vTFhOMVoyZGxjM1JwYjI0dExYZHlZWEJ3WlhJZ2UxeHVJQ0IzYVdSMGFEb2dNVEF3SlR0Y2JpQWdabXh2WVhRNklHeGxablE3WEc0Z0lIQmhaR1JwYm1jNklEaHdlQ0F3SURBZ01EdGNibjFjYmx4dUxtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1TFMxemRXSmpZWFJsWjI5eWVTMWpiMngxYlc0Z2UxeHVJQ0JtYkc5aGREb2diR1ZtZER0Y2JpQWdkMmxrZEdnNklETXdKVHRjYmlBZ1pHbHpjR3hoZVRvZ2JtOXVaVHRjYmlBZ2NHRmtaR2x1Wnkxc1pXWjBPaUF3TzF4dUlDQjBaWGgwTFdGc2FXZHVPaUJ5YVdkb2REdGNiaUFnY0c5emFYUnBiMjQ2SUhKbGJHRjBhWFpsTzF4dUlDQndZV1JrYVc1bk9pQTFMak16TXpNemNIZ2dNVEF1TmpZMk5qZHdlRHRjYmlBZ1kyOXNiM0k2SUNNMk56ZGpPR0U3WEc0Z0lHWnZiblF0YzJsNlpUb2dNQzQ1WlcwN1hHNGdJSGR2Y21RdGQzSmhjRG9nWW5KbFlXc3RkMjl5WkR0Y2JuMWNibHh1TG1Gc1oyOXNhV0V0WVhWMGIyTnZiWEJzWlhSbElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMUzF6ZFdKallYUmxaMjl5ZVMxamIyeDFiVzQ2WW1WbWIzSmxJSHRjYmlBZ1kyOXVkR1Z1ZERvZ0p5YzdYRzRnSUhCdmMybDBhVzl1T2lCaFluTnZiSFYwWlR0Y2JpQWdaR2x6Y0d4aGVUb2dZbXh2WTJzN1hHNGdJSFJ2Y0RvZ01EdGNiaUFnYUdWcFoyaDBPaUF4TURBbE8xeHVJQ0IzYVdSMGFEb2dNWEI0TzF4dUlDQmlZV05yWjNKdmRXNWtPaUFqWkdSa08xeHVJQ0J5YVdkb2REb2dNRHRjYm4xY2JseHVRRzFsWkdsaElITmpjbVZsYmlCaGJtUWdLRzFoZUMxM2FXUjBhRG9nTlRBd2NIZ3BJSHRjYmlBZ0xtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1TFMxemRXSmpZWFJsWjI5eWVTMWpiMngxYlc0NlltVm1iM0psSUh0Y2JpQWdJQ0JrYVhOd2JHRjVPaUJ1YjI1bE8xeHVJQ0I5WEc1OVhHNWNiaTVoYkdkdmJHbGhMV0YxZEc5amIyMXdiR1YwWlNBdVlXeG5iMnhwWVMxa2IyTnpaV0Z5WTJndGMzVm5aMlZ6ZEdsdmJpMHRjM1ZpWTJGMFpXZHZjbmt0WTI5c2RXMXVJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1TFMxb2FXZG9iR2xuYUhRZ2UxeHVJQ0JpWVdOclozSnZkVzVrTFdOdmJHOXlPaUJwYm1obGNtbDBPMXh1SUNCamIyeHZjam9nYVc1b1pYSnBkRHRjYm4xY2JseHVMbUZzWjI5c2FXRXRZWFYwYjJOdmJYQnNaWFJsSUM1aGJHZHZiR2xoTFdSdlkzTmxZWEpqYUMxemRXZG5aWE4wYVc5dUxTMXpkV0pqWVhSbFoyOXllUzFwYm14cGJtVWdlMXh1SUNCa2FYTndiR0Y1T2lCdWIyNWxPMXh1ZlZ4dVhHNHVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1VnTG1Gc1oyOXNhV0V0Wkc5amMyVmhjbU5vTFhOMVoyZGxjM1JwYjI0dExYUnBkR3hsSUh0Y2JpQWdiV0Z5WjJsdUxXSnZkSFJ2YlRvZ05IQjRPMXh1SUNCamIyeHZjam9nSXpBd01EdGNiaUFnWm05dWRDMXphWHBsT2lBd0xqbGxiVHRjYmlBZ1ptOXVkQzEzWldsbmFIUTZJR0p2YkdRN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVZV3huYjJ4cFlTMWtiMk56WldGeVkyZ3RjM1ZuWjJWemRHbHZiaTB0ZEdWNGRDQjdYRzRnSUdScGMzQnNZWGs2SUdKc2IyTnJPMXh1SUNCc2FXNWxMV2hsYVdkb2REb2dNUzR5WlcwN1hHNGdJR1p2Ym5RdGMybDZaVG9nTUM0NE5XVnRPMXh1SUNCamIyeHZjam9nSXpCbE16ZzBaanRjYm4xY2JseHVMbUZzWjI5c2FXRXRZWFYwYjJOdmJYQnNaWFJsSUM1aGJHZHZiR2xoTFdSdlkzTmxZWEpqYUMxemRXZG5aWE4wYVc5dUxTMXVieTF5WlhOMWJIUnpJSHRjYmlBZ2QybGtkR2c2SURFd01DVTdYRzRnSUhCaFpHUnBibWM2SURod2VDQXdPMXh1SUNCMFpYaDBMV0ZzYVdkdU9pQmpaVzUwWlhJN1hHNGdJR1p2Ym5RdGMybDZaVG9nTVM0eVpXMDdYRzU5WEc1Y2JpNWhiR2R2YkdsaExXRjFkRzlqYjIxd2JHVjBaU0F1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmkwdGJtOHRjbVZ6ZFd4MGN6bzZZbVZtYjNKbElIdGNiaUFnWkdsemNHeGhlVG9nYm05dVpUdGNibjFjYmx4dUxtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1SUdOdlpHVWdlMXh1SUNCd1lXUmthVzVuT2lBeGNIZ2dOWEI0TzF4dUlDQm1iMjUwTFhOcGVtVTZJRGt3SlR0Y2JpQWdZbTl5WkdWeU9pQnViMjVsTzF4dUlDQmpiMnh2Y2pvZ0l6SXlNakl5TWp0Y2JpQWdZbUZqYTJkeWIzVnVaQzFqYjJ4dmNqb2dJMFZDUlVKRlFqdGNiaUFnWW05eVpHVnlMWEpoWkdsMWN6b2dNM0I0TzF4dUlDQm1iMjUwTFdaaGJXbHNlVG9nVFdWdWJHOHNUVzl1WVdOdkxFTnZibk52YkdGekxGd2lRMjkxY21sbGNpQk9aWGRjSWl4dGIyNXZjM0JoWTJVN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVZV3huYjJ4cFlTMWtiMk56WldGeVkyZ3RjM1ZuWjJWemRHbHZiaUJqYjJSbElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMUzFvYVdkb2JHbG5hSFFnZTF4dUlDQmlZV05yWjNKdmRXNWtPaUJ1YjI1bE8xeHVmVnh1WEc0dVlXeG5iMnhwWVMxaGRYUnZZMjl0Y0d4bGRHVWdMbUZzWjI5c2FXRXRaRzlqYzJWaGNtTm9MWE4xWjJkbGMzUnBiMjR1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2Ymw5ZmJXRnBiaUF1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmkwdFkyRjBaV2R2Y25rdGFHVmhaR1Z5SUh0Y2JpQWdaR2x6Y0d4aGVUb2dZbXh2WTJzN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVZV3huYjJ4cFlTMWtiMk56WldGeVkyZ3RjM1ZuWjJWemRHbHZiaTVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1WDE5elpXTnZibVJoY25rZ0xtRnNaMjlzYVdFdFpHOWpjMlZoY21Ob0xYTjFaMmRsYzNScGIyNHRMWE4xWW1OaGRHVm5iM0o1TFdOdmJIVnRiaUI3WEc0Z0lHUnBjM0JzWVhrNklHSnNiMk5yTzF4dWZWeHVYRzVBYldWa2FXRWdjMk55WldWdUlHRnVaQ0FvYldGNExYZHBaSFJvT2lBMU1EQndlQ2tnZTF4dUlDQXVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1VnTG1Gc1oyOXNhV0V0Wkc5amMyVmhjbU5vTFhOMVoyZGxjM1JwYjI0dVlXeG5iMnhwWVMxa2IyTnpaV0Z5WTJndGMzVm5aMlZ6ZEdsdmJsOWZjMlZqYjI1a1lYSjVJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1TFMxemRXSmpZWFJsWjI5eWVTMWpiMngxYlc0Z2UxeHVJQ0FnSUdScGMzQnNZWGs2SUc1dmJtVTdYRzRnSUgxY2JuMWNibHh1TG1Gc1oyOXNhV0V0WVhWMGIyTnZiWEJzWlhSbElDNXpkV2RuWlhOMGFXOXVMV3hoZVc5MWRDMXphVzF3YkdVdVlXeG5iMnhwWVMxa2IyTnpaV0Z5WTJndGMzVm5aMlZ6ZEdsdmJpQjdYRzRnSUdKdmNtUmxjaTFpYjNSMGIyMDZJSE52Ykdsa0lERndlQ0FqWldWbE8xeHVJQ0J3WVdSa2FXNW5PaUE0Y0hnN1hHNGdJRzFoY21kcGJqb2dNRHRjYm4xY2JseHVMbUZzWjI5c2FXRXRZWFYwYjJOdmJYQnNaWFJsSUM1emRXZG5aWE4wYVc5dUxXeGhlVzkxZEMxemFXMXdiR1VnTG1Gc1oyOXNhV0V0Wkc5amMyVmhjbU5vTFhOMVoyZGxjM1JwYjI0dExXTnZiblJsYm5RZ2UxeHVJQ0IzYVdSMGFEb2dNVEF3SlR0Y2JpQWdjR0ZrWkdsdVp6b2dNRHRjYm4xY2JseHVMbUZzWjI5c2FXRXRZWFYwYjJOdmJYQnNaWFJsSUM1emRXZG5aWE4wYVc5dUxXeGhlVzkxZEMxemFXMXdiR1VnTG1Gc1oyOXNhV0V0Wkc5amMyVmhjbU5vTFhOMVoyZGxjM1JwYjI0dExXTnZiblJsYm5RNk9tSmxabTl5WlNCN1hHNGdJR1JwYzNCc1lYazZJRzV2Ym1VN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVjM1ZuWjJWemRHbHZiaTFzWVhsdmRYUXRjMmx0Y0d4bElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMUzFqWVhSbFoyOXllUzFvWldGa1pYSWdlMXh1SUNCdFlYSm5hVzQ2SURBN1hHNGdJSEJoWkdScGJtYzZJREE3WEc0Z0lHUnBjM0JzWVhrNklHSnNiMk5yTzF4dUlDQjNhV1IwYURvZ01UQXdKVHRjYmlBZ1ltOXlaR1Z5T2lCdWIyNWxPMXh1ZlZ4dVhHNHVZV3huYjJ4cFlTMWhkWFJ2WTI5dGNHeGxkR1VnTG5OMVoyZGxjM1JwYjI0dGJHRjViM1YwTFhOcGJYQnNaU0F1WVd4bmIyeHBZUzFrYjJOelpXRnlZMmd0YzNWbloyVnpkR2x2YmkwdFkyRjBaV2R2Y25rdGFHVmhaR1Z5TFd4MmJEQWdlMXh1SUNCdmNHRmphWFI1T2lBdU5qdGNiaUFnWm05dWRDMXphWHBsT2lBd0xqZzFaVzA3WEc1OVhHNWNiaTVoYkdkdmJHbGhMV0YxZEc5amIyMXdiR1YwWlNBdWMzVm5aMlZ6ZEdsdmJpMXNZWGx2ZFhRdGMybHRjR3hsSUM1aGJHZHZiR2xoTFdSdlkzTmxZWEpqYUMxemRXZG5aWE4wYVc5dUxTMWpZWFJsWjI5eWVTMW9aV0ZrWlhJdGJIWnNNU0I3WEc0Z0lHOXdZV05wZEhrNklDNDJPMXh1SUNCbWIyNTBMWE5wZW1VNklEQXVPRFZsYlR0Y2JuMWNibHh1TG1Gc1oyOXNhV0V0WVhWMGIyTnZiWEJzWlhSbElDNXpkV2RuWlhOMGFXOXVMV3hoZVc5MWRDMXphVzF3YkdVZ0xtRnNaMjlzYVdFdFpHOWpjMlZoY21Ob0xYTjFaMmRsYzNScGIyNHRMV05oZEdWbmIzSjVMV2hsWVdSbGNpMXNkbXd4T2pwaVpXWnZjbVVnZTF4dUlDQmlZV05yWjNKdmRXNWtMV2x0WVdkbE9pQjFjbXdvSjJSaGRHRTZhVzFoWjJVdmMzWm5LM2h0YkR0MWRHWTRMRHh6ZG1jZ2QybGtkR2c5WENJeE1Gd2lJR2hsYVdkb2REMWNJakV3WENJZ2RtbGxkMEp2ZUQxY0lqQWdNQ0F5TUNBek9Gd2lJSGh0Ykc1elBWd2lhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtZGNJajQ4Y0dGMGFDQmtQVndpVFRFdU5Ea2dOQzR6TVd3eE5DQXhOaTR4TWpZdU1EQXlMVEl1TmpJMExURTBJREUyTGpBM05DMHhMak14TkNBeExqVXhJRE11TURFM0lESXVOakkySURFdU16RXpMVEV1TlRBNElERTBMVEUyTGpBM05TQXhMakUwTWkweExqTXhNeTB4TGpFMExURXVNekV6TFRFMExURTJMakV5TlV3ekxqSXVNVGd1TVRnZ01pNDRiREV1TXpFZ01TNDFNWHBjSWlCbWFXeHNMWEoxYkdVOVhDSmxkbVZ1YjJSa1hDSWdabWxzYkQxY0lpVXlNekZFTXpZMU4xd2lJQzgrUEM5emRtYytKeWs3WEc0Z0lHTnZiblJsYm5RNklDY25PMXh1SUNCM2FXUjBhRG9nTVRCd2VEdGNiaUFnYUdWcFoyaDBPaUF4TUhCNE8xeHVJQ0JrYVhOd2JHRjVPaUJwYm14cGJtVXRZbXh2WTJzN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVjM1ZuWjJWemRHbHZiaTFzWVhsdmRYUXRjMmx0Y0d4bElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMUzEzY21Gd2NHVnlJSHRjYmlBZ2QybGtkR2c2SURFd01DVTdYRzRnSUdac2IyRjBPaUJzWldaME8xeHVJQ0J0WVhKbmFXNDZJREE3WEc0Z0lIQmhaR1JwYm1jNklEQTdYRzU5WEc1Y2JpNWhiR2R2YkdsaExXRjFkRzlqYjIxd2JHVjBaU0F1YzNWbloyVnpkR2x2Ymkxc1lYbHZkWFF0YzJsdGNHeGxJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1TFMxemRXSmpZWFJsWjI5eWVTMWpiMngxYlc0c0lDNWhiR2R2YkdsaExXRjFkRzlqYjIxd2JHVjBaU0F1YzNWbloyVnpkR2x2Ymkxc1lYbHZkWFF0YzJsdGNHeGxJQzVoYkdkdmJHbGhMV1J2WTNObFlYSmphQzF6ZFdkblpYTjBhVzl1TFMxa2RYQnNhV05oZEdVdFkyOXVkR1Z1ZEN3Z0xtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzV6ZFdkblpYTjBhVzl1TFd4aGVXOTFkQzF6YVcxd2JHVWdMbUZzWjI5c2FXRXRaRzlqYzJWaGNtTm9MWE4xWjJkbGMzUnBiMjR0TFhOMVltTmhkR1ZuYjNKNUxXbHViR2x1WlNCN1hHNGdJR1JwYzNCc1lYazZJRzV2Ym1VZ0lXbHRjRzl5ZEdGdWREdGNibjFjYmx4dUxtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzV6ZFdkblpYTjBhVzl1TFd4aGVXOTFkQzF6YVcxd2JHVWdMbUZzWjI5c2FXRXRaRzlqYzJWaGNtTm9MWE4xWjJkbGMzUnBiMjR0TFhScGRHeGxJSHRjYmlBZ2JXRnlaMmx1T2lBd08xeHVJQ0JqYjJ4dmNqb2dJekUwT1RGbE56dGNiaUFnWm05dWRDMXphWHBsT2lBd0xqbGxiVHRjYmlBZ1ptOXVkQzEzWldsbmFIUTZJRzV2Y20xaGJEdGNibjFjYmx4dUxtRnNaMjlzYVdFdFlYVjBiMk52YlhCc1pYUmxJQzV6ZFdkblpYTjBhVzl1TFd4aGVXOTFkQzF6YVcxd2JHVWdMbUZzWjI5c2FXRXRaRzlqYzJWaGNtTm9MWE4xWjJkbGMzUnBiMjR0TFhScGRHeGxPanBpWldadmNtVWdlMXh1SUNCamIyNTBaVzUwT2lCY0lpTmNJanRjYmlBZ1ptOXVkQzEzWldsbmFIUTZJR0p2YkdRN1hHNGdJR052Ykc5eU9pQWpNVFE1TVdVM08xeHVJQ0JrYVhOd2JHRjVPaUJwYm14cGJtVXRZbXh2WTJzN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVjM1ZuWjJWemRHbHZiaTFzWVhsdmRYUXRjMmx0Y0d4bElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMUzEwWlhoMElIdGNiaUFnYldGeVoybHVPaUEwY0hnZ01DQXdPMXh1SUNCa2FYTndiR0Y1T2lCaWJHOWphenRjYmlBZ2JHbHVaUzFvWldsbmFIUTZJREV1TkdWdE8xeHVJQ0J3WVdSa2FXNW5PaUExTGpNek16TXpjSGdnT0hCNE8xeHVJQ0JpWVdOclozSnZkVzVrT2lBalpqaG1PR1k0TzF4dUlDQm1iMjUwTFhOcGVtVTZJREF1T0RWbGJUdGNiaUFnYjNCaFkybDBlVG9nTGpnN1hHNTlYRzVjYmk1aGJHZHZiR2xoTFdGMWRHOWpiMjF3YkdWMFpTQXVjM1ZuWjJWemRHbHZiaTFzWVhsdmRYUXRjMmx0Y0d4bElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMUzEwWlhoMElDNWhiR2R2YkdsaExXUnZZM05sWVhKamFDMXpkV2RuWlhOMGFXOXVMUzFvYVdkb2JHbG5hSFFnZTF4dUlDQmpiMnh2Y2pvZ0l6QXlNR0V3WlR0Y2JpQWdabTl1ZEMxM1pXbG5hSFE2SUdKdmJHUTdYRzRnSUdKdmVDMXphR0ZrYjNjNklHNXZibVU3WEc1OVhHNWNiaTVoYkdkdmJHbGhMV0YxZEc5amIyMXdiR1YwWlNBdVlXeG5iMnhwWVMxa2IyTnpaV0Z5WTJndFptOXZkR1Z5SUh0Y2JpQWdkMmxrZEdnNklERXdNSEI0TzF4dUlDQm9aV2xuYUhRNklESXdjSGc3WEc0Z0lIb3RhVzVrWlhnNklESXdNREE3WEc0Z0lHMWhjbWRwYmkxMGIzQTZJREV3TGpZMk5qWTNjSGc3WEc0Z0lHWnNiMkYwT2lCeWFXZG9kRHRjYmlBZ1ptOXVkQzF6YVhwbE9pQXdPMXh1SUNCc2FXNWxMV2hsYVdkb2REb2dNRHRjYm4xY2JseHVMbUZzWjI5c2FXRXRZWFYwYjJOdmJYQnNaWFJsSUM1aGJHZHZiR2xoTFdSdlkzTmxZWEpqYUMxbWIyOTBaWEl0TFd4dloyOGdlMXh1SUNCaVlXTnJaM0p2ZFc1a0xXbHRZV2RsT2lCMWNtd29KMlJoZEdFNmFXMWhaMlV2YzNabkszaHRiRHQxZEdZNExEeHpkbWNnZDJsa2RHZzlYQ0k0Tmx3aUlHaGxhV2RvZEQxY0lqRXpYQ0lnZG1sbGQwSnZlRDFjSWpBZ01DQTROaUF4TTF3aUlIaHRiRzV6UFZ3aWFIUjBjRG92TDNkM2R5NTNNeTV2Y21jdk1qQXdNQzl6ZG1kY0lqNDhaeUJtYVd4c1BWd2libTl1WlZ3aUlHWnBiR3d0Y25Wc1pUMWNJbVYyWlc1dlpHUmNJajQ4Y0dGMGFDQmtQVndpVFRVdU1ERWdPQzR4WXpBZ0xqWXpMUzR5TWpnZ01TNHhNaTB1TmpnMElERXVORGN0TGpRMU5pNHpOVEl0TVM0d056UXVOVEk0TFRFdU9EVTFMalV5T0MwdU9EUWdNQzB4TGpRNUxTNHhNUzB4TGprMUxTNHpNamQyTFM0NFl5NHpMakV6TGpZeExqSXlMamsyTGpNdU16VXVNRGN1TmprdU1URWdNUzR3TXk0eE1TNDFOaUF3SUM0NU55MHVNU0F4TGpJMUxTNHpNWE11TkRJdExqVXVOREl0TGpnM1l6QXRMakkxTFM0d05TMHVORFV0TGpFMUxTNDJNUzB1TURrdExqRTFMUzR5TmkwdU15MHVORGt0TGpRekxTNHlOQzB1TVRRdExqVTVMUzR5T1MweExqQTNMUzQwTmkwdU5qWXRMakl6TFRFdU1UTXRMalV5TFRFdU5ESXRMamcwUXk0M05pQTFMalV5TGpZeElEVXVNUzQyTVNBMExqVTNZekF0TGpVMUxqSXhMUzQ1T0RVdU5qSXRNUzR6TVM0ME1pMHVNekkxTGprMkxTNDBPRGdnTVM0Mk5DMHVORGc0TGpjeElEQWdNUzR6Tnk0eE15QXhMamsyTGpNNWJDMHVNall1TnpKakxTNDFPUzB1TWpVdE1TNHhOaTB1TXpjdE1TNDNNUzB1TXpjdExqUTBJREF0TGpjNExqQTVMVEV1TURNdU1qZ3RMakkxTGpFNUxTNHpOeTQwTlMwdU16Y3VOemdnTUNBdU1qUXVNRFV1TkRVdU1UUXVOaTR3T1M0eE5pNHlORFl1TXk0ME5pNDBNeTR5TWk0eE15NDFOUzR5TnpjdU9UazJMalF6Tnk0M05TNHlOaUF4TGpJMk5DNDFOU0F4TGpVME5TNDROaTR5T1M0ekxqUXpMamN1TkRNZ01TNHhPWHB0TXk0MU9UZ2dNbU10TGpjNUlEQXRNUzQwTVRVdExqSTBMVEV1T0RjeUxTNDNNak10TGpRMU9DMHVORGd5TFM0Mk9EWXRNUzR4TlMwdU5qZzJMVEl1TURBM0lEQXRMamcyTWk0eU1USXRNUzQxTkRndU5qTTNMVEl1TURVMkxqUXlOUzB1TlRBM0xqazVOUzB1TnpZZ01TNDNNUzB1TnpZdU5qY3lJREFnTVM0eU1ETXVNaklnTVM0MU9UTXVOall1TXprdU5EUXVOVGcySURFdU1ESXpMalU0TmlBeExqYzBObll1TlRFelNEWXVPRGxqTGpBeE5pNDJNeTR4TnpVZ01TNHhNRFl1TkRjMklERXVORE11TXk0ek1qY3VOekkxTGpRNUlERXVNamN5TGpRNUxqVTNOaUF3SURFdU1UUTJMUzR4TWlBeExqY3hMUzR6TmpKMkxqY3pZeTB1TWpnNExqRXpMUzQxTmk0eU1pMHVPREUwTGpJM0xTNHlOVFl1TURZdExqVTJOQzR3T0MwdU9USTJMakE0ZW0wdExqSXlMVFF1T0RkakxTNDBNeUF3TFM0M056SXVNVFF0TVM0d01qY3VOREl0TGpJMUxqSTRMUzQwTGpZM0xTNDBOU0F4TGpFMk5HZ3lMamhqTUMwdU5URXlMUzR4TVMwdU9UQXpMUzR6TkMweExqRTNOUzB1TWpNdExqSTRMUzQxTlMwdU5ERXRMamszTFM0ME1YcE5NVFV1TWpVZ01UQnNMUzR4TmkwdU56WXlhQzB1TURSakxTNHlOall1TXpNMkxTNDFNekl1TlRZekxTNDNPVGd1TmpndExqSTJOUzR4TWkwdU5UazJMakU0TFM0NU9UTXVNVGd0TGpVeklEQXRMamsxTFM0eE16Y3RNUzR5TlMwdU5ERXRMak10TGpJM05DMHVORFV0TGpZMk15MHVORFV0TVM0eE5qY2dNQzB4TGpBNExqZzJMVEV1TmpRZ01pNDFPUzB4TGpkc0xqa3hMUzR3TW5ZdExqTTBZekF0TGpReUxTNHdPUzB1TnpNdExqSTNMUzQ1TXkwdU1UZ3RMakl0TGpRM0xTNHpMUzQ0TnkwdU15MHVORFVnTUMwdU9UVXVNVFF0TVM0MU1pNDBNV3d0TGpJMUxTNDJNbU11TWpZdExqRTBMalUxTFM0eU5TNDROaTB1TXpNdU16RXRMakE0TGpZekxTNHhNaTQ1TkMwdU1USXVOak1nTUNBeExqRXhMakUxSURFdU5ERXVORE11TXk0eU9TNDBOaTQzTkM0ME5pQXhMak0zVmpFd2FDMHVObnB0TFRFdU9ETXRMalUzWXk0MU1EVWdNQ0F1T1MwdU1UUWdNUzR4T1MwdU5ERTJMakk0TnkwdU1qYzNMalF6TFM0Mk5qUXVORE10TVM0eE5qSjJMUzQwT0RSc0xTNDRNUzR3TXpSakxTNDJORFF1TURJekxURXVNVEV1TVRJekxURXVNemswTGpNdExqSTROQzR4TnpndExqUXlOeTQwTlRRdExqUXlOeTQ0TWpnZ01DQXVNamt6TGpBNUxqVXhOaTR5Tmk0Mk55NHhPQzR4TlRJdU5ESXVNak11TnpRdU1qTjZiVFl1TlRRekxUUXVPRGhqTGpJMElEQWdMalExTWk0d01pNDJOQzR3Tm13dExqRXhNaTQzTldNdExqSXlMUzR3TkRndExqUXhMUzR3TnpJdExqVTRMUzR3TnpJdExqUXpJREF0TGpndU1UYzJMVEV1TVRFdU5USTNMUzR6TVM0ek5USXRMalEyTGpjNUxTNDBOaUF4TGpNeE5GWXhNR2d0TGpneFZqUXVOalE0YUM0Mk4yd3VNUzQ1T1RKb0xqQTBZeTR5TFM0ek5TNDBOQzB1TmpFM0xqY3lMUzQ0TURZdU1qZ3RMakU1TGpVNUxTNHlPRE11T1RNdExqSTRNM3B0TXk0MU9DQTFMalUwT0dNdExqYzNOU0F3TFRFdU16YzBMUzR5TkMweExqZ3RMamN4TmkwdU5ESTBMUzQwTnpjdExqWXpOaTB4TGpFMUxTNDJNell0TWk0d01qUWdNQzB1T0RrMUxqSXhOUzB4TGpVNE55NDJORFl0TWk0d056VXVORE15TFM0ME9EZ2dNUzR3TkRZdExqY3pNaUF4TGpnME5DMHVOek15TGpJMU55QXdJQzQxTVRRdU1ETXVOemN1TURrdU1qVTRMakEyTGpRMkxqRXlMall3Tnk0eWJDMHVNalE0TGpZNVl5MHVNVGd0TGpBM0xTNHpOelV0TGpFekxTNDFPRFl0TGpFNExTNHlNVEl0TGpBMExTNDBMUzR3TnkwdU5UWXlMUzR3TnkweExqQTROeUF3TFRFdU5qTXVOeTB4TGpZeklESXVNRGdnTUNBdU5qWXVNVE15SURFdU1UWXVNemszSURFdU5USXVNalkzTGpNMUxqWTJMalV6SURFdU1UZ3VOVE11TkRRMklEQWdMamt3TkMwdU1Ea2dNUzR6TnpNdExqSTRkaTQzTW1NdExqTTFPQzR4T1MwdU9ERXVNamd0TVM0ek5UTXVNamg2YlRZdU1qZ3RMakE1T0ZZMkxqVXpPR013TFM0ME16WXRMakV0TGpjMk1pMHVNamszTFM0NU56WXRMakU1T0MwdU1qRTFMUzQxTVMwdU16SXpMUzQ1TXpJdExqTXlNeTB1TlRZeklEQXRMamszTkM0eE5TMHhMakl6TXk0ME5TMHVNalV1TXkwdU16Z3VPREV0TGpNNElERXVOVll4TUdndExqZ3hWakl1TkRBeWFDNDRNWFl5TGpOak1DQXVNamMzTFM0d01TNDFNRFl0TGpBMExqWTVhQzR3TldNdU1UWXRMakkyTGpNNUxTNDBOaTQyT0MwdU5qRXVNeTB1TVRRM0xqWXpMUzR5TWlBeExqQXhMUzR5TWk0Mk5pQXdJREV1TVRVdU1UVTBJREV1TkRjdU5EWTFMak16TGpNeExqUTVMamd3TlM0ME9TQXhMalE0TWxZeE1HZ3RMamd4ZW0wM0xqVTJOUzAxTGpRMFl5NDNNRE1nTUNBeExqSTFMakkwSURFdU5qTTRMamN5TGpNNUxqUTRMalU0TXlBeExqRTJMalU0TXlBeUxqQTBJREFnTGpnM09DMHVNaUF4TGpVMkxTNDFPU0F5TGpBME9DMHVNemt1TkRnMkxTNDVOQzQzTXkweExqWTBMamN6TFM0ek5TQXdMUzQyTnkwdU1EWTFMUzQ1TmkwdU1Ua3pMUzR5T1MwdU1UTXRMalV6TFM0ek1qY3RMamN6TFM0MU9UTm9MUzR3Tm13dExqRTNMalk0T0dndExqVTRWakl1TkRBeWFDNDRNWFl4TGpnME5tTXdJQzQwTVRNdExqQXlMamM0TlMwdU1EUWdNUzR4TVROb0xqQTBZeTR6T0MwdU5UTXVPVFF0TGpnZ01TNDJPQzB1T0hwdExTNHhNVGN1TmpoakxTNDFOU0F3TFM0NU5TNHhOVGd0TVM0eE9TNDBOelV0TGpJMExqTXhPQzB1TXpZdU9EVXlMUzR6TmlBeExqWXdOQ0F3SUM0M05TNHhNeUF4TGpJNUxqTTRJREV1TmpFdU1qVXVNekl1TmpZdU5EZ2dNUzR5TVM0ME9DNDFJREFnTGpnM0xTNHhPU0F4TGpFeUxTNDFOUzR5TlMwdU16Y3VNemN0TGpnNUxqTTNMVEV1TlRjZ01DMHVOeTB1TVRJdE1TNHlNaTB1TXpZdE1TNDFOaTB1TWpRdExqTTFMUzQyTWkwdU5USXRNUzR4TXkwdU5USjZiVEl1TnpFdExqVTVNbWd1T0RkTU5ESXVNRElnTnk0M1l5NHlOVGN1TmprM0xqUXhOaUF4TGpJdU5EZ2dNUzQxTVdndU1ETTNZeTR3TkRJdExqRTJOeTR4TXkwdU5EVXVNalkyTFM0NE5UTXVNVE0yTFM0ME1ESXVOVGM0TFRFdU5qTTRJREV1TXpJM0xUTXVOekZJTkRWc0xUSXVNeUEyTGpBNU5XTXRMakl6TGpZd01pMHVORGsySURFdU1ETXRMamdnTVM0eU9ESXRMak13TkM0eU5USXRMalkzT0M0ek56Z3RNUzR4TWk0ek56Z3RMakkwT0NBd0xTNDBPUzB1TURJM0xTNDNNek10TGpBNE0zWXRMalkxWXk0eE9DNHdOQzR6T0M0d05pNDJMakEyTGpVMU55QXdJQzQ1TlRRdExqTXlJREV1TVRreUxTNDVOR3d1TWprdExqYzJMVEl1TVRZdE5TNHpPWHBjSWlCbWFXeHNQVndpSlRJek56azNPVGM1WENJdlBqeHdZWFJvSUdROVhDSk5OekF1T0RneklEUXVPR3d0TGpVMU15QXlMakEySURFdU56Z3lMVEV1TURJMVl5MHVNall0TGpVdExqY3RMamczTmkweExqSXpMVEV1TURNMGVrMDJOeTQzTmlBekxqUXhZeTB1TWpNM0xTNHlOUzB1TmpJMExTNHlOUzB1T0RZeUlEQnNMUzR4TURndU1URTBZeTB1TWpNNExqSTFNaTB1TWpNNExqWTJJREFnTGpreGJDNHhNVFV1TVRJeVl5NHlORFF0TGpReE5DNDFOVFl0TGpjNExqa3hPQzB4TGpBNGJDMHVNRFl5TFM0d05qWjZiVE11TnpNdExqWXpZekF0TGpBeE5DNHdNRE10TGpBeU55NHdNRE10TGpBME1uWXRMak15TW1Nd0xTNHpOVFV0TGpJM015MHVOalEwTFM0Mk1TMHVOalEwYUMweExqQTJPR010TGpNek55QXdMUzQyTVM0eU9EZ3RMall4TGpZME5IWXVNekUzWXk0ek5DMHVNUzQyT1RndExqRTFOaUF4TGpBMk9DMHVNVFUyTGpReU5DQXdJQzQ0TXpNdU1EY3lJREV1TWpFMkxqSXdNMXdpSUdacGJHdzlYQ0lsTWpNME5rRkZSRUZjSWk4K1BIQmhkR2dnWkQxY0lrMDNNQzR6TVRZZ05DNHlORE5qTVM0ek5DQXdJREl1TkRJNElERXVNVFExSURJdU5ESTRJREl1TlRVeUlEQWdNUzQwTURndE1TNHdPU0F5TGpVMU15MHlMalF5T0NBeUxqVTFNeTB4TGpNMElEQXRNaTQwTWpndE1TNHhORFV0TWk0ME1qZ3RNaTQxTlRNZ01DMHhMalF3TnlBeExqQTVMVEl1TlRVeUlESXVOREk0TFRJdU5UVXliUzB6TGpRZ01pNDFOVEpqTUNBeExqazNOQ0F4TGpVeU1pQXpMalUzTkNBekxqUWdNeTQxTnpSek15NDBMVEV1TmlBekxqUXRNeTQxT0MweExqVXlNaTB6TGpVNExUTXVOQzB6TGpVNExUTXVOQ0F4TGpZdE15NDBJRE11TlRkNlhDSWdabWxzYkQxY0lpVXlNelEyUVVWRVFWd2lMejQ4Y0dGMGFDQmtQVndpVFRVMExqYzFPQ0F4TUM0eE56VmpMUzR4TkMwdU16a3RMakkzTFM0M056WXRMak01TlMweExqRTFOUzB1TVRJMExTNHpOemd0TGpJMUxTNDNOak10TGpNNE15MHhMakUxTkVnMU1DNHhiQzB1TnpnZ01pNHpNV2d0TVM0eU5EZGpMak16TFM0NU5UZ3VOalF0TVM0NE5ETXVPVEkzTFRJdU5qVTJMakk0T0MwdU9ERXpMalUzTFRFdU5UZzFMamcwTnkweUxqTXhOaTR5TnpVdExqY3pMalUxTFRFdU5ETXVPREl0TWk0d09UUXVNamN6TFM0Mk5qWXVOVFUzTFRFdU16STBMamcxTkMweExqazNOMmd4TGpGakxqTXVOalV6TGpVNElERXVNekV1T0RZZ01TNDVOemN1TWpjdU5qWTFMalUxSURFdU16WXpMamd5SURJdU1EazBMakk0TGpjekxqVTJJREV1TlRBekxqZzFJREl1TXpFMkxqSTVMamd4TXk0MklERXVOams0TGpreklESXVOalUxYUMweExqTXhlbTB0TVM0eE1qUXRNeTR6TlROakxTNHlOalF0TGpjMU55MHVOVEkxTFRFdU5Ea3RMamM0TlMweUxqSXRMakkyTFM0M01EZ3RMalV6TFRFdU16ZzRMUzQ0TVMweUxqQTBMUzR5T1M0Mk5USXRMalUzSURFdU16TXlMUzQ0TXlBeUxqQTBMUzR5Tmk0M01TMHVOVElnTVM0ME5ETXRMamMzSURJdU1tZ3pMakU1ZW0wMUxqUTFJRE11TkRnell5MHVOekV0TGpBeE55MHhMakl4TFM0eE56Z3RNUzQxTURndExqUTRNaTB1TWprM0xTNHpNRFV0TGpRME5TMHVOemd0TGpRME5TMHhMalF5TTFZdU1qWnNNUzR4TlMwdU1qRjJPQzR4TlRWak1DQXVNaTR3TWk0ek5qVXVNRFV1TkRrMkxqQTBMakV6TGpBNUxqSTBMakUyTGpNeUxqQTRMakE0TGpFNExqRTBMak11TVRndU1UTXVNRFF1TWpndU1EZ3VORFl1TVd3dExqRTJJREV1TURKdE5TNDBOeTB1T0RGakxTNHhMakEzTFM0eU9TNHhOaTB1TlRjdU1qY3RMakk0TGpFeExTNDJNUzR4TnkwdU9Ua3VNVGR6TFM0M05TMHVNRFl0TVM0d09TMHVNVGxqTFM0ek5DMHVNVE10TGpZMExTNHpNeTB1T0RrdExqWXRMakkyTFM0eU55MHVORFl0TGpZeExTNDJNUzB4TGpBeUxTNHhOUzB1TkMwdU1qSXRMamc1TFM0eU1pMHhMalEySURBdExqUTVMakEzTFM0NU5TNHlNUzB4TGpNMkxqRTBMUzQwTVM0ek5TMHVOemN1TmpFdE1TNHdOeTR5TnkwdU15NDJMUzQxTXk0NU9TMHVOeTR6T1MwdU1UY3VPRE10TGpJMUlERXVNekV0TGpJMUxqVTBJREFnTVM0d01TNHdOQ0F4TGpReExqRXlMalF1TURrdU56TXVNVFlnTVM0d01TNHlNMVk1TGpkak1DQXhMakEwTkMwdU1qVWdNUzQ0TFM0M05pQXlMakkzY3kweExqSTVMamN3TlMweUxqTXpMamN3TldNdExqUWdNQzB1TnpndExqQXpOUzB4TGpFMExTNHhNRFF0TGpNMkxTNHdOeTB1TmpjdExqRTFMUzQ1TXkwdU1qUnNMakl4TFRFdU1EWmpMakl6TGpBNUxqVXhMakU0TGpnMExqSTFMak0wTGpBNExqWTRMakV4SURFdU1EVXVNVEV1TmprZ01DQXhMakU0TFM0eE5DQXhMalE0TFM0ME15NHpMUzR5T0M0ME5TMHVOelF1TkRVdE1TNHpOM1l0TGpONmJTMHVORGN0TlM0eE0yTXRMakU1TFM0d015MHVORFV0TGpBMExTNDNPQzB1TURRdExqWXhJREF0TVM0d09TNHlNaTB4TGpReUxqWTBMUzR6TXk0ME15MHVOUzQ1T1MwdU5TQXhMamNnTUNBdU16a3VNRFV1TnpNdU1UUWdNUzR3TVM0eExqSTRMakl6TGpVeExqTTVMalk1TGpFMkxqRTRMak0xTGpNeUxqVTJMalF4TGpJeExqQTVMalF6TGpFekxqWTFMakV6TGpNeElEQWdMalU1TFM0d05DNDROQzB1TVRNdU1qWXRMakE1TGpRMkxTNHlMall4TFM0ek1uWXROR010TGpFeExTNHdNeTB1TWpjdExqQTJMUzQwTmkwdU1EbDZiVEV5TGpjM0lEVXVPVFZqTFM0M0xTNHdNaTB4TGpJeExTNHhPQzB4TGpVdExqUTRMUzR5T1MwdU15MHVORFF0TGpjNExTNDBOQzB4TGpReVZpNHlObXd4TGpFMUxTNHlNWFk0TGpFMU5XTXdJQzR5TGpBeUxqTTJOUzR3TlM0ME9UWXVNRFF1TVRNdU1Ea3VNalF1TVRZdU16SXVNRGd1TURndU1UZ3VNVFF1TXk0eE9DNHhNeTR3TkM0eU9DNHdPQzQwTmk0eGJDMHVNVFlnTVM0d01tMHlMakF4TFRndU1UUmpMUzR5SURBdExqTTRMUzR3TnkwdU5USXRMakl4TFM0eE5DMHVNVFF0TGpJeExTNHpOQzB1TWpFdExqVTRJREF0TGpJMExqQTNMUzQwTkM0eU1pMHVOVGd1TVRRdExqRTBMak15TFM0eU1TNDFNaTB1TWpFdU1qRWdNQ0F1TXpndU1EZ3VOVE11TWpJdU1UVXVNVFV1TWpJdU16UXVNakl1TlRnZ01DQXVNalV0TGpBM0xqUTBMUzR5TVM0MU9DMHVNVFV1TVRVdExqTXlMakl5TFM0MU15NHlNbnB0TFM0MU55QXhMakl6YURFdU1UVjJOaTQzT1VnM09DNHpWak11TkhwdE5TNHlMUzR4TjJNdU5EWWdNQ0F1T0RVdU1EY2dNUzR4Tnk0eE9TNHpNaTR4TXk0MU55NHpNUzQzTnk0MU5DNHlMakl6TGpNekxqVXVOREl1T0RJdU1EZ3VNekl1TVRJdU5qY3VNVElnTVM0d05YWTBMakkwYkMwdU5ERXVNRGRqTFM0eE55NHdNeTB1TXpjdU1EWXRMall1TURrdExqSXlMakF6TFM0ME5pNHdOUzB1TnpJdU1EZ3RMakkyTGpBeUxTNDFNUzR3TXkwdU56Y3VNRE10TGpNMklEQXRMalk1TFM0d05DMHhMUzR4TWkwdU15MHVNRGd0TGpVMkxTNHlMUzQzT1MwdU16Y3RMakl5TFM0eE55MHVNemt0TGpNNUxTNDFNaTB1TmpkVE9ERWdPQzQxTnlBNE1TQTRMakU0WXpBdExqTTNMakEzTFM0Mk9USXVNakl0TGprMk1uTXVNelF0TGpRNUxqVTVMUzQyTlROakxqSTFMUzR4TmpRdU5UTXRMakk0Tmk0NE5pMHVNelkwTGpNekxTNHdPQzQyT0MwdU1USWdNUzR3TkMwdU1USXVNVElnTUNBdU1qUXVNREV1TXpZdU1ESXVNVE11TURJdU1qUXVNRE11TXpVdU1EVXVNVEl1TURNdU1qRXVNRFF1TXk0d05pNHdPRE11TURJdU1UUXVNRE11TVRjMExqQTBWalV1T1dNd0xTNHlMUzR3TWkwdU16azFMUzR3TnkwdU5Ua3RMakEwTFM0eE9UZ3RMakV4TkMwdU16Y3RMakl5TFM0MU1qUXRMakV3TnkwdU1UVXRMakkxTXkwdU1qY3pMUzQwTkMwdU16WTBMUzR4T0RNdExqQTVMUzQwTWpRdExqRXpMUzQzTWkwdU1UTXRMak00SURBdExqY3hMakF6TFM0NU9UWXVNRGt0TGpJNUxqQTFOUzB1TlM0eE1UUXRMalkwTGpFM05Xd3RMakUwTFRFdU1EQTNZeTR4TkRjdExqQTNMak01TlMwdU1UTXpMamMwTFM0eUxqTTBOaTB1TURZMExqY3lMUzR4SURFdU1USTFMUzR4ZW0wdU1TQTJMakE0WXk0eU55QXdJQzQxTWlBd0lDNDNNeTB1TURJdU1qRXRMakF4TGpNNUxTNHdNeTQxTXkwdU1EZFdOeTR4T1dNdExqQTRMUzR3TkRRdExqSXlMUzR3T0MwdU5DMHVNVEV0TGpFNExTNHdNeTB1TkRFdExqQTBOeTB1TmpjdExqQTBOeTB1TVRjZ01DMHVNell1TURJdExqVTFMakEwTFM0eE9TNHdNeTB1TXpjdU1EZ3RMalV6TGpFM0xTNHhOaTR3T0RVdExqSTVMakU1T0MwdU5DNHpOQzB1TVRFdU1UUTFMUzR4Tmk0ek15MHVNVFl1TlRjZ01DQXVORE0xTGpFekxqY3pOeTR6T1M0NU1EWXVNall1TVRjdU5qSXVNalVnTVM0d055NHlOWHBjSWlCbWFXeHNQVndpSlRJek1VUXpOalUzWENJdlBqd3ZaejQ4TDNOMlp6NG5LVHRjYmlBZ1ltRmphMmR5YjNWdVpDMXlaWEJsWVhRNklHNXZMWEpsY0dWaGREdGNiaUFnWW1GamEyZHliM1Z1WkMxd2IzTnBkR2x2YmpvZ1kyVnVkR1Z5TzF4dUlDQmlZV05yWjNKdmRXNWtMWE5wZW1VNklERXdNQ1U3WEc0Z0lHOTJaWEptYkc5M09pQm9hV1JrWlc0N1hHNGdJSFJsZUhRdGFXNWtaVzUwT2lBdE9UQXdNSEI0TzF4dUlDQndZV1JrYVc1bk9pQXdJQ0ZwYlhCdmNuUmhiblE3WEc0Z0lIZHBaSFJvT2lBeE1EQWxPMXh1SUNCb1pXbG5hSFE2SURFd01DVTdYRzRnSUdScGMzQnNZWGs2SUdKc2IyTnJPMXh1ZlZ4dUlsMTkgKi8iXX0= */ \ No newline at end of file diff --git a/source/assets/css/highlight-github-gist.min.css b/source/assets/css/highlight-github-gist.min.css new file mode 100644 index 0000000000..f17caac9a3 --- /dev/null +++ b/source/assets/css/highlight-github-gist.min.css @@ -0,0 +1 @@ +.hljs{display:block;background:white;padding:0.5em;color:#333333;overflow-x:auto}.hljs-comment,.hljs-meta{color:#969896}.hljs-string,.hljs-variable,.hljs-template-variable,.hljs-strong,.hljs-emphasis,.hljs-quote{color:#df5000}.hljs-keyword,.hljs-selector-tag,.hljs-type{color:#a71d5d}.hljs-literal,.hljs-symbol,.hljs-bullet,.hljs-attribute{color:#0086b3}.hljs-section,.hljs-name{color:#63a35c}.hljs-tag{color:#333333}.hljs-title,.hljs-attr,.hljs-selector-id,.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo{color:#795da3}.hljs-addition{color:#55a532;background-color:#eaffea}.hljs-deletion{color:#bd2c00;background-color:#ffecec}.hljs-link{text-decoration:underline} \ No newline at end of file diff --git a/source/assets/css/open-sans-font.css b/source/assets/css/open-sans-font.css new file mode 100644 index 0000000000..f766458267 --- /dev/null +++ b/source/assets/css/open-sans-font.css @@ -0,0 +1,27 @@ +/* open-sans-regular - latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans Regular'), local('OpenSans-Regular'), + url('../fonts/open-sans-v15-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../fonts/open-sans-v15-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* open-sans-600 - latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), + url('../fonts/open-sans-v15-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../fonts/open-sans-v15-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* open-sans-700 - latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), + url('../fonts/open-sans-v15-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../fonts/open-sans-v15-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} \ No newline at end of file diff --git a/source/assets/docsearch/README.md b/source/assets/docsearch/README.md index 000b869304..f55d63aa3f 100644 --- a/source/assets/docsearch/README.md +++ b/source/assets/docsearch/README.md @@ -1,8 +1,9 @@ -# Algoia DocSearch custom styling +# Algolia DocSearch custom styling This section of the devdocs contains the custom styling for the [Algolia Docsearch](https://github.com/algolia/docsearch). ## Installation -First make sure you have `sass` installed on your system. If you haven't installed it already, follow this [guide](http://sass-lang.com/install). +First make sure you have `sass` installed on your system. +If you haven't installed it already, follow this [guide](http://sass-lang.com/install). Next resolve the Node.js dependencies using the following command: @@ -19,4 +20,5 @@ The build script can be called using NPM: npm run build:css ``` -The compiled CSS file will be placed in the `source/assets/css` directory, so it will be automatically copied to the correct destination when compiling the devdocs using the provided commands / scripts. \ No newline at end of file +The compiled CSS file will be placed in the `source/assets/css` directory, +so it will be automatically copied to the correct destination when compiling the devdocs using the provided commands / scripts. diff --git a/source/assets/docsearch/build-css b/source/assets/docsearch/build-css index 7600a9224e..74bcc2ef5a 100755 --- a/source/assets/docsearch/build-css +++ b/source/assets/docsearch/build-css @@ -17,7 +17,6 @@ echo "$LICENSE" > "$DIST_FILE"; >> "$DIST_FILE" # ./dist/cdn/docsearch.min.css -./node_modules/.bin/postcss --use cssnano "$DIST_FILE" \ - >> "$DIST_FILE_MIN" +./node_modules/.bin/postcss "$PWD/$DIST_FILE" --use cssnano -o "$PWD/$DIST_FILE_MIN" echo "=> $DIST_FILE_MIN compiled" diff --git a/source/assets/docsearch/package-lock.json b/source/assets/docsearch/package-lock.json new file mode 100644 index 0000000000..e23809e366 --- /dev/null +++ b/source/assets/docsearch/package-lock.json @@ -0,0 +1,3692 @@ +{ + "name": "devdocs-docsearch", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devdocs-docsearch", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.18", + "cssnano": "^6.1.0", + "node-sass": "^9.0.0", + "postcss-cli": "^11.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==", + "engines": { + "node": "*" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001596", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz", + "integrity": "sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz", + "integrity": "sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.0.tgz", + "integrity": "sha512-e2v4w/t3OFM6HTuSweI4RSdABaqgVgHlJp5FZrQsopHnKKHLFIvK2D3C4kHWeFIycN/1L1J5VIrg5KlDzn3r/g==", + "dependencies": { + "cssnano-preset-default": "^6.1.0", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.0.tgz", + "integrity": "sha512-4DUXZoDj+PI3fRl3MqMjl9DwLGjcsFP4qt+92nLUcN1RGfw2TY+GwNoG2B38Usu1BrcTs8j9pxNfSusmvtSjfg==", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.1.1", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.4", + "postcss-merge-rules": "^6.1.0", + "postcss-minify-font-values": "^6.0.3", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.3", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.3" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.695", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.695.tgz", + "integrity": "sha512-eMijZmeqPtm774pCZIOrfUHMs/7ls++W1sLhxwqgu8KQ8E2WmMtzwyqOMt0XXUJ3HTIPfuwlfwF+I5cwnfItBA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globule": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", + "dependencies": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/globule/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globule/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp/node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/node-gyp/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/node-gyp/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/node-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/node-gyp/node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/node-gyp/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/node-gyp/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-gyp/node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/node-gyp/node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/node-sass": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-9.0.0.tgz", + "integrity": "sha512-yltEuuLrfH6M7Pq2gAj5B6Zm7m+gdZoG66wTqG6mIZV/zijq3M2OO2HswtT6oBspPyFhHDcaxWpsBm0fRNDHPg==", + "hasInstallScript": true, + "dependencies": { + "async-foreach": "^0.1.3", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "lodash": "^4.17.15", + "make-fetch-happen": "^10.0.4", + "meow": "^9.0.0", + "nan": "^2.17.0", + "node-gyp": "^8.4.1", + "sass-graph": "^4.0.1", + "stdout-stream": "^1.4.0", + "true-case-path": "^2.2.1" + }, + "bin": { + "node-sass": "bin/node-sass" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-cli": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-11.0.0.tgz", + "integrity": "sha512-xMITAI7M0u1yolVcXJ9XTZiO9aO49mcoKQy6pCDFdMh9kGqhzLVpWxeD/32M/QBmkhcGypZFFOLNLmIW4Pg4RA==", + "dependencies": { + "chokidar": "^3.3.0", + "dependency-graph": "^0.11.0", + "fs-extra": "^11.0.0", + "get-stdin": "^9.0.0", + "globby": "^14.0.0", + "picocolors": "^1.0.0", + "postcss-load-config": "^5.0.0", + "postcss-reporter": "^7.0.0", + "pretty-hrtime": "^1.0.3", + "read-cache": "^1.0.0", + "slash": "^5.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "postcss": "index.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-cli/node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-load-config": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.0.3.tgz", + "integrity": "sha512-90pBBI5apUVruIEdCxZic93Wm+i9fTrp7TXbgdUCH+/L+2WnfpITSpq5dFU/IPvbv7aNiMlQISpUkAm3fEcvgQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.4.tgz", + "integrity": "sha512-vAfWGcxUUGlFiPM3nDMZA+/Yo9sbpc3JNkcYZez8FfJDv41Dh7tAgA3QGVTocaHCZZL6aXPXPOaBMJsjujodsA==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.0.tgz", + "integrity": "sha512-lER+W3Gr6XOvxOYk1Vi/6UsAgKMg6MDBthmvbNqi2XxAk/r9XfhdYZSigfWjuWWn3zYw2wLelvtM8XuAEFqRkA==", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.3.tgz", + "integrity": "sha512-SmAeTA1We5rMnN3F8X9YBNo9bj9xB4KyDHnaNJnBfQIPi+60fNiR9OTRnIaMqkYzAQX0vObIw4Pn0vuKEOettg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.3.tgz", + "integrity": "sha512-IcV7ZQJcaXyhx4UBpWZMsinGs2NmiUC60rJSkyvjPCPqhNjVGsrJUM+QhAtCaikZ0w0/AbZuH4wVvF/YMuMhvA==", + "dependencies": { + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reporter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.1.0.tgz", + "integrity": "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "picocolors": "^1.0.0", + "thenby": "^1.3.4" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.3.tgz", + "integrity": "sha512-NFXbYr8qdmCr/AFceaEfdcsKGCvWTeGO6QVC9h2GvtWgj0/0dklKQcaMMVzs6tr8bY+ase8hOtHW8OBTTRvS8A==", + "dependencies": { + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, + "node_modules/sass-graph": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz", + "integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==", + "dependencies": { + "glob": "^7.0.0", + "lodash": "^4.17.11", + "scss-tokenizer": "^0.4.3", + "yargs": "^17.2.1" + }, + "bin": { + "sassgraph": "bin/sassgraph" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/scss-tokenizer": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", + "integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==", + "dependencies": { + "js-base64": "^2.4.9", + "source-map": "^0.7.3" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/stdout-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stdout-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/stdout-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylehacks": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.0.tgz", + "integrity": "sha512-ETErsPFgwlfYZ/CSjMO2Ddf+TsnkCVPBPaoB99Ro8WMAxf7cglzmFsRBhRmKObFjibtcvlNxFFPHuyr3sNlNUQ==", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/thenby": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", + "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/true-case-path": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-2.2.1.tgz", + "integrity": "sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==" + }, + "node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/source/assets/docsearch/package.json b/source/assets/docsearch/package.json index a8693cd524..8a933a6b62 100644 --- a/source/assets/docsearch/package.json +++ b/source/assets/docsearch/package.json @@ -1,31 +1,31 @@ { - "name": "devdocs-docsearch", - "private": true, - "version": "1.0.0", - "description": "Custom styling for the Algolia Docsearch", - "main": "index.js", - "scripts": { - "build:css": "./build-css" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/shopware/devdocs.git" - }, - "keywords": [ - "docsearch", - "algolia", - "shopware" - ], - "author": "klarstil", - "license": "MIT", - "bugs": { - "url": "https://github.com/shopware/devdocs/issues" - }, - "homepage": "https://github.com/shopware/devdocs#readme", - "dependencies": { - "autoprefixer": "^6.5.1", - "cssnano": "^3.8.0", - "node-sass": "^3.10.1", - "postcss-cli": "^2.6.0" - } + "name": "devdocs-docsearch", + "private": true, + "version": "1.0.0", + "description": "Custom styling for the Algolia Docsearch", + "main": "index.js", + "scripts": { + "build:css": "./build-css" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/shopware5/devdocs.git" + }, + "keywords": [ + "docsearch", + "algolia", + "shopware" + ], + "author": "klarstil", + "license": "MIT", + "bugs": { + "url": "https://github.com/shopware5/devdocs/issues" + }, + "homepage": "https://github.com/shopware5/devdocs#readme", + "dependencies": { + "autoprefixer": "^10.4.18", + "cssnano": "^6.1.0", + "node-sass": "^9.0.0", + "postcss-cli": "^11.0.0" + } } diff --git a/source/assets/favicons/android-icon-144x144.png b/source/assets/favicons/android-icon-144x144.png new file mode 100644 index 0000000000..04d700dbb0 Binary files /dev/null and b/source/assets/favicons/android-icon-144x144.png differ diff --git a/source/assets/favicons/android-icon-192x192.png b/source/assets/favicons/android-icon-192x192.png new file mode 100644 index 0000000000..fd9df72da3 Binary files /dev/null and b/source/assets/favicons/android-icon-192x192.png differ diff --git a/source/assets/favicons/android-icon-36x36.png b/source/assets/favicons/android-icon-36x36.png new file mode 100644 index 0000000000..6f4be288e1 Binary files /dev/null and b/source/assets/favicons/android-icon-36x36.png differ diff --git a/source/assets/favicons/android-icon-48x48.png b/source/assets/favicons/android-icon-48x48.png new file mode 100644 index 0000000000..36eab144c7 Binary files /dev/null and b/source/assets/favicons/android-icon-48x48.png differ diff --git a/source/assets/favicons/android-icon-72x72.png b/source/assets/favicons/android-icon-72x72.png new file mode 100644 index 0000000000..e49a642402 Binary files /dev/null and b/source/assets/favicons/android-icon-72x72.png differ diff --git a/source/assets/favicons/android-icon-96x96.png b/source/assets/favicons/android-icon-96x96.png new file mode 100644 index 0000000000..ea791f9f2a Binary files /dev/null and b/source/assets/favicons/android-icon-96x96.png differ diff --git a/source/assets/favicons/apple-icon-114x114.png b/source/assets/favicons/apple-icon-114x114.png new file mode 100644 index 0000000000..58b82c5875 Binary files /dev/null and b/source/assets/favicons/apple-icon-114x114.png differ diff --git a/source/assets/favicons/apple-icon-120x120.png b/source/assets/favicons/apple-icon-120x120.png new file mode 100644 index 0000000000..b810a03c6d Binary files /dev/null and b/source/assets/favicons/apple-icon-120x120.png differ diff --git a/source/assets/favicons/apple-icon-144x144.png b/source/assets/favicons/apple-icon-144x144.png new file mode 100644 index 0000000000..04d700dbb0 Binary files /dev/null and b/source/assets/favicons/apple-icon-144x144.png differ diff --git a/source/assets/favicons/apple-icon-152x152.png b/source/assets/favicons/apple-icon-152x152.png new file mode 100644 index 0000000000..50cb73661b Binary files /dev/null and b/source/assets/favicons/apple-icon-152x152.png differ diff --git a/source/assets/favicons/apple-icon-180x180.png b/source/assets/favicons/apple-icon-180x180.png new file mode 100644 index 0000000000..771bec620c Binary files /dev/null and b/source/assets/favicons/apple-icon-180x180.png differ diff --git a/source/assets/favicons/apple-icon-57x57.png b/source/assets/favicons/apple-icon-57x57.png new file mode 100644 index 0000000000..163d922c41 Binary files /dev/null and b/source/assets/favicons/apple-icon-57x57.png differ diff --git a/source/assets/favicons/apple-icon-60x60.png b/source/assets/favicons/apple-icon-60x60.png new file mode 100644 index 0000000000..fab6e5d64f Binary files /dev/null and b/source/assets/favicons/apple-icon-60x60.png differ diff --git a/source/assets/favicons/apple-icon-72x72.png b/source/assets/favicons/apple-icon-72x72.png new file mode 100644 index 0000000000..e49a642402 Binary files /dev/null and b/source/assets/favicons/apple-icon-72x72.png differ diff --git a/source/assets/favicons/apple-icon-76x76.png b/source/assets/favicons/apple-icon-76x76.png new file mode 100644 index 0000000000..0a18507238 Binary files /dev/null and b/source/assets/favicons/apple-icon-76x76.png differ diff --git a/source/assets/favicons/apple-icon-precomposed.png b/source/assets/favicons/apple-icon-precomposed.png new file mode 100644 index 0000000000..0fca5e76c5 Binary files /dev/null and b/source/assets/favicons/apple-icon-precomposed.png differ diff --git a/source/assets/favicons/apple-icon.png b/source/assets/favicons/apple-icon.png new file mode 100644 index 0000000000..0fca5e76c5 Binary files /dev/null and b/source/assets/favicons/apple-icon.png differ diff --git a/source/assets/favicons/favicon-16x16.png b/source/assets/favicons/favicon-16x16.png new file mode 100644 index 0000000000..74fb08e096 Binary files /dev/null and b/source/assets/favicons/favicon-16x16.png differ diff --git a/source/assets/favicons/favicon-32x32.png b/source/assets/favicons/favicon-32x32.png new file mode 100644 index 0000000000..25c32ccb9b Binary files /dev/null and b/source/assets/favicons/favicon-32x32.png differ diff --git a/source/assets/favicons/favicon-64x64.png b/source/assets/favicons/favicon-64x64.png new file mode 100644 index 0000000000..7c6c932db5 Binary files /dev/null and b/source/assets/favicons/favicon-64x64.png differ diff --git a/source/assets/favicons/favicon-72x72.png b/source/assets/favicons/favicon-72x72.png new file mode 100644 index 0000000000..f14b5ee4aa Binary files /dev/null and b/source/assets/favicons/favicon-72x72.png differ diff --git a/source/assets/favicons/favicon-96x96.png b/source/assets/favicons/favicon-96x96.png new file mode 100644 index 0000000000..ea791f9f2a Binary files /dev/null and b/source/assets/favicons/favicon-96x96.png differ diff --git a/source/assets/favicons/favicon.ico b/source/assets/favicons/favicon.ico new file mode 100644 index 0000000000..bc1699848f Binary files /dev/null and b/source/assets/favicons/favicon.ico differ diff --git a/source/assets/favicons/ms-icon-144x144.png b/source/assets/favicons/ms-icon-144x144.png new file mode 100644 index 0000000000..04d700dbb0 Binary files /dev/null and b/source/assets/favicons/ms-icon-144x144.png differ diff --git a/source/assets/favicons/ms-icon-150x150.png b/source/assets/favicons/ms-icon-150x150.png new file mode 100644 index 0000000000..1447d6ca09 Binary files /dev/null and b/source/assets/favicons/ms-icon-150x150.png differ diff --git a/source/assets/favicons/ms-icon-310x310.png b/source/assets/favicons/ms-icon-310x310.png new file mode 100644 index 0000000000..3f64c31f47 Binary files /dev/null and b/source/assets/favicons/ms-icon-310x310.png differ diff --git a/source/assets/favicons/ms-icon-70x70.png b/source/assets/favicons/ms-icon-70x70.png new file mode 100644 index 0000000000..82a9524076 Binary files /dev/null and b/source/assets/favicons/ms-icon-70x70.png differ diff --git a/source/assets/fixtures/fastorder.csv b/source/assets/fixtures/fastorder.csv new file mode 100644 index 0000000000..a1de3c2ab8 --- /dev/null +++ b/source/assets/fixtures/fastorder.csv @@ -0,0 +1,6 @@ +bestellnummer,losgröße +SW10178,10 +B2BNotAvailable,10 +SW10179,5 +SW10180,2 +SW10181,3 diff --git a/source/assets/fonts/InterVariable-Italic.woff2 b/source/assets/fonts/InterVariable-Italic.woff2 new file mode 100644 index 0000000000..f22ec25549 Binary files /dev/null and b/source/assets/fonts/InterVariable-Italic.woff2 differ diff --git a/source/assets/fonts/InterVariable.woff2 b/source/assets/fonts/InterVariable.woff2 new file mode 100644 index 0000000000..22a12b04e1 Binary files /dev/null and b/source/assets/fonts/InterVariable.woff2 differ diff --git a/source/assets/fonts/open-sans-v15-latin-600.woff b/source/assets/fonts/open-sans-v15-latin-600.woff new file mode 100644 index 0000000000..5a604b3a01 Binary files /dev/null and b/source/assets/fonts/open-sans-v15-latin-600.woff differ diff --git a/source/assets/fonts/open-sans-v15-latin-600.woff2 b/source/assets/fonts/open-sans-v15-latin-600.woff2 new file mode 100644 index 0000000000..a0965b7a89 Binary files /dev/null and b/source/assets/fonts/open-sans-v15-latin-600.woff2 differ diff --git a/source/assets/fonts/open-sans-v15-latin-700.woff b/source/assets/fonts/open-sans-v15-latin-700.woff new file mode 100644 index 0000000000..2523e953cb Binary files /dev/null and b/source/assets/fonts/open-sans-v15-latin-700.woff differ diff --git a/source/assets/fonts/open-sans-v15-latin-700.woff2 b/source/assets/fonts/open-sans-v15-latin-700.woff2 new file mode 100644 index 0000000000..2b04b15bb7 Binary files /dev/null and b/source/assets/fonts/open-sans-v15-latin-700.woff2 differ diff --git a/source/assets/fonts/open-sans-v15-latin-regular.woff b/source/assets/fonts/open-sans-v15-latin-regular.woff new file mode 100644 index 0000000000..e495e6f010 Binary files /dev/null and b/source/assets/fonts/open-sans-v15-latin-regular.woff differ diff --git a/source/assets/fonts/open-sans-v15-latin-regular.woff2 b/source/assets/fonts/open-sans-v15-latin-regular.woff2 new file mode 100644 index 0000000000..c8050c25f8 Binary files /dev/null and b/source/assets/fonts/open-sans-v15-latin-regular.woff2 differ diff --git a/source/assets/fonts/shopware.woff b/source/assets/fonts/shopware.woff new file mode 100755 index 0000000000..8ffc1bfc10 Binary files /dev/null and b/source/assets/fonts/shopware.woff differ diff --git a/source/assets/fonts/shopware_website.eot b/source/assets/fonts/shopware_website.eot new file mode 100644 index 0000000000..b6463569f9 Binary files /dev/null and b/source/assets/fonts/shopware_website.eot differ diff --git a/source/assets/fonts/shopware_website.svg b/source/assets/fonts/shopware_website.svg new file mode 100644 index 0000000000..1ff1f7329a --- /dev/null +++ b/source/assets/fonts/shopware_website.svg @@ -0,0 +1,326 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/assets/fonts/shopware_website.ttf b/source/assets/fonts/shopware_website.ttf new file mode 100644 index 0000000000..f054641774 Binary files /dev/null and b/source/assets/fonts/shopware_website.ttf differ diff --git a/source/assets/fonts/shopware_website.woff b/source/assets/fonts/shopware_website.woff new file mode 100644 index 0000000000..8ffc1bfc10 Binary files /dev/null and b/source/assets/fonts/shopware_website.woff differ diff --git a/source/assets/img/arrow-up.svg b/source/assets/img/arrow-up.svg new file mode 100644 index 0000000000..116b26f8d7 --- /dev/null +++ b/source/assets/img/arrow-up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/source/assets/img/b2b-suite/features/adresses_overview.png b/source/assets/img/b2b-suite/features/adresses_overview.png new file mode 100644 index 0000000000..126108ff6a Binary files /dev/null and b/source/assets/img/b2b-suite/features/adresses_overview.png differ diff --git a/source/assets/img/b2b-suite/features/contacts_overview.png b/source/assets/img/b2b-suite/features/contacts_overview.png new file mode 100644 index 0000000000..9ab084b7e2 Binary files /dev/null and b/source/assets/img/b2b-suite/features/contacts_overview.png differ diff --git a/source/assets/img/b2b-suite/features/contingents1.png b/source/assets/img/b2b-suite/features/contingents1.png new file mode 100644 index 0000000000..69a6cedd19 Binary files /dev/null and b/source/assets/img/b2b-suite/features/contingents1.png differ diff --git a/source/assets/img/b2b-suite/features/custom_ordernumber_grid.png b/source/assets/img/b2b-suite/features/custom_ordernumber_grid.png new file mode 100644 index 0000000000..e5a7dea789 Binary files /dev/null and b/source/assets/img/b2b-suite/features/custom_ordernumber_grid.png differ diff --git a/source/assets/img/b2b-suite/features/custom_ordernumber_upload.png b/source/assets/img/b2b-suite/features/custom_ordernumber_upload.png new file mode 100644 index 0000000000..3ea272a264 Binary files /dev/null and b/source/assets/img/b2b-suite/features/custom_ordernumber_upload.png differ diff --git a/source/assets/img/b2b-suite/features/dashboard.png b/source/assets/img/b2b-suite/features/dashboard.png new file mode 100644 index 0000000000..fe947d3178 Binary files /dev/null and b/source/assets/img/b2b-suite/features/dashboard.png differ diff --git a/source/assets/img/b2b-suite/features/fastorder.png b/source/assets/img/b2b-suite/features/fastorder.png new file mode 100644 index 0000000000..96b66c44b0 Binary files /dev/null and b/source/assets/img/b2b-suite/features/fastorder.png differ diff --git a/source/assets/img/b2b-suite/features/orderlists.png b/source/assets/img/b2b-suite/features/orderlists.png new file mode 100644 index 0000000000..86ac112cc8 Binary files /dev/null and b/source/assets/img/b2b-suite/features/orderlists.png differ diff --git a/source/assets/img/b2b-suite/features/orders1.png b/source/assets/img/b2b-suite/features/orders1.png new file mode 100644 index 0000000000..704c96c5fd Binary files /dev/null and b/source/assets/img/b2b-suite/features/orders1.png differ diff --git a/source/assets/img/b2b-suite/features/orders_overview.png b/source/assets/img/b2b-suite/features/orders_overview.png new file mode 100644 index 0000000000..6ebd1457e1 Binary files /dev/null and b/source/assets/img/b2b-suite/features/orders_overview.png differ diff --git a/source/assets/img/b2b-suite/features/roles1.png b/source/assets/img/b2b-suite/features/roles1.png new file mode 100644 index 0000000000..0ec8ea9697 Binary files /dev/null and b/source/assets/img/b2b-suite/features/roles1.png differ diff --git a/source/assets/img/b2b-suite/features/stats1.png b/source/assets/img/b2b-suite/features/stats1.png new file mode 100644 index 0000000000..8a5b25bc8c Binary files /dev/null and b/source/assets/img/b2b-suite/features/stats1.png differ diff --git a/source/assets/img/b2b-suite/features/variant-filter.png b/source/assets/img/b2b-suite/features/variant-filter.png new file mode 100644 index 0000000000..46f80d485e Binary files /dev/null and b/source/assets/img/b2b-suite/features/variant-filter.png differ diff --git a/source/assets/img/b2b-suite/mockups/clearance.png b/source/assets/img/b2b-suite/mockups/clearance.png new file mode 100644 index 0000000000..c5064019ff Binary files /dev/null and b/source/assets/img/b2b-suite/mockups/clearance.png differ diff --git a/source/assets/img/b2b-suite/mockups/fast-order.png b/source/assets/img/b2b-suite/mockups/fast-order.png new file mode 100644 index 0000000000..98111f4885 Binary files /dev/null and b/source/assets/img/b2b-suite/mockups/fast-order.png differ diff --git a/source/assets/img/b2b-suite/mockups/list.png b/source/assets/img/b2b-suite/mockups/list.png new file mode 100644 index 0000000000..4cef8fccb0 Binary files /dev/null and b/source/assets/img/b2b-suite/mockups/list.png differ diff --git a/source/assets/img/b2b-suite/mockups/order-lists.png b/source/assets/img/b2b-suite/mockups/order-lists.png new file mode 100644 index 0000000000..6fd580aaf3 Binary files /dev/null and b/source/assets/img/b2b-suite/mockups/order-lists.png differ diff --git a/source/assets/img/b2b-suite/mockups/order-overview.png b/source/assets/img/b2b-suite/mockups/order-overview.png new file mode 100644 index 0000000000..4629084a7e Binary files /dev/null and b/source/assets/img/b2b-suite/mockups/order-overview.png differ diff --git a/source/assets/img/b2b-suite/v2/address-edit.png b/source/assets/img/b2b-suite/v2/address-edit.png new file mode 100644 index 0000000000..bfbfe24b68 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/address-edit.png differ diff --git a/source/assets/img/b2b-suite/v2/address-index.png b/source/assets/img/b2b-suite/v2/address-index.png new file mode 100644 index 0000000000..011499ebeb Binary files /dev/null and b/source/assets/img/b2b-suite/v2/address-index.png differ diff --git a/source/assets/img/b2b-suite/v2/alternative-listing-view-category.png b/source/assets/img/b2b-suite/v2/alternative-listing-view-category.png new file mode 100644 index 0000000000..262db3acc6 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/alternative-listing-view-category.png differ diff --git a/source/assets/img/b2b-suite/v2/alternative-listing-view-search.png b/source/assets/img/b2b-suite/v2/alternative-listing-view-search.png new file mode 100644 index 0000000000..f619383878 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/alternative-listing-view-search.png differ diff --git a/source/assets/img/b2b-suite/v2/backend-checkout.png b/source/assets/img/b2b-suite/v2/backend-checkout.png new file mode 100644 index 0000000000..e6c83f59f2 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/backend-checkout.png differ diff --git a/source/assets/img/b2b-suite/v2/backend-clearance-payment-method.png b/source/assets/img/b2b-suite/v2/backend-clearance-payment-method.png new file mode 100644 index 0000000000..f151b12e28 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/backend-clearance-payment-method.png differ diff --git a/source/assets/img/b2b-suite/v2/backend-customer-filter.png b/source/assets/img/b2b-suite/v2/backend-customer-filter.png new file mode 100644 index 0000000000..d1db9f62b5 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/backend-customer-filter.png differ diff --git a/source/assets/img/b2b-suite/v2/backend-customergroup-landingpage.png b/source/assets/img/b2b-suite/v2/backend-customergroup-landingpage.png new file mode 100644 index 0000000000..11a5dee2ac Binary files /dev/null and b/source/assets/img/b2b-suite/v2/backend-customergroup-landingpage.png differ diff --git a/source/assets/img/b2b-suite/v2/backend-debtor-flag.png b/source/assets/img/b2b-suite/v2/backend-debtor-flag.png new file mode 100644 index 0000000000..ec241ccd91 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/backend-debtor-flag.png differ diff --git a/source/assets/img/b2b-suite/v2/backend-debtor-landingpage-declaration.png b/source/assets/img/b2b-suite/v2/backend-debtor-landingpage-declaration.png new file mode 100644 index 0000000000..890a0d434c Binary files /dev/null and b/source/assets/img/b2b-suite/v2/backend-debtor-landingpage-declaration.png differ diff --git a/source/assets/img/b2b-suite/v2/backend-payment-risk-management.png b/source/assets/img/b2b-suite/v2/backend-payment-risk-management.png new file mode 100644 index 0000000000..358bfe4601 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/backend-payment-risk-management.png differ diff --git a/source/assets/img/b2b-suite/v2/budget/budget-fssp.png b/source/assets/img/b2b-suite/v2/budget/budget-fssp.png new file mode 100644 index 0000000000..5c97236d78 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/budget/budget-fssp.png differ diff --git a/source/assets/img/b2b-suite/v2/budget/budget.png b/source/assets/img/b2b-suite/v2/budget/budget.png new file mode 100644 index 0000000000..86435084e1 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/budget/budget.png differ diff --git a/source/assets/img/b2b-suite/v2/checkout-cart.png b/source/assets/img/b2b-suite/v2/checkout-cart.png new file mode 100644 index 0000000000..af24393cb1 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/checkout-cart.png differ diff --git a/source/assets/img/b2b-suite/v2/checkout-confirm-budget.png b/source/assets/img/b2b-suite/v2/checkout-confirm-budget.png new file mode 100644 index 0000000000..258504f35b Binary files /dev/null and b/source/assets/img/b2b-suite/v2/checkout-confirm-budget.png differ diff --git a/source/assets/img/b2b-suite/v2/checkout-confirm-contingent-rules.png b/source/assets/img/b2b-suite/v2/checkout-confirm-contingent-rules.png new file mode 100644 index 0000000000..87f9776280 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/checkout-confirm-contingent-rules.png differ diff --git a/source/assets/img/b2b-suite/v2/checkout-confirm.png b/source/assets/img/b2b-suite/v2/checkout-confirm.png new file mode 100644 index 0000000000..dc5c63b036 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/checkout-confirm.png differ diff --git a/source/assets/img/b2b-suite/v2/checkout-finish.png b/source/assets/img/b2b-suite/v2/checkout-finish.png new file mode 100644 index 0000000000..eaf217ce0e Binary files /dev/null and b/source/assets/img/b2b-suite/v2/checkout-finish.png differ diff --git a/source/assets/img/b2b-suite/v2/checkout.png b/source/assets/img/b2b-suite/v2/checkout.png new file mode 100644 index 0000000000..c0af6a42d6 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/checkout.png differ diff --git a/source/assets/img/b2b-suite/v2/company-index.png b/source/assets/img/b2b-suite/v2/company-index.png new file mode 100644 index 0000000000..c92a4b4b39 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/company-index.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-detail-billing.png b/source/assets/img/b2b-suite/v2/contact-detail-billing.png new file mode 100644 index 0000000000..259bb81eaa Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-detail-billing.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-detail-master-passsword-mail.png b/source/assets/img/b2b-suite/v2/contact-detail-master-passsword-mail.png new file mode 100644 index 0000000000..18143b300c Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-detail-master-passsword-mail.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-detail-master.png b/source/assets/img/b2b-suite/v2/contact-detail-master.png new file mode 100644 index 0000000000..65ffd4d7cd Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-detail-master.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-detail-permission.png b/source/assets/img/b2b-suite/v2/contact-detail-permission.png new file mode 100644 index 0000000000..4582cff2e1 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-detail-permission.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-detail-roles.png b/source/assets/img/b2b-suite/v2/contact-detail-roles.png new file mode 100644 index 0000000000..cae70df9e7 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-detail-roles.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-detail-shipping.png b/source/assets/img/b2b-suite/v2/contact-detail-shipping.png new file mode 100644 index 0000000000..f52f6ae813 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-detail-shipping.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-index.png b/source/assets/img/b2b-suite/v2/contact-index.png new file mode 100644 index 0000000000..e811f33565 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-index.png differ diff --git a/source/assets/img/b2b-suite/v2/contact-password-activation.png b/source/assets/img/b2b-suite/v2/contact-password-activation.png new file mode 100644 index 0000000000..661bc2f8d9 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contact-password-activation.png differ diff --git a/source/assets/img/b2b-suite/v2/contingent-detail-master.png b/source/assets/img/b2b-suite/v2/contingent-detail-master.png new file mode 100644 index 0000000000..4dee8368ef Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contingent-detail-master.png differ diff --git a/source/assets/img/b2b-suite/v2/contingent-detail-rules.png b/source/assets/img/b2b-suite/v2/contingent-detail-rules.png new file mode 100644 index 0000000000..f3e8bd5cc3 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contingent-detail-rules.png differ diff --git a/source/assets/img/b2b-suite/v2/contingent-index.png b/source/assets/img/b2b-suite/v2/contingent-index.png new file mode 100644 index 0000000000..d52605b572 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/contingent-index.png differ diff --git a/source/assets/img/b2b-suite/v2/dashboard-contingent-rules.png b/source/assets/img/b2b-suite/v2/dashboard-contingent-rules.png new file mode 100644 index 0000000000..bc50ef1ecd Binary files /dev/null and b/source/assets/img/b2b-suite/v2/dashboard-contingent-rules.png differ diff --git a/source/assets/img/b2b-suite/v2/dashboard-index.png b/source/assets/img/b2b-suite/v2/dashboard-index.png new file mode 100644 index 0000000000..ac756cb470 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/dashboard-index.png differ diff --git a/source/assets/img/b2b-suite/v2/dashboard-landingpage.png b/source/assets/img/b2b-suite/v2/dashboard-landingpage.png new file mode 100644 index 0000000000..12ba65e1b8 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/dashboard-landingpage.png differ diff --git a/source/assets/img/b2b-suite/v2/fastorder.png b/source/assets/img/b2b-suite/v2/fastorder.png new file mode 100644 index 0000000000..9ac36b8123 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/fastorder.png differ diff --git a/source/assets/img/b2b-suite/v2/frontend-checkout-clearance-payment-method-selection.png b/source/assets/img/b2b-suite/v2/frontend-checkout-clearance-payment-method-selection.png new file mode 100644 index 0000000000..6a63d571d0 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/frontend-checkout-clearance-payment-method-selection.png differ diff --git a/source/assets/img/b2b-suite/v2/frontend-checkout-clearance-payment-method.png b/source/assets/img/b2b-suite/v2/frontend-checkout-clearance-payment-method.png new file mode 100644 index 0000000000..9000859c70 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/frontend-checkout-clearance-payment-method.png differ diff --git a/source/assets/img/b2b-suite/v2/mockup-contact-index-slim.jpg b/source/assets/img/b2b-suite/v2/mockup-contact-index-slim.jpg new file mode 100644 index 0000000000..cfbebd805b Binary files /dev/null and b/source/assets/img/b2b-suite/v2/mockup-contact-index-slim.jpg differ diff --git a/source/assets/img/b2b-suite/v2/mockup-contact-index.jpg b/source/assets/img/b2b-suite/v2/mockup-contact-index.jpg new file mode 100644 index 0000000000..5d4b91c67f Binary files /dev/null and b/source/assets/img/b2b-suite/v2/mockup-contact-index.jpg differ diff --git a/source/assets/img/b2b-suite/v2/order-clearance-accept.png b/source/assets/img/b2b-suite/v2/order-clearance-accept.png new file mode 100644 index 0000000000..2f954e1947 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-clearance-accept.png differ diff --git a/source/assets/img/b2b-suite/v2/order-clearance-decline.png b/source/assets/img/b2b-suite/v2/order-clearance-decline.png new file mode 100644 index 0000000000..56d574cf27 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-clearance-decline.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/checkout-order-list-add-to-list.png b/source/assets/img/b2b-suite/v2/order-list/checkout-order-list-add-to-list.png new file mode 100644 index 0000000000..63d369b478 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/checkout-order-list-add-to-list.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/checkout-order-list-create-list.png b/source/assets/img/b2b-suite/v2/order-list/checkout-order-list-create-list.png new file mode 100644 index 0000000000..b88fa2edbc Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/checkout-order-list-create-list.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/checkout-position-add-order-list.png b/source/assets/img/b2b-suite/v2/order-list/checkout-position-add-order-list.png new file mode 100644 index 0000000000..afead1ae25 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/checkout-position-add-order-list.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/detail-order-list-add-failure.png b/source/assets/img/b2b-suite/v2/order-list/detail-order-list-add-failure.png new file mode 100644 index 0000000000..51565c223a Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/detail-order-list-add-failure.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/detail-order-list-add-success.png b/source/assets/img/b2b-suite/v2/order-list/detail-order-list-add-success.png new file mode 100644 index 0000000000..b45520a2a2 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/detail-order-list-add-success.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/detail-order-list.png b/source/assets/img/b2b-suite/v2/order-list/detail-order-list.png new file mode 100644 index 0000000000..12156d3329 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/detail-order-list.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/order-list-create.png b/source/assets/img/b2b-suite/v2/order-list/order-list-create.png new file mode 100644 index 0000000000..ef48823393 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/order-list-create.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/order-list-masterdata.png b/source/assets/img/b2b-suite/v2/order-list/order-list-masterdata.png new file mode 100644 index 0000000000..6d8c5404e4 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/order-list-masterdata.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/order-list-overview.png b/source/assets/img/b2b-suite/v2/order-list/order-list-overview.png new file mode 100644 index 0000000000..db432e819e Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/order-list-overview.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/order-list-positions-add.png b/source/assets/img/b2b-suite/v2/order-list/order-list-positions-add.png new file mode 100644 index 0000000000..42998c5a86 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/order-list-positions-add.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/order-list-positions-edit.png b/source/assets/img/b2b-suite/v2/order-list/order-list-positions-edit.png new file mode 100644 index 0000000000..ba242b9df6 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/order-list-positions-edit.png differ diff --git a/source/assets/img/b2b-suite/v2/order-list/order-list-positions.png b/source/assets/img/b2b-suite/v2/order-list/order-list-positions.png new file mode 100644 index 0000000000..547c69d13f Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order-list/order-list-positions.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-create-order-list.png b/source/assets/img/b2b-suite/v2/order/order-create-order-list.png new file mode 100644 index 0000000000..88214c78cd Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-create-order-list.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-detail-1.png b/source/assets/img/b2b-suite/v2/order/order-detail-1.png new file mode 100644 index 0000000000..6ae40dc0cb Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-detail-1.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-detail-2.png b/source/assets/img/b2b-suite/v2/order/order-detail-2.png new file mode 100644 index 0000000000..ca650f3907 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-detail-2.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-detail-order-list.png b/source/assets/img/b2b-suite/v2/order/order-detail-order-list.png new file mode 100644 index 0000000000..9e2c2360dd Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-detail-order-list.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-detail-positions-order-list.png b/source/assets/img/b2b-suite/v2/order/order-detail-positions-order-list.png new file mode 100644 index 0000000000..8afae99aa9 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-detail-positions-order-list.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-history.png b/source/assets/img/b2b-suite/v2/order/order-history.png new file mode 100644 index 0000000000..c1ae351b9a Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-history.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-overview.png b/source/assets/img/b2b-suite/v2/order/order-overview.png new file mode 100644 index 0000000000..2859f00b40 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-overview.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-positions-closed.png b/source/assets/img/b2b-suite/v2/order/order-positions-closed.png new file mode 100644 index 0000000000..8585c1a4f2 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-positions-closed.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-positions-open-edit.png b/source/assets/img/b2b-suite/v2/order/order-positions-open-edit.png new file mode 100644 index 0000000000..6e76d296f6 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-positions-open-edit.png differ diff --git a/source/assets/img/b2b-suite/v2/order/order-positions-open.png b/source/assets/img/b2b-suite/v2/order/order-positions-open.png new file mode 100644 index 0000000000..b2eeecbbe9 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/order/order-positions-open.png differ diff --git a/source/assets/img/b2b-suite/v2/orderclearance-index.png b/source/assets/img/b2b-suite/v2/orderclearance-index.png new file mode 100644 index 0000000000..00a56fa294 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/orderclearance-index.png differ diff --git a/source/assets/img/b2b-suite/v2/price-before-update.png b/source/assets/img/b2b-suite/v2/price-before-update.png new file mode 100644 index 0000000000..69a7127e13 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/price-before-update.png differ diff --git a/source/assets/img/b2b-suite/v2/price-updated.png b/source/assets/img/b2b-suite/v2/price-updated.png new file mode 100644 index 0000000000..2a2c7c6909 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/price-updated.png differ diff --git a/source/assets/img/b2b-suite/v2/profile-page.png b/source/assets/img/b2b-suite/v2/profile-page.png new file mode 100644 index 0000000000..eb254f5b87 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/profile-page.png differ diff --git a/source/assets/img/b2b-suite/v2/role-detail-billing.png b/source/assets/img/b2b-suite/v2/role-detail-billing.png new file mode 100644 index 0000000000..13705f568d Binary files /dev/null and b/source/assets/img/b2b-suite/v2/role-detail-billing.png differ diff --git a/source/assets/img/b2b-suite/v2/role-detail-master.png b/source/assets/img/b2b-suite/v2/role-detail-master.png new file mode 100644 index 0000000000..a0ec84a9f4 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/role-detail-master.png differ diff --git a/source/assets/img/b2b-suite/v2/role-detail-permission.png b/source/assets/img/b2b-suite/v2/role-detail-permission.png new file mode 100644 index 0000000000..c2dac58c44 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/role-detail-permission.png differ diff --git a/source/assets/img/b2b-suite/v2/role-index.png b/source/assets/img/b2b-suite/v2/role-index.png new file mode 100644 index 0000000000..4bc4dd5c95 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/role-index.png differ diff --git a/source/assets/img/b2b-suite/v2/role-inheritance.png b/source/assets/img/b2b-suite/v2/role-inheritance.png new file mode 100644 index 0000000000..b800feed32 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/role-inheritance.png differ diff --git a/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend-attributes.png b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend-attributes.png new file mode 100644 index 0000000000..6fa93c8b0e Binary files /dev/null and b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend-attributes.png differ diff --git a/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend-client-list.png b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend-client-list.png new file mode 100644 index 0000000000..f2cf9f11c8 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend-client-list.png differ diff --git a/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend.png b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend.png new file mode 100644 index 0000000000..10d2ffca88 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-backend.png differ diff --git a/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-logged-in.png b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-logged-in.png new file mode 100644 index 0000000000..25cc526a55 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-logged-in.png differ diff --git a/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-overview.png b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-overview.png new file mode 100644 index 0000000000..2ccd5b0a36 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/sales-representative/sales-representative-overview.png differ diff --git a/source/assets/img/b2b-suite/v2/screenshot-guide.md b/source/assets/img/b2b-suite/v2/screenshot-guide.md new file mode 100644 index 0000000000..32aca71244 --- /dev/null +++ b/source/assets/img/b2b-suite/v2/screenshot-guide.md @@ -0,0 +1,17 @@ +# Screenshot Guide + +## Setup +* Mac OS X +* Google Chrome + +## Viewport Resolution +* Width: 1350 Pixel +* Height: 984 Pixel + +## Screenshot Options +* Outer Box Shadow +* Retina: Disabled + +## Screenshot Resolution +* Width: 1462 Pixel +* Height: 1170 Pixel \ No newline at end of file diff --git a/source/assets/img/b2b-suite/v2/statistics-grid.png b/source/assets/img/b2b-suite/v2/statistics-grid.png new file mode 100644 index 0000000000..23058ace93 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/statistics-grid.png differ diff --git a/source/assets/img/b2b-suite/v2/statistics-index.png b/source/assets/img/b2b-suite/v2/statistics-index.png new file mode 100644 index 0000000000..f607dfad22 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/statistics-index.png differ diff --git a/source/assets/img/b2b-suite/v2/swagger-price-update.png b/source/assets/img/b2b-suite/v2/swagger-price-update.png new file mode 100644 index 0000000000..410acb0ef9 Binary files /dev/null and b/source/assets/img/b2b-suite/v2/swagger-price-update.png differ diff --git a/source/assets/img/b2b/Admin.svg b/source/assets/img/b2b/Admin.svg new file mode 100644 index 0000000000..1fced01046 --- /dev/null +++ b/source/assets/img/b2b/Admin.svg @@ -0,0 +1,3 @@ + + +
Domain Core
Domain Core
Shopware 5
Shopware 5
Shopware &
Shopware &
Backend
Backend
Frontend
Frontend
Ext-Backend
Ext-Backend
Vue-Admin
Vue-Admin
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/assets/img/b2b/Basket-to-offer.png b/source/assets/img/b2b/Basket-to-offer.png new file mode 100644 index 0000000000..ac3d2b2a3c Binary files /dev/null and b/source/assets/img/b2b/Basket-to-offer.png differ diff --git a/source/assets/img/b2b/DIP_SW5.svg b/source/assets/img/b2b/DIP_SW5.svg new file mode 100644 index 0000000000..6504ba8b03 --- /dev/null +++ b/source/assets/img/b2b/DIP_SW5.svg @@ -0,0 +1,3 @@ + + +
Domain Core
Domain Core
Bridge
Bridge
Shopware 5
Shopware 5
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/assets/img/b2b/DIP_SW5_Front.svg b/source/assets/img/b2b/DIP_SW5_Front.svg new file mode 100644 index 0000000000..ec24a72389 --- /dev/null +++ b/source/assets/img/b2b/DIP_SW5_Front.svg @@ -0,0 +1,3 @@ + + +
Domain Core
Domain Core
Bridge
Bridge
Shopware 5
Shopware 5
Store Frontend
Store Frontend
Front-Bridge
Front-Bridge
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/assets/img/b2b/DIP_SW6_Front.svg b/source/assets/img/b2b/DIP_SW6_Front.svg new file mode 100644 index 0000000000..b213d86af1 --- /dev/null +++ b/source/assets/img/b2b/DIP_SW6_Front.svg @@ -0,0 +1,3 @@ + + +
Store Frontend
Store Frontend
Front-Bridge
Front-Bridge
Domain Core
Domain Core
Bridge
Bridge
Shopware 5
Shopware 5
Bridge Platform
Bridge Platform
Shopware &
Shopware &
Store Frontend
Store Frontend
Front-Bridge
Front-Bridge
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/assets/img/b2b/DIP_sw6.svg b/source/assets/img/b2b/DIP_sw6.svg new file mode 100644 index 0000000000..83e08b5783 --- /dev/null +++ b/source/assets/img/b2b/DIP_sw6.svg @@ -0,0 +1,3 @@ + + +
Domain Core
Domain Core
Bridge
Bridge
Shopware 5
Shopware 5
Bridge Platform
Bridge Platform
Shopware 6
Shopware 6
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/assets/img/b2b/acl-address-schema.svg b/source/assets/img/b2b/acl-address-schema.svg new file mode 100644 index 0000000000..e24a99b9a3 --- /dev/null +++ b/source/assets/img/b2b/acl-address-schema.svg @@ -0,0 +1 @@ +idName3Pascal1Daniel2LinusContactidName3Azubi1Admin2RoleEntwickleridWhere3Asia1Europe2AfricaAddresscontact_idrole_id331221ContactRoleContextSubjectACLContextquerystartpointContextquerystartpointentity_id (role)referenced_entity_id311322ACLRoleAddressentity_id (contact)referenced_entity_id331122ACLContactAddress \ No newline at end of file diff --git a/source/assets/img/b2b/acl-architecture.svg b/source/assets/img/b2b/acl-architecture.svg new file mode 100644 index 0000000000..436c06a329 --- /dev/null +++ b/source/assets/img/b2b/acl-architecture.svg @@ -0,0 +1 @@ +HTTPRequestB2BControllerB2BServiceB2BRepositoryFirewallGuardControllerAccessGuardEntityReadAccessGuardEntityWriteAcessProvideContextforACLHTTPResponseViewHideButtonsLinks \ No newline at end of file diff --git a/source/assets/img/b2b/ajax-panel-abstract.svg b/source/assets/img/b2b/ajax-panel-abstract.svg new file mode 100644 index 0000000000..49843b3560 --- /dev/null +++ b/source/assets/img/b2b/ajax-panel-abstract.svg @@ -0,0 +1 @@ +B2BShopDemoShopFOOTERContactsOrdersRolesDebtorsHTTP-ResponseHTTP-RequestHTTP-ResponseHTTP-RequestHTTP-ResponseHTTP-RequestHTTP-ResponseHTTP-Request \ No newline at end of file diff --git a/source/assets/img/b2b/ajax-panel-structure.svg b/source/assets/img/b2b/ajax-panel-structure.svg new file mode 100644 index 0000000000..b48b35e8c7 --- /dev/null +++ b/source/assets/img/b2b/ajax-panel-structure.svg @@ -0,0 +1 @@ +AjaxPanelCoreImplementation-DefaultCSS-ClassBinding-EventSystemAjaxPanelPluginLoaderPerpanelpluginstatehandlingACLFormDisablesformelementsandremovessubmitbuttons,ifuserhasonlydetailsaccessACLGridDisablesrowclicks,ifuserhasonlylistingaccessFormDisablePreventformeditingonpendingloadModalOpencontentinmodaldialogboxTabHandleactivestateintabnavigationTriggerReloadTriggerreloadofotherpanelsTreeSelectTree-viewwithenableddraganddrop,tomovenodes \ No newline at end of file diff --git a/source/assets/img/b2b/architecture-acl.svg b/source/assets/img/b2b/architecture-acl.svg new file mode 100644 index 0000000000..39fa2d84a6 --- /dev/null +++ b/source/assets/img/b2b/architecture-acl.svg @@ -0,0 +1 @@ +ACLACLRoute \ No newline at end of file diff --git a/source/assets/img/b2b/architecture-component.svg b/source/assets/img/b2b/architecture-component.svg new file mode 100644 index 0000000000..9521291399 --- /dev/null +++ b/source/assets/img/b2b/architecture-component.svg @@ -0,0 +1 @@ +ShopwareShop-BridgeFrameworkREST-APIB2B-PluginB2B-CommonInfrastructureDomainApplicationFrontend \ No newline at end of file diff --git a/source/assets/img/b2b/architecture-components-complete.svg b/source/assets/img/b2b/architecture-components-complete.svg new file mode 100644 index 0000000000..3e93a313cd --- /dev/null +++ b/source/assets/img/b2b/architecture-components-complete.svg @@ -0,0 +1 @@ +CommonStoreFrontAuthenticationContactDebtorRoleContingenGroupContingentGroupContactContingentRuleCartOrderClearanceRoleContinentGroupAddressRoleContactOrderLineItemListOrderListOrderListProjectFastOrderShopACL \ No newline at end of file diff --git a/source/assets/img/b2b/architecture-order.svg b/source/assets/img/b2b/architecture-order.svg new file mode 100644 index 0000000000..68530eaf8b --- /dev/null +++ b/source/assets/img/b2b/architecture-order.svg @@ -0,0 +1 @@ +CartOrderOrderClearanceContingentGroupContingentRuleContingentGroupContactContingentGroupRoleLineItemListOrderListProjectOrderList \ No newline at end of file diff --git a/source/assets/img/b2b/architecture-users.svg b/source/assets/img/b2b/architecture-users.svg new file mode 100644 index 0000000000..50d9b00175 --- /dev/null +++ b/source/assets/img/b2b/architecture-users.svg @@ -0,0 +1 @@ +StoreFrontAuthenticationDebtorContactRoleRoleContactAddress \ No newline at end of file diff --git a/source/assets/img/b2b/assignment-service.svg b/source/assets/img/b2b/assignment-service.svg new file mode 100644 index 0000000000..e8beaa21fe --- /dev/null +++ b/source/assets/img/b2b/assignment-service.svg @@ -0,0 +1 @@ +RepositoryAssignmentServiceDoctrine\DBAL\Connection \ No newline at end of file diff --git a/source/assets/img/b2b/audit_log_structure.svg b/source/assets/img/b2b/audit_log_structure.svg new file mode 100644 index 0000000000..787f45a01d --- /dev/null +++ b/source/assets/img/b2b/audit_log_structure.svg @@ -0,0 +1,2 @@ + +

b2b_audit_log_index


id
audit_log_id
reference_table
reference_id

[Not supported by viewer]

b2b_audit_log


id

log_value

log_type

event_date

author_hash

[Not supported by viewer]
n
n

b2b_audit_log_author


hash
salutation
title
firstname
lastname
email

[Not supported by viewer]
1
1
n
n
1
1
\ No newline at end of file diff --git a/source/assets/img/b2b/authentication-overview.svg b/source/assets/img/b2b/authentication-overview.svg new file mode 100644 index 0000000000..3fa59d28ab --- /dev/null +++ b/source/assets/img/b2b/authentication-overview.svg @@ -0,0 +1 @@ +StoreFrontAuthenticationContactDebtorOrderContactContingentGroupRoleLineItemListAsProviderAsOwnerAsContextSalesRepresentativeDebtorSalesRepresentativeBudgetAsOwner[....]SalesRepresentativeContact \ No newline at end of file diff --git a/source/assets/img/b2b/b2b_backend_auditlog.png b/source/assets/img/b2b/b2b_backend_auditlog.png new file mode 100644 index 0000000000..fb7201ecf2 Binary files /dev/null and b/source/assets/img/b2b/b2b_backend_auditlog.png differ diff --git a/source/assets/img/b2b/b2b_backend_menu.png b/source/assets/img/b2b/b2b_backend_menu.png new file mode 100644 index 0000000000..8cc2f76de7 Binary files /dev/null and b/source/assets/img/b2b/b2b_backend_menu.png differ diff --git a/source/assets/img/b2b/b2b_new_comment.png b/source/assets/img/b2b/b2b_new_comment.png new file mode 100644 index 0000000000..cf2a4ae738 Binary files /dev/null and b/source/assets/img/b2b/b2b_new_comment.png differ diff --git a/source/assets/img/b2b/b2b_offer_history.png b/source/assets/img/b2b/b2b_offer_history.png new file mode 100644 index 0000000000..aa371bf63a Binary files /dev/null and b/source/assets/img/b2b/b2b_offer_history.png differ diff --git a/source/assets/img/b2b/b2b_offer_menu_history.png b/source/assets/img/b2b/b2b_offer_menu_history.png new file mode 100644 index 0000000000..178b37feb8 Binary files /dev/null and b/source/assets/img/b2b/b2b_offer_menu_history.png differ diff --git a/source/assets/img/b2b/b2b_offer_notification_check_box.png b/source/assets/img/b2b/b2b_offer_notification_check_box.png new file mode 100644 index 0000000000..ff56802e14 Binary files /dev/null and b/source/assets/img/b2b/b2b_offer_notification_check_box.png differ diff --git a/source/assets/img/b2b/company-management.svg b/source/assets/img/b2b/company-management.svg new file mode 100644 index 0000000000..e7ea1a88cb --- /dev/null +++ b/source/assets/img/b2b/company-management.svg @@ -0,0 +1 @@ +CompanyContingentgroupAddressContactBudgetContactEntityAddressEntityContingentGroupEntityBudgetEntity \ No newline at end of file diff --git a/source/assets/img/b2b/component-layers.svg b/source/assets/img/b2b/component-layers.svg new file mode 100644 index 0000000000..161f14f3f7 --- /dev/null +++ b/source/assets/img/b2b/component-layers.svg @@ -0,0 +1,3 @@ + + +
Frontend
Frontend
B2B-Plugin
B2B-Plugin
REST-API
REST-API
Framework
Framework
B2B.Common
B2B.Common
Shop-Bridge
Shop-Bridge
Shopware
Shopware
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/assets/img/b2b/components-layers-sw6.svg b/source/assets/img/b2b/components-layers-sw6.svg new file mode 100644 index 0000000000..c194f9cbd6 --- /dev/null +++ b/source/assets/img/b2b/components-layers-sw6.svg @@ -0,0 +1,3 @@ + + +
Shop-Bridge
Shop-Bridge
B2B-Common
B2B-Common
Framework
Framework
Admin
Admin
Shop-Bridge
Shop-Bridge
B2B-Plugin-6
B2B-Plugin-6
Frontend
Frontend
B2B-Plugin
B2B-Plugin
Backend
Backend
Shopware
Shopware
Rest-API
Rest-API
Shopware 6
Shopware 6
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/assets/img/b2b/contact-controller-complex-example.svg b/source/assets/img/b2b/contact-controller-complex-example.svg new file mode 100644 index 0000000000..b9c351e67a --- /dev/null +++ b/source/assets/img/b2b/contact-controller-complex-example.svg @@ -0,0 +1 @@ +ContactContingentContactAddressContactRoleindexActionContactContactRouteindexActionassignActionassignActionindexActionassignActionbillingActionindexActionassignActionshippingActionindexActiondetailActioneditActionupdateActionremoveActionnewActioncreateActionContactContingentGroupContactRoleContactAddressAclRoute \ No newline at end of file diff --git a/source/assets/img/b2b/crud-service.svg b/source/assets/img/b2b/crud-service.svg new file mode 100644 index 0000000000..f27f923384 --- /dev/null +++ b/source/assets/img/b2b/crud-service.svg @@ -0,0 +1 @@ +Common\Repository\DbalHelperDoctrine\DBAL\ConnectionAcl\Framework\AclRepositoryCommon\Validator\ValidationBuilderValidator\Validator\ValidatorInterfaceCommon\Validator\ValidatorRepositoryCrudServiceValidationServiceEntity \ No newline at end of file diff --git a/source/assets/img/b2b/currency-usage.svg b/source/assets/img/b2b/currency-usage.svg new file mode 100644 index 0000000000..d8339cffd8 --- /dev/null +++ b/source/assets/img/b2b/currency-usage.svg @@ -0,0 +1 @@ +CurrencyLineItemListBudgetContingentRuleCartProductPriceTypeBudgetEntityLineItemListEntityShopOrderTimeRestrictionTypeBudgetTransactionEntityContingentRuleOfferEntityAuditLogDiscountEntityAuditLogValueLineItemPriceEntity \ No newline at end of file diff --git a/source/assets/img/b2b/line-item-list-outer-dependencies.svg b/source/assets/img/b2b/line-item-list-outer-dependencies.svg new file mode 100644 index 0000000000..ae0e665c4f --- /dev/null +++ b/source/assets/img/b2b/line-item-list-outer-dependencies.svg @@ -0,0 +1 @@ +LineItemListFastOrderOrderListOrderClearanceOrderOrderContextOrderContextOrderListEntityFastOrderItemStructShopOrderBudgetOrderContextBudgetTransactionOfferOfferEntityOrderContext \ No newline at end of file diff --git a/source/assets/img/b2b/line-item-list-with-listing.svg b/source/assets/img/b2b/line-item-list-with-listing.svg new file mode 100644 index 0000000000..cd7f1ab972 --- /dev/null +++ b/source/assets/img/b2b/line-item-list-with-listing.svg @@ -0,0 +1 @@ +LineItemListLineItenReference:SW101010LineItemReference:SW202020LineItemReference:SW303030ListProductStruct:SW101010ListProductStruct:SW202020ListProductStruct:SW202020 \ No newline at end of file diff --git a/source/assets/img/b2b/line-item-list-with-order-context.svg b/source/assets/img/b2b/line-item-list-with-order-context.svg new file mode 100644 index 0000000000..5eaaa11c17 --- /dev/null +++ b/source/assets/img/b2b/line-item-list-with-order-context.svg @@ -0,0 +1 @@ +LineItemListLineItenReference:SW101010LineItemReference:SW202020LineItemReference:SW303030OrderContextShopwareOrderPaymentInstanceShippingMean.... \ No newline at end of file diff --git a/source/assets/img/b2b/line-item-list-with-order.svg b/source/assets/img/b2b/line-item-list-with-order.svg new file mode 100644 index 0000000000..6642fb0c5c --- /dev/null +++ b/source/assets/img/b2b/line-item-list-with-order.svg @@ -0,0 +1 @@ +LineItemListLineItenReference:SW101010LineItemReference:SW202020LineItemReference:SW303030OrderDetail:SW101010OrderDetail:SW202020OrderDetail:SW202020 \ No newline at end of file diff --git a/source/assets/img/b2b/listing-service.svg b/source/assets/img/b2b/listing-service.svg new file mode 100644 index 0000000000..4f6be4d028 --- /dev/null +++ b/source/assets/img/b2b/listing-service.svg @@ -0,0 +1 @@ +Doctrine\DBAL\ConnectionAcl\Framework\AclRepositoryRepositoryShopware\B2B\Common\Controller\GridHelperSearchStruct\Enlight_Controller_Request_RequestShopware\B2B\Common\Controller\GridRepository \ No newline at end of file diff --git a/source/assets/img/b2b/new-offer.png b/source/assets/img/b2b/new-offer.png new file mode 100644 index 0000000000..939f5d6d48 Binary files /dev/null and b/source/assets/img/b2b/new-offer.png differ diff --git a/source/assets/img/b2b/offer-accept-offer.png b/source/assets/img/b2b/offer-accept-offer.png new file mode 100644 index 0000000000..a5ef35204e Binary files /dev/null and b/source/assets/img/b2b/offer-accept-offer.png differ diff --git a/source/assets/img/b2b/offer-backend-detail.png b/source/assets/img/b2b/offer-backend-detail.png new file mode 100644 index 0000000000..bb91b9ea54 Binary files /dev/null and b/source/assets/img/b2b/offer-backend-detail.png differ diff --git a/source/assets/img/b2b/offer-backend-grid.png b/source/assets/img/b2b/offer-backend-grid.png new file mode 100644 index 0000000000..4d78592fbb Binary files /dev/null and b/source/assets/img/b2b/offer-backend-grid.png differ diff --git a/source/assets/img/b2b/offer-customers.png b/source/assets/img/b2b/offer-customers.png new file mode 100644 index 0000000000..0f7a5c9531 Binary files /dev/null and b/source/assets/img/b2b/offer-customers.png differ diff --git a/source/assets/img/b2b/offer-detail.png b/source/assets/img/b2b/offer-detail.png new file mode 100644 index 0000000000..a5ef35204e Binary files /dev/null and b/source/assets/img/b2b/offer-detail.png differ diff --git a/source/assets/img/b2b/offer-grid.png b/source/assets/img/b2b/offer-grid.png new file mode 100644 index 0000000000..4f3a83058b Binary files /dev/null and b/source/assets/img/b2b/offer-grid.png differ diff --git a/source/assets/img/bg--github.jpg b/source/assets/img/bg--github.jpg new file mode 100755 index 0000000000..b2e21d4e6e Binary files /dev/null and b/source/assets/img/bg--github.jpg differ diff --git a/source/assets/img/connect/sc-environment-overview.png b/source/assets/img/connect/sc-environment-overview.png new file mode 100644 index 0000000000..577477a989 Binary files /dev/null and b/source/assets/img/connect/sc-environment-overview.png differ diff --git a/source/assets/img/connect/sc-start-header.jpg b/source/assets/img/connect/sc-start-header.jpg new file mode 100644 index 0000000000..73c277cbae Binary files /dev/null and b/source/assets/img/connect/sc-start-header.jpg differ diff --git a/source/assets/img/connect/structure.svg b/source/assets/img/connect/structure.svg new file mode 100644 index 0000000000..6ea453d435 --- /dev/null +++ b/source/assets/img/connect/structure.svg @@ -0,0 +1,4463 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 articles0.00 € + + + + Awesome Shop + + + + + + + + + + + + + + + + + + + + + + + + Smartphone + + 1337,-- €5 items in stock + Lorem ipsum dolor sit amet,consectetur adipisicing elit,sed do eiusmod temporincididunt ut labore et doloremagna aliqua. + + + + Comments + + + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consecteturadipisicing elit, sed do eiusmod tempor. + + + + + + + + + + + + + + + + 0 articles0.00 € + + + + Awesome Shop + + + + + + + + + + + + + + + + + + + + + + + + Smartphone + + 1337,-- €5 items in stock + Lorem ipsum dolor sit amet,consectetur adipisicing elit,sed do eiusmod temporincididunt ut labore et doloremagna aliqua. + + + + Comments + + + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consecteturadipisicing elit, sed do eiusmod tempor. + + + + + + + ProductExport + ProductImport + ProductCheckout + Connect + Local Shop + Remote Shop + + diff --git a/source/assets/img/connect/user-guide/add_stream.jpeg b/source/assets/img/connect/user-guide/add_stream.jpeg new file mode 100644 index 0000000000..8729e02da0 Binary files /dev/null and b/source/assets/img/connect/user-guide/add_stream.jpeg differ diff --git a/source/assets/img/connect/user-guide/export.jpg b/source/assets/img/connect/user-guide/export.jpg new file mode 100644 index 0000000000..a05b3d3b55 Binary files /dev/null and b/source/assets/img/connect/user-guide/export.jpg differ diff --git a/source/assets/img/connect/user-guide/exported_products.jpeg b/source/assets/img/connect/user-guide/exported_products.jpeg new file mode 100644 index 0000000000..51a2d55daf Binary files /dev/null and b/source/assets/img/connect/user-guide/exported_products.jpeg differ diff --git a/source/assets/img/connect/user-guide/import.jpg b/source/assets/img/connect/user-guide/import.jpg new file mode 100644 index 0000000000..667b21986f Binary files /dev/null and b/source/assets/img/connect/user-guide/import.jpg differ diff --git a/source/assets/img/connect/user-guide/import_activate.jpg b/source/assets/img/connect/user-guide/import_activate.jpg new file mode 100644 index 0000000000..37d830d603 Binary files /dev/null and b/source/assets/img/connect/user-guide/import_activate.jpg differ diff --git a/source/assets/img/connect/user-guide/import_dragdrop.jpg b/source/assets/img/connect/user-guide/import_dragdrop.jpg new file mode 100644 index 0000000000..6a8f9f1031 Binary files /dev/null and b/source/assets/img/connect/user-guide/import_dragdrop.jpg differ diff --git a/source/assets/img/connect/user-guide/offerwizard_details.jpeg b/source/assets/img/connect/user-guide/offerwizard_details.jpeg new file mode 100644 index 0000000000..701c194c9e Binary files /dev/null and b/source/assets/img/connect/user-guide/offerwizard_details.jpeg differ diff --git a/source/assets/img/connect/user-guide/offerwizard_step1.jpeg b/source/assets/img/connect/user-guide/offerwizard_step1.jpeg new file mode 100644 index 0000000000..176bf84747 Binary files /dev/null and b/source/assets/img/connect/user-guide/offerwizard_step1.jpeg differ diff --git a/source/assets/img/connect/user-guide/offerwizard_step2.jpeg b/source/assets/img/connect/user-guide/offerwizard_step2.jpeg new file mode 100644 index 0000000000..dc8dcc5d1e Binary files /dev/null and b/source/assets/img/connect/user-guide/offerwizard_step2.jpeg differ diff --git a/source/assets/img/connect/user-guide/offerwizard_step3.jpeg b/source/assets/img/connect/user-guide/offerwizard_step3.jpeg new file mode 100644 index 0000000000..b6fbfbb975 Binary files /dev/null and b/source/assets/img/connect/user-guide/offerwizard_step3.jpeg differ diff --git a/source/assets/img/connect/user-guide/offerwizard_step4.jpeg b/source/assets/img/connect/user-guide/offerwizard_step4.jpeg new file mode 100644 index 0000000000..46288a48e4 Binary files /dev/null and b/source/assets/img/connect/user-guide/offerwizard_step4.jpeg differ diff --git a/source/assets/img/connect/user-guide/pricetype.jpeg b/source/assets/img/connect/user-guide/pricetype.jpeg new file mode 100644 index 0000000000..24961098e4 Binary files /dev/null and b/source/assets/img/connect/user-guide/pricetype.jpeg differ diff --git a/source/assets/img/connect/user-guide/start_export.jpeg b/source/assets/img/connect/user-guide/start_export.jpeg new file mode 100644 index 0000000000..386cf94e9a Binary files /dev/null and b/source/assets/img/connect/user-guide/start_export.jpeg differ diff --git a/source/assets/img/connect/user-guide/status_green.jpeg b/source/assets/img/connect/user-guide/status_green.jpeg new file mode 100644 index 0000000000..13833c3f7a Binary files /dev/null and b/source/assets/img/connect/user-guide/status_green.jpeg differ diff --git a/source/assets/img/connect/user-guide/stream_offer.jpeg b/source/assets/img/connect/user-guide/stream_offer.jpeg new file mode 100644 index 0000000000..bc90555a7f Binary files /dev/null and b/source/assets/img/connect/user-guide/stream_offer.jpeg differ diff --git a/source/assets/img/contributing/github-create-pull-request.png b/source/assets/img/contributing/github-create-pull-request.png new file mode 100644 index 0000000000..3840df4ad5 Binary files /dev/null and b/source/assets/img/contributing/github-create-pull-request.png differ diff --git a/source/assets/img/contributing/github-fork-button.png b/source/assets/img/contributing/github-fork-button.png new file mode 100644 index 0000000000..5126264b3b Binary files /dev/null and b/source/assets/img/contributing/github-fork-button.png differ diff --git a/source/assets/img/des-img-one.jpg b/source/assets/img/des-img-one.jpg new file mode 100644 index 0000000000..0e98ec9b6a Binary files /dev/null and b/source/assets/img/des-img-one.jpg differ diff --git a/source/assets/img/design.png b/source/assets/img/design.png new file mode 100644 index 0000000000..d560457591 Binary files /dev/null and b/source/assets/img/design.png differ diff --git a/source/assets/img/dev-img-one.jpg b/source/assets/img/dev-img-one.jpg new file mode 100644 index 0000000000..330db369cc Binary files /dev/null and b/source/assets/img/dev-img-one.jpg differ diff --git a/source/assets/img/dnoegel.png b/source/assets/img/dnoegel.png new file mode 100644 index 0000000000..7cd876cc3c Binary files /dev/null and b/source/assets/img/dnoegel.png differ diff --git a/source/assets/img/forum.svg b/source/assets/img/forum.svg new file mode 100644 index 0000000000..04198c032c --- /dev/null +++ b/source/assets/img/forum.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/assets/img/four.jpg b/source/assets/img/four.jpg new file mode 100644 index 0000000000..6ead092ace Binary files /dev/null and b/source/assets/img/four.jpg differ diff --git a/source/assets/img/github.svg b/source/assets/img/github.svg new file mode 100644 index 0000000000..8582194c49 --- /dev/null +++ b/source/assets/img/github.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/assets/img/icon--github.svg b/source/assets/img/icon--github.svg new file mode 100644 index 0000000000..570d7d869c --- /dev/null +++ b/source/assets/img/icon--github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/img/icon_labs.png b/source/assets/img/icon_labs.png new file mode 100644 index 0000000000..d69630c99b Binary files /dev/null and b/source/assets/img/icon_labs.png differ diff --git a/source/assets/img/icon_labs_hover.png b/source/assets/img/icon_labs_hover.png new file mode 100644 index 0000000000..4783bba7e3 Binary files /dev/null and b/source/assets/img/icon_labs_hover.png differ diff --git a/source/assets/img/img-big-one.jpg b/source/assets/img/img-big-one.jpg new file mode 100644 index 0000000000..fb0455cdc7 Binary files /dev/null and b/source/assets/img/img-big-one.jpg differ diff --git a/source/assets/img/img-four.jpg b/source/assets/img/img-four.jpg new file mode 100644 index 0000000000..6ead092ace Binary files /dev/null and b/source/assets/img/img-four.jpg differ diff --git a/source/assets/img/img-one.jpg b/source/assets/img/img-one.jpg new file mode 100644 index 0000000000..330db369cc Binary files /dev/null and b/source/assets/img/img-one.jpg differ diff --git a/source/assets/img/img-three.jpg b/source/assets/img/img-three.jpg new file mode 100644 index 0000000000..33b75efe70 Binary files /dev/null and b/source/assets/img/img-three.jpg differ diff --git a/source/assets/img/img-two.jpg b/source/assets/img/img-two.jpg new file mode 100644 index 0000000000..8a3a43e428 Binary files /dev/null and b/source/assets/img/img-two.jpg differ diff --git a/source/assets/img/install.png b/source/assets/img/install.png new file mode 100644 index 0000000000..f3d931c005 Binary files /dev/null and b/source/assets/img/install.png differ diff --git a/source/assets/img/issue-tracker.svg b/source/assets/img/issue-tracker.svg new file mode 100644 index 0000000000..a208424375 --- /dev/null +++ b/source/assets/img/issue-tracker.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/assets/img/labs.svg b/source/assets/img/labs.svg new file mode 100644 index 0000000000..60472533a0 --- /dev/null +++ b/source/assets/img/labs.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/assets/img/menu.png b/source/assets/img/menu.png new file mode 100644 index 0000000000..83ae360de8 Binary files /dev/null and b/source/assets/img/menu.png differ diff --git a/source/assets/img/performance/graph.png b/source/assets/img/performance/graph.png new file mode 100644 index 0000000000..efd6b3d96b Binary files /dev/null and b/source/assets/img/performance/graph.png differ diff --git a/source/assets/img/performance/jm-edit.png b/source/assets/img/performance/jm-edit.png new file mode 100644 index 0000000000..18e8182584 Binary files /dev/null and b/source/assets/img/performance/jm-edit.png differ diff --git a/source/assets/img/performance/jmeter-result.png b/source/assets/img/performance/jmeter-result.png new file mode 100644 index 0000000000..94a65d2878 Binary files /dev/null and b/source/assets/img/performance/jmeter-result.png differ diff --git a/source/assets/img/performance/jmeter-run.png b/source/assets/img/performance/jmeter-run.png new file mode 100644 index 0000000000..195269865a Binary files /dev/null and b/source/assets/img/performance/jmeter-run.png differ diff --git a/source/assets/img/plugin.png b/source/assets/img/plugin.png new file mode 100644 index 0000000000..d1601cfd21 Binary files /dev/null and b/source/assets/img/plugin.png differ diff --git a/source/assets/img/pricing-engine/backend_article_pricelist_tab.png b/source/assets/img/pricing-engine/backend_article_pricelist_tab.png new file mode 100644 index 0000000000..9fa7cae7fb Binary files /dev/null and b/source/assets/img/pricing-engine/backend_article_pricelist_tab.png differ diff --git a/source/assets/img/pricing-engine/backend_menu_item.png b/source/assets/img/pricing-engine/backend_menu_item.png new file mode 100644 index 0000000000..659b81f4fe Binary files /dev/null and b/source/assets/img/pricing-engine/backend_menu_item.png differ diff --git a/source/assets/img/pricing-engine/backend_overview.png b/source/assets/img/pricing-engine/backend_overview.png new file mode 100644 index 0000000000..23a6558974 Binary files /dev/null and b/source/assets/img/pricing-engine/backend_overview.png differ diff --git a/source/assets/img/pricing-engine/backend_pricelist_add_view.png b/source/assets/img/pricing-engine/backend_pricelist_add_view.png new file mode 100644 index 0000000000..d102a1149a Binary files /dev/null and b/source/assets/img/pricing-engine/backend_pricelist_add_view.png differ diff --git a/source/assets/img/pricing-engine/backend_pricelist_detailview.png b/source/assets/img/pricing-engine/backend_pricelist_detailview.png new file mode 100644 index 0000000000..6a6166eda7 Binary files /dev/null and b/source/assets/img/pricing-engine/backend_pricelist_detailview.png differ diff --git a/source/developers-guide/header.jpg b/source/assets/img/pricing-engine/header.jpg similarity index 100% rename from source/developers-guide/header.jpg rename to source/assets/img/pricing-engine/header.jpg diff --git a/source/assets/img/rd-hellohuman-bg.png b/source/assets/img/rd-hellohuman-bg.png new file mode 100644 index 0000000000..6c7ddb0428 Binary files /dev/null and b/source/assets/img/rd-hellohuman-bg.png differ diff --git a/source/assets/img/screen-des-styletile.svg b/source/assets/img/screen-des-styletile.svg new file mode 100644 index 0000000000..e6784ff1f1 --- /dev/null +++ b/source/assets/img/screen-des-styletile.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/assets/img/screen-des-udemy.jpg b/source/assets/img/screen-des-udemy.jpg new file mode 100644 index 0000000000..47dd1f241d Binary files /dev/null and b/source/assets/img/screen-des-udemy.jpg differ diff --git a/source/assets/img/screen-dev-slack.jpg b/source/assets/img/screen-dev-slack.jpg new file mode 100644 index 0000000000..86675aecd4 Binary files /dev/null and b/source/assets/img/screen-dev-slack.jpg differ diff --git a/source/assets/img/screen-dev-udemy.jpg b/source/assets/img/screen-dev-udemy.jpg new file mode 100644 index 0000000000..567039d66f Binary files /dev/null and b/source/assets/img/screen-dev-udemy.jpg differ diff --git a/source/assets/img/search/and_or/and_config.png b/source/assets/img/search/and_or/and_config.png new file mode 100644 index 0000000000..15b65102e4 Binary files /dev/null and b/source/assets/img/search/and_or/and_config.png differ diff --git a/source/assets/img/search/and_or/and_result.png b/source/assets/img/search/and_or/and_result.png new file mode 100644 index 0000000000..ba7b34efea Binary files /dev/null and b/source/assets/img/search/and_or/and_result.png differ diff --git a/source/assets/img/search/and_or/or_config.png b/source/assets/img/search/and_or/or_config.png new file mode 100644 index 0000000000..2363ae2da9 Binary files /dev/null and b/source/assets/img/search/and_or/or_config.png differ diff --git a/source/assets/img/search/and_or/or_result.png b/source/assets/img/search/and_or/or_result.png new file mode 100644 index 0000000000..078755f241 Binary files /dev/null and b/source/assets/img/search/and_or/or_result.png differ diff --git a/source/assets/img/search/boosting.png b/source/assets/img/search/boosting.png new file mode 100644 index 0000000000..56b4fa1f01 Binary files /dev/null and b/source/assets/img/search/boosting.png differ diff --git a/source/assets/img/search/boosting_detail.png b/source/assets/img/search/boosting_detail.png new file mode 100644 index 0000000000..7c642911b5 Binary files /dev/null and b/source/assets/img/search/boosting_detail.png differ diff --git a/source/assets/img/search/indexing-settings.png b/source/assets/img/search/indexing-settings.png new file mode 100644 index 0000000000..da174e026b Binary files /dev/null and b/source/assets/img/search/indexing-settings.png differ diff --git a/source/assets/img/search/overlay.png b/source/assets/img/search/overlay.png new file mode 100644 index 0000000000..f0e8ca2ed2 Binary files /dev/null and b/source/assets/img/search/overlay.png differ diff --git a/source/assets/img/search/per-entity.png b/source/assets/img/search/per-entity.png new file mode 100644 index 0000000000..74e048b58d Binary files /dev/null and b/source/assets/img/search/per-entity.png differ diff --git a/source/assets/img/search/preview.png b/source/assets/img/search/preview.png new file mode 100644 index 0000000000..a7d96ef134 Binary files /dev/null and b/source/assets/img/search/preview.png differ diff --git a/source/assets/img/search/profiles.png b/source/assets/img/search/profiles.png new file mode 100644 index 0000000000..ba5d4357b8 Binary files /dev/null and b/source/assets/img/search/profiles.png differ diff --git a/source/assets/img/search/relevance.png b/source/assets/img/search/relevance.png new file mode 100644 index 0000000000..b0fc3deaf1 Binary files /dev/null and b/source/assets/img/search/relevance.png differ diff --git a/source/assets/img/search/serp.png b/source/assets/img/search/serp.png new file mode 100644 index 0000000000..8baaabc544 Binary files /dev/null and b/source/assets/img/search/serp.png differ diff --git a/source/assets/img/search/settings.png b/source/assets/img/search/settings.png new file mode 100644 index 0000000000..76a3528550 Binary files /dev/null and b/source/assets/img/search/settings.png differ diff --git a/source/assets/img/search/synonyms.png b/source/assets/img/search/synonyms.png new file mode 100644 index 0000000000..e73491209b Binary files /dev/null and b/source/assets/img/search/synonyms.png differ diff --git a/source/assets/img/shopware_developers_logo_white.svg b/source/assets/img/shopware_developers_logo_white.svg new file mode 100644 index 0000000000..673f760760 --- /dev/null +++ b/source/assets/img/shopware_developers_logo_white.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/assets/img/styleguide.svg b/source/assets/img/styleguide.svg new file mode 100644 index 0000000000..3b8836ade7 --- /dev/null +++ b/source/assets/img/styleguide.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/assets/img/styletile-cheatsheet.svg b/source/assets/img/styletile-cheatsheet.svg new file mode 100644 index 0000000000..2e31a972d8 --- /dev/null +++ b/source/assets/img/styletile-cheatsheet.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/assets/img/styletile-preview.svg b/source/assets/img/styletile-preview.svg new file mode 100644 index 0000000000..917d93eac5 --- /dev/null +++ b/source/assets/img/styletile-preview.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/assets/img/twitter.svg b/source/assets/img/twitter.svg new file mode 100644 index 0000000000..37a10c5093 --- /dev/null +++ b/source/assets/img/twitter.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/assets/js/docsearch.min.js b/source/assets/js/docsearch.min.js new file mode 100644 index 0000000000..279afdc31d --- /dev/null +++ b/source/assets/js/docsearch.min.js @@ -0,0 +1,2 @@ +/*! docsearch 2.5.2 | © Algolia | github.com/algolia/docsearch */ +(function webpackUniversalModuleDefinition(root,factory){if(typeof exports==="object"&&typeof module==="object")module.exports=factory();else if(typeof define==="function"&&define.amd)define([],factory);else if(typeof exports==="object")exports["docsearch"]=factory();else root["docsearch"]=factory()})(typeof self!=="undefined"?self:this,function(){return function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{configurable:false,enumerable:true,get:getter})}};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module["default"]}:function getModuleExports(){return module};__webpack_require__.d(getter,"a",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p="";return __webpack_require__(__webpack_require__.s=21)}([function(module,exports,__webpack_require__){"use strict";var DOM=__webpack_require__(1);function escapeRegExp(str){return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}module.exports={isArray:null,isFunction:null,isObject:null,bind:null,each:null,map:null,mixin:null,isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:false},escapeRegExChars:function(str){return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isNumber:function(obj){return typeof obj==="number"},toStr:function toStr(s){return s===undefined||s===null?"":s+""},cloneDeep:function cloneDeep(obj){var clone=this.mixin({},obj);var self=this;this.each(clone,function(value,key){if(value){if(self.isArray(value)){clone[key]=[].concat(value)}else if(self.isObject(value)){clone[key]=self.cloneDeep(value)}}});return clone},error:function(msg){throw new Error(msg)},every:function(obj,test){var result=true;if(!obj){return result}this.each(obj,function(val,key){result=test.call(null,val,key,obj);if(!result){return false}});return!!result},any:function(obj,test){var found=false;if(!obj){return found}this.each(obj,function(val,key){if(test.call(null,val,key,obj)){found=true;return false}});return found},getUniqueId:function(){var counter=0;return function(){return counter++}}(),templatify:function templatify(obj){if(this.isFunction(obj)){return obj}var $template=DOM.element(obj);if($template.prop("tagName")==="SCRIPT"){return function template(){return $template.text()}}return function template(){return String(obj)}},defer:function(fn){setTimeout(fn,0)},noop:function(){},formatPrefix:function(prefix,noPrefix){return noPrefix?"":prefix+"-"},className:function(prefix,clazz,skipDot){return(skipDot?"":".")+prefix+clazz},escapeHighlightedString:function(str,highlightPreTag,highlightPostTag){highlightPreTag=highlightPreTag||"";var pre=document.createElement("div");pre.appendChild(document.createTextNode(highlightPreTag));highlightPostTag=highlightPostTag||"";var post=document.createElement("div");post.appendChild(document.createTextNode(highlightPostTag));var div=document.createElement("div");div.appendChild(document.createTextNode(str));return div.innerHTML.replace(RegExp(escapeRegExp(pre.innerHTML),"g"),highlightPreTag).replace(RegExp(escapeRegExp(post.innerHTML),"g"),highlightPostTag)}}},function(module,exports,__webpack_require__){"use strict";module.exports={element:null}},function(module,exports){var hasOwn=Object.prototype.hasOwnProperty;var toString=Object.prototype.toString;module.exports=function forEach(obj,fn,ctx){if(toString.call(fn)!=="[object Function]"){throw new TypeError("iterator must be a function")}var l=obj.length;if(l===+l){for(var i=0;i was loaded but did not call our provided callback"),JSONPScriptError:createCustomError("JSONPScriptError","",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},r]}]}}),e.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"symbol",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link",e:"$"}}]}]}}),e.registerLanguage("nginx",function(e){var t={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},r={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,t],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[t]},{cN:"regexp",c:[e.BE,t],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},t]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:r}],r:0}],i:"[^\\s\\}]"}}),e.registerLanguage("objectivec",function(e){var t={cN:"built_in",b:"(AV|CA|CF|CG|CI|MK|MP|NS|UI|XC)\\w+"},r={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},a=/[a-zA-Z@][a-zA-Z0-9_]*/,n="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:r,l:a,i:""}]}]},{cN:"class",b:"("+n.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:n,l:a,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}}),e.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},a={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],s=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),a,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=s,a.c=s,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:s}}),e.registerLanguage("php",function(e){var t={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},r={cN:"meta",b:/<\?(php)?|\?>/},a={cN:"string",c:[e.BE,r],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},n={v:[e.BNM,e.CNM]};return{aliases:["php3","php4","php5","php6"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[r]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},r,t,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",t,e.CBCM,a,n]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},a,n]}}),e.registerLanguage("python",function(e){var t={cN:"meta",b:/^(>>>|\.\.\.) /},r={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[t],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[t],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},a={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},n={cN:"params",b:/\(/,e:/\)/,c:["self",t,a,r]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[t,a,r,e.HCM,{v:[{cN:"function",bK:"def",r:10},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,n,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}}),e.registerLanguage("ruby",function(e){var t="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},a={cN:"doctag",b:"@[A-Za-z]+"},n={b:"#<",e:">"},i=[e.C("#","$",{c:[a]}),e.C("^\\=begin","^\\=end",{c:[a],r:10}),e.C("^__END__","\\n$")],s={cN:"subst",b:"#\\{",e:"}",k:r},c={cN:"string",c:[e.BE,s],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},o={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},l=[c,n,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(i)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:t}),o].concat(i)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[c,{b:t}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+")\\s*",c:[n,{cN:"regexp",c:[e.BE,s],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(i),r:0}].concat(i);s.c=l,o.c=l;var u="[>?]>",d="[\\w#]+\\(\\w+\\):\\d+:\\d+>",b="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",p=[{b:/^\s*=>/,starts:{e:"$",c:l}},{cN:"meta",b:"^("+u+"|"+d+"|"+b+")",starts:{e:"$",c:l}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:i.concat(p).concat(l)}}),e.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek", + literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}}),e}); \ No newline at end of file diff --git a/source/assets/js/jquery.api-badge-copy-button.js b/source/assets/js/jquery.api-badge-copy-button.js new file mode 100644 index 0000000000..1b161a403c --- /dev/null +++ b/source/assets/js/jquery.api-badge-copy-button.js @@ -0,0 +1,59 @@ +;(function ($) { + "use strict"; + + var pluginName = 'apiBadgeCopyButton', + defaults = { + rawSelector: '.api-badge .icon-copy', + }; + + function Plugin(element, options) { + this.el = element; + this.$el = $(element); + this.opts = $.extend({}, defaults, options); + + this.init(); + + return this; + } + + Plugin.prototype.init = function () { + this.addEventListeners(); + }; + + Plugin.prototype.addEventListeners = function () { + this.$el.on('click', $.proxy(this.onClick, this)); + }; + + Plugin.prototype.onClick = function (event) { + var me = this; + + navigator.clipboard.writeText(me.getBodyText()).then( + function () { + me.$el.children('span').text('Copied'); + + window.setTimeout(function () { + me.$el.children('span').text(''); + }, 2500); + } + ); + }; + + Plugin.prototype.getBodyText = function () { + return this.$el.parent().next('pre').find('code.hljs').text(); + }; + + $.fn[pluginName] = function (options) { + return this.each(function () { + var element = this, + pluginData = $.data(this, 'plugin_' + pluginName); + + if (!pluginData) { + $.data(element, 'plugin_' + pluginName, new Plugin(element, options)); + } + }); + }; + + $(function () { + $(defaults.rawSelector).apiBadgeCopyButton(); + }); +})(jQuery); diff --git a/source/assets/js/jquery.backtotop.js b/source/assets/js/jquery.backtotop.js new file mode 100644 index 0000000000..9cd8ac6029 --- /dev/null +++ b/source/assets/js/jquery.backtotop.js @@ -0,0 +1,28 @@ +$(function() { + // browser window scroll (in pixels) after which the "back to top" link is shown + var offset = 300, + //browser window scroll (in pixels) after which the "back to top" link opacity is reduced + offset_opacity = 1200, + //duration of the top scrolling animation (in ms) + scroll_top_duration = 700, + //grab the "back to top" link + $back_to_top = $('.dev-top'); + + //hide or show the "back to top" link + $(window).scroll(function(){ + ( $(this).scrollTop() > offset ) ? $back_to_top.addClass('dev-is-visible') : $back_to_top.removeClass('dev-is-visible dev-fade-out'); + if( $(this).scrollTop() > offset_opacity ) { + $back_to_top.addClass('dev-fade-out'); + } + }); + + //smooth scroll to top + $back_to_top.on('click', function(event){ + event.preventDefault(); + $('body,html').animate({ + scrollTop: 0 , + }, scroll_top_duration + ); + }); + +}); diff --git a/source/assets/js/jquery.min.js b/source/assets/js/jquery.min.js new file mode 100644 index 0000000000..0de648ed3b --- /dev/null +++ b/source/assets/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.4 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.4",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssHas=ce(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssHas||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0
'); toc = new $.TableOfContents(this, scope, options); delete toc; // Free memory diff --git a/source/blog/_posts/2015-01-28-phpbenelux-2015.md b/source/blog/_posts/2015-01-28-phpbenelux-2015.md index d74ecc4cb0..f314048cbb 100644 --- a/source/blog/_posts/2015-01-28-phpbenelux-2015.md +++ b/source/blog/_posts/2015-01-28-phpbenelux-2015.md @@ -36,7 +36,7 @@ After his talk I felt temped to rewrite my json API endpoints in go. Decorating Applications with Stack by Beau D. Simensen: -Beau explained the ideas behind composing HttpKernelInterface middlewares and the stack project. Shopware is HttpKernelInterface compatible since version 4.2. +Beau explained the ideas behind composing HttpKernelInterface middlewares and the stack project. Shopware is HttpKernelInterface compatible since version 4.2. Say What? Ubiquitous Language and You!, also by Beau: diff --git a/source/blog/_posts/2015-02-11-understanding-the-shopware-http-cache.md b/source/blog/_posts/2015-02-11-understanding-the-shopware-http-cache.md index 14fffdd79a..e3e8fb7cc0 100644 --- a/source/blog/_posts/2015-02-11-understanding-the-shopware-http-cache.md +++ b/source/blog/_posts/2015-02-11-understanding-the-shopware-http-cache.md @@ -62,7 +62,7 @@ A second aspect of the cache is the cache keys: Usually the cache tells apart th ### Cache invalidation IDs -A third mechanism is the automated invalidation of pages. When rendering a listing or detail page, the HTTP cache will automatically check, which products and categories are shown on the current page and stores this information with the cached page (`x-shopware-cache-id` header). The cache plugin will then monitor products and categories for changes: When you change a product, the corresponding cache pages will be invalidated, so for the next request an uncached page will be returned. This way e.g. price changes will immediately reflect in the frontend. This automatization applies for any change which happens through doctrine models and can also be triggered by event. +A third mechanism is the automated invalidation of pages. When rendering a listing or detail page, the HTTP cache will automatically check, which products and categories are shown on the current page and stores this information with the cached page (`x-shopware-cache-id` header). The cache plugin will then monitor products and categories for changes: When you change a product, the corresponding cache pages will be invalidated, so for the next request an uncached page will be returned. This way e.g. price changes will immediately reflect in the frontend. This automatisation applies for any change which happens through doctrine models and can also be triggered by event. The actual invalidation is done by so called "BAN" requests: So once the article with ID 713 is changed, shopware's cache plugin will send a HTTP BAN request with the header `x-shopware-invalidates: a713` (`a` for articles, `c` for categories). The reverse proxy (Shopware's build in one or Varnish) will then search through all cached pages and delete all pages which have `a713` in their `x-shopware-cache-id` header. diff --git a/source/blog/_posts/2015-02-17-mastering-legacy-software-reengineering.md b/source/blog/_posts/2015-02-17-mastering-legacy-software-reengineering.md index 7ad157ebfa..0f802b2212 100644 --- a/source/blog/_posts/2015-02-17-mastering-legacy-software-reengineering.md +++ b/source/blog/_posts/2015-02-17-mastering-legacy-software-reengineering.md @@ -1,5 +1,5 @@ --- -title: New is Always Better: Mastering Legacy Software Reengineering +title: New is Always Better - Mastering Legacy Software Reengineering tags: - legacy software - architecture diff --git a/source/blog/_posts/2015-03-05-unboxing-myo-gesture-control-armband.md b/source/blog/_posts/2015-03-05-unboxing-myo-gesture-control-armband.md index a57e8cafe9..f1f22086c6 100644 --- a/source/blog/_posts/2015-03-05-unboxing-myo-gesture-control-armband.md +++ b/source/blog/_posts/2015-03-05-unboxing-myo-gesture-control-armband.md @@ -1,5 +1,5 @@ --- -title: Unboxing: Myo – Gesture Control Armband +title: Unboxing - Myo – Gesture Control Armband tags: [tech, odl] categories: @@ -36,7 +36,7 @@ This was just my first review of the Myo. We’re looking forward to test the ar **Links:** diff --git a/source/blog/_posts/2015-04-09-shopware-5-from-a-technical-point-of-view.md b/source/blog/_posts/2015-04-09-shopware-5-from-a-technical-point-of-view.md index b0f545b9f8..a6be50f3e6 100644 --- a/source/blog/_posts/2015-04-09-shopware-5-from-a-technical-point-of-view.md +++ b/source/blog/_posts/2015-04-09-shopware-5-from-a-technical-point-of-view.md @@ -10,7 +10,7 @@ indexed: true github_link: blog/_posts/2015-04-09-shopware-5-from-a-technical-point-of-view.md --- -Shopware moves with the times and ships Shopware 5 with a completely overhauled default template, which is now fully responsive. With more and more shoppers preferring to use mobile devices for online shopping, this is an important step to take. The core classes have also been revised and especially with the new SearchBundle classes, plugin authors and Shopware agencies now have a much easier time to customize article listings. +Shopware moves with the times and ships Shopware 5 with a completely overhauled default template, which is now fully responsive. With more and more shoppers preferring to use mobile devices for online shopping, this is an important step to take. The core classes have also been revised and especially with the new SearchBundle classes, plugin authors and Shopware agencies now have a much easier time to customize article listings. ![image](/blog/img/shopware-5-from-a-technical-point-of-view_1.png) @@ -55,7 +55,7 @@ class Shopware_Controllers_Frontend_ViisonLatestArticles extends Enlight_Control } } ``` -The ArticleAgeCondition is a custom filter criterion that performs the specific filtering that our plugin needs. Shopware also comes with a number of predefined conditions, so you do not always have to write these conditions yourself. The predefined conditions can be found in the SearchBundle documentation. The ArticleAgeCondition class is defined as follows: +The ArticleAgeCondition is a custom filter criterion that performs the specific filtering that our plugin needs. Shopware also comes with a number of predefined conditions, so you do not always have to write these conditions yourself. The predefined conditions can be found in the SearchBundle documentation. The ArticleAgeCondition class is defined as follows: ```php class ArticleAgeCondition implements ConditionInterface diff --git a/source/blog/_posts/2015-05-04-unboxing-meta.md b/source/blog/_posts/2015-05-04-unboxing-meta.md index 9946f22da7..825e346336 100644 --- a/source/blog/_posts/2015-05-04-unboxing-meta.md +++ b/source/blog/_posts/2015-05-04-unboxing-meta.md @@ -1,5 +1,5 @@ --- -title: Unboxing: Meta - Augmented Reality Glasses +title: Unboxing - Meta - Augmented Reality Glasses tags: [tech, odl] categories: diff --git a/source/blog/_posts/2015-06-09-understanding-the-hook-system.md b/source/blog/_posts/2015-06-09-understanding-the-hook-system.md index 3eb815ca15..13bcd2bd67 100644 --- a/source/blog/_posts/2015-06-09-understanding-the-hook-system.md +++ b/source/blog/_posts/2015-06-09-understanding-the-hook-system.md @@ -19,7 +19,7 @@ menu_order: 50 --- Shopware was built with plugin developers in mind, so there are powerful ways to modify the default behaviour of the system -without loosing backward compatibility. In this post I want to discuss the technical details of Shopware's hook system. +without losing backward compatibility. In this post I want to discuss the technical details of Shopware's hook system. ## Hook? Generally there are several ways to extend Shopware. By default we distinguish *global events*, *application events* and *hooks* @@ -57,7 +57,7 @@ extensibility come in? The proxies can be found in `cache/production____YOUR_REVISION___/proxies`. When you subscribe to a hook using ```php -$this->subscribeEvent('sArticles::sGetArticleById::before', 'myCallback); +$this->subscribeEvent('sArticles::sGetArticleById::before', 'myCallback'); ``` in your plugin's bootstrap, Shopware will regenerate those proxies and create a file like this: diff --git a/source/blog/_posts/2015-08-04-aop.md b/source/blog/_posts/2015-08-04-aop.md index 2ea41bfe40..0a6644cbf1 100644 --- a/source/blog/_posts/2015-08-04-aop.md +++ b/source/blog/_posts/2015-08-04-aop.md @@ -292,4 +292,4 @@ protected and public methods can be used as pointcuts, so it is not possible to # Further reading * [Shopware AOP](https://github.com/dnoegel/shopware-aop) Github repo of the aop shopware branch * [Discussing aspects of AOP](http://www.researchgate.net/publication/220425704_Discussing_aspects_of_AOP) Summary of AOP and cross cutting concerns in an interview with Gregor Kiczales and others -* [Strict PHP](https://github.com/Roave/StrictPhp) Strict PHP type handling; library by Marco Pivetta and Jefersson Nathan \ No newline at end of file +* [Strict PHP](https://github.com/Roave/StrictPhp) Strict PHP type handling; library by Marco Pivetta and Jefersson Nathan diff --git a/source/blog/_posts/2015-08-26-dispatch-loop.md b/source/blog/_posts/2015-08-26-dispatch-loop.md index de09d2b761..baa2646a18 100644 --- a/source/blog/_posts/2015-08-26-dispatch-loop.md +++ b/source/blog/_posts/2015-08-26-dispatch-loop.md @@ -1,5 +1,5 @@ --- -title: Bootstrapping Shopware: The dispatch loop +title: Bootstrapping Shopware - The dispatch loop tags: - dispatch loop - controller diff --git a/source/blog/_posts/2015-09-01-plugin-generation.md b/source/blog/_posts/2015-09-01-plugin-generation.md index f68241a474..bf8d5f14f9 100644 --- a/source/blog/_posts/2015-09-01-plugin-generation.md +++ b/source/blog/_posts/2015-09-01-plugin-generation.md @@ -97,7 +97,7 @@ With Shopware 5, the new `StoreFrontBundle` was introduced. It will allow you to This way, adding additional filters, sorters and conditions is very easy - even though the changed mindset behind this components might seem confusing at first. -By running `sw plugin:create --haveFilters SwagFilterTest` you can easily create the boilerplate code for a simple example +By running `sw plugin:create --haveFilter SwagFilterTest` you can easily create the boilerplate code for a simple example which you can then extend to your needs: ``` diff --git a/source/blog/_posts/2015-10-28-extending-the-shopware-cli-tools.md b/source/blog/_posts/2015-10-28-extending-the-shopware-cli-tools.md index 44dbbe5885..50d8348baf 100644 --- a/source/blog/_posts/2015-10-28-extending-the-shopware-cli-tools.md +++ b/source/blog/_posts/2015-10-28-extending-the-shopware-cli-tools.md @@ -36,7 +36,7 @@ In these first steps, we will explain how we created the `DataGenerator` extensi As the Shopware CLI tools are designed to be extended, we started by creating our own extension directory. We called it `DataGenerator`, and you can find it inside the `src/Extensions/Shopware` directory. Next, we declared our extension. That was done using the `Bootstrap.php`. The file itself is not very big, but has some relevant content: -+ `class Bootstrap implements ContainerAwareExtension, ConsoleAwareExtension`: the `Bootstrap` class name and implementing `ConsoleAwareExtension` are both required. As we plan on using the DI Container, we also implement the optional `ContainerAwareExtension` interface. ++ `class Bootstrap implements ContainerAwareExtension, ConsoleAwareExtension`: the `Bootstrap` class name and implementing `ConsoleAwareExtension` are both required. As we plan on using the DI container, we also implement the optional `ContainerAwareExtension` interface. + `public function setContainer(ContainerBuilder $container)`: this is where we specify our services that will be available in the DI container. Don't worry about its content for now. + `public function getConsoleCommands()`: this is where we declared our custom command. Our DataGenerator extension has only one command, but each extension can have as many commands as we (reasonably) want. @@ -46,7 +46,7 @@ Now that the `Bootstrap.php` file has declared our extension, it's time to imple Implementing any command in the Shopware CLI tools is as easy as [creating a Symfony Console Command](http://symfony.com/doc/current/cookbook/console/console_command.html). If you have used Symfony's Console Commands before, you will feel right at home. -We created a `src/Extensions/Shopware/Command` directory to house our custom `CreateDataCommand.php` file. The command itself has no Shopware specific logic: this is a pure Symfony2 Console Command implementation. The only feature we added is that, by extending the `ShopwareCli\Command\BaseCommand` class, we automatically inject the DI Container into the command. If your custom command doesn't need the DI Container, feel free to skip this intermediate abstract class, and have your command directly extend the Symfony's `Symfony\Component\Console\Command\Command` class, like any Symfony Console Command would. +We created a `src/Extensions/Shopware/Command` directory to house our custom `CreateDataCommand.php` file. The command itself has no Shopware specific logic: this is a pure Symfony2 Console Command implementation. The only feature we added is that, by extending the `ShopwareCli\Command\BaseCommand` class, we automatically inject the DI container into the command. If your custom command doesn't need the DI container, feel free to skip this intermediate abstract class, and have your command directly extend the Symfony's `Symfony\Component\Console\Command\Command` class, like any Symfony Console Command would. Next, we implemented some empty stub methods to make our command respect the requirements of the abstract class it implements, and voila, we had a fully functional command that did... absolutely nothing. @@ -58,7 +58,7 @@ In the next steps, we will discuss how we implemented the Data Generator. This i ### The Command class -As a rule of thumb, the Command class shouldn't do much besides handling input validation and delegating work to actual worker services. If you take a closer look at all the existing Command classes in the Shopware CLI Tools, you will see that all of them parse and validate the input the user provides (or should have provided), and then make extensive usage of the DI Container and its services to do the actual actions the command is supposed to do. +As a rule of thumb, the Command class shouldn't do much besides handling input validation and delegating work to actual worker services. If you take a closer look at all the existing Command classes in the Shopware CLI Tools, you will see that all of them parse and validate the input the user provides (or should have provided), and then make extensive usage of the DI container and its services to do the actual actions the command is supposed to do. ### The Services directory diff --git a/source/blog/_posts/2015-11-04-javascript-interfaces.md b/source/blog/_posts/2015-11-04-javascript-interfaces.md index a47e1697ab..50b15e4309 100644 --- a/source/blog/_posts/2015-11-04-javascript-interfaces.md +++ b/source/blog/_posts/2015-11-04-javascript-interfaces.md @@ -22,6 +22,6 @@ It means that an object with the methods `walk()`, `swim()` and `quack()` can al But JavaScript is also a very flexible language and of course there are different ways to implement some kind of interface functionality. You will find many articles around the web about this topic. The problem is, without native support, you always have to manually ensure that a class implements the interface you're providing. So you can mitigate the problem, but there is no realistic way to force a third party developer to use your interface as intended. It is all about providing good documentation to your code. -In Shopware we're using ExtJS for the administration panel of the shop system, which is a really complex JavaScript framework. Of course it has features for inheritance like creating and extending classes. But sometimes we're really missing the functionality of an interface, especially when encouraging third party developers to build new modules on top of the existing platform. In the new Digital Publishing module for example, you are able to create custom elements which can be used inside the module. To provide an easy way for creating these elements, we built a kind of abstract class, which defines the basic set of methods and properties the element should use. Developers can extend this abstract class to create new elements right out of the box, without taking care about data handling and all that stuff. But also this is no real interface, so we have to encourage all developers to work through the documentation and use the code as intended. +In Shopware we're using ExtJS for the administration panel of the shop system, which is a really complex JavaScript framework. Of course it has features for inheritance like creating and extending classes. But sometimes we're really missing the functionality of an interface, especially when encouraging third party developers to build new modules on top of the existing platform. In the new Digital Publishing module for example, you are able to create custom elements which can be used inside the module. To provide an easy way for creating these elements, we built a kind of abstract class, which defines the basic set of methods and properties the element should use. Developers can extend this abstract class to create new elements right out of the box, without taking care about data handling and all that stuff. But also this is no real interface, so we have to encourage all developers to work through the documentation and use the code as intended. -What are your experiences with such a kind of abstraction in JavaScript? Write me on twitter. \ No newline at end of file +What are your experiences with such a kind of abstraction in JavaScript? Write me on twitter. diff --git a/source/blog/_posts/2015-11-11-best-practices.md b/source/blog/_posts/2015-11-11-best-practices.md index 8a842ad389..4607224e2d 100644 --- a/source/blog/_posts/2015-11-11-best-practices.md +++ b/source/blog/_posts/2015-11-11-best-practices.md @@ -26,7 +26,7 @@ but I think they should be helpful, if you are wondering which way to go. ## Cleaning up the Bootstrap.php The `Bootstrap.php` file, as the main entry point of every plugin, tends to be bloated in many plugins I've seen. There isn't actually a good reason for this. The fact that there is a `Bootstrap.php` doesn't mean that you have to put all -you plugin's logic into it. +your plugin's logic into it. ### Install / update @@ -189,7 +189,7 @@ public function registerNamespaces() ``` It will register the namespace `Shopware\Plugins\MyPlugin` to the current plugin directory. Instead of calling this -from the `onStartDispatch` callback, you can also call it in the `afterInit` method of you plugin. Either way: once you have +from the `onStartDispatch` callback, you can also call it in the `afterInit` method of your plugin. Either way: once you have this call in place, creating a service is really easy: ``` @@ -427,7 +427,7 @@ This is not only good for testability - but also for the code quality. ## plugin.json Some time ago, we started to move all the meta data from the plugin's `Boostrap.php` to a file called `plugin.json`. -An example can be found in the [Paypal plugin on github](https://github.com/shopwareLabs/SwagPaymentPaypalPlus/blob/master/plugin.json). +An example can be found in the [Paypal plugin on github](https://github.com/shopware5/SwagPaymentPaypalPlus/blob/master/plugin.json). I find it very convenient to use this file - it will give you a good overview regarding Shopware version compatibility, change logs and other relevant information. If we release our Shopware account API at some point in the future, the @@ -680,4 +680,4 @@ release, as some plugin versions just have code changes. So you will probably ne for this reason your update logic should be written in a way, that it applies to all versions e.g. "being smaller than 2.1.0". Also keep in mind that moving the update logic to separate classes (for big plugins, consider per-version migration files) -might help keeping the `Bootstrap.php` file small. \ No newline at end of file +might help keeping the `Bootstrap.php` file small. diff --git a/source/blog/_posts/2015-12-04-working-with-the-http-cache.md b/source/blog/_posts/2015-12-04-working-with-the-http-cache.md deleted file mode 100644 index e6bf2fe1d9..0000000000 --- a/source/blog/_posts/2015-12-04-working-with-the-http-cache.md +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/source/blog/_posts/2016-01-26-shopware-environment.md b/source/blog/_posts/2016-01-26-shopware-environment.md index 45952bac59..74a607de5f 100644 --- a/source/blog/_posts/2016-01-26-shopware-environment.md +++ b/source/blog/_posts/2016-01-26-shopware-environment.md @@ -17,7 +17,7 @@ This can be used to load different config files and to separate cache files. ## How the environment is handled -This environment variable is picked by [`shopware.php`](https://github.com/shopware/shopware/blob/5.2/shopware.php#L92) and [`bin/console`](https://github.com/shopware/shopware/blob/5.2/bin/console#L34) and then propagated to the kernel: +This environment variable is picked by [`shopware.php`](https://github.com/shopware5/shopware/blob/5.2/shopware.php#L92) and [`bin/console`](https://github.com/shopware5/shopware/blob/5.2/bin/console#L34) and then propagated to the kernel: ```php $environment = getenv('SHOPWARE_ENV') ?: 'production'; @@ -51,7 +51,7 @@ var ## Config loading -The config loading order is defined in [engine/Shopware/Configs/Default.php](https://github.com/shopware/shopware/blob/cbafdd378de2bf5afab7504a90a2bc184ebbd681/engine/Shopware/Configs/Default.php#L3). +The config loading order is defined in [engine/Shopware/Configs/Default.php](https://github.com/shopware5/shopware/blob/cbafdd378de2bf5afab7504a90a2bc184ebbd681/engine/Shopware/Configs/Default.php#L3). The config loader looks for a file named `config_[ENVIRONMENT].php` first. If that file does not exists it falls back to `config.php`. On my development machine all my installations contain a file named `config_dev.php`. diff --git a/source/blog/_posts/2016-02-11-hackathon-2016-1.md b/source/blog/_posts/2016-02-11-hackathon-2016-1.md index 96fe2202a0..e569ea324f 100644 --- a/source/blog/_posts/2016-02-11-hackathon-2016-1.md +++ b/source/blog/_posts/2016-02-11-hackathon-2016-1.md @@ -226,7 +226,7 @@ your complex shopware project can now have a dependency to the `shopware/shopwar To archive this, a few changes in the Shopware core had to be made as no files should be created and modified inside the vendor/shopware/shopware directory. So all user generated files, like plugins, user themes, downloads etc. had to be -moved up the the project level. +moved up the project level.
@@ -257,4 +257,3 @@ there is little control over password quality and change intervals. In this proj to have a secure and unified login, that doesn't need further password changes on the machines itself.
- diff --git a/source/blog/_posts/2016-02-26-umask.md b/source/blog/_posts/2016-02-26-umask.md index ddbc3f408e..44b895af95 100644 --- a/source/blog/_posts/2016-02-26-umask.md +++ b/source/blog/_posts/2016-02-26-umask.md @@ -163,7 +163,7 @@ Also the permissions of cache files are now using the umask instead of hard code ] ``` -You can find all the changes in the [Github Pull Request](https://github.com/shopware/shopware/pull/410). +You can find all the changes in the [Github Pull Request](https://github.com/shopware5/shopware/pull/410). This changes will be included in the upcoming Shopware 5.1.4 release. @@ -172,5 +172,3 @@ This changes will be included in the upcoming Shopware 5.1.4 release. Now that Shopware respects the umask for all cache files there no need to configure individual file creation permissions anymore. Just set the umask and Shopware will create files accordingly. The umask can be set in your Apache or PHP-FPM configuration, or by calling `umask(0077);` in you projects `config.php`. - - diff --git a/source/blog/_posts/2016-04-07-introducing-project-tango.md b/source/blog/_posts/2016-04-07-introducing-project-tango.md index 8a2280c69c..2f82786fdd 100644 --- a/source/blog/_posts/2016-04-07-introducing-project-tango.md +++ b/source/blog/_posts/2016-04-07-introducing-project-tango.md @@ -42,8 +42,8 @@ The constructor application uses all the sensors to create a 3D mesh of the spac When you're done with the capturing you can save the mesh to the SD card and view it in the constructor app. You can virtually move through the created mesh by using the touch screen. All meshes are primary saved as ```.srb``` files, but you can choose to export a mesh to common file types like ```.obj``` or ```.ply```. You can transfer the files to your computer to import them to Unity or use it in other 3D applications. -The features of the device really got me excited. The way of how the device capture objects in real time can bring a whole new level to augmented reality applications and 3D creation. This kind of device can fill a hole in the process of realizing new concepts like virtual shopping or visualizing 3D products. +The features of the device really got me excited. The way of how the device capture objects in real time can bring a whole new level to augmented reality applications and 3D creation. This kind of device can fill a hole in the process of realizing new concepts like virtual shopping or visualizing 3D products. If you want to learn more about Project Tango you should visit the official website. -When you have questions or new ideas for this interesting device, feel free to get in touch on twitter. \ No newline at end of file +When you have questions or new ideas for this interesting device, feel free to get in touch on twitter. diff --git a/source/blog/_posts/2016-07-11-on-action-tags.md b/source/blog/_posts/2016-07-11-on-action-tags.md index 80e6c89046..b0cbb1577a 100644 --- a/source/blog/_posts/2016-07-11-on-action-tags.md +++ b/source/blog/_posts/2016-07-11-on-action-tags.md @@ -170,7 +170,7 @@ are inevitable, make sure that they *are cached as well* or that they are *not u Some time ago, I was wondering, if we couldn't just handle ESI tags with Ajax queries. As those queries could be handled all at once, we might be able to handle 3 queries with 1000ms each in (theoretically) 1000ms - instead of 3000 as in the example before. -The proof of concept plugin I wrote will basically subscribe to PostDsipatch events and remote the "Surrogate-Control" +The proof of concept plugin I wrote will basically subscribe to PostDispatch events and remote the "Surrogate-Control" header. This way, caches will ignore the ESI tags and deliver those tags to the client. A simple jQuery plugin will then find those tags and perform the ESI request via Ajax: @@ -231,12 +231,12 @@ There are two downsides of this approach, however: 1. This approach will require a custom controller, that is able to dispatch multiple URLs. This is doable - but increases the complexity of the system. 2. When multiple ESI tags are requested in one HTTP request, the handling of the cache time also becomes more complex: -You will need to figure out the lowest common cache time of all routes and set that for the request. But if the overal +You will need to figure out the lowest common cache time of all routes and set that for the request. But if the overall cache time is defined by the lowest cache time - what is the point in having varying cache times in the first place? So even though this approach looks promising on a first glance, I assume that it would introduce too much complexity to solve a problem that should better be handled in your application's logic: If you need e.g. many ESI requests for prices -or instock info, having a distinct Ajax requests that will fetch this info in one Ajax call is a way better solutions then +or instock info, having a distinct Ajax requests that will fetch this info in one Ajax call is a way better solution then to introduce the "batch request" ESI handling discussed here. ## Conclusion diff --git a/source/blog/_posts/2016-09-05-psh-managing-build-complexity.md b/source/blog/_posts/2016-09-05-psh-managing-build-complexity.md index c90770d0b9..a1dfba5d16 100644 --- a/source/blog/_posts/2016-09-05-psh-managing-build-complexity.md +++ b/source/blog/_posts/2016-09-05-psh-managing-build-complexity.md @@ -277,7 +277,7 @@ I think a quick adaption rate to changes is entirely preferable. #### Bash only, why not? -So why even use `PSH`? And I get the point. [As was pointed out to me](https://github.com/shopware/devdocs/pull/353#issuecomment-244027980) bash already brings some capabilities that PSH emulates. +So why even use `PSH`? And I get the point. [As was pointed out to me](https://github.com/shopware5/devdocs/pull/353#issuecomment-244027980) bash already brings some capabilities that PSH emulates. So let's discuss this a little bit here. **`set -e` - Put it at the top of your script and execution fails if a statement fails.** diff --git a/source/blog/_posts/2016-09-12-99-paper-cuts-status.md b/source/blog/_posts/2016-09-12-99-paper-cuts-status.md index 1a33ef2f04..f0ca0c1643 100644 --- a/source/blog/_posts/2016-09-12-99-paper-cuts-status.md +++ b/source/blog/_posts/2016-09-12-99-paper-cuts-status.md @@ -6,12 +6,12 @@ authors: [bc] We are now on day 8 of our project 99 Papercuts, with 7 days to go. -From now until Monday, 19th September, you can participate in our [99 Papercuts Project](https://developers.shopware.com/99-paper-cuts/) and make Shopware better - one papercut at a time. +From now until Monday, 19th September, you can participate in our 99 Papercuts Project and make Shopware better - one papercut at a time. -The feedback so far has been pretty good; we've already received several contributions and [merged](https://github.com/shopware/shopware/pulls?q=is%3Apr+%5BPapercut%5D+is%3Aclosed) a good amount of pull requests. -We also noticed increased activity in our [IRC Channel](/contributing/irc/), where you can get in touch with the Community. +The feedback so far has been pretty good; we've already received several contributions and [merged](https://github.com/shopware5/shopware/pulls?q=is%3Apr+%5BPapercut%5D+is%3Aclosed) a good amount of pull requests. +We also noticed increased activity in our [IRC Channel](/community/irc/), where you can get in touch with the Community. -You can help by reviewing the [open](https://github.com/shopware/shopware/pulls?utf8=%E2%9C%93&q=%3Apr%20is%3Aopen%20%5BPapercut%5D) pull requests and providing feedback. +You can help by reviewing the [open](https://github.com/shopware5/shopware/pulls?utf8=%E2%9C%93&q=%3Apr%20is%3Aopen%20%5BPapercut%5D) pull requests and providing feedback. ## New Pull Request Workflow @@ -37,7 +37,4 @@ Tickets have already been created in our Issue Tracker, and we have no schedulin ## Get involved and participate Now it's your turn. Find a papercut bug that bothers you and try to fix it. - - - - + diff --git a/source/blog/_posts/2016-10-06-qa-at-speed.md b/source/blog/_posts/2016-10-06-qa-at-speed.md index 5bbdc7f2cc..2ea39db3d5 100644 --- a/source/blog/_posts/2016-10-06-qa-at-speed.md +++ b/source/blog/_posts/2016-10-06-qa-at-speed.md @@ -9,7 +9,7 @@ little more insight into our quality assurance (QA) process. As a software company, we try to cover the whole development process in an agile culture, and since the majority of our teams are using Scrum or Kanban, we try to strictly follow the agile manifesto. However, in an agile environment, it’s sometimes difficult to fluently integrate the quality assurance process. -Before I go into what works well, let me share an attitude that doesn’t work at all: "We don't need extra testers. Tell the developers they can take more time developing and testing things by themselves, but they have to stop creating bugs." +Before I go into what works well, let me share an attitude that doesn't work at all: "We don't need extra testers. Tell the developers they can take more time developing and testing things by themselves, but they have to stop creating bugs." I don’t think I need to explain why this statement is completely counterproductive, and I’m glad this viewpoint never found its way to Shopware. diff --git a/source/blog/_posts/2016-10-12-promises.md b/source/blog/_posts/2016-10-12-promises.md index d483006f37..261d5bcb6c 100644 --- a/source/blog/_posts/2016-10-12-promises.md +++ b/source/blog/_posts/2016-10-12-promises.md @@ -346,8 +346,8 @@ jQuery uses the [CommonJS Promises/A interface](http://wiki.commonjs.org/wiki/Pr There's just one downside you have to consider when working with promises or the Deferred object. jQuery's promises are linked to a Deferred object stored on the `.data()` for an element. Since the `.remove()` method removes the element's data as well as the element itself, it will prevent any of the element's unresolved Promises from resolving. ## Conclusion -Promises are looking like a brand new technology but apparently they are not. jQuery introduced them with version 1.5 which was released back in 2011. They provide a great and easy to use way to overcome the problem of having nesting callbacks in your application. As we saw in the blog post it's very easy to transform your callback code into a promised based approach which provides a higher flexibility and an easier maintainable code base. +Promises are looking like a brand new technology, but apparently they are not. jQuery introduced them with version 1.5 which was released back in 2011. They provide a great and easy to use way to overcome the problem of having nesting callbacks in your application. As we saw in the blog post it's very easy to transform your callback code into a promised based approach which provides a higher flexibility and an easier maintainable code base. The compatibility is great, there are a lot of polyfills out there and the fact that jQuery comes with an own implementation of promises and deferred functions let me think, that there's no reason not to use promises now. -We went over the basics on what you can do with promises. There are a bunch of different methods in the offical standard which allow you to do much more advanced things. We'll cover them in one of the next blog posts. +We went over the basics on what you can do with promises. There are a bunch of different methods in the official standard which allow you to do much more advanced things. We'll cover them in one of the next blog posts. diff --git a/source/blog/_posts/2016-12-05-large-scale-plugin-architecture.md b/source/blog/_posts/2016-12-05-large-scale-plugin-architecture.md index c392096a49..79c937c34a 100644 --- a/source/blog/_posts/2016-12-05-large-scale-plugin-architecture.md +++ b/source/blog/_posts/2016-12-05-large-scale-plugin-architecture.md @@ -35,7 +35,7 @@ And how complex might the tasks get? | ------------- | ------------- | ----- | | Simple Entity management | Is a simple use case with an easy implementation. The only caveats here is extensibility. | CRUD | | Complex Workflows | Unknown complex stuff, might not have common solutions. | The real fun :) | -| Complex due to Shopware | Should be a simple use case, but is not easily implemented. | Uggly code | +| Complex due to Shopware | Should be a simple use case, but is not easily implemented. | Ugly code | So now we have different required parts, learned a little bit about the actors using these parts. And we also gained some insight as to how complex these parts might get. @@ -257,4 +257,4 @@ One could argue that singling out reasons to change is over engineering. And dep The main concern one could therefore have is that an architecture like that encourages the *[Not invented here syndrome](https://en.wikipedia.org/wiki/Not_invented_here)*, which certainly can be the case. The bridges encourage cherry picking, and it suddenly becomes a conscious and meaningful decision if you want to reuse something provided by Shopware or are more comfortable with deploying your own solution. But this simply is how modern development works. And opening a topic for discussion should hardly be a problem. -I usually always recommend the idea over the implementation. But after developing in this structure for the better part of the past year I see a great deal of long time potential in this type of technical architecture. \ No newline at end of file +I usually always recommend the idea over the implementation. But after developing in this structure for the better part of the past year I see a great deal of long time potential in this type of technical architecture. diff --git a/source/blog/_posts/2016-12-09-new-cart-bundle.md b/source/blog/_posts/2016-12-09-new-cart-bundle.md index 3abe5331af..1bdc5333d4 100644 --- a/source/blog/_posts/2016-12-09-new-cart-bundle.md +++ b/source/blog/_posts/2016-12-09-new-cart-bundle.md @@ -16,16 +16,27 @@ Since the last Community Day, we've received several questions about Shopware's - *How far are you with the development* - *Which new features come with the new shopping cart?* -At the Community Day, we also announced that we are striving for open development in the refactoring process in order to get as much feedback as possible and to be able to work more closely with other developers and partners. We want to realize this now by sharing the first concept of the new shopping cart. You can see the development process on Github, where we created a new repository which allows the Community to create pull requests and issues. The new repository contains a new bundle in `/engine/Shopware/Bundle/CartBundle`, which contains a first proof of concept for a new cart process. This first concept can change steadily due to growing requirements and that refactoring will be an ongoing process. The first features we implemented are the following: +At the Community Day, we also announced that we are striving for open development in the refactoring process in order to get as much feedback as possible and to be able to work more closely with other developers and partners. +We want to realize this now by sharing the first concept of the new shopping cart. +You can see the development process on Github, +where we created a new repository which allows the Community to create pull requests and issues. +The new repository contains a new bundle in `/engine/Shopware/Bundle/CartBundle`, which contains a first proof of concept for a new cart process. +This first concept can change steadily due to growing requirements and that refactoring will be an ongoing process. +The first features we implemented are the following: - Add, delete and change quantity of product line items - Add and delete percentage-based vouchers - First concept for partial delivery -There's currently no storefront integration. A view layer will follow after all calculation processes are tested. For that reason, the classes are only used inside unit tests. From a technical perspective, the cart already contains the following concepts/features: +There's currently no storefront integration. A view layer will follow after all calculation processes are tested. +For that reason, the classes are only used inside unit tests. +From a technical perspective, the cart already contains the following concepts/features: - Percentage price calculation - Gross and net price calculation - Proportional tax calculation - Exchangeable gateway for product prices and delivery information - First proof of concept for a partial delivery to different addresses and delivery dates -If you want to get more information about the technical concept and current implementation, take a look at our new developers guide article. This article is created to document the current state of the implementation and will contain more content regarding progressive refactoring. Due to the complexity and importance of the shopping cart inside Shopware, this will be a long-term project. Short-term integration into the core product is therefore not to be expected. +If you want to get more information about the technical concept and current implementation, take a look at our new developers guide article. +This article is created to document the current state of the implementation and will contain more content regarding progressive refactoring. +Due to the complexity and importance of the shopping cart inside Shopware, this will be a long-term project. +Short-term integration into the core product is therefore not to be expected. diff --git a/source/blog/_posts/2017-02-06-javascript-currency-formatting.md b/source/blog/_posts/2017-02-06-javascript-currency-formatting.md index 8b5dda717c..e4114e86ca 100644 --- a/source/blog/_posts/2017-02-06-javascript-currency-formatting.md +++ b/source/blog/_posts/2017-02-06-javascript-currency-formatting.md @@ -25,7 +25,7 @@ is to have a function like this in place: ``` function formatCurrency (val) { - return val.toString.replace('.', ',') + ' €'; + return val.toString().replace('.', ',') + ' €'; } ``` *Very simple example for a currency formatter function* diff --git a/source/blog/_posts/2017-03-14-call-for-papers.md b/source/blog/_posts/2017-03-14-call-for-papers.md index 5a303816fd..9ef773a532 100644 --- a/source/blog/_posts/2017-03-14-call-for-papers.md +++ b/source/blog/_posts/2017-03-14-call-for-papers.md @@ -1,5 +1,5 @@ --- -title: Call for papers: Shopware Community Day 2017 +title: Call for papers - Shopware Community Day 2017 tags: - cfp - shopware community day @@ -19,4 +19,4 @@ The [Shopware Community Day](https://scd.shopware.com/) is on June 09, and we ar Whatever you want to tell or show the shopware community, don't hesitate to submit your talk. Whatever it is, we will have a look into it. -See you there! :) \ No newline at end of file +See you there! :) diff --git a/source/blog/_posts/2017-06-26-shopping-worlds-without-ajax.md b/source/blog/_posts/2017-06-26-shopping-worlds-without-ajax.md new file mode 100644 index 0000000000..7dcfabb7f0 --- /dev/null +++ b/source/blog/_posts/2017-06-26-shopping-worlds-without-ajax.md @@ -0,0 +1,41 @@ +--- +title: Quick Tip - Shopping worlds without AJAX +tags: +- javascript +- shoppingworlds +- emotions + +categories: +- dev + +authors: [stp] +github_link: blog/_posts/2017-06-26-shopping-worlds-without-ajax.md + +--- + +Today we're back with a short & simple tip which allows you to load shopping worlds at any place in your store front without the need of loading them using AJAX. Do to so, please create your own frontend theme, if you haven't one in place already. Please refer to our [Templating Getting Started Guide](https://developers.shopware.com/designers-guide/getting-started/#custom-themes) on how to create your own custom theme. + +After creating your own theme, please create a new JavaScript file in your `_public/src/js` directory & place the following content into it: + +``` +window.StateManager + .removePlugin('.emotion--wrapper', 'swEmotionLoader') + .addPlugin('.emotion--wrapper:not(.emotion--non-ajax)', 'swEmotionLoader') + .addPlugin('.emotion--non-ajax *[data-emotion="true"]', 'swEmotion'); +``` + +We're removing the `swEmotionLoader` jQuery plugin which is the entry point for an AJAX shopping world. We're removing it because we have to modify the selector for the jQuery plugin. In the next line we're adding the same plugin but with a different selector. The selector allows us to add the class `emotion--non-ajax` to the element which should contain the shopping world later on. Last but not least, we're adding the jQuery plugin `swEmotion` to the plugin queue with our `emotion--non-ajax` class in place. + +After you've added the content to the JavaScript file, make sure you've registered the file in the `$javascript` array in your `Theme.php` file. If you need further information, please head over to our [CSS & JS Files Usage Guide](https://developers.shopware.com/designers-guide/css-and-js-files-usage/#add-javascript-files). + +With these changes in place, you're now able to include shopping worlds literally at any place in your store front with the following code snippet: + +``` +
+ {action module="widgets" controller="emotion" action="index" emotionId="7"} +
+``` + +The argument `emotionId` in the widget call lets you choose what shopping world you would like to include. To get an overview of all available shopping worlds, please refer to the `s_emotion` database table. + +You may have to customize the styling of the shopping world, depending on what section of the store front you're using it. diff --git a/source/blog/_posts/2017-07-24-seo-urls-in-plugins.md b/source/blog/_posts/2017-07-24-seo-urls-in-plugins.md new file mode 100644 index 0000000000..e342eaff03 --- /dev/null +++ b/source/blog/_posts/2017-07-24-seo-urls-in-plugins.md @@ -0,0 +1,435 @@ +--- +title: SEO URLs in plugins +tags: +- seo +- plugins + +categories: +- dev + +authors: [ps] +github_link: blog/_posts/2017-07-24-seo-urls-in-plugins.md + +--- + +In the world of eCommerce, SEO is a very important and recurrent topic. +Thus, Shopware offers some tools to create a SEO friendly shop by default, including SEO friendly URLs. +Make sure to have a look at the following SEO blog post, covering detailed information for the Shopware SEO engine: [The Shopware SEO engine](/blog/2015/08/11/the-shopware-seo-engine/) + +But for now, how do we actually create proper SEO URLs for our custom plugins? + +It must have been about a year ago when I stumbled across the same issue while reworking our premium plugin +[Shopping advisor](http://store.shopware.com/en/swagproductadvisor/shopping-advisor.html). + +In this blog post, I want to provide a short tutorial on how to implement custom SEO URLs for your plugins. +I'll also attach an example plugin for both Shopware 5.3 and 5.2 at the end of the tutorial. + +## Generating a SEO URL for a custom controller + +For this short tutorial I will use a very basic plugin based on the new plugin system. + +### The scenario + +As an example, I would like to create a glossary plugin. + +Using the glossary plugin, the shop owner should be able to create a description for a word. +The plugin will provide an overview, showing all given words and their description. + +To store those, I created a table with the name `s_glossary` and the columns `id`, `word` and `description`. + +Later in this tutorial we also want to add a detail page, only showing a given word and its respective description. + +Let's assume the basic plugin structure contains a registered Frontend controller called `Glossary`, as well as the mentioned +database table. + +### Let's get started + +For the glossary overview, we would implement an `indexAction` in our `Glossary` controller to handle the overview. +In order to call our action now, we'd open the following URL: `http://myShop.com/glossary/` + +That URL looks smooth and SEO friendly already, doesn't it? + +What happens, if we want the glossary page to be internationally available? +For your german customers, you would want the glossary to be available using `http://myShop.com/glossar/` as well. + +This can and should be done using SEO URLs. + +First of all, SEO URLs in Shopware are stored in the database table `s_core_rewrite_urls`. +We could just create a new entry in that table during the installation process of the plugin. +That would actually work for now. + +Yet, we want to create those SEO URLs depending on the 'refresh strategies' configuration. + +This configuration can be found in the backend: `Configuration > Cache/performance > Settings > SEO > Refresh strategy`. +Our SEO URLs are generated in three different ways, being configurable in the backend. + +Available options are: +- Manually +- Via cronjob +- Live + +Again, refer to this blog post to get more detailed information on how those work: [The Shopware SEO engine](/blog/2015/08/11/the-shopware-seo-engine/) + +As each of the options mentioned above requires slightly different plugin logic, I'll explain them step by step. + +### Implement logic for 'Via cronjob' + +
+ The following code is only compatible with Shopware version 5.3 or higher. +
+ +In **Shopware 5.3** we implemented a new event to SEO URL generation using the cronjob.
+Everytime the cronjob `RefreshSeoIndex` is triggered, the method `onRefreshSeoIndex` in [engine/Shopware/Plugins/Default/Core/RebuildIndex/Bootstrap.php](https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Plugins/Default/Core/RebuildIndex/Bootstrap.php#L134) is called.
+It now contains a new notify event called `Shopware_CronJob_RefreshSeoIndex_CreateRewriteTable`, which we will use to add our own SEO URL generation process. +The event is called once for each shop after every other SEO URL (e.g. Products, Categories, ...) has been generated for this shop. + +``` +public static function getSubscribedEvents() +{ + return [ + 'Shopware_CronJob_RefreshSeoIndex_CreateRewriteTable' => 'createGlossaryRewriteTable' + ]; +} + +public function createGlossaryRewriteTable() +{ + /** @var \sRewriteTable $rewriteTableModule */ + $rewriteTableModule = Shopware()->Container()->get('modules')->sRewriteTable(); + + // Insert new rewrite URL for our custom controller + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); +} +``` + +In the example mentioned above, we would create a new rewrite URL for each shop. +Of course, in this code we could and should now build our logic to create the translated rewrite URLs, e.g. `http://myShop.com/glossar`, which would be the german translation for it. + +### Implement logic for 'Live' + +This option does **not** mean, that with each and every request the SEO URLs are re-generated. +You can configure the refresh interval in the backend under `Configuration > Cache/performance > Settings > SEO > Refresh strategy`. + +Basically, whenever a request is sent to the shop and the response is about to be sent back, Shopware checks if it's time to re-generate the SEO URLs. +In only that case (refresh strategy is 'live' AND the interval has passed), the method `sCreateRewriteTable` from our core module [sRewriteTable](https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Core/sRewriteTable.php#L220) is called. + +This method only generates the SEO URLs for the **currently** active shop. + +Therefore we could use an after hook on the method mentioned above. +The code to actually insert our URL into the database is the same, so we can just re-use the same code with a different event. + +``` +public static function getSubscribedEvents() +{ + return [ + 'Shopware_CronJob_RefreshSeoIndex_CreateRewriteTable' => 'createGlossaryRewriteTable', + 'sRewriteTable::sCreateRewriteTable::after' => 'createGlossaryRewriteTable', + ]; +} + +public function createGlossaryRewriteTable() +{ + /** @var \sRewriteTable $rewriteTableModule */ + $rewriteTableModule = Shopware()->Container()->get('modules')->sRewriteTable(); + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); +} +``` + +That's it for the live mode. + +### Implement logic for 'Manual' + +
+ The following code is only compatible with Shopware version 5.3 or higher. +
+
+ Overview of the SEO URL concept +
The manual SEO URL generation window
+
+ +This is where things become a little tricky. +The manual URL generation is actually handled in ExtJs, generating the URLs in a batch mode. + +You can choose a batch size, which defines how many URLs should be generated with each request. + +We want to have our own progress bar at the bottom of the window now to generate our SEO URLs for the currently selected shop in batch mode. + +First of all we have to extend the file [themes/Backend/ExtJs/backend/performance/view/main/multi_request_tasks.js](https://github.com/shopware5/shopware/blob/5.3/themes/Backend/ExtJs/backend/performance/view/main/multi_request_tasks.js#L83). +We have to extend the property 'seo', which contains all progress bars, their snippets and, most important, the request URL to be called for each batch call to generate the SEO URLs. + +So, let's overwrite the ExtJs window. +I won't go into detail on how to extend an ExtJs file. Refer to this guide about [extending the backend](/developers-guide/backend-extension/#example-#1:-simple-extension) instead. + +*Register new event:* +``` +public static function getSubscribedEvents() +{ + return [ + ... + 'Enlight_Controller_Action_PostDispatch_Backend_Performance' => 'loadPerformanceExtension' + ]; +} +``` + +
+ +*The respective listener:* +``` +public function loadPerformanceExtension(\Enlight_Controller_ActionEventArgs $args) +{ + $subject = $args->getSubject(); + $request = $subject->Request(); + + if ($request->getActionName() !== 'load') { + return; + } + + $subject->View()->addTemplateDir(__DIR__ . '/Resources/views/'); + $subject->View()->extendsTemplate('backend/performance/view/glossary.js'); +} +``` + +With Shopware 5.3 we implemented a new method called `addProgressBar` to `multi_request_tasks.js`. +As the first parameter you have to provide an object containing an 'initialText' to be shown initially, a 'progressText' to be shown while generating the SEO URLs +and a 'requestUrl' to be called with each step in the batch processing. +The second parameter has to be a name for the new progress bar - we need this one later. +The third parameter should be the target. Possible values are 'seo' and 'httpCache'. As we want to create a new progress bar to the SEO window, we'll use 'seo' here obviously. + +``` +//{block name="backend/performance/view/main/multi_request_tasks" append} +Ext.define('Shopware.apps.Performance.view.main.Glossary', { + override: 'Shopware.apps.Performance.view.main.MultiRequestTasks', + + initComponent: function() { + this.addProgressBar( + { + initialText: 'Glossary URLs', + progressText: '[0] of [1] glossary URLs', + requestUrl: '{url controller=glossary action=generateSeoUrl}' + }, + 'glossary', + 'seo' + ); + + this.callParent(arguments); + } +}); +//{/block} +``` + +Once we refresh the backend and probably clear the cache, the SEO window should now contain our new progress bar. +Now we need to create our backend controller and a `generateSeoUrlAction`. + +With each AJAX request for the batch processing, we'll get a shopId, an offset and a limit to properly generate our SEO URLs. +We can ignore offset and the limit **for the moment**, since there is only a single URL to be generated for each shop. +Just remember them for later in this tutorial. + + +*Controllers/Backend/Glossary.php* +``` +Request()->getParam('shopId'); + + /** @var Shopware_Components_SeoIndex $seoIndex */ + $seoIndex = $this->container->get('seoindex'); + $seoIndex->registerShop($shopId); + + /** @var sRewriteTable $rewriteTableModule */ + $rewriteTableModule = $this->container->get('modules')->RewriteTable(); + $rewriteTableModule->baseSetup(); + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); + + $this->View()->assign(['success' => true]); + } +} +``` + +We're fetching the shopId, register a shop using the given shopId and then simply insert our rewrite URL again. + +Now there's one more thing missing. +Once we select a shop in the backend SEO module, an AJAX call is sent to collect the total counts of URLs to be created with each progress bar. +Our glossary URLs are not collected yet, so the module can't handle our glossary URLs properly yet. + +To collect the URLs, the `getCountAction` of the [SEO controller](https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Plugins/Default/Core/RebuildIndex/Controllers/Seo.php#L72) is called. +Thankfully it provides a filter event `Shopware_Controllers_Seo_filterCounts` to properly add our own counts. For this we need to use the name we used earlier for the progress bar. + +``` +public static function getSubscribedEvents() +{ + return [ + ... + 'Shopware_Controllers_Seo_filterCounts' => 'addGlossaryCount' + ]; +} + +public function addGlossaryCount(\Enlight_Event_EventArgs $args) +{ + $counts = $args->getReturn(); + + $counts['glossary'] = 1; + + return $counts; +} +``` + +Currently, there's only a single URL to be generated for each shop, so we'll just return a static 1. + +So, now let's try it. +If you've implemented everything properly, it should work perfectly now. + +## Custom parameters in SEO URL + +Now we've implemented a simple SEO URL generation for our glossary plugin. +The overview is now supported by SEO friendly URLs and we even generate those URLs in a proper way, depending on the given configuration. + +Now we want to have some kind of "detail" page for each word. +When calling this detailed page, we only see a single word with its related description. + +For this we need a new action in our **Frontend** Controller, e.g. "detailAction". +We could call this action by using an URL like `http://myShop.com/glossary/detail`. +In this case though, we would have to attach an ID for the word we want to show now. + +Sounds easy, let's just attach it to the URL: +`http://myShop.com/glossary/detail?wordId=1` + +This link would now display the word with the ID 1. +Wouldn't it be cooler to have the word itself as a part of the URL now? +E.g. you'd want to explain the word 'recursion', then the URL could look like this: `http://myShop.com/glossary/recursion` + +Way better, isn't it? + +Now this already requires several changes in our code. +First of all, every time we generate our SEO URLs, we have to iterate through all words in our database. + +``` +public function createGlossaryRewriteTable() +{ + /** @var \sRewriteTable $rewriteTableModule */ + $rewriteTableModule = Shopware()->Container()->get('modules')->sRewriteTable(); + $rewriteTableModule->sInsertUrl('sViewport=glossary', 'glossary/'); + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + + $words = $dbalQueryBuilder->select('glossary.id, glossary.word') + ->from('s_glossary', 'glossary') + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + foreach ($words as $wordId => $word) { + $rewriteTableModule->sInsertUrl('sViewport=glossary&sAction=detail&wordId=' . $wordId, 'glossary/' . $word); + } +} +``` + +Also, we need to adjust the URL counts for the backend now. + +``` +public function addGlossaryCount(\Enlight_Event_EventArgs $args) +{ + $counts = $args->getReturn(); + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + $wordsCount = $dbalQueryBuilder->select('COUNT(glossary.id)') + ->from('s_glossary', 'glossary') + ->execute() + ->fetchAll(\PDO::FETCH_COLUMN); + + $counts['glossary'] = $wordsCount; + + return $counts; +} +``` + +Do you still remember the `offset` and the `limit` parameter from the batch processing for the SEO URLs? +Now we do have to implement those, to only generate as many SEO URLs as configured in the batch process. + +``` +public function generateSeoUrlAction() +{ + ... + + /** @var QueryBuilder $dbalQueryBuilder */ + $dbalQueryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); + $words = $dbalQueryBuilder->select('glossary.id, glossary.word') + ->from('s_glossary', 'glossary') + ->setMaxResults($limit) + ->setFirstResult($offset) + ->execute() + ->fetchAll(\PDO::FETCH_KEY_PAIR); + + foreach ($words as $wordId => $word) { + $rewriteTableModule->sInsertUrl('sViewport=glossary&sAction=detail&wordId=' . $wordId, 'glossary/' . $word); + } + + $this->View()->assign(['success' => true]); +} +``` + +### Add foreign parameters + +While this already looks good, there's one more thing to do. +Shopware needs to know our custom parameter "**wordId**" first. +Otherwise our parameter would just get stripped and our SEO URL wouldn't work. + +The possible cases for parameters are handled in the [RewriteGenerator](https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Components/Routing/Generators/RewriteGenerator.php#L166). +It has a whole lot of cases, e.g. the parameter "**sArticle**" is only allowed when used with the **detail** controller. + +Thankfully, since Shopware 5.2, this method provides an event to add custom parameters. + +So, let's add the event and implement our custom parameter. + +``` +public static function getSubscribedEvents() +{ + return [ + ... + 'Shopware_Components_RewriteGenerator_FilterQuery' => 'filterParameterQuery' + ]; +} +``` + +``` +public function filterParameterQuery(\Enlight_Event_EventArgs $args) +{ + $orgQuery = $args->getReturn(); + $query = $args->getQuery(); + + if ($query['controller'] === 'glossary' && isset($query['wordId'])) { + $orgQuery['wordId'] = $query['wordId']; + } + + return $orgQuery; +} +``` + +So, what did I do here? +First of all, Shopware doesn't know things like "controllers" or "actions" like that. +Due to legacy reasons, Shopware still needs them to be handled as 'sViewport', which would be the controller, and 'sAction', +which obviously represents action. +That's what `$orgQuery` contains: The controller mapped to 'sViewport' and the action mapped to the array element 'sAction'. +Since `$orgQuery` will be used for assembling our SEO URL later, we need to add our parameter to it. + +Meanwhile, `$query` contains the actual request parameters as we know them. + +We only need to add our custom parameter `wordId` if both the controller equals 'glossary' and the parameter itself is set. +In that case, we add `wordId` to `$orgQuery` and return it afterwards. + + +## Example plugin +You can find the example plugin for **Shopware 5.3** here. + +Just to make sure: **This is not a fully functional plugin as it is only supposed to be an example.** +It will create the necessary plugin table *s_glossary* with a few example words. +This plugin does not provide a backend module to work with and the frontend templates are very slim to show the basic functionality. + +### Shopware 5.2 plugin +We've also created an example plugin for **Shopware 5.2**, which can be found here. + +There's several differences, e.g. the whole *Resources/views/backend* directory is different. +Additional to that, the logic to count the available glossary URLs had to be changed, since the event we used above was implemented with 5.3. diff --git a/source/blog/_posts/2017-08-09-custom-fonts-tinymce.md b/source/blog/_posts/2017-08-09-custom-fonts-tinymce.md new file mode 100644 index 0000000000..f5fa1c22e4 --- /dev/null +++ b/source/blog/_posts/2017-08-09-custom-fonts-tinymce.md @@ -0,0 +1,27 @@ +--- +title: Quick Tip - Custom fonts in the TinyMCE editor +tags: +- tinymce +- custom-fonts +- shopware-plugin + +categories: +- dev + +authors: [stp] +github_link: blog/_posts/2017-08-09-shopping-custom-fonts-tinymce.md + +--- + +![Screenshot Storefront](/blog/img/custom-font-example.png) + +*Custom font example in the Shopware storefront* + +Lately we're getting more & more requests on how to register / add custom fonts to the TinyMCE WYSIWYG editor in the Shopware administration. +The editor is used throughout every Shopware module where you can insert HTML text, therefore it would be handy to have your own fonts in there. + +We've created an open source Shopware plugin called `SwagTinyMceCustomFont` which covers your needs. +It allows you to integrate custom fonts from [Google Fonts](https://fonts.google.com/). +The plugin automatically loads the font in the administration interface as well as in the storefront of your shop. + +You can find the plugin as well as the feature overview, installation guide and usage example [on GitHub](https://github.com/shopware5/SwagTinyMceCustomFont). diff --git a/source/blog/_posts/2017-08-24-mutation-testing.md b/source/blog/_posts/2017-08-24-mutation-testing.md new file mode 100644 index 0000000000..82cf4306e6 --- /dev/null +++ b/source/blog/_posts/2017-08-24-mutation-testing.md @@ -0,0 +1,212 @@ +--- +title: Mutation Testing +tags: +- phpunit +- humbug +- testing + +categories: +- dev + +authors: [tn] +github_link: blog/_posts/2017-08-24-mutation-testing.md + +--- + +
+Anyone who is just searching for the example source code and does not want to read the complete blog post, here it is. +
+ +In this blog post I want to present the concept of mutation testing and a simple example with [humbug](https://github.com/humbug/humbug) for you. A few months ago, we have reached the 100 % code coverage goal in our actual project, the [b2b-suite](https://docs.enterprise.shopware.com/b2b-suite/). But what does this number stand for? Yes it ***only*** says you created enough unit-tests to execute every single line of code in your application. Nothing more. In the following sections I will create a simple class which is completely covered with unit tests. After that we will improve the tests and show how mutation testing can support us there. + +## Create an example class and unit tests + +Let us have a look on this simple comparison example class: +```php + $y; + } + + public function isSmallerThan(int $x, int $y): bool + { + return $x < $y; + } +} +``` + +To create a unit-test to reach 100 % coverage is very simple. See the example below: + +```php +comparison = new Comparison(); + } + + public function test_isGreaterThan() + { + self::assertTrue($this->comparison->isGreaterThan(5, 3)); + } + + public function test_isSmallerThan() + { + self::assertTrue($this->comparison->isSmallerThan(3, 5)); + } +} +``` +The execution of these unit tests creates the following output: +``` +OK (2 tests, 2 assertions) + + +Code Coverage Report: + 2017-08-22 12:42:16 + + Summary: + Classes: 100.00% (1/1) + Methods: 100.00% (2/2) + Lines: 100.00% (2/2) + +Comparison + Methods: 100.00% (2/2) Lines: 100.00% (2/2) + +``` + +So we created a class with two methods which are covered by tests. We could think this class is bullet proof and every invalid change will be discovered from our test. But really? What if some developer adds a small equal sign to our methods? Our new `Comparison` class looks like this: + +```php += $y; + } + + public function isSmallerThan(int $x, int $y): bool + { + return $x <= $y; + } +} +``` + +And the result of our unit test is the same like above. Every test passed. But now false positive results are possible . If we use the method `isGreaterThan` with the parameters `$x = 5; $y = 5;` we will get true as return value instead of the supposed false value. + +So what happened? At the moment we only test the happy execution path of these methods and don't observe of the threshold values. For every developer it is obviously that 5 is greater than 3 and 3 is smaller than 5. So we create this kind of test. But how we have to improve our test to cover oll existing threshold values? + +First, we should test the nearest combination of parameters which causes an false return value. In our example methods we can easily use the equal number for `$x` and `$y`. After that we should test the farthest combination which causes a true return value. For this we can easily use the PHP constants `PHP_INT_MAX` and `PHP_INT_MIN`. The new created test can be seen below: + +```php +comparison->isGreaterThan(5, 3)); + self::assertFalse($this->comparison->isGreaterThan(4, 4)); + self::assertTrue($this->comparison->isGreaterThan(PHP_INT_MAX, PHP_INT_MIN)); + } + + public function test_isSmallerThan() + { + self::assertTrue($this->comparison->isSmallerThan(3, 5)); + self::assertFalse($this->comparison->isSmallerThan(4, 4)); + self::assertTrue($this->comparison->isSmallerThan(PHP_INT_MIN, PHP_INT_MAX)); + } +} +``` + +## Mutation Testing + +In this small example it is very easy to find the needed test range and threshold values. But how does it work in bigger applications with hundred of classes and thousand lines of code? I guess in the most cases only the happy path will be tested. So how can mutation testing help us to create better tests? + +The basic concept of mutation testing sounds very easy. You change comparison statements as an example from `===` to `!==` or changes return values of methods like `return true;` to `return false;`. This new versions of your application are called "mutants". After your change you execute the test suite. If the suite fails your tests "killed the mutant". This means your tests detected the wrong behaviour. + +Mutation testing introduces a new quality score the so-called "Mutation Score Indicator". This score is the ratio of the number of Dead Mutants over all created Mutants. Usually this score is calculated like the code coverage in percent. + +In order to make this kind of testing automatically we can use [humbug](https://github.com/humbug/humbug) for that. Humbug has a wide range of mutators like the described mutations above. A good overview can be found [here](https://github.com/humbug/humbug#mutators). + +So let us revert the new assertions and execute humbug for the first time. Humbug executes phpunit in the first place. After that it will create the mutants and execute the test suite again for every created mutant. To improve the execution time humbug only uses those test classes which cover the specific file and line on which the mutation was inserted. + +Humbug creates the following output: +``` +Humbug has completed the initial test run successfully. +Tests: 2 Line Coverage: 100.00% + +Humbug is analysing source files... + +Mutation Testing is commencing on 1 files... +(.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out) + +M.M. + +4 mutations were generated: + 2 mutants were killed + 0 mutants were not covered by tests + 2 covered mutants were not detected + 0 fatal errors were encountered + 0 time outs were encountered + +Metrics: + Mutation Score Indicator (MSI): 50% + Mutation Code Coverage: 100% + Covered Code MSI: 50% +``` + +As we can see, humbug created 4 mutations, 2 mutants were killed and 2 mutants were not detected. So let us have a look at the generated mutations which are not detected: + +```php + public function isGreaterThan(int $x, int $y): bool + { + return $x >= $y; + } + + public function isSmallerThan(int $x, int $y): bool + { + return $x <= $y; + } +``` + +Humbug automatically detects the same issues which we found above manually. If we add the new assertions which we already created above we should reach an Mutation Score Indicator of 100%. The created output stands below: +``` +Humbug has completed the initial test run successfully. +Tests: 2 Line Coverage: 100.00% + +Humbug is analysing source files... + +Mutation Testing is commencing on 1 files... +(.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out) + +.... + +4 mutations were generated: + 4 mutants were killed + 0 mutants were not covered by tests + 0 covered mutants were not detected + 0 fatal errors were encountered + 0 time outs were encountered + +Metrics: + Mutation Score Indicator (MSI): 100% + Mutation Code Coverage: 100% + Covered Code MSI: 100% +``` + +## Conclusion +Mutation Testing especially humbug is a powerful tool to rate the quality of your unit tests. It checks the hole test suite and gives you the safety that your created tests are useful. Our b2b-suite has at the moment a Mutation Score Indicator of 79%. So I think there is some space left for improvements ;-). + +If you are interested in the source code, it can be found [here](https://github.com/teiling88/mutation-testing). diff --git a/source/blog/_posts/2017-09-25-ajax-panel.md b/source/blog/_posts/2017-09-25-ajax-panel.md new file mode 100644 index 0000000000..c07623d3b5 --- /dev/null +++ b/source/blog/_posts/2017-09-25-ajax-panel.md @@ -0,0 +1,130 @@ +--- +title: Ajax Panel +tags: +- frontend +- ajax +- javascript +- jquery +- framework + +categories: +- dev + +authors: [lh] +github_link: blog/_posts/2017-09-25-ajax-panel.md + +--- + +In this blog post I want to present the concept of our ajax based panel system which we use in our B2B-Suite. +In the B2B-Suite we had to develop a backend user interface inside the Shopware frontend. + +On a single page in a typical backend view you may find: +* list of entities +* form to enter new data +* list of related entities +* some statistics + +Serving all this data from a single controller action may already be hard, now imagine the sheer number of parameters you may +have to exchange with this single action when you want to enable pagination, validation and searching on this single page, +through this single action. + +This is pretty much the reason why stateless services and stateful frontends are an important topic in today's web development. +And it is exactly the reason why we created the ajax panel. It provides us with the means to load local states from the +server and create a rich ui. + +We evaluated different frameworks to achieve this target. AngularJS and Vue.JS were possible frameworks which allows +two way data binding and stateful frontend access. + +image + +The small controller actions don't respond with the full page dom tree anymore. Each controller is only responsible for a +specific panel content. + +One ouf our targets was also to use most of the already existing dependencies instead of adding new frameworks just for +the B2B-Suite. We use jQuery because it is a base dependency of Shopware 5. + +We decided to develop our own lightweight frontend framework on top of jQuery which allows asynchronous HTTP requests for our frontend. +The main target of this framework is to execute asynchronous calls and render the response in a given and zoned DOM element by using an event. +The behaviour is very similar to angular's [zone.js](https://github.com/angular/zone.js). + +## Code Example +The base structure of our ajax panel index action looks like this: +```html +
+``` + +On page load our jQuery ajax panel plugin will search for the class `ajax-panel` and use the data attribute `data-url`. +If this attribute contains a valid url the jQuery plugin will perform an asynchronous http request on the attribute url. +If the response is signed with a HTTP Status Code of 200 the response will be rendered back in the original zoned div with the data-id `example`. + +## Response Example + +The ajax panel controller action responds with the content of the ajax panel element and moves the dom structure inside the given element. +The response could be like: + +```html +Example Ajax Content +``` + +After the response is rendered in the parent Ajax Panel `div` the full dom structure will look like this: + +```html +
+ + Example Ajax Content + +
+``` + +## Ajax Panel Plugins +After we build our first views we run in several problems. The biggest problem was, that we want to use jQuery in the +response's rendered content. We decided to develop an own plugin loader for our ajax panel which loads automatically +JavaScript plugins after the panel load. We throw many events for third party plugins that developers can use for their +own plugin. Also the shopware default plugins can be registered in the ajax panel. + +To achieve this approach we added an optional `data-plugins` attribute which can contain multiple JavaScript plugins: + +```html +
+``` + +## Basic Ajax Panel Plugin +Our JavaScript example plugin code looks like that: + +```javascript +/** + * Remove all elements with triggerSelector Class + */ +$.plugin('b2bAjaxPanelExamplePlugin', { + defaults: { + triggerSelector: '.should--removed' + }, + + init: function () { + var me = this; + me._on(document, 'b2b--ajax-panel_loaded', $.proxy(me.addClasses, me)); + }, + + addClasses: function (event, eventData) { + var $panel = $(eventData.panel); + + $panel + .find(me.defaults.triggerSelector) + .remove(); + }, + + destroy: function() { + var me = this; + me._destroy(); + } +}); +``` + +## Dependencies +With the ajax panel we build a lightweight frontend framework which only depends on jQuery. Shopware delivers jQuery in the +Responsive theme, so we don't need to require any additional component. Awesome, isn't it? + +## Conclusion +Our Ajax Panel is a complete flexible and lightweight framework with many possibilities and jQuery as a single dependency. +The panel can be handled with simple data attributes and additional plugins allows to use JavaScript plugins. +We use this technology in our B2B-Suite in each module very successfully. diff --git a/source/blog/_posts/2017-09-29-gitter.md b/source/blog/_posts/2017-09-29-gitter.md new file mode 100644 index 0000000000..a0df9a3a9a --- /dev/null +++ b/source/blog/_posts/2017-09-29-gitter.md @@ -0,0 +1,81 @@ +--- +title: Shopware goes gitter +tags: +- community +- irc +- gitter +- chat + +categories: +- dev + +authors: [nd] +github_link: blog/_posts/2017-09-29-gitter.md + +--- + +## A huge change for the community + +For several years now, since 2011 to be precise, there is a small but vibrant chat community on freenode in the #shopware channel. + +All these people chatting there have one thing in common, they are working on the Shopware ecommerce system, in one way or the other. + +Now these times will be over. As of the end of October 2017, the IRC channel will be closed. +If you hear a faint scream in the distance it may be one of the guys from the IRC channel who is just reading this post as you do now. + +****But!**** of course we will not leave you alone in the dark. And we are not closing the IRC channel because we don't like people chatting. +In fact, we love your passion for shopware and we want to communicate with you and we want you to have a platform where you can communicate directly to other people involved with Shopware. + +So, why the change? What changes? What are the consequences? Just read further, friend, and find out. + +## What will change? + +For a short period of time (read: one month) the IRC channel will stay open. During this period we will actively transfer people to [Gitter](https://gitter.im/shopware/shopware). + +After this, the IRC channel will be closed and all chat activity will take place on Gitter. + +If you are an IRC user by now, you can use your client for Gitter as well, just follow the steps mentioned at the end of the contribution page. +Of course we encourage you to use the clients provided by Gitter, since most IRC clients don't handle markdown that well. + +Since there are a lot of german users, those will find a german channel to make their lifes easier. + +If you are new to the show, just [![Gitter](https://badges.gitter.im/shopware/shopware.svg)](https://gitter.im/shopware/shopware?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)! + +## What is Gitter? + +In case you are new to this, Gitter is a free, [open source](https://gitlab.com/gitlab-org/gitter/webapp) chat where you login with your github or twitter account and are able to chat immediatly. +Like IRC, but with more features and more or less integrated with our github repository. Like Slack, but more community-centric. + +If you want to know the full story, there is a nice [wikipedia article](https://en.wikipedia.org/wiki/Gitter) about the service. + +*One of the biggest changes for former IRC users:* Gitter chatrooms are public and save history indefinitely. So everything you say is wisdom for eternity. +Or embarrassment, but this is an unusual state of mind for shopware enthusiasts. So be careful. There be dragons. On the other hand, you don't need that icky [bouncer](https://en.wikipedia.org/wiki/BNC_(software)) anymore. ;) + +## But... why? + +To cite the devdocs irc page: + +> Since 2011, the shopware IRC channel was active and well established. +> Today, IRC is a little bit nerdy and we really love it. Most of the cool kids are playing on freenode. +> +> But besides the coolness there are a few disadvantages that come with using IRC: +> +> - No history without 3rd party software +> - No mapping between the contributor and the IRC nick (Who is xenomorph again?) +> - No easy access except through anonymous web IRC clients +> - Lacks modern features like image sharing, formatting and such +> +> Of course, all these points are arguable. +> But in the end, we wanted to have a chat community with easier access and more modern features. +> +> We hope to not lose any old IRC users (we learned to love you all at #shopware) and get many new and interesting people to join Gitter. + +## The transition + +First step is, the IRC channels will get +m and +s and all users will get a message similar to: + +> Come join us and [![Gitter](https://badges.gitter.im/shopware/shopware.svg)](https://gitter.im/shopware/shopware?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +From then on, every user who joins the IRC channel will automatically be notified of the new situation. + +After one month, the channel will become invite only. diff --git a/source/blog/_posts/2017-10-23-meetnext-recap.md b/source/blog/_posts/2017-10-23-meetnext-recap.md new file mode 100644 index 0000000000..28fd4114f8 --- /dev/null +++ b/source/blog/_posts/2017-10-23-meetnext-recap.md @@ -0,0 +1,137 @@ +--- +title: MeetNext - a recap +tags: +- community +- next +- meetnext +- shopware + +categories: +- dev + +authors: [nd] +github_link: blog/_posts/2017-10-23-meetnext-recap.md + +--- + +# Introduction + +[![Army of Codys](/blog/img/2017-10-23-meetnext-recap/cody_army_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/cody_army.jpg) + +## When + +#MeetNext took place from October 18th to 20th + +## Where + +[![Germania Campus](/blog/img/2017-10-23-meetnext-recap/campus_view_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/campus_view.jpg) + +Münster, Germany + +## What + +The participants were informed about the current state of the Shopware research. + +So, what is the `Next` in #MeetNext about? In a nutshell, it is the next big thing. A completely new product different to 5.x which will be developed simultaneously to Shopware 5. They will both receive patches and new features. Which leads us to the next question: + +## Why + +Shopware is a well established, mature product. As the people who are working with Shopware know, there are a lot of things in the software that work in a certain way because it is our legacy. And we can't change it without effectively breaking the functionality of almost every plugin out there. Example: Ever wondered why products are called `articles` in shopware? Because the german word for a product is `Artikel`. Ever wondered why there are multiple terms for manufacturer? Legacy. So yeah, that's not what we want and of course there's much more, especially regarding the architecture. + +So the main reason is: We want to continue to make Shopware better, faster, ready for the future and easier to develop for. To achieve this, we have to break a lot of stuff, and we don't want to throw away all the work we and you guys out there already invested. We're always telling you, like a mantra, that we love our community. Guess what, we do this because we mean it. You're part of Shopware, and we wouldn't want to have a different community. + +All that said, we want to create a second version of Shopware which only carries the good part of our legacy. We don't want to do this in the dark, doing the jack-in-the-box at some time and presenting the new and shiny brother of Shopware 5. We want to have you aboard as soon as possible. We want to hear from you if the things we are planning and implementing are what you need in the end. We want to hear what you think is a mistake, what we forgot and what is even more awesome than before. + +The #MeetNext was the first feedback event and it was, marketing speak aside, a huge success. Before we dive in, always remember you can see for yourself what is happening, either on [github](https://github.com/shopware5/shopware/tree/labs) or in the [documentation](https://developers.shopware.com/labs/). + +# A brief summary + +[![Room overview](/blog/img/2017-10-23-meetnext-recap/room_overview_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/room_overview.jpg) + +There were five topics: + +- [housekeeping](https://developers.shopware.com/labs/housekeeping/) +- [API](https://developers.shopware.com/labs/api/) +- [i18n](https://developers.shopware.com/labs/internationalization/) +- [administration (former backend)](https://developers.shopware.com/labs/new-administration/) +- [basket & order process](https://developers.shopware.com/labs/shopping-cart-and-ordering-processes/) + +The main topics that were discussed at #MeetNext were *API*, *Administration* and *Basket*. + +If you follow the links above you can find the updated documentation. + +# The results + +Now for the results. And by saying results we are talking about the feedback the participants gave us, sorted by topic. + +These things are the "top feedback", of course there were a lot more valuable ideas, and we heard them all and wrote them down. + +## API + +| Good things | Bad things, improvements & new ideas | +|------------------------|------------------------------------------------------| +| UUIDs | REST API should not be used as ERP API | +| Foreign Keys | Plugin support | +| Error Handling | No attribute support | +| SwagQL | A lot of class changes needed for structural changes | +| Single Source Of Truth | Use HTTP status codes | +| Performance | ACL | +| Upsert/Sync | Audit-Log | + +## Basket + +| Good things | Bad things, improvements & new ideas | +|-------------------------|------------------------------------------------------| +| Stateless | Dynamic processor should be splitted | +| Price & tax calculation | part-delivery system needs to be highly configurable | +| Concept for deliveries | Use money objects instead of floats | +| Highly modular | Priority of processors should be a dependency system | +| Concept of processors | Persist the calculations alongside an order | +| Expandability | | +| Upsert/Sync | | + +## Administration + +> **Note:** We're creating a new "backend" without extJS and we're calling it "administration" + +| Good things | Bad things, improvements & new ideas | +|-----------------------------|-------------------------------------------------| +| UX concept | Vue.js could be too complex | +| Routing | Client-side twig rendering | +| Less boilerpate code needed | View-layer abstraction | +| No more client-side models | No Typescript | +| Saving of changesets | Huge bootstrapping process | +| Hot-reloading | Better accessibility for people with disability | +| Upsert/Sync | | + +# Hacking + +After the sessions ended, a lot of the participants started hacking and tried to break things. + +Since we didn't manage to memorize (or write down) the names of all hackers and their projects, here is a list of examples in no particular order and without credit. But the list shows two things, first how awesome Shopware next will be (short time, great projects) and how awesome the Shopware community is. Especially you guys who managed to attend #MeetNext. ;-) + +## Some of the "hack projects" + +- Implement reactive window management in vue.js +- Tracing library for class performance analysis +- Port an existing cart-related plugin to Next +- Revive the old ExtJS backend and let it speak to the new API +- Create a different concept of component extension for vue.js + +# Visual Impressions + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_00_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_00.jpg) + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_01_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_01.jpg) + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_02_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_02.jpg) + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_03_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_03.jpg) + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_04_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_04.jpg) + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_05_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_05.jpg) + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_06_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_06.jpg) + +[![Visual impressions](/blog/img/2017-10-23-meetnext-recap/impressions_07_thumb.jpg)](/blog/img/2017-10-23-meetnext-recap/impressions_07.jpg) diff --git a/source/blog/_posts/2017-11-08-exclusive-enterprise-dev-training.md b/source/blog/_posts/2017-11-08-exclusive-enterprise-dev-training.md new file mode 100644 index 0000000000..518aa0035c --- /dev/null +++ b/source/blog/_posts/2017-11-08-exclusive-enterprise-dev-training.md @@ -0,0 +1,65 @@ +--- +title: Exclusive Enterprise Developer Training +tags: +- b2b-suite +- enterprise +- training + +categories: +- dev + +authors: [nd] +github_link: blog/_posts/2017-11-08-exclusive-enterprise-dev-training.md + +--- + +![banner](/blog/img/2017-11-08-exclusive-enterprise-dev-training/banner.png) + +# Exclusive Enterprise Developer Training - Only five places remaining + +In this one-day workshop, you will get insight into the technology behind Shopware Enterprise, namely the Shopware Accelerator “B2B Suite”. Led by the developers who created the software itself, you will learn everything about this key feature, including practical application. This training is designed for experienced developers who already have the “Shopware Certified Developer” certificate. + +## Info: + +### Date +November 15, 2017 + +### Time +Check-in: From 8:30 am +Start: 9 am +End: 5 pm + +### Location: +shopware AG +Ebbinghoff 10  +48624 Schöppingen +Germany + +### Fee: +795 Euro per person + +## Registration +Please send an email to sales@shopware.com + +## What you can expect +- Training held in English +- Intro to the Enterprise architecture +- Working with the B2B Suite +- Intro to the order management +- Intro to the user management +- Extending the UI + +## Target group +- Experienced PHP developers +- max. 5 participants + +## Requirements +- Basic knowledge of Doctrine +- Basic knowledge of Smarty +- Basic Knowledge of ExtJS + +## Services +- One-day training +- Fully equipped computer +- catering +  diff --git a/source/blog/_posts/2017-11-17-on-testing-strategies.md b/source/blog/_posts/2017-11-17-on-testing-strategies.md new file mode 100644 index 0000000000..57c08b7b73 --- /dev/null +++ b/source/blog/_posts/2017-11-17-on-testing-strategies.md @@ -0,0 +1,282 @@ +--- +title: On testing strategies +tags: + - test pyramid + - test diamond + - integration test + - functional test + - unit test + - stability + - software quality +indexed: false +github_link: blog/_posts/2017-11-17-on-testing-strategies.md + +authors: [jp] +--- + +In this post I want to introduce you to the kind of strategies we evaluated to secure the functionality of the Shopware B2B-Suite. + +When we set of to write the B2B-Suite we as a team set a few technical goals we wanted to achieve. One of them was to embrace automatic testing at its fullest and harness the potential implied by it: + +* Allow refactoring even a day before release +* Help instill trust into the stability of the application for our customers +* Create a good application architecture that feels about right for the complexity introduced + +## The Application + +First we take a look at a fairly standard application design. We need this to have a shared base for the comparison of the different testing strategies. This model omits the framework and data structures which are not essential for our evaluation. + +We have a controller, that uses a domain service and two auxiliary services to handle requests. The services themselves have dependencies on other services or storage implementations. + + + +So how should we test this? Let's discuss some strategies: + +## Testing Strategy: Functional testing only + +Many projects out there use acceptance tests as their only way of automated testing. So why is it a bad idea? One word: Performance! Functional tests use a real browser to check the application. Even high performance applications have response times of at least 50ms, excluding the JavaScript execution time you are quickly at 500ms or more - for a **single page**. So let's math with an average of 250ms per page: + +| Page Loads | Time | +|-------------|---------| +| 1 | 250 ms | +| 5 | 1.25 s | +| 10 | 2.5 s | +| 50 | 12.5 s | +| 100 | 25 s | +| 400 | 100 s | + +Now you have to ask yourself: Do you want to maintain this? Is it useful? Creating an application that has 400 different click paths is quite easy, but waiting 100 seconds to validate every single change is tedious at least and at worst it stops being helpful quickly. + +The main advantage is that - at least for settled applications - introducing testing late in a development flow makes this a quick win because usually you have to write the least amount of code for this. And even if the Structure of your source code does not allow to easily introduce testing into every layer, these tests at least tend to solidify the basic functionality. So this is possible by just introducing a single layer on top of your application: + + + +**Good:** + +* Gives you 99% certainty that the application works correctly. +* Adds minimal code to maintain. + +**Bad:** + +* Takes a long time even for small applications +* Does not help developers +* Does nothing to your application architecture, but add a few css classes +* Usually very fragile, small changes tend to have an unreasonably big impact (you change a buttons position and the login breaks) + +**Verdict:** Run! + +## Testing Strategy: The Pyramid + +There is a testing pyramid favored by people like Robert C. Martin, it looks like this: + + + +"A Testing Pyramid that puts the focus on **unit tests** proposes to write a few integration tests and if really necessary few functional test. So for our example application this looks like this: + + + +So what do we have here? We have a Unit Test for each class of the application. Each dependency is mocked so we can test the code of a single service in absolute isolation. This is the core of your test suite: The unit tests! Now we need to integrate in a few places, I personally would propose that `MyService` and `ContextProvider` should be integrated with their subsidiary services, so we add this: + + + +These two integration tests that integrate the central services with their storage. Last but not least we have a small functional test for our controller. + + + +Pyramid successfully implemented! + +But this did not work for us. Since we use minimal Integration tests and mock all outputs of the dependencies we quickly found ourselves in a place where the test suite did not produce certain edge cases correctly. Mocks are simply a lie in your system. Let's take a closer look at `MyServiceTest`: + + + +The Problem is the dependency between the mocks and the actual implementations. Although one can connect them through a *realization* arrow, mocks treat real implementations like interfaces. So if the signature does not change, but the result itself changes over time a mock will not catch this error. And changing the result over time is quite easy: + +At first a method might look like this: + +```php +public function isValidResponseCode(int $code): bool +{ + return in_array($code, [200, 300]); +} +``` + +And then gets changed to this: + +```php +public function isValidResponseCode(int $code): bool +{ + return in_array($code, [200, 300, 202, 303]); +} +``` + +Boom, a mock will never automatically produce this result, but for all means it will keep the test suite green. There are certain other problems with mocks, so... Time for a digression: + +---- + +## Digression: Mocking + +All I really can say about mocking is this: it is a pain in the ass! There are currently a few contenders for mocking in PHPUnit tests and they all work internally the same way and are kind of awful. So lets say we have a class called `Something` that has a method called `getResponse` and we want it to return `'foo'` for our test, how would we accomplish this through mocking? + +```php +class Something { + public function getResponse(): string { + return 'bar'; + } +} +``` + +#### PHPUnit Mocks + +The default for PHPUnit Mocks is to use the built in Mocking Framework. This usually looks like this: + +```php +class SomeTest extends PHPUnit_Framework_TestCase { + + public function test_something() + { + // create a mock + $stub = $this->createMock(Something::class); + + // Configure the mock + $stub->method('getResponse') + ->willReturn('foo'); + + $this->assertEquals('foo', $stub->getResponse()); + } +} +``` + +Cool? No! Actually there are a few problems with that: + +* If you refactor the `getResponse` method no IDE will match this with this string automatically. +* When you write this you have to remember what the `getResponse` Method was called. The IDE will not help you here. +* `'foo'` is a totally made up value. As you can see the real implementation is not able to create this value, so why is there a test that checks against impossible values? + +#### Prophecy + +Prophecy is part of the newer generation of mocking frameworks. It has a two step approach, where you first configure the mock and then create the result. So how would this look like? + +```php +class SomeTest extends PHPUnit_Framework_TestCase { + + public function test_something() + { + // create a prophet + $prophet = $this->prophesize(Something::class); + + // Configure the prophet + $prophet->getResponse() + ->willReturn('foo'); + + // create the mock + $stub = $prophet->reveal(); + + $this->assertEquals('foo', $stub->getResponse()); + } +} +``` + +Cool? A little bit cooler at least. It is possible to annotate the real class for prophets, at least. But other then that it has the same issues as `PHPUnit Mocks`. + +#### Just use PHP + +Of course we can create mocks with the built in features of the language itself. Just like this: + +```php +class SomeTest extends PHPUnit_Framework_TestCase { + + public function test_something() + { + // create a prophet + $stub = new class extends Something { + public function getResponse(): string { + return 'foo'; + } + }; + + $this->assertEquals('foo', $stub->getResponse()); + } +} +``` + +Although this is by far my favorite approach because the IDE can help you best with this approach, it still is messy. + +**It is impossible to get `foo` as a return value from the tested method** This means that tests with Mocks tend to use made up values that can diverge quite heavily from real data. + +---- + +So how did the pyramid fair for us? + +**Good:** + +* Helps during development, creates entry points for every possible error +* Always fast to execute +* Creates extremely well designed classes + +**Bad:** + +* Almost doubles the amount of code in your project +* Mocks need to be changed in sync with their loosely coupled subjects +* Produces false positives +* Creates sometimes awkward to integrate classes + +**Verdict:** Can be better... + +## Testing Strategy: The Diamond + +There is a third strategy, that puts all the effort into Integration tests. This looks something like that: + + + +I have color coded the application diagram to show you how we interpreted the diamond: + +* **Green:** No dependencies, a test becomes automatically a **unit test** +* **Yellow:** Has dependencies (either on other classes, or on infrastructure) must be an **integration test** +* **Blue:** Is front facing so it must be a **functional test** + + + +By applying this simple ruleset we still have a one to one parity of test classes to production classes. This means that for each bug or feature there is an easy entry point in our test suite. And the test suite is therefore useful for developers and gets actively supported. If bugs are introduced during development there is usually a test that shows it immediately. If a contract between objects is changed this change is immediately available in the suite and in many cases even leads to a failing test. (depending on the test quality: see [Mutation Testing by my colleague Thomas Eiling](https://developers.shopware.com/blog/2017/08/24/mutation-testing/)). + +So now let's take a look at our `MyServiceTest` the way it looks like in integration testing: + + + +This test still covers all n-paths of the class `MyService`! It covers most paths in it's directly dependant classes `MyRepository` and `ValidationService` and covers a few paths in their dependencies, and so on. You can imagine this like different saturations of a color: + + + +Now we no longer make up the output of any subsidiary service. We even noticed that using the real storage has virtually no impact on the test performance, but creates a huge benefit by testing the friction between these different services in our test suite at all times [(see factor X)](https://12factor.net/dev-prod-parity). + +The big picture then looks like this: + + + +The mocks in this case come from a specially set up database and this is the only real downside to this approach. Your fixtures may diverge from a production setting! We have seen this in the past and usually got real bugs from this. But even in these cases the problem arose from a single point of failure which is far better then mocking in every single place. + +**Good:** + +* Helps during development, creates entry points for every possible error +* Almost as fast as unit tests +* The real data is used +* Creates an architecture of well integrated parts + +**Bad:** + +* Almost doubles the amount of code in your project +* Fixture data will never show 100% production state +* Will also not give you a 100% certainty that everything works + +**Verdict:** We use it! + +## A word on TDD + +I personally am a huge fan of TDD and practice it almost always when developing or exploring software. This practice is not lost here. Just because you have to implement stuff that you are using does not reduce the amount of freedom you have in the 30 second cycle of test driven development. On the contrary - at least for me - it tends to create more realistic assumptions on the scope of classes. By doing almost only integration testing all my tests look and feel the same. The usual friction that occurs when combining multiple unit tested classes is by far reduced because the integration assumptions all - by definition - hold up against the real implementations. + +## Conclusion + +The Diamond seems to be the best solution to the testing strategy problem. In fact we have been and still are developing the B2B-Suite with that strategy in mind and have seen quite good results with it. Currently our complete suite takes around 3 minutes to execute which is still passable for a quick overview. None of our tests depend on its predecessor and can all be executed in isolation (which makes Mutation testing possible in the first place), so during development we usually single out a few tests to run repeatedly. + +The issue of software architecture comes a little short in this blog post, let me tell you why: It is a problem where tests only are a part of the solution! As you could read in a [previous post I published](https://developers.shopware.com/blog/2016/12/05/large-scale-plugin-architecture/) where I used the actor model to create a plugin architecture there are numerous ways that need to be taken into account when creating a sustainable system architecture. Testing will help you a lot with *friction* and *cohesion* (as introduced by Robert C. Martin) but not help you derive a greater truth from this. This is a place where [DDD](https://en.wikipedia.org/wiki/Domain-driven_design) seems to provide the better tools. + +So all in all, at least to us this is the best overall approach to develop stable software, with the least tradeoffs and the most benefits. Thanks for reading and as always feel free to get in touch and discuss with me! diff --git a/source/blog/_posts/2018-01-09-exclusive-enterprise-dev-training.md b/source/blog/_posts/2018-01-09-exclusive-enterprise-dev-training.md new file mode 100644 index 0000000000..c53e2f6e09 --- /dev/null +++ b/source/blog/_posts/2018-01-09-exclusive-enterprise-dev-training.md @@ -0,0 +1,70 @@ +--- +title: Exclusive Enterprise Developer Training +tags: +- b2b-suite +- enterprise +- training + +categories: +- dev + +authors: [nd] +github_link: blog/_posts/2018-01-09-exclusive-enterprise-dev-training.md + +--- + +![banner](/blog/img/2017-11-08-exclusive-enterprise-dev-training/banner.png) + +# Exclusive Enterprise Developer Training - Only five places remaining + +In this one-day workshop, you will get insight into the technology behind Shopware Enterprise, namely the Shopware Accelerator “B2B Suite”. Led by the developers who created the software itself, you will learn everything about this key feature, including practical application. This training is designed for experienced developers who already have the “Shopware Certified Developer” certificate. + +## Info: + +### Date +February 23, 2018 + +### Time +Check-in: From 8:30 am + +Start: 9 am + +End: 5 pm + +### Location: +shopware AG + +Ebbinghoff 10  + +48624 Schöppingen + +Germany + +### Fee: +795 Euro per person + +## Registration +Please send an email to sales@shopware.com + +## What you can expect +- Training held in English +- Intro to the Enterprise architecture +- Working with the B2B Suite +- Intro to the order management +- Intro to the user management +- Extending the UI + +## Target group +- Experienced PHP developers +- max. 5 participants + +## Requirements +- Basic knowledge of Doctrine +- Basic knowledge of Smarty +- Basic Knowledge of ExtJS + +## Services +- One-day training +- Fully equipped computer +- catering +  diff --git a/source/blog/_posts/2018-01-11-mutation-testing-with-infection.md b/source/blog/_posts/2018-01-11-mutation-testing-with-infection.md new file mode 100644 index 0000000000..1d33951ab0 --- /dev/null +++ b/source/blog/_posts/2018-01-11-mutation-testing-with-infection.md @@ -0,0 +1,374 @@ +--- +title: Mutation Testing with Infection +tags: +- phpunit +- infection +- testing + +categories: +- dev + +authors: [tn] +github_link: blog/_posts/2018-01-11-mutation-testing-with-infection.md + +--- + +
+Anyone who is just searching for the updated example source code and does not want to read the complete blog post, here it is. +
+ +This blog post is a follow up for my mutation testing post from [24.08.2017](https://developers.shopware.com/blog/2017/08/24/mutation-testing/). Since [31.12.2017](https://github.com/humbug/humbug/commit/53730b3306efebf85bd66b6f7ec870d500f5ccbd) humbug is marked as deprecated with a link to infection as an alternative. Maks Rafalko is the author of infection and well known as [borNfreee](https://github.com/borNfreee) on github. The last released version of Infection is 0.7.0. So let us have a closer look into it. + +## Installation and Integration +I will use this commit as the starting point for the next steps. If you execute infection the first time, it will guide you through a small wizard to configure infection for your application. It is not necessary to keep a special configuration command in mind. Foremost the wizard wants to configure your source directory: + +```bash +Welcome to the Infection config generator + +We did not find a configuration file. The following questions will help us to generate it for you. + +Which source directories do you want to include (comma separated)? [src]: + [0] . + [1] build + [2] src + [3] tests + [4] vendor + > +``` +After that, you can exclude folders in your source directory: + +```bash +There can be situations when you want to exclude some folders from generating mutants. +You can use glob pattern (*Bundle/**/*/Tests) for them or just regular dir path. +It should be relative to the source directory. +Press to stop/skip adding dirs. + +Any directories to exclude from within your source directories?: +``` +Next step is to configure the timeout for each test: + +```bash +Single test suite timeout in seconds [10]: +``` + +At last we have to configure a path for your infection log file: + +``` +Where do you want to store the text log file? [infection-log.txt]: build/infection.log +``` +After successfully configuration Infection is executed for the first time: + +```bash +Configuration file "infection.json.dist" was created. + + ____ ____ __ _ + / _/___ / __/__ _____/ /_(_)___ ____ + / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ + _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / +/___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ + +Running initial test suite... + +Phpunit version: 5.7.2 + + 12 [============================] < 1 sec + +Generate mutants... + +Processing source code files: 1/1 +Creating mutated files and processes: 8/8 +.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out + +........ (8 / 8) + +8 mutations were generated: + 8 mutants were killed + 0 mutants were not covered by tests + 0 covered mutants were not detected + 0 errors were encountered + 0 time outs were encountered + +Metrics: + Mutation Score Indicator (MSI): 100% + Mutation Code Coverage: 100% + Covered Code MSI: 100% + +Please note that some mutants will inevitably be harmless (i.e. false positives). +``` + +The result page looks very similar to the humbug result page. Infection created 8 mutations which all were killed. Finally the initial configuration process is easy and the generated `infection.json.dist` file is ready for more detailed settings. + +## Changes under the hood + +As we learned infection is easy to setup and run. But in which way does infection differ from humbug? + +* the generated mutations are based on [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). +* more mutators are available like `Function Signature` and `Loop` +* great performance improvements + +Let us have a detailed look into some points. + +## Abstract Syntax Tree + +What kind of benefit gets infection from using an Abstract Syntax Tree? + +* the sourcecode is easier to maintain +* easier to write new mutators +* much easier to handle false-positives and different edge cases, e.g. deciding when mutation should be done or should not in difficult situation + +To prove this benefits I decided to compare some mutators from infection with the equivalents in humbug. So let us see the Plus mutator which is changing `+` into `-` from infection first and the mutator from humbug as second: + +```php +// https://github.com/infection/infection/blob/master/src/Mutator/Arithmetic/Plus.php + +class Plus extends FunctionBodyMutator +{ + /** + * Replaces "+" with "-" + * + * @param Node $node + * + * @return Node\Expr\BinaryOp\Minus + */ + public function mutate(Node $node) + { + return new Node\Expr\BinaryOp\Minus($node->left, $node->right, $node->getAttributes()); + } + public function shouldMutate(Node $node): bool + { + if (!($node instanceof Node\Expr\BinaryOp\Plus)) { + return false; + } + if ($node->left instanceof Array_ && $node->right instanceof Array_) { + return false; + } + return true; + } +} +``` + +```php +// https://github.com/humbug/humbug/blob/master/src/Mutator/Arithmetic/Addition.php + +class Addition extends MutatorAbstract +{ + /** + * Replace plus sign (+) with minus sign (-) + * + * @param array $tokens + * @param int $index + * @return array + */ + public static function getMutation(array &$tokens, $index) + { + $tokens[$index] = '-'; + } + /** + * Not all additions can be mutated. + * + * The PHP language allows union of arrays : $var = ['foo' => true] + ['bar' => true] + * see http://php.net/manual/en/language.operators.array.php for details. + * + * So for this case, we can't create a mutation. + * + * @param array $tokens + * @param $index + * @return bool + */ + public static function mutates(array &$tokens, $index) + { + $t = $tokens[$index]; + if (!is_array($t) && $t == '+') { + $tokenCount = count($tokens); + for ($i = $index + 1; $i < $tokenCount; $i++) { + // check for short array syntax + if (!is_array($tokens[$i]) && $tokens[$i][0] == '[') { + return false; + } + // check for long array syntax + if (is_array($tokens[$i]) && $tokens[$i][0] == T_ARRAY && $tokens[$i][1] == 'array') { + return false; + } + // if we're at the end of the array + // and we didn't see any array, we + // can probably mutate this addition + if (!is_array($tokens[$i]) && $tokens[$i] == ';') { + return true; + } + } + return true; + } + return false; + } +} +``` + +As we can see the approach from infection with AST is much smaller, easier to read and understand. If you need another example to prove the benefits on your own just have a look into the FunctionCall mutator from [humbug](https://github.com/humbug/humbug/blob/1.0.0-alpha2/src/Mutator/ReturnValue/FunctionCall.php) and [infection](https://github.com/infection/infection/blob/0.2.1/src/Mutator/ReturnValue/FunctionCall.php). + +## More Mutators + +Infection has two additional mutator types as humbug. The first is the `Function Signature` mutator. It will +change the visibility of a method and change it to protected or private. If no error occurs it might be possible to change +the visibility to a more restricted one. + +Another one is the Loop mutator. This mutation changes some special keywords within a loop. You can see a table with possible +changes below: + + Name | Original | Mutated +-----------|-------------------------------|-------------------- + Break_ | break; | continue; + Continue_ | continue; | break; + Foreach_ | foreach ($someVar as …); | foreach ([] as …); + +## Performance + +To have a small test I used our [psh](https://github.com/shopwareLabs/psh) test suite as base for the performance measurements. First I started humbug with the psh test suite. As we can see, humbug created 156 mutations and needed overall ~ 54 seconds. + +```bash + _ _ _ +| || |_ _ _ __ | |__ _ _ __ _ +| __ | || | ' \| '_ \ || / _` | +|_||_|\_,_|_|_|_|_.__/\_,_\__, | + |___/ +Humbug 1.0-dev + +Humbug running test suite to generate logs and code coverage data... + + 76 [==========================================================] 4 secs + +Humbug has completed the initial test run successfully. +Tests: 76 Line Coverage: 69.15% + +Humbug is analysing source files... + +Mutation Testing is commencing on 32 files... +(.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out) + +......M.....M..SSS...................M.......M..M......M.M.. | 60 (17/32) +MMM.MS.....SSM.M...T........TSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS | 120 (25/32) +SSSSSSSS.....M......MSSS...M....M.M. + +156 mutations were generated: + 88 mutants were killed + 48 mutants were not covered by tests + 18 covered mutants were not detected + 0 fatal errors were encountered + 2 time outs were encountered + +Metrics: + Mutation Score Indicator (MSI): 58% + Mutation Code Coverage: 69% + Covered Code MSI: 83% + +Time: 53.84 seconds Memory: 8.00MB +Humbug results are being logged as TEXT to: humbuglog.txt +``` + +Next I had to create a small script to have the possibility to take the execution time of infection. No rocket science but it does the job. + +```bash +START=`date +%s%N` + +./bin/infection + +END=$((`date +%s%N` - $START)) + +bc <<< "scale=2; $END/1000000000" +``` + +Let us start infection and take the time. In the first run infection needed overall ~ 42 seconds and created 223 mutations. As we can see infection creates more mutations and needs less time. Awesome :-) + +```bash + ____ ____ __ _ + / _/___ / __/__ _____/ /_(_)___ ____ + / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ + _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / +/___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ + +Running initial test suite... + +Phpunit version: 6.5.5 + + 82 [============================] 2 secs + +Generate mutants... + +Processing source code files: 32/32 +Creating mutated files and processes: 223/223 +.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out + +....TMM.....ME.ES..S............................E. ( 50 / 223) +................M....M.M..M....EE...E....MMM.SM..M (100 / 223) +MMM.M.SS............SSSM...T......TSSSSSSSSSSSSSSS (150 / 223) +SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS....S...M.. (200 / 223) +.......M......M.M...... (223 / 223) + +223 mutations were generated: + 130 mutants were killed + 63 mutants were not covered by tests + 21 covered mutants were not detected + 6 errors were encountered + 3 time outs were encountered + +Metrics: + Mutation Score Indicator (MSI): 62% + Mutation Code Coverage: 72% + Covered Code MSI: 87% + +Please note that some mutants will inevitably be harmless (i.e. false positives). +42.33 +``` + +At last we should test the threading option of infection. Be aware this option can give you many false-positives results if your tests depends on each other or use a non stateless database for testing purpose. I started infection with 4 threads and the result is amazing. Instead of 42 Seconds infection needs only 19 seconds to execute the whole mutation stuff. + +```bash + ____ ____ __ _ + / _/___ / __/__ _____/ /_(_)___ ____ + / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ + _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / +/___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ + +Running initial test suite... + +Phpunit version: 6.5.5 + + 80 [============================] 2 secs + +Generate mutants... + +Processing source code files: 32/32 +Creating mutated files and processes: 223/223 +.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out + +....M.M....EESM.S..............................E.. ( 50 / 223) +................M...M.M...M...EE..E.....MMSM.M..MM (100 / 223) +M..SSM...........SSS..M.........SSSSSSSSSSSSSSSSSS (150 / 223) +SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS....S...M..... (200 / 223) +....M......M.M......TTT (223 / 223) + +223 mutations were generated: + 131 mutants were killed + 63 mutants were not covered by tests + 20 covered mutants were not detected + 6 errors were encountered + 3 time outs were encountered + +Metrics: + Mutation Score Indicator (MSI): 63% + Mutation Code Coverage: 72% + Covered Code MSI: 88% + +Please note that some mutants will inevitably be harmless (i.e. false positives). +19.11 +``` + +mutation framework | created mutations | execution time | processed mutations per second +-------------------------|-------------------|----------------|-------------------------------- +humbug | 156 | 54 seconds | 2.88 mutations +infection | 223 | 42 seconds | 5.30 mutations +infection with 4 threads | 223 | 19 seconds | 11.74 mutations + +## Conclusion +Compared to Humbug, Infection does a lot of things differently. I like the way, infection solves the mutation challenges. I hope this tool will be still maintained in the future and we will see some new features. Maybe in the future infection won't have to execute the whole test suite before doing the mutation stuff. This would be another great performance improvement. :-) + +If you are interested in the updated source code, it can be found [here](https://github.com/teiling88/mutation-testing). diff --git a/source/blog/_posts/2018-01-23-environment-variables-for-plugin-configuration.md b/source/blog/_posts/2018-01-23-environment-variables-for-plugin-configuration.md new file mode 100644 index 0000000000..724ef4c9a2 --- /dev/null +++ b/source/blog/_posts/2018-01-23-environment-variables-for-plugin-configuration.md @@ -0,0 +1,52 @@ +--- +title: How to use Environment Variables for Plugin Configuration +tags: +- environment +- deployment +- configuration + +categories: +- dev + +authors: [tn] +github_link: blog/_posts/2018-01-23-environment-variables-for-plugin-configuration.md + +--- + +
+Anyone who is just searching for the example source code and does not want to read the complete blog post, here it is. +
+ +This blog post describes a small proof of concept how we can handle different plugin configurations for multi stage environments. I use our Paypal plugin as an example and will overwrite the two config values `paypalUsername` and `paypalPassword`. + +## Preparation +At first, we have to create our environment variables. For a quick test, I added the environment variables to my `.htaccess` file: + +``` +SetEnv paypalUsername EnvPayPalUsername +SetEnv paypalPassword EnvPaypalPassword +``` + +I recommend for a production environment to set this kind of environment variables in your vhosts configuration file to protect you for unintentionally changes. After creating our environment variables we should make them available via the dependency injection container. +You have to add the following lines to your config.php: + +```php + [...], + 'custom' => + [ + 'paypalUsername' => getenv('paypalUsername'), + 'paypalPassword' => getenv('paypalPassword'), + ], +]; +``` + +With this addition our environment variables are available as parameter in the %shopware.custom% array. + +## Plugin Installation +In the last step we can install the ShopwareEnvironmentVariables plugin and extend our mapping in: [Reader](https://github.com/teiling88/shopware-environment-variables/blob/master/Reader.php#L35) If you don't know the name of the configuration element, you can easily create a small debug statement in the Reader.php. + + +# Conclusion + +This is a very small proof of concept how to overwrite plugin configurations by environment variables. **Nothing more**. You can extends this proof of concept to overwrite other configurations as well. diff --git a/source/blog/_posts/2018-01-31-free-online-course-shopware-developer-basic-training.md b/source/blog/_posts/2018-01-31-free-online-course-shopware-developer-basic-training.md new file mode 100644 index 0000000000..45dcfbe03a --- /dev/null +++ b/source/blog/_posts/2018-01-31-free-online-course-shopware-developer-basic-training.md @@ -0,0 +1,33 @@ +--- +title: Free Online Course - Shopware Developer Basic Training +tags: +- udemy +- training + +categories: +- dev + +authors: [dk] +github_link: blog/_posts/2018-01-31-free-online-course-shopware-developer-basic-training.md +--- + +[![Udemy Developer Basic Training Banner](/blog/img/udemy_dev_basic_training.jpg)](https://www.udemy.com/developer-training-basic-english) + +Interested in learning the basics of Shopware development? Or have you always wanted to create a plugin for Shopware? You now have the opportunity to dive into the technology behind Shopware 5 in our free course on Udemy, where you’ll be taken through the fundamentals of working with one of Europe’s leading open source eCommerce platforms. + +The course starts with setting up multiple shop configurations for different system environments and takes you through creating your first plugin from scratch. The result should serve as a foundation so that you can gradually extend the plugin by applying more knowledge throughout the remainder of the course. + +While creating new pages and setting up a redirection for unauthenticated users, you will +cover topics like: + +* Controllers and routing +* Templates +* Events +* Using the service container + +Two examples will round off the course by using what you’ve learned to create a new service and implement the plugin lifecycle methods. + +Your trainer will be Dominic Klein, Core Developer at Shopware. + +  +[Enrol now and start writing your first plugin!](https://www.udemy.com/developer-training-basic-english) diff --git a/source/blog/_posts/2018-03-15-online-certification.md b/source/blog/_posts/2018-03-15-online-certification.md new file mode 100644 index 0000000000..f77f6c812c --- /dev/null +++ b/source/blog/_posts/2018-03-15-online-certification.md @@ -0,0 +1,22 @@ +--- +title: New - Get your template or developer certification online +tags: +- certification +- training + +categories: +- dev + +authors: [nd] +github_link: blog/_posts/2018-03-15-online-certification.md +--- + +Interested in sharing your Shopware expertise with potential clients? Why not make it official - you can now take the exam to become a certified Shopware developer or template designer and developer online. Not only does a certificate increase your opportunities as a developer, but it’s proof of your expertise and ability to develop innovative solutions for Shopware. Equipped with everything you need to know to pass, our free trainings on Udemy are the perfect way to prepare for the exam. Those who are looking to become re-certified can also do so online. + +After booking the certification, you will receive an email with a link to the test and your individual access data. You do not have to take the test straight away - you are free to determine when and where to take the exam. There are between 19 and 20 questions for each test. You can find out what minimum score is required in order to pass plus further information about the certification on our FAQ page. + +**[To the FAQ page](https://en.shopware.com/academy/online-trainings/faq/)** + +After passing the test, your certificate will be made available for download in your account. The certificate will automatically appear in the partner listing and on your partner detail page, permitting you have an existing partner profile on the Shopware website. Your certificates help Shopware customers identify which partner is best suited for their project needs. If you are a plugin manufacturer, your certificate appears in your profile in the Community Store. Next to having an impact on your trust level, your certification affects whether your plugin qualifies for bronze, silver or gold. Every certificate is valid for 18 months. You will automatically receive a notification before your certificate is set to expire. + +**[Register to become certified today](https://en.shopware.com/academy/online-trainings/)** diff --git a/source/blog/_posts/2018-03-20-angular-master-class.md b/source/blog/_posts/2018-03-20-angular-master-class.md new file mode 100644 index 0000000000..d65a531ea7 --- /dev/null +++ b/source/blog/_posts/2018-03-20-angular-master-class.md @@ -0,0 +1,41 @@ +--- +title: Thoughtram Angular Master Class at Shopware +tags: +- training +- angular +- angularjs + +categories: +- dev + +authors: [nd] +github_link: blog/_posts/2018-03-20-angular-master-class.md +--- + +For three days, from **20th to 22nd June 2018** Shopware will host an awesome event that is all about [AngularJS](https://angularjs.org/). + +During these three days, you'll build a real-world Angular application, learning everything you need to call yourself an angular master! It's fun, and if you plan to do something with angular anytime soon, you definitely should join. + +What will you learn? + +- Components and Modules +- Fetching data using Http +- Advanced Dependency Injection +- Basic Forms +- Observables deep-dive +- Architectural patterns +- Component communication +- ContentChildren and ViewChildren +- [**ngrx**](http://ngrx.github.io/) + +More information about the event can be found on the thoughtram [website](http://thoughtram.io/angular-master-class.html) and [blog](https://blog.thoughtram.io/announcements/2018/02/07/announcing-angular-master-class-at-shopware.html). Do yourself a favour, check it out! + +...ok, if you've made it so far, you probably want to attend the event. + +Tickets can be bought via [eventbrite](https://www.eventbrite.de/e/angular-master-class-shopware-tickets-42705611634) - and you get **15% off**! + +Just enter the promitional code `ANGULAR_SHOPWARE` when ordering your ticket via [eventbrite](https://www.eventbrite.de/e/angular-master-class-shopware-tickets-42705611634): + + + +Be quick - while I'm writing this, only 4 tickets are left! \ No newline at end of file diff --git a/source/blog/_posts/2018-06-07-pair-programming.md b/source/blog/_posts/2018-06-07-pair-programming.md new file mode 100644 index 0000000000..3e7e4ca55e --- /dev/null +++ b/source/blog/_posts/2018-06-07-pair-programming.md @@ -0,0 +1,136 @@ +--- +title: An anecdote about pair programming +tags: +- Agile Development +- Extreme programming +- Development techniques + +categories: +- dev + +authors: [cr] +github_link: /blog/_posts/2018-06-07-pair-programming.md + +--- + +When we decided to rebuild a central piece of our Merchant Integration Architecture, I was assigned to write a new component, that handles asynchronous function calls through event queueing. +So from the get go it was assumed that this was quite a large task. After having completed a rough prototype to interact with our queue service, I hit a problem with deciding how to proceed. +There were many paths that needed to be worked on: error handling, at least once execution, backing up the queue to our database, et cetera. +So I asked for ideas during our daily stand-up meeting, our team lead suggested to team up with a colleague who's out of work to try [pair programming](http://www.extremeprogramming.org/rules/pair.html). + +## Parallel Thinking +At first we both had doubts about the benefits of sitting at the same computer, working at the same task and fighting over the keyboard. +But we quickly realized that the thought process was streamlined even though the typing speed remained the same. + +It's a well known fact, that in any moderately complex project or component there are many possible paths to think about. Generally this isn't a problem. +The developer figures out a [happy path](https://en.wikipedia.org/wiki/Happy_path) and thinks about the obvious edge cases, then they write down this path as code and add handling for the edge cases. + +Here the first advantage of pair programming takes effect: any two people don't exactly think the same. For example: +I tried to handle errors by writing a time stamp to the database and having an external script run periodically to re-queue eligible events. But then my coworker came up with the idea to simply use the build in delay ability of our +queue service. This is the strength of _parallel thinking_. While I was occupied thinking how to structure my code and where to put it, he had a whole different path in mind. +And the synthesis of these two ideas turned out to be the ideal structure for our use case. + +## It's not only the lines of code that count +"Figuring out solutions together is nice but a single dev could have done it by themselves.", some of you might say. + +But coming up with solutions, arguably the most fun part of the job, is by far not the only time sink developing software. Another significant source of delays and frustration are bugs. +And im not strictly talking about the ones being found by QA or customers, that need intensive research and tracing to fix. I mean those tiny bugs that keep the tests from succeeding and the dev from continuing their work. + +Here pair programming helps out again. Not only can two pairs of eyes see more than one, also two brains can hold more context than one. But what is context in this case? + +By context I mean the mental picture a developer has about the code while working on it. It consists of: +- Landmarks (central & important pieces of code) +- Call paths leading into and out of the code +- Well known pitfalls +- Limitations (real or imagined) + +This picture may not be accurate but it's the framework in which the dev actually makes changes to the code. + +Having two people keeping track of and building their context, instead of one, leads to many otherwise hard to find bugs being found in matter of minutes sometimes even instantly. That might sound overblown but I lost track of +how many time as soon a local test failed one of us went "Ahh that's the problem" and fixed something that was hard to see, like this: +```php +class Foo { + + /** + * @var string + **/ + private $prefix; + + /** + * @var array + **/ + private $config; + + /** + * @var SomeService + **/ + private $someService; + + /** + * @var array + **/ + private $stringVariations; + + public function __construct(array $config, SomeService $someService, string $prefix, string $str) + { + $this->config = $config; + $this->stringVariations = $this->createStringVariations($str); + $this->someService = $someService; + $this->prefix = $prefix; + } + + private function createStringVariations(string $str): array + { + $arr = []; + for($i = 1; i< 5; $i++) { + + $arr[] = $this->prefix . $str . $i; + } + + return $arr; + } +} +``` + +A problem like this caused really weird behavior locally and in the failing tests, but my coworker spotted it right away: + +The lines `$this->stringVariations = $this->createStringVariations($str);` and +`$this->prefix = $prefix;` simply had to be swapped. + +This bug was an artifact left over from a round of refactoring that simply was not present in my mind because I worked on the `createStringVariations` method before running the tests. + +These things make pair programming itself enjoyable and productive for the developers actively participating. But lets switch gears and talk about the effects on the team. + +## Islands made of knowledge +As is often the case every member of our team knows some parts of the project better than others and this is fine. But when this specialisation develops too far, it leads to pretty big problems. + +In the extreme case the [bus factor](https://en.wikipedia.org/wiki/Bus_factor) drops to 1 and should somebody be absent no progress can be made. +Even before the problem escalates that much, it will reduce the ability of the team to spread the work load properly. +Because not every team member can handle every task, some team members will inevitably get more work assigned than others. This can stall progress while leaving a part of the team bored. + +Pair programming prevents this by simply involving two developers. The effects of that are obvious. My coworker and I both know the component we wrote well, and already briefed other team members on it. + +## Logistics & Agility +Now for something that's well known: Most agile developers are not writing code nonstop through their workday. + +Of course this is not a bad thing, great ideas can spring from meetings and team performance is best evaluated through dialogue between its members. + +Nonetheless meetings are not necessarily relevant to the current task of the developer and these meetings therefore seem to stop progress for their duration. +At least in our case there were quite a few days when either I or my coworker were held up in meetings or one of us had a day off. + +Regardless we were able to keep the pace up, because the other one of us was still free to work on the task. +This was one of the really big speedups that pair programming gave to us. + +# Epilogue +Once we finished the component feedback from the team was very positive. +Together we managed to write a core component that performs well and is easy to adapt to changes. +Even though it took two developers 3 weeks to finish a task that would have taken 4 weeks by a single one . + +It's not only the result that convinced us to use pair programming in the future. +It also felt very productive and the steady progress throughout the development process satisfied the whole team. + +Although this post describes the success we had with pair programming, pair programming should be seen for what it is: just another technique to help you and your team. +It may not help your team as much as it helped us. It may not be needed for the size of tasks you regularly encounter. And you may not be comfortable with taking the backseat when writing code. +Due to these very valid concerns, I think every team should judge pair programming themselves. + +Still don't be put off at first and give a try the next time you are in a tight spot. diff --git a/source/blog/_posts/2018-07-25-switching-to-composer-installation.md b/source/blog/_posts/2018-07-25-switching-to-composer-installation.md new file mode 100644 index 0000000000..f42ecf2ebc --- /dev/null +++ b/source/blog/_posts/2018-07-25-switching-to-composer-installation.md @@ -0,0 +1,92 @@ +--- +title: An easy example how to switch from classic installation to composer installation +tags: +- Deployment +- Composer Installation + +categories: +- dev + +authors: [tn] +github_link: /blog/_posts/2018-07-25-switching-to-composer-installation.md + +--- + +
+Before you start please make a backup of your whole shop system! +
+ +In this blog post you will learn how to change your Shopware classic installation to the composer installation. I already mentioned it in the shopware [livestream](https://www.youtube.com/watch?v=oUME-FnlUKE) (a format you should know ;-) ). + +The requirements are very easy to fulfill. We need Shopware in the latest Version of 5.4. Nothing more. + +### Clean up classic installation +So let's start. Please go into the shopware root directory and delete some files and directories which are no longer needed from the classic installation: +```bash +rm -rv bin recovery vendor composer.* shopware.php +``` + +### Create composer installation + +Now we create a new composer project with the following command: + +```bash +composer create-project shopware/composer-project composer-installation \ + --no-interaction --stability=dev +``` +You should receive an error message like the following because we didn't create a .env file: + +```bash +Could not load .env file +Script ./app/post-update.sh handling the post-update-cmd event returned with error code 1 +``` + +### Merge classic and composer installation + +The command creates a new directory composer-installation wich contains all needed files for our composer installation. After that we have to move some files and directories stored in the new directory a level up with this commands: +```bash +mv composer-installation/{app,bin,Plugins,vendor,composer.json,shopware.php} ./ +mv composer-installation/.env.example ./.env +``` + +The directories `custom`, `files`, `media`, `themes`, `var` and `web` are equal with the composer installation so we can still use them and don't lose our files, plugins and themes. If you have plugins in `engine/Shopware/Plugins/Community` or `engine/Shopware/Plugins/Local` they must be moved to ```./Plugins```! Now we can delete the old `engine` directory because from now on the Shopware core files will be provided by the `shopware/shopware` composer package inside the `vendor` directory. + +After that we should reduce the .env file. Please use your database credentials from your `config.php`. If you have further config settings there move them to `app/config/config.php` and delete the `config.php` afterwards. The `.env` file should look like the following: + +``` +# Database credentials +DB_HOST=localhost +DB_DATABASE=shopware +DB_USERNAME=root +DB_PASSWORD=root +DB_PORT=3306 + +# This variable is checked by Shopware +DATABASE_URL=mysql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE} + +# Environment +SHOPWARE_ENV="production" +``` + +The next step is to set your database credentials as an environment variable. An example virtual host configuration can be found bellow: + +``` + + ... + SetEnv DB_USERNAME "root" + SetEnv DB_PASSWORD "root" + SetEnv DB_DATABASE "shopware" + SetEnv DB_HOST "localhost" + SetEnv DB_PORT "3306" + SetEnv SHOPWARE_ENV "dev" + +``` + +The last step is to uninstall the Shopware Auto Update Plugin. If you try an update with this Plugin, it will not work correctly and harm your Installation. You can uninstall the SwagUpdate Plugin via the following command: + +```bash +./bin/console sw:plugin:uninstall SwagUpdate +``` + +## Conclusion +The switch from classic installation to the new composer installation is not very difficult. Please use it if you want to achieve easier deployment and shopware updates. Our colleague Soner aka shyim develops an extension for the composer project to handle community store plugins as a composer dependency. If you are interested please have a look [here](https://github.com/shyim/store-plugin-installer). diff --git a/source/blog/_posts/2018-08-22-an-argument-against-microservices.md b/source/blog/_posts/2018-08-22-an-argument-against-microservices.md new file mode 100644 index 0000000000..ab9465320d --- /dev/null +++ b/source/blog/_posts/2018-08-22-an-argument-against-microservices.md @@ -0,0 +1,131 @@ +--- +title: An Argument against Microservices +tags: + - software architecture + - modularized application + - microkernel + - microservices +indexed: false +github_link: blog/_posts/2018-08-22-an-argument-against-microservices.md + +authors: [jp] +--- + +Microservices... I have problems with that. And from many discussions at conferences I get the impression that I might not be the only one who's *not getting* it. So in the following article I want to attempt to deep dive into the promises behind the Microservice architecture, lament on solutions and show the costs behind it. + +## Micro What? + +Distributed systems are all the rage now and have been at least over the past decade. The Cloud innovation, especially in the PaaS and FaaS (= keep Moore's Law alive by increasing the number of parallel processes) area, pushes software development to create finer grained executable stacks for more and more abstract machines. Technologies invented for [CORBA](https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture), [SOA](https://en.wikipedia.org/wiki/Service-oriented_architecture) or [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) are joining forces in Microservices to rule the world. + +Microservices are an architecture style. They are a possible solution to key demands for large scale development. Large software, large userbase, large development team, large data and so on. The central promise is that they allow for **far greater scaling** than any other style of architecture by reducing the need for **global decisions**. + +They usually get contrasted with the vast space of **monolithic** architecture styles => an application as a single interconnected entity of functionality. + +## So what is a Microservice? + +If you look at the - always insightful - [blog of Martin Fowler](https://martinfowler.com/articles/microservices.html) he describes the style as + +> [...] a particular way of designing software applications as suites of **independently deployable services** [...] + +How would this work? When planing a green field application one typically slices the project up front in **layers/components/milestones/building blocks**. If you plan to create Microservices you are now free to slice these building blocks into different applications. Communication is only allowed to go through an API and in many cases publish and subscribe to an event system. These Services do not share infrastructure. They are (potentially) deployed to different servers, write to their own storage and know only about necessary peers, that they themselves only access through a well defined API. The term *micro* comes from the idea that these services themselves should be *really small* or do *one thing only*. + +A single service then has to be a fully working application, that performs a miniscule part of the workload of the whole application. Well known and published examples of this architecture styles are [Netflix](https://medium.com/netflix-techblog) and Amazon, or eCommerce specific [Otto Group](https://dev.otto.de/tag/self-contained-systems/) or the [Spryker plattform](https://academy.spryker.com/). + +Of course the devil is in the detail. But simply put Microservices reduce code coupling in favor of networking and reduce organization coordination by giving more freedom to the individual teams and developers. So let's investigate these two promises separately: + +## Reduced Coupling + +It should be common knowledge by now, that coupling code is not only the one thing responsible for software to actually do something, but also the main cause of death for legacy systems. Systems need to be intertwined because one of the main benefits is, that existing data and functionality can be rearranged into new and interesting functionality. A byproduct of this rearrangement usually is friction, because the original system was not designed to behave in the newly implemented way. As a system gains new capabilities it internally starts to accumulate more and more friction. When handling this friction outweighs the time spent on the actual feature implementation you are in trouble. + +This insight is not only well known, but also a quite old. And of course multiple - not mutually exclusive - strategies are available to prevent this already: [DDD](https://en.wikipedia.org/wiki/Domain-driven_design), [Ports & Adapters](http://alistair.cockburn.us/Hexagonal+architecture), [SOLID](https://en.wikipedia.org/wiki/SOLID), [TDD](https://en.wikipedia.org/wiki/Test-driven_development), [Clean Code](https://medium.com/mindorks/how-to-write-clean-code-lessons-learnt-from-the-clean-code-robert-c-martin-9ffc7aef870c) and many other sources try to help you here. + +Microservices now add three main strategies for prevention. + +**Services share the least viable amount of information with each other.** If executed correctly, this is a very good thing! [Information hiding](https://en.wikipedia.org/wiki/Information_hiding) is by no means a new concept, and by no means a solved problem. Microservices effectively try to achieve this, by making the process simply **more painful**, than it was in the past. If you need an API, that contains additional information this needs additional planning and agreement between the developers. However the incentive still is to create a too broad interface if not checked otherwise. + +*Verdict: Adding pain is an interesting concept, but by no means a sufficient guarantee for good design.* + +**Make services small, so they can easily be replaced** Also a nice idea! Create a service in hours or days, if it doesn't add the expected value, just discard it. If a service that is currently not owned needs changes, the next developer can start at a green field, if he so chooses - just the API needs to be the same. I would bet you have done this countless times in the past, but without calling it service. Reengineering a class, a namespace, a feature. This is not new. Our programming languages actually have constructs for this. And at least in my experience, changing the internal structure without changing the external behaviour only works to a certain degree. Imagine moving from [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) to [CQRS](https://martinfowler.com/bliki/CQRS.html)+[ES](https://martinfowler.com/eaaDev/EventSourcing.html) for a single building block. This either has consequences for external usage or just adds friction. + +*Verdict: Really no help* + +**Use unreliable communication technologies so that all coupling is taken with a grain of salt.** This one is interesting: Networking adds the problem of delays, retries, unavailabilities to something that a monolith would do and guarantee in process. Failure tolerant networking adds the ability to silence functionality by only removing a single service without having to change anything else about the system. But there is a tradeoff: There needs to be an enforced convention to secure all applications behave like this and you need documentation to know which service needs to be killed for this. Exactly the thing you are now missing for your existing application, too. If you want to enforce methodologies like this I would make a case, that it is actually easier to do it through static analysis in a monolith. + +*Verdict: Has a desirable effect on your architecture.* + +Microservices effectively reduce coupling mainly by making it harder to use common abstractions across a project. Since these are all different applications you need some kind of package management system to achieve code sharing. But more on that in the next chapter. + +Ok, so where is the Microservice advantage exactly? Quite easily put: **Best practices get enforced through pain**. If you don't behave according to common wisdom they show bugs very early. But basically - at least I would argue - there are not that many benefits to a monolith from a software architecture standpoint here. + +## Freedom for teams + +Developers, developers, developers!!! Maybe Microservices show strengths here? As [Conway's Law](https://en.wikipedia.org/wiki/Conway%27s_law) states. + +> "organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations." - M. Conway + +While this abstract definition might be a little hard to grasp, there is a very good example that showcases the effect your organizational structure has on the software you produce: + +> "If you have four groups working on a compiler, you'll get a 4-pass compiler." - E. S. Raymond + +... because communication is most intense inside the team and gets sparser and sparser the further away another team is organization wise. Microservices are the logical conclusion to this. There needs to be a formal way to negotiate API interfaces between teams and that's it. This actually solves a real problem in software development: **Scale** If you ever started a green field project with a projection for more then two developers and a initial runtime of more then a year you should know how hard it is to get everyone working. Huge Silicon Valley corporations want to start projects with hundreds of developers or maintain flagship products with even more people. Communication becomes an overhead that delays a product significantly think of it like an expression of the big upfront design antipattern. The more people you have that need to work the more expensive it becomes to negotiate a single decision. + +By giving the highest degree of freedom to your teams the structure of your software changes, too. Dependencies in such a huge product can no longer be organized like a top down tree but are in fact more like a **bee hive**. Microservices can potentially be even more than that. The hive is multinational! Your services can be written in any language, come from any source and as long as network communication is possible you are fine. In extreme cases there can and will be C code communicating happily with a PHP script that notifies a Python app that provides data for Node app. Every one of these apps is self contained and uses a different set of tools for its build, deployment, integration and monitoring. Yeah! + +Now imagine something is broken. How can you inspect, debug and fix any issue in such a system? You need conventions! + +> "With great power comes great responsibility" - B. Parker + +If you want to base each new service on different technology you will find that your Microservices have multiple different solutions to the same common problem. [*Not invented here*](https://en.wikipedia.org/wiki/Not_invented_here) is the built-in result from this approach to team organizations. + +To stay with the bee hive metaphor: Even they grow over time from just a founding queen. + +In the end the freedom simply does not come for free. Where does scale outweigh central technical decision management? Some huge companies that develop concurrently on a single product of course have good reasons to go into that direction. And in the wild you will find scaled back solutions (e.g. single programming language, central logging and monitoring, centrally enforced architecture, even single process) that try to get the pros without the cons. But what is left in these cases? + + +## Problems arise + +I get hyped by talks on conferences. Either watching them directly live or in recording. But Microservices tend to tickle a nerve, where I constantly ask myself: "What aren't they talking about?" Microservices add pressure on sophisticated technological solutions, where a monolith can get away with far less effort. Take logging for example. While a single process application may be inspectable through a file log initially, a distributed system can not. + +Or how do you secure, that the negotiated API interfaces actually work? Integration tests of course! All neighboring services must be integrated with one another, through the network, which means a lot of of tests for a lot of of services with a lot of of communication. + +How will developers set up the application locally? Docker containers of course! Each Service runs in its own container, comes usually with at least one storage container. Also: Somewhere is a routing configuration. + +Scaling - contrary to common belief - is an issue too. There may be many independent services that need to be scaled appropriately to improve the overall performance of the application. + +While all these issues are of course solvable, they will block development resources. They may delay the product significantly. They are the real **cost of Microservices**. + +## The PHP factor + +Actually we are developing PHP here. PHP is single threaded, shared nothing, often stateless function execution at it's heart. So when we talk about scaling we actually do not mean the application itself but the limits in the infrastructure. MySQL too slow? Use ElasticSearch! Webserver responds too slow? Put a load balancer in front of two! Horizontal scaling is actually not that hard in PHP. + +Oh and while we are at it. The *Deployment Monolith Antipattern* is solvable through package management too. Just look at the shopware plugin store. PHP is an interpreted language, source changes in production are actually not the hardest problem. + +## Benefits in cherry picking + +Of course I do not want to discard the whole notion of Microservices. There is value, but there are also other options. From a technical standpoint I would argue that projects usually do not fail because the test suite was too sophisticated, the documentation too helpful, or the architecture was too clear and predictable. + +And there are real world benefits to all of them! Good old static code analysis will help you greatly with dependency management and finally this can all be part of the global CI process. + +And it even might be necessary to create different applications for parts of your problem. Maybe the requirements for a single part of your application are so drastically different that this becomes necessary. Feel free to use concepts from Microservices. **Hybridization** of architecture is not necessarily a bad thing. + +Problem solved? Well there are still the organizational difficulties... + +## Alternative: (Micro-)Kernel + +Microservices and monoliths are on opposing sides of the static dependency spectrum. While a monolith is interconnected and interdependent, a Microservice is *(almost)* not. Well this spectrum does not only consist of black and white. My favorite alternative is the kernel style. A kernel represents a core domain of an application. In our case eCommerce. This kernel then provides an extension mechanism for others to extend, alter and replace these core concepts. A plugin system if you want. + +Apart from the basic eCommerce workflows the kernel provides the technical groundwork for all plugins to use. It contains a deployment mechanism, package management and lays the technological groundwork for the necessary infrastructure coupling. Most importantly it provides **the base quality of the product as a whole**. + +In a company driven by Conways law the natural limit for a kernel is the amount of features a single team can handle. A basic runtime that shares a technical and functional base for everyone to harness. One arranges orthogonal features in orthogonal teams and creates a core, that is owned and used by everyone. The viability of this approach is proven by big and small companies alike, think operating system vendors, framework vendors and the like. There is however a critical situation when a kernel's size outgrows the team. A need for action arises. A split is necessary. + +Conway's insight should mean that an organization in order to produce software needs to be fluid enough to support good design. So if an application changes the communication and therefore team structure needs to be changed in the best interest of the product. + +## Conclusion + +From a technical standpoint there is no clear benefit in using Microservices as the main application design. They just have the potential to add risk and cost to a project. In my opinion this style just complicates stuff from the get go. Sizing is a really hard problem in software and I have seen many applications, that where surprisingly large in relation to the work they actually performed. If you have a fairly simple problem to solve a single developer will be faster to fix it then twenty. Or as we say: + +> Not everybody is Netflix + +If in the past you were not able to create a good application through concepts of OOP, why would you be able to create such a thing by adding layers of indirection on top of the actual problem. If the application design rots, there is no reason to expect adding networking to the communication will help. Microservices are by no means automatically cheaper in development or maintenance than a monolith. So when investing into new technology, maybe one should rather invest in solving the actuall problems of the past, directly. + +That's it for my rant. Thanks for reading! diff --git a/source/blog/_posts/2018-09-28-clickstream-analysis-with-server-logs.md b/source/blog/_posts/2018-09-28-clickstream-analysis-with-server-logs.md new file mode 100644 index 0000000000..6feb1bce1f --- /dev/null +++ b/source/blog/_posts/2018-09-28-clickstream-analysis-with-server-logs.md @@ -0,0 +1,68 @@ +--- +title: Clickstream analysis with server logs +tags: + - marketing + - Big data + - machine learning + - clickstream +indexed: false +github_link: blog/_posts/2018-09-28-clickstream-analysis-with-server-logs.md + +authors: [rc] +--- + +Demographic data like server logs are more valuable than many shop user think. With server logs we identify user groups, push valuable products or do customer specific shop marketing with data lying around on the server. In this blog post I will describe how to use the data in a theoretical way with a book selling online shop. + +### Identify the customer + +First of all, lets divide the customers in two groups. Traditional customers buy printed books and innovative ones buy other types like audio books. To filter the customers we have to prepare the log data and bring the data together to form sessions. Sessions contain all server log entries from a specific IP address starting with the the first view of the shop until he leaves it. An access to a SuperTracker Database can help to validate and add additional attributes and characteristics to the sessions. After defining the sessions they have to be filtered into useful and useless ones. Useless sessions are caused by bots and customers with only one or two log entries who accidentally visited the shop. +> (Suchacka & Chodak, 2017) + +
+ + +###### Association rule generation framework for a B2C website (Suchacka & Chodak, 2017) +
+ +### Classification of sessions + +After the session filter, every session will be classified. Basic Classifications can be the time duration on the shop, how many pages were visited or the time duration on a specific page. These classified sessions can also be more detailed classified. The classification parameters can be wich browser was used, the login status of the customer, did the customer add some products into the basket or actually performed an order. Using the data with an A-priori algorithm, events can be identified, which improves the chances of customer orders. +> (Suchacka & Chodak, 2017) + +### Result click stream analysis + +
+ + +###### Histogram of session lengths for buying sessions for innovative and traditional customers (Suchacka & Chodak, 2017) +
+ +In our case traditional customer perform orders very fast and innovative customers wander longer through the shop before perform an order. Another result of the analysis determine 89 percent of innovative customers perform an order by entering the shop with a link from a search engine, add an article in the basket and open up to 45 pages on the system. Traditional customers have a tendency of 92 percent to perform an order after login in, visit 30 to 75 pages and be in the shop around 10 to 25 minutes. With these results customer group specific marketing can be performed. We can improve our page views of innovative customers by adding more optimised ads in search engines. +> (Suchacka & Chodak, 2017) + +### Gender prediction + +Another way to use the session data is to predict the customers gender and show specific landing pages to them. For gender prediction we have to classify and filter the session in other ways. We do not want only to know how long the customer was on one page or in the shop but also in which season and on which day and month of the year. The amount of viewed products is also a possible filter for the sessions. Another group of data can be formed by display the categories and products in a binary tree view and show every switch and view of the category and product pages on this tree. With machine learning algorithm like [Random Forest](https://en.wikipedia.org/wiki/Random_forest), [Support Vector Machine](https://en.wikipedia.org/wiki/Support_vector_machine), [BayesNet](https://en.wikipedia.org/wiki/Bayesian_network) or [Gradient Boosting Decision Trees](https://en.wikipedia.org/wiki/Gradient_boosting), we can predict the gender from sessions. +> (Duc Duong et al., 2016) +> (Lu et al., 2015) + +### Machine learning algorithms + +* [Random Forest](https://en.wikipedia.org/wiki/Random_forest) is a classification method working with decision trees. Through randomising the decision tree is growing and at the end, the class with the most votes will be chosen. +* [Support Vector Machine](https://en.wikipedia.org/wiki/Support_vector_machine) classifies the data with hyperplanes in the vector space of the objects. +* [BayesNet](https://en.wikipedia.org/wiki/Bayesian_network) consists of a directed acyclic graph with various nodes and edges, which are randomised variables and conditional dependencies between the variables. +* [Gradient Boosting Decision Trees](https://en.wikipedia.org/wiki/Gradient_boosting) create weak classifications which will be combined to one strong classification. + +To choose the right method for learning with test data depends on how balanced the data is. With unbalanced Data like 80 percent women and 20 percent men, a “Cost-Sensitive Learning” method is needed to minimise the total costs and therefore get a higher statistically percentage for the right gender prediction. +> (Duc Duong et al., 2016) + +### Result of gender prediction + +In the end it depends on the quality of the data and the chosen algorithm how high the percentage for the right gender prediction is. Two teams at a tournament from [FPT Corporation of PAKDD’15](https://knowledgepit.fedcsis.org/contest/view.php?id=107) performed with the given dataset a 81,4 percent correct prediction for both gender and the other team a 95 percent correct prediction of the female and a 77 percent correct prediction of the male gender. +> (Duc Duong et al., 2016) (Lu et al., 2015) + +### Literature + +* Suchacka, G., & Chodak, G. (2017). Using association rules to assess purchase probability in online stores. Information Systems and E-Business Management, 15(3), 751–780. https://doi.org/10.1007/s10257-016-0329-4 +* Duc Duong, Hanh Tan, & Son Pham. (2016). Customer gender prediction based on E-commerce data. In 2016 Eighth International Conference on Knowledge and Systems Engineering (KSE) (pp. 91–95). IEEE. https://doi.org/10.1109/KSE.2016.7758035 +* Lu, S., Zhao, M., Zhang, H., Zhang, C., Wang, W., & Wang, H. (2015). GenderPredictor: A Method to Predict Gender of Customers from E-commerce Website. In 2015 IEEE/WIC/ACM International Conference on Web Intelligence and Intelligent Agent Technology (WI-IAT) (pp. 13–16). IEEE. https://doi.org/10.1109/WI-IAT.2015.106 \ No newline at end of file diff --git a/source/blog/_posts/2018-11-30-how-to-use-phive.md b/source/blog/_posts/2018-11-30-how-to-use-phive.md new file mode 100644 index 0000000000..e7e01c2109 --- /dev/null +++ b/source/blog/_posts/2018-11-30-how-to-use-phive.md @@ -0,0 +1,74 @@ +--- +title: What is phive and why you should use it! +tags: +- Deployment +- Phive +- Phar + +categories: +- dev + +authors: [tn] +github_link: /blog/_posts/2018-11-30-how-to-use-phive.md + +--- + +At the beginning of this year we decided to create a new enterprise accelerator for our enterprise eco system. The so called Pricing Engine. With this new project we evaluated the PHAR Installation and Verification Environment (PHIVE) to manage all our *.phar dev tools. + +Normally we store our dev tools in the document root of our git repository. As an example in the B2B-Suite we have seven several phar files (deptrac, phpDocumentator, phpcpd, phploc, phpstan, psh and security-checker). With phive we can manage all our dev tools with a phive.xml file. As you can see in the below example, you can define the version which should be used, the position where it should be placed and at last if it should be a symlink or a real copy of the phar file. Normally phive downloads the specific phar file and symlinks it in the place where it has to be. + +```xml + + + + + + + +``` + +This sounds very interesting isn't it? But how difficult is it to create a new software project and use phive for managing all dev tools? + +## Setup a new Project with phive +The question is very easy to answer, it is as easy as to create a new composer project. As a preparation we have to install phive on our system. To do it use the snippet below: + +```bash +wget -O phive.phar https://phar.io/releases/phive.phar +wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc +gpg --keyserver pool.sks-keyservers.net --recv-keys 0x9D8A98B29B2D5D79 +gpg --verify phive.phar.asc phive.phar +chmod +x phive.phar +sudo mv phive.phar /usr/local/bin/phive +``` + +After the installation we can easily install php-cs-fixer with ```phive install php-cs-fixer```. In this example php-cs-fixer is an alias and corresponds to the GitHub repository [FriendsOfPHP/PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer). You can find alternative installation instructions below: + +```bash +phive install php-cs-fixer +phive install FriendsOfPHP/PHP-CS-Fixer +phive install https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v2.13.1/php-cs-fixer.phar +``` + +You can use the already mentioned alias, or an github repository with a maintained release section or the direct url to the specific file. + +## Own phar files +To install own phar files with phive you have several ways to achieve this: + +* GitHub Releases (phive install username/project) +* registered alias on phar.io (phive install alias) +* explicit url (phive install https://server/file-1.0.0.phar) + +We will have a more detailed look in the GitHub Releases way and we will use [psh](https://github.com/shopwareLabs/psh) as an example for that. We have the following requirements: + +* GPG Key for your project +* GPG Key must be public available on the SKS Keyservers +* already generated phar file. + +If all requirements fit we open the console and go to our project directory where the phar file is stored. Now we will use this command ```gpg -u psh@example.com --detach-sign --output psh.phar.asc psh.phar``` to sign our generated phar file. After that we have a new file psh.phar.asc. Both files has to be uploaded to our existing version under https://github.com/shopwareLabs/psh/releases. That was all the work we needed for that. If you want access your phar file via url you have to store the .asc file in the same directory. + +Now you can use ```phive install shopwareLabs/psh``` to install psh.phar. We have already [created](https://github.com/phar-io/phar.io/commit/d88298103c9fe7a99fdc00d930f505c83b67ada0) an alias for psh so a ```phive install psh``` is also possible. If you want to create your own alias you just have to create a pull request for [phar-io/phar.io](https://github.com/phar-io/phar.io) + +## Conclusion +Phive is a very good tool to manage the ecosystem around the needed dev tools. You escape easily the dependency hell and manage updates of your tool has never been so easy. Furthermore you save much space in your repository. The usage of OpenPGP/GnuPG secures the hole workflow of getting the needed phar files. + +At the moment we uses phive only for the Pricing Engine. In the future we will integrate phive in our other projects. Who does not use phive at the moment, should start not later than now. diff --git a/source/blog/_posts/2019-06-11-tech-keynote.md b/source/blog/_posts/2019-06-11-tech-keynote.md new file mode 100644 index 0000000000..e5e3cc4883 --- /dev/null +++ b/source/blog/_posts/2019-06-11-tech-keynote.md @@ -0,0 +1,154 @@ +--- +title: Shopware Tech Keynote +tags: +- Tech Keynote +- Shopware Community Day +- Shopware 6 + +categories: +- dev + +robots: + hide: true + +authors: [dn] +github_link: /blog/_posts/2019-06-11-tech-keynote.md + +--- + +# Shopware Tech Keynote + +Shopware Community Day 2019 was the first time, we had two keynotes at the same time: The main keynote by our CEO and co-founder Sebastian Hamann - and the tech keynote by myself. For me it was a great honor and pleasure to give you a technical overview of Shopware 6; in this blog post, I want to summarize my thoughts about Shopware 6 + +# Why we went Shopware 6 + +Shopware 6 is a complete rewrite of Shopware. Of course we didn't reinvent the wheel for things, that we were happy with in Shopware 5; at the same time we wanted to improve many topics we have not been able to address in Shopware 5. + +Furthermore we thought a lot, how E-Commerce might look like in a few years, which requirements we might need to address - and which customer segments we want to reach. We quickly found, that it was not about hype technologies - but about creating a broad foundation, that small and smallest merchants as well as enterprise merchants could be successful with. In addition to many technological considerations this especially means, that merchants needs to know what customers want - perhaps without the customers knowing, what they want. + +In order to reach existing and new customers, I pointed out three dimensions in my keynote: + +- Internationalisation +- New channels +- Customer loyalty + +In Shopware 6 we placed great emphasis on these dimensions. For internationalisation we have new concepts for translations (and inheritance of translations). Also handling of gross and net prices as well as prices per currency has improved. The new SalesChannel concept allows for a new perspective on additional channels such as eBay, Amazon, Instagram etc. The new RuleBuilder allows you to address every SalesChannel specifically - and is also a powerful regarding customer loyalty, as you can shape very individual customer segments and address any customer exactly as you want to. + +In order to set the potential of those dimensions free, complexity must be reduced. Merchants need to be able to try out new things and want to quickly identify failures and successes in order to learn from their experiences. At the same time enabling creativity and speed also means, that developers want to know, which direction to go. "Everything is possible somehow" is not a sentence, that will help you to create awesome software. That's why we reconsidered how e.g. our data model should look like, how extensions should take place and how developers want to work with the shopping cart. + +Enabling people to use the new technologies also implies, that we need to reduce legal barriers. By switching to the more permissive MIT license, shopware clearly commits to the open source community. + +For me, this is the philosophy behind Shopware 6: We enjoy new technologies and are excited to try them out. But the developers and merchants are the ones, who are truly innovative in their everyday projects. And they do neither need a tight e-commerce corset nor a spaceship toolbox. They need a reliable foundation on which to build. And that's Shopware 6: Simple, flexible, state of the art and open source. + +# Tech + +Now let's have a look at the technology. The most obvious change is the switch to Symfony, for sure. Symfony is probably the most popular PHP framework and has shaped a whole generation of developers. By using Symfony in Shopware, we make sure, that it becomes easier to onboard developers on Shopware 6. + +For the same reason, we are switching from ExtJS to VueJS as a javascript framework for our new admin. VueJS describes itself as a "progressive and incrementally adoptable" framework. For us this means, that it can be tailored to our needs and requirements. And again: We are convinced, that it makes it easier to onboard new developers to Shopware 6. + +## DAL + +Another major change is the new data abstraction layer (DAL). This is a layer between your database storage and the actual application and takes care of all your data operations - be it reads, writes, searches or aggregations. + +![DAL schema](/blog/img/dal.png) + +In Shopware 5 we introduced a similar layer for read operations only for products in the storefront. We found, that it made Shopware much more understandable and predictable for the development community. But in difference to the product services in Shopware 5, the DAL takes care of all entities and all kind of data access. For that reason, no more custom SQL queries should be required anymore. Furthermore, the DAL can also take care of syncing various storages: In modern e-commerce infrastructures you will easily find ElasticSearch and Redis in addition to MySQL. The DAL can be used to sync those storages, so that MySQL is used as a primary storage but ElasticSearch is always kept in sync. And there are even more functionalities such as versioning or translations, that you can make use of easily: + +```php +class ProductManufacturerDefinition extends EntityDefinition +{ + public function getEntityName(): string + { + return 'product_manufacturer'; + } + + protected function defineFields(): FieldCollection + { + return new FieldCollection([ + (new IdField('id', 'id'))->addFlags(new PrimaryKey(), new Required()), + new VersionField(), + + new FkField('media_id', 'mediaId', MediaDefinition::class), + new StringField('link', 'link'), + new TranslatedField('name'), + new TranslatedField('description'), + new TranslatedField('customFields'), + + new ManyToOneAssociationField('media', 'media_id', MediaDefinition::class, 'id', true), + (new OneToManyAssociationField('products', ProductDefinition::class, 'product_manufacturer_id', 'id'))->addFlags(new RestrictDelete(), new ReverseInherited('manufacturer')), + (new TranslationsAssociationField(ProductManufacturerTranslationDefinition::class, 'product_manufacturer_id'))->addFlags(new Required()), + ]); + } +} + +``` + +The above example show, how to define a product manufacturer entity with various translatable fields and relations to e.g. media and products. For more details please have a look at [the description in our documentation](). + +## Extensions + +If you are already familiar with the Shopware 5 plugin system, I've good news for your: Many of the patterns will look familiar to you. + +``` + +└── custom + └── plugins + └── PluginQuickStart + ├── src + │ ├── Controller + │ │ └── MyController.php + │ ├── Resources + │ │ ├── config + │ │ │ ├── config.xml + │ │ │ ├── routes.xml + │ │ │ └── services.xml + │ ├── Service + │ │ └── MyService.php + │ ├── Subscriber + │ │ └── MySubscriber.php + │ └── PluginQuickStart.php + └── composer.json +``` + +So you still have a base plugin file, where you can implement various methods to define the behaviour of your plugin during installation or uninstallation. Creating new services or extending existing ones is possible using the `service.xml` definition file. And if you want to use events, you can write the same kind of subscribers, you've been using in Shopware 5, too. For more details, have a look at our [plugin quickstart guide](). + +## Summary + +All in all a lot of things have changed in Shopware 6. But we are convinced, that this will make things easier for you in the long run. If you are looking for additional resources, have a look at our [documentation]() or our [training program](). There are a lot of free remote trainings, that will help to get started with Shopware 6. + +# Business Models + +Talking about "business models" in a tech keynote might seem a little strange. But if we really want to understand, what kind of requirements are relevant for our development community, we had to understand, what kind of requirements you get in projects - and where things became tedious in the past. + +## RuleBuilder + +The RuleBuilder is a whole new concept regarding customer targeting. In Shopware 5 (and many other e-commerce systems) you will just find some kind of "groups" that can be used for pricing, promotion, content, category restrictions, countries etc. The more usecases one had, the more complicated it became to maintain all the groups. + +The RuleBuilder goes another approach: It allows you to create complex and nested rules that describe a customer you want to address for a certain usecase. This not only make easier to create "dynamic groups" (e.g. a group of all customers older than 18 or a group with all customers from a certain area) but also makes it easier to give those customers special pricing, special content or restrict orders / payments / shipments for them. + +The new RuleBuilder allows Shopware 6 to meet requirements that would have required a custom extension in the past. + +## New Cart + +One of the earliest parts of Shopware 6 is the new cart. Actually the first concepts where created in 2016. We analyzed many usecases where people had to modify the cart in order to meet the merchant's requirements. + +As the new RuleBuilder can also be used to create price definitions, price handling becomes much more flexible with the new cart. Not only is the cart much more powerful when it comes to the question which customer gets which price: it also is more flexible regarding threshold prices per currency and handling of gross / net prices. + +Another big topic was database performance. On the one hand, you want to make sure, that during the cart process all pricing and stock information is consistent and correct and no order is possibly lost. On the other hand, you want to reduce the impact of the cart onto the database as much as possible, as this critical part of most e-commerce-systems is notoriously known for its heavy write operations. This is why the new cart is much more lightweight and reduces read / write operations as much as possible. The price, to which this comes, are conventions regarding the information you should / could access with your extensions. + +## Shopping experiences + +A new tool for merchants are the so called "shopping experiences". With this tool, it becomes easier to create unique and appealing content for the online shop. Of course - as you might now it from shopping worlds in Shopware 5 - you can create content. But the shopping experiences set another focus: + +There a dozens of prefabricated blocks, which show you all kind of combinations between images, texts, sliders, videos and commerce components. You can just pick the block, that comes closest to your imagination and drop it to the designer. This designer will always give you a realistic overview of how the shopping experience could look like in the frontend. At the same time it allows you to configure the elements to your needs: You can edit texts, swap elements with others or rearrange blocks. Every block is responsive by default. So you don't need to think about viewports and columns any more. + +![data mapping with the CMS](/blog/img/cms-data-mapping.png) + +What makes the shopping experience so powerful is the fact, that every page you created this way, can be used as a layout for category pages, product pages or landing pages. You can even dynamically reference properties of those pages. For example: If you create a layout for categories, you can dynamically reference the category's name. For a product layout, you could dynamically reference an image of the product or its description. + +And for the future, we are planning more functionalities, which will come in handy in many projects: E.g. editorial workflows for content pages or versioning. + +# Conclusion + +Of course this can just be a glimps of the new technologies, functionalities and concepts in Shopware 6. But what makes this awesome from my perspective is the fact, that based on our experiences with small, medium and large e-commerce projects we asked ourselves: What do customers want? What do merchants need? What do you need as a developer? This common theme makes Shopware 6 more fun to use - and more powerful in projects. +At the SCD 19 we released the developer preview of Shopware 6. With this version we want to collect your feedback as a developer. Feel free to check out our [Shopware 6 repository on Github](https://github.com/shopware/platform) and give us feedback. diff --git a/source/blog/img/2017-10-23-meetnext-recap/campus_view.jpg b/source/blog/img/2017-10-23-meetnext-recap/campus_view.jpg new file mode 100644 index 0000000000..de57fbd811 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/campus_view.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/campus_view_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/campus_view_thumb.jpg new file mode 100644 index 0000000000..964885e22a Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/campus_view_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/cody_army.jpg b/source/blog/img/2017-10-23-meetnext-recap/cody_army.jpg new file mode 100644 index 0000000000..0da3367460 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/cody_army.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/cody_army_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/cody_army_thumb.jpg new file mode 100644 index 0000000000..36a6af5c6d Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/cody_army_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_00.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_00.jpg new file mode 100644 index 0000000000..e42bd5f5b9 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_00.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_00_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_00_thumb.jpg new file mode 100644 index 0000000000..a4b5927123 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_00_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_01.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_01.jpg new file mode 100644 index 0000000000..27dda6b73a Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_01.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_01_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_01_thumb.jpg new file mode 100644 index 0000000000..8d58c99439 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_01_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_02.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_02.jpg new file mode 100644 index 0000000000..01f724ddb4 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_02.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_02_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_02_thumb.jpg new file mode 100644 index 0000000000..f051ae2d0d Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_02_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_03.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_03.jpg new file mode 100644 index 0000000000..39e8304a06 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_03.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_03_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_03_thumb.jpg new file mode 100644 index 0000000000..305e305d62 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_03_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_04.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_04.jpg new file mode 100644 index 0000000000..8f70cd492c Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_04.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_04_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_04_thumb.jpg new file mode 100644 index 0000000000..df1f92d6b8 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_04_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_05.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_05.jpg new file mode 100644 index 0000000000..d8cc8823e8 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_05.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_05_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_05_thumb.jpg new file mode 100644 index 0000000000..5a255129c0 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_05_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_06.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_06.jpg new file mode 100644 index 0000000000..36f7a36c5b Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_06.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_06_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_06_thumb.jpg new file mode 100644 index 0000000000..31edb137a3 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_06_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_07.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_07.jpg new file mode 100644 index 0000000000..b57c40ba50 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_07.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/impressions_07_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/impressions_07_thumb.jpg new file mode 100644 index 0000000000..eb7ab9458f Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/impressions_07_thumb.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/room_overview.jpg b/source/blog/img/2017-10-23-meetnext-recap/room_overview.jpg new file mode 100644 index 0000000000..7f4e2112b6 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/room_overview.jpg differ diff --git a/source/blog/img/2017-10-23-meetnext-recap/room_overview_thumb.jpg b/source/blog/img/2017-10-23-meetnext-recap/room_overview_thumb.jpg new file mode 100644 index 0000000000..745531ce50 Binary files /dev/null and b/source/blog/img/2017-10-23-meetnext-recap/room_overview_thumb.jpg differ diff --git a/source/blog/img/2017-11-08-exclusive-enterprise-dev-training/banner.png b/source/blog/img/2017-11-08-exclusive-enterprise-dev-training/banner.png new file mode 100644 index 0000000000..263ec6df02 Binary files /dev/null and b/source/blog/img/2017-11-08-exclusive-enterprise-dev-training/banner.png differ diff --git a/source/blog/img/2018-09-28-clickstream-analysis-with-server-logs/framework.jpg b/source/blog/img/2018-09-28-clickstream-analysis-with-server-logs/framework.jpg new file mode 100644 index 0000000000..58ba75fa0d Binary files /dev/null and b/source/blog/img/2018-09-28-clickstream-analysis-with-server-logs/framework.jpg differ diff --git a/source/blog/img/2018-09-28-clickstream-analysis-with-server-logs/histogram.jpg b/source/blog/img/2018-09-28-clickstream-analysis-with-server-logs/histogram.jpg new file mode 100644 index 0000000000..0b8bfce4ca Binary files /dev/null and b/source/blog/img/2018-09-28-clickstream-analysis-with-server-logs/histogram.jpg differ diff --git a/source/99-paper-cuts/img/paper-cuts-logo.png b/source/blog/img/99-paper-cuts/paper-cuts-logo.png similarity index 100% rename from source/99-paper-cuts/img/paper-cuts-logo.png rename to source/blog/img/99-paper-cuts/paper-cuts-logo.png diff --git a/source/blog/img/advantages-of-integration-testing/application-diamond-testing.svg b/source/blog/img/advantages-of-integration-testing/application-diamond-testing.svg new file mode 100644 index 0000000000..1cca890360 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/application-diamond-testing.svg @@ -0,0 +1 @@ +MyControllerContextProviderMyServiceMyRepositoryValidationServiceSessionStorageControllerHelperStorageStorage \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/application-functional-testing.svg b/source/blog/img/advantages-of-integration-testing/application-functional-testing.svg new file mode 100644 index 0000000000..13d4607401 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/application-functional-testing.svg @@ -0,0 +1 @@ +MyControllerContextProviderMyServiceMyRepositoryValidationServiceSessionStorageControllerHelperStorageStorageApplicationFunctionalTest \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/mocking-error.svg b/source/blog/img/advantages-of-integration-testing/mocking-error.svg new file mode 100644 index 0000000000..1e083e8de5 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/mocking-error.svg @@ -0,0 +1 @@ +MyServiceTestMyRepositoryMockValidationServiceMockMyRepositoryValidationServiceMyService \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/my-service-integration-test-with-saturation.svg b/source/blog/img/advantages-of-integration-testing/my-service-integration-test-with-saturation.svg new file mode 100644 index 0000000000..bd195f64c4 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/my-service-integration-test-with-saturation.svg @@ -0,0 +1 @@ +MyServiceTestMyRepositoryValidationServiceMyServiceStorage \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/my-service-integration-test.svg b/source/blog/img/advantages-of-integration-testing/my-service-integration-test.svg new file mode 100644 index 0000000000..096bdfb703 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/my-service-integration-test.svg @@ -0,0 +1 @@ +MyServiceTestMyRepositoryValidationServiceMyServiceStorage \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/standard-application-with-diamond.svg b/source/blog/img/advantages-of-integration-testing/standard-application-with-diamond.svg new file mode 100644 index 0000000000..d452039c6a --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/standard-application-with-diamond.svg @@ -0,0 +1 @@ +ContextProviderMyServiceMyRepositoryValidationServiceSessionStorageControllerHelperStorageStorageMyControllerContextProviderMyServiceMyRepositoryValidationServiceSessionStorageControllerHelperStorageStorageMyServiceTestContextProviderTestControllerHelperTestMyRepositoryValidationServiceSessionStorageStorageStorageMyRepositoryTestValidationServiceTestSessionStorageTestMyControllerTest \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid-functional.svg b/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid-functional.svg new file mode 100644 index 0000000000..d1d1d71a19 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid-functional.svg @@ -0,0 +1 @@ +MyControllerContextProviderMyServiceMyRepositoryValidationServiceSessionStorageControllerHelperStorageStorageApplicationFunctionalTest \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid-integration.svg b/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid-integration.svg new file mode 100644 index 0000000000..65a5788a3b --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid-integration.svg @@ -0,0 +1 @@ +ContextProviderMyServiceMyRepositoryValidationServiceSessionStorageMyServiceIntegrationTestContextProviderIntegrationTestStorageMockStorageMock \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid.svg b/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid.svg new file mode 100644 index 0000000000..9b24e8cb88 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/standard-application-with-pyramid.svg @@ -0,0 +1 @@ +MyControllerContextProviderMyServiceMyRepositoryValidationServiceControllerHelperContextProviderMockMyServiceMockControllerHelperMockMyRepositoryMockValidationServiceMockSessionStorageMockDatabaseMockSessionStorageDatabaseMockMyControllerTestMyServiceTestContextProviderTestControllerHelperTestMyRepositoryTestValidationServiceTestSessionStorageTest \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/standard-application.svg b/source/blog/img/advantages-of-integration-testing/standard-application.svg new file mode 100644 index 0000000000..ffd5fc3f7a --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/standard-application.svg @@ -0,0 +1 @@ +MyControllerContextProviderMyServiceMyRepositoryValidationServiceSessionStorageControllerHelperStorageStorage \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/test-diamond.svg b/source/blog/img/advantages-of-integration-testing/test-diamond.svg new file mode 100644 index 0000000000..75446a67d2 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/test-diamond.svg @@ -0,0 +1 @@ +UnitTestsIntegrationTestsFunctionalTests \ No newline at end of file diff --git a/source/blog/img/advantages-of-integration-testing/unit-test-pyramid.svg b/source/blog/img/advantages-of-integration-testing/unit-test-pyramid.svg new file mode 100644 index 0000000000..d5942bbf12 --- /dev/null +++ b/source/blog/img/advantages-of-integration-testing/unit-test-pyramid.svg @@ -0,0 +1 @@ +UnitTestsIntegrationTestsFunctionalTests \ No newline at end of file diff --git a/source/blog/img/ajax-panel-abstract.svg b/source/blog/img/ajax-panel-abstract.svg new file mode 100644 index 0000000000..49843b3560 --- /dev/null +++ b/source/blog/img/ajax-panel-abstract.svg @@ -0,0 +1 @@ +B2BShopDemoShopFOOTERContactsOrdersRolesDebtorsHTTP-ResponseHTTP-RequestHTTP-ResponseHTTP-RequestHTTP-ResponseHTTP-RequestHTTP-ResponseHTTP-Request \ No newline at end of file diff --git a/source/blog/img/authors/author_cr.jpg b/source/blog/img/authors/author_cr.jpg new file mode 100755 index 0000000000..384b580619 Binary files /dev/null and b/source/blog/img/authors/author_cr.jpg differ diff --git a/source/blog/img/authors/author_dk.jpg b/source/blog/img/authors/author_dk.jpg new file mode 100644 index 0000000000..ddd9d335c5 Binary files /dev/null and b/source/blog/img/authors/author_dk.jpg differ diff --git a/source/blog/img/authors/author_lh.jpg b/source/blog/img/authors/author_lh.jpg new file mode 100644 index 0000000000..cd95daef2b Binary files /dev/null and b/source/blog/img/authors/author_lh.jpg differ diff --git a/source/blog/img/authors/author_ps.jpg b/source/blog/img/authors/author_ps.jpg new file mode 100644 index 0000000000..692327a364 Binary files /dev/null and b/source/blog/img/authors/author_ps.jpg differ diff --git a/source/blog/img/authors/author_rc.jpg b/source/blog/img/authors/author_rc.jpg new file mode 100644 index 0000000000..ed3c56f993 Binary files /dev/null and b/source/blog/img/authors/author_rc.jpg differ diff --git a/source/blog/img/authors/author_tn.jpg b/source/blog/img/authors/author_tn.jpg new file mode 100755 index 0000000000..f0cbd9e2b6 Binary files /dev/null and b/source/blog/img/authors/author_tn.jpg differ diff --git a/source/blog/img/cms-data-mapping.png b/source/blog/img/cms-data-mapping.png new file mode 100644 index 0000000000..f83e78fdd0 Binary files /dev/null and b/source/blog/img/cms-data-mapping.png differ diff --git a/source/blog/img/custom-font-example.png b/source/blog/img/custom-font-example.png new file mode 100644 index 0000000000..ecdf8d51ed Binary files /dev/null and b/source/blog/img/custom-font-example.png differ diff --git a/source/blog/img/dal.png b/source/blog/img/dal.png new file mode 100644 index 0000000000..635d26b727 Binary files /dev/null and b/source/blog/img/dal.png differ diff --git a/source/blog/img/manual-seo-generation-win.png b/source/blog/img/manual-seo-generation-win.png new file mode 100644 index 0000000000..8f2b458c4c Binary files /dev/null and b/source/blog/img/manual-seo-generation-win.png differ diff --git a/source/blog/img/udemy_dev_basic_training.jpg b/source/blog/img/udemy_dev_basic_training.jpg new file mode 100644 index 0000000000..2f8d9a20ff Binary files /dev/null and b/source/blog/img/udemy_dev_basic_training.jpg differ diff --git a/source/community/contribution-guideline/index.md b/source/community/contribution-guideline/index.md new file mode 100644 index 0000000000..e3650a66aa --- /dev/null +++ b/source/community/contribution-guideline/index.md @@ -0,0 +1,17 @@ +--- +layout: default +title: Contribution Guideline +github_link: community/contribution-guideline/index.md +indexed: true +tags: [pullrequest, github, guideline, contribute, git, pull-request, fork] +menu_title: Contribution Guideline +menu_order: 20 +group: Community +--- + +
+ +## Introduction +First of all, thank you that you have decided to contribute to Shopware 5. + +Here is a link with all information you need to contribute. [Contribute to Shopware 5](https://github.com/shopware5/shopware/wiki/Contribute) diff --git a/source/community/gitter/index.md b/source/community/gitter/index.md new file mode 100644 index 0000000000..ac52f7c03c --- /dev/null +++ b/source/community/gitter/index.md @@ -0,0 +1,36 @@ +--- +layout: default +title: Shopware Gitter Channel +github_link: community/gitter/index.md +indexed: false +menu_title: +group: Community +--- + +## The shopware Gitter channel is discontinued + +*It is replaced by Slack.* Please use invite link to connect and (re)join the awesome shopware community via chat! + +The official chatroom for shopware is [shopware/shopware](https://gitter.im/shopware/shopware?utm_source=share-link&utm_medium=link&utm_campaign=share-link) on `gitter.im`. + +You can find active community members and even some shopware employees there. + +Please note, the main channel is used for shopware related topics and english only. + +There is a separate channel for [offtopic](https://gitter.im/shopware/offtopic?utm_source=share-link&utm_medium=link&utm_campaign=share-link) talk. + +Because of our strong german community, for the time being there is a [german channel](https://gitter.im/shopware/shopwareDE?utm_source=share-link&utm_medium=link&utm_campaign=share-link) as well. + +## How to connect + +Use one of the links above or choose your channel from [the overview](https://gitter.im/shopware/home). You can login using either your github or your twitter account. + +If you do not want to use the website, there are Gitter [apps](https://gitter.im/apps) available as well. + +Mobile users can use the [iPhone](http://appstore.com/gitter) or [android](https://play.google.com/store/apps/details?id=im.gitter.gitter) app. + +## Why not IRC or Slack? +The #shopware IRC channel was active since 2011 and had quite a few regular active users. It is no longer available. The reasons for discontinuing can be found here. + +Ultimately, we choose Gitter over slack because shopware is an open source product with git as VCS and with Gitter there is a direct connection between the chat and our repository. +The community evolves around github, so Gitter was the most integrated and easily accessible medium for a chat. diff --git a/source/contributing/img/github-create-pull-request.png b/source/community/img/github-create-pull-request.png similarity index 100% rename from source/contributing/img/github-create-pull-request.png rename to source/community/img/github-create-pull-request.png diff --git a/source/contributing/img/github-fork-button.png b/source/community/img/github-fork-button.png similarity index 100% rename from source/contributing/img/github-fork-button.png rename to source/community/img/github-fork-button.png diff --git a/source/contributing/img/label-accepted.jpg b/source/community/img/label-accepted.jpg similarity index 100% rename from source/contributing/img/label-accepted.jpg rename to source/community/img/label-accepted.jpg diff --git a/source/contributing/img/label-declined.jpg b/source/community/img/label-declined.jpg similarity index 100% rename from source/contributing/img/label-declined.jpg rename to source/community/img/label-declined.jpg diff --git a/source/contributing/img/label-incomplete.jpg b/source/community/img/label-incomplete.jpg similarity index 100% rename from source/contributing/img/label-incomplete.jpg rename to source/community/img/label-incomplete.jpg diff --git a/source/contributing/img/label-pick.jpg b/source/community/img/label-pick.jpg similarity index 100% rename from source/contributing/img/label-pick.jpg rename to source/community/img/label-pick.jpg diff --git a/source/contributing/img/label-question.jpg b/source/community/img/label-question.jpg similarity index 100% rename from source/contributing/img/label-question.jpg rename to source/community/img/label-question.jpg diff --git a/source/contributing/img/label-scheduled.jpg b/source/community/img/label-scheduled.jpg similarity index 100% rename from source/contributing/img/label-scheduled.jpg rename to source/community/img/label-scheduled.jpg diff --git a/source/contributing/img/label-tests.jpg b/source/community/img/label-tests.jpg similarity index 100% rename from source/contributing/img/label-tests.jpg rename to source/community/img/label-tests.jpg diff --git a/source/community/index.html b/source/community/index.html new file mode 100644 index 0000000000..1cfd499214 --- /dev/null +++ b/source/community/index.html @@ -0,0 +1,59 @@ +--- +layout: default +title: Community +github_link: community/index.html +indexed: true +menu_title: Community +menu_order: 35 +menu_style: bullet +--- +

+ Shopware is open source and we want you to be part of our awesome community. +

+

+ Get in touch with us and other members of the Shopware Community: +

+ + + +

Social Media

+ + +

Community Projects

+ + +

Contributing

+ +

+ If you want to contribute code and make Shopware even better, + have a look at our guides and our sources . +

+ +

Sources

+ + + + +

Guides

+ diff --git a/source/community/irc/index.md b/source/community/irc/index.md new file mode 100644 index 0000000000..5d588e46f2 --- /dev/null +++ b/source/community/irc/index.md @@ -0,0 +1,39 @@ +--- +layout: default +title: Shopware IRC Channel +github_link: community/irc/index.md +indexed: false +menu_title: +group: Community +--- + +## The shopware IRC channel is discontinued + +*It is replaced by Slack.* Please use invite link to connect and (re)join the awesome shopware community via chat! + +## Why was IRC discontinued? + +Since 2011, the shopware IRC channel was active and well established. +Today, IRC is a little bit nerdy and we really love it. Most of the cool kids are playing on freenode. + +But besides the coolness there are a few disadvantages that come with using IRC: + +- No history without 3rd party software +- No mapping between the contributor and the IRC nick (Who is xenomorph again?) +- No easy access except through anonymous web IRC clients +- Lacks modern features like image sharing, formatting and such + +Of course, all these points are arguable. +But in the end, we wanted to have a chat community with easier access and more modern features. + +We hope to not lose any old IRC users (we learned to love you all at #shopware) and get many new and interesting people to join Gitter. + +## "But I don't want to use Gitter! In fact, I hate it!" + +Don't worry, we've got you covered. + +There is an official bridge you can use to connect via IRC: [gitterHQ/irc-bridge](https://github.com/gitterHQ/irc-bridge) (you can host your own version if you want to). + +You need to get a token, which you can obtain from [irc.gitter.im](https://irc.gitter.im/), and if you need help configuring there are some [guides](https://github.com/gitterHQ/irc-bridge/wiki/Client-configuration) for that. + +Remember how we stated we love our IRC users? We do, indeed. Including you. <3 diff --git a/source/contributing/contributing-code/index.md b/source/contributing/contributing-code/index.md deleted file mode 100644 index e164d253a3..0000000000 --- a/source/contributing/contributing-code/index.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -layout: default -title: Contributing Code -github_link: contributing/contributing-code/index.md -indexed: true -tags: [pullrequest, github, contribute, git, pull-request, fork] -menu_title: Contributing Code -menu_order: 10 -group: Contributing ---- - -
- -## Configure Git - -Set up your user information with your real name and a working email address: - -```git config --global user.name "Your Name"``` - -```git config --global user.email you@example.com``` - - -## Create a Fork -Navigate to the [Shopware Github Repository](https://github.com/shopware/shopware) and click the **"Fork"**-Button in the upper right hand corner. - - - -This will create a "copy" of the entire Shopware repository into your personal user namespace. - -## Clone your fork to your local machine - -After the "forking action" has completed, clone your fork locally (this will create a `shopware` directory): - -```git clone git@github.com:USERNAME/shopware.git``` - -Add the shopware repository as `upstream` remote: - -```cd shopware``` - -```git remote add upstream https://github.com/shopware/shopware.git``` - -Verify the new remote named `upstream`: - -```git remote -v``` - -```origin git@github.com:USERNAME/shopware.git (fetch)``` - -```origin git@github.com:USERNAME/shopware.git (push)``` - -```upstream https://github.com/shopware/shopware.git (fetch)``` - -```upstream https://github.com/shopware/shopware.git (push)``` - -Now that you have the shopware source code locally on your machine please follow the [Git Installation Instructions](https://github.com/shopware/shopware#installation-via-git). - -## Create a new Feature branch - -Each time you want to work on a patch, create a feature branch: - -```git fetch upstream``` - -```git checkout -b my-new-feature upstream/5.2``` - -The first command will fetch the latest updates from the upstream project (shopware). -The second will create a new branch named `my-new-feature`, that is based off the `5.2`-branch of the `upstream` remote. - -## Submit your pull request - -Push your branch to your github fork: - -```git push origin my-new-feature``` - -## Create a Pull Request on Github -Navigate back to the [Shopware Github Repository](https://github.com/shopware/shopware) and click the **"Compare & pull request"-Button**. - - - -Before creating your pull request make sure that it fits our [contribution guideline](/contributing/contribution-guideline/). - -### How to create a Pull Request - -- [https://git-scm.com/book](https://git-scm.com/book) -- [https://try.github.io](https://try.github.io) \ No newline at end of file diff --git a/source/contributing/contribution-guideline/index.md b/source/contributing/contribution-guideline/index.md deleted file mode 100644 index fa50573bc2..0000000000 --- a/source/contributing/contribution-guideline/index.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -layout: default -title: Contribution Guideline -github_link: contributing/contribution-guideline/index.md -indexed: true -tags: [pullrequest, github, guideline, contribute, git, pull-request, fork] -menu_title: Contribution Guideline -menu_order: 20 -group: Contributing ---- - -
- -## Introduction -First of all, thank you! You have decided to contribute code to our software and become a member of the large shopware community. We appreciate your hard work and want to handle it with the most possible respect. - -To ensure the quality of our code and our products we have created a small guideline we all should endorse to. It helps you and us to collaborate with our software. Following these guidelines will help us to integrate your changes in our daily workflow. - -## Requirements for a successful pull request -To avoid that your pull request gets rejected, you should always check that you provided all necessary information, so that we can integrate your changes in our internal workflow very easily. Here is a check-list with some requirements you should always consider when committing new changes. - -- Did you fill out the [pull request info template](https://github.com/shopware/shopware/blob/5.2/.github/PULL_REQUEST_TEMPLATE.md) as detailed as possible? -- Do you made entries in the correct `Upgrade.md` file with a small documentation of your changes? -- Does your pull request address the correct shopware version? Breaks and features cannot be merged in a patch release. -- Is your implementation missing some important parts? For example translations, downward compatibility, compatibility to important plugins, etc. -- Did you provide the necessary tests for your implementation? -- Is there already an existing pull request tackling the same issue? - -Pull requests which do not fulfill these requirements will never be accepted by our team. To avoid that your changes go through unnecessary workflow cycles make sure to check this list with every pull request. - -## The developing workflow on GitHub -When you create a new pull request on GitHub normally it will get a first sight within a week. We do regular meetings to screen all new pull requests on GitHub. In this meeting there is a team of up to five shopware developers of different specialisations which will discuss your changes. Together we decide what will happen next to your pull request. We will set one of the following labels which indicate the status of the pull request. Here is a list of all possible states. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LabelWhat does it mean?
- - - Your pull request is incomplete. It is either missing some of the necessary information, or your code implementation is not sufficient to fix the issue. Mostly there will be a comment by our developers which gives you further information of what is missing. -
- - - When you want to commit a new feature or bigger change it is highly necessary that you provide the corresponding tests for it. We only accept new features or bug fixes which are completely tested. -
- - - Our developers have a question about your code and want to talk with you. They will either comment directly in your code or in the main conversation of the pull request. Try to give them all needed information as detailed as possible so that they can understand what you want to achieve with your changes. -
- - - Your pull request was declined by our developers and is closed. No reason to be sad. It can have very different reasons. We understand that it sometimes can be hard to understand the reason behind this. Mostly there will be a comment by our developers about why it was declined. -
- - - Yeaha! You made the first step towards the holy grail. Your changes had been reviewed by our developers and they decided that you provided a good benefit for our product. Your pull request will be imported to our ticket system and will go through our internal workflow. You will find a comment containing the ticket number to follow the status. -
- - - You are a lucky one! The changes you provide are only a small fix which is easy to test and implement. Our developers decided to quickly integrate this to our software. -
- - - Your changes are finally accepted. The pull request passed our internal workflow. Your changes will be released with one of the next releases. -
- -
-Important: The first three labels (Incomplete, Missing Tests, Question) mean that you have to take action! After the label was added you have up to one week to update the pull request and provide the missing information or implementation. If there is no reaction from you within that week the pull request will be declined without further reason. -
- -## Why a pull request gets declined -So the worst thing happened, your pull request was declined. No reason to be upset. We know that it sometimes can be hard to understand why your pull request was rejected. We want be as transparent as possible, but sometimes it can also rely on internal decisions. Here is a list with common reasons why we reject a pull request. - -- The pull request does not fulfill the requirements of the list above. -- You did not updated your pull request with the necessary info after a specific label was added. -- The change you made is already a part of a current change by shopware and is handled internally. -- The benefit of your change is not relevant for the whole product but only for your personal intent. -- The benefit of your change is too minor. Sometimes we do not have enough resources to handle every small change. -- Your change implements a feature which does not fit to our roadmap or our company values. - - diff --git a/source/contributing/index.html b/source/contributing/index.html deleted file mode 100644 index 7bf35954fe..0000000000 --- a/source/contributing/index.html +++ /dev/null @@ -1,38 +0,0 @@ ---- -layout: default -title: Contributing -github_link: contributing/index.html -indexed: true -menu_title: Contributing -menu_order: 40 -menu_style: bullet ---- -

Community

-

- Shopware is open source and wants to involve you as a developer. -

- -

- For that reason there are several ways to get in touch with us: -

- - - - -

Guides

- diff --git a/source/contributing/irc/index.md b/source/contributing/irc/index.md deleted file mode 100644 index 68b9570497..0000000000 --- a/source/contributing/irc/index.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -layout: default -title: Shopware IRC Channel -github_link: contributing/irc/index.md -indexed: true -menu_title: IRC Channel -menu_order: 40 -group: Contributing ---- - -The official IRC Channel is [\#shopware](irc://irc.freenode.net/shopware) on the Freenode IRC network. - -You can find active community members and even some shopware employees there. - -## How to connect -Connect to `irc.freenode.org` via an IRC client like `HexChat`. You can also use web-based tools. - -- Web-based: [Freenode Webchat](http://webchat.freenode.net/?channels=shopware&prompt=1), [IRCCloud](https://www.irccloud.com/#!/ircs://chat.freenode.net:6697/%23shopware) -- Windows clients: mIRC, HexChat. -- Mac OS X clients: Colloquy, Adium, LimeChat, Textual, Komanda -- Linux clients: HexChat, Chatzilla, Irssi, WeeChat. -- Browser extensions: ChatZilla. - -## Why not Gitter or Slack? -The #shopware IRC channel is active since 2011 and has quite a few regular active users. We do not want to divide the community into small separated groups by providing yet another tool of communication. -Please read the following [comment](https://github.com/symfony/symfony/issues/12885#issuecomment-65935540) by [wouterj](https://github.com/wouterj) from the Symfony Community, this comment summarizes our viewpoint quite well. diff --git a/source/designers-guide/best-practice-theme-development/grunt-modularized.png b/source/designers-guide/best-practice-theme-development/grunt-modularized.png new file mode 100644 index 0000000000..7dba727905 Binary files /dev/null and b/source/designers-guide/best-practice-theme-development/grunt-modularized.png differ diff --git a/source/designers-guide/best-practice-theme-development/index.md b/source/designers-guide/best-practice-theme-development/index.md index eeba69acdb..60314c204f 100644 --- a/source/designers-guide/best-practice-theme-development/index.md +++ b/source/designers-guide/best-practice-theme-development/index.md @@ -4,8 +4,6 @@ title: Using Grunt for theme development github_link: designers-guide/best-practice-theme-development/index.md shopware_version: 5.0.1 indexed: true -redirect: - - /designers-guide/using-grunt/ group: Frontend Guides subgroup: General Resources menu_title: Using the Grunt watcher @@ -23,13 +21,13 @@ In Shopware, we use Grunt to speed up the development of themes. In detail, we i ## Installation -LESS and Grunt are based on Node.js, which makes it necessary to have [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed on your system. Node.js is available on a majority of systems and distrubition. If your system isn't listed below, please use the [official Node.js installation guide](https://github.com/joyent/node/wiki/Installation). +LESS and Grunt are based on Node.js, which makes it necessary to have [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed on your system. Node.js is available on a majority of systems and distribution. If your system isn't listed below, please use the [official Node.js installation guide](https://github.com/joyent/node/wiki/Installation). ### Install Node.js/npm on Ubuntu 14.04 ```bash sudo apt-get update -sudo apt-get install nodejs-legacy npm +sudo apt-get install nodejs npm ``` ### Install Node.js/npm on Mac OS X @@ -56,7 +54,7 @@ Install this globally and you'll have access to the ```grunt``` command anywhere ## How to use it -
Note: The grunt tasks are only meant for development purpose. We can't guarantee that the compiled files are working in your production enviroment, therefore use the built-in LESS compiler in Shopware.
+
Note: The grunt tasks are only meant for development purpose. We can't guarantee that the compiled files are working in your production environment, therefore use the built-in LESS compiler in Shopware.
### Clearing caches Before you start the file watch you need to clear the Shopware smarty cache. If you don't do this smarty will load the js and css files with an old timestamp instead of the files which were generated by grunt. @@ -88,7 +86,68 @@ We have installed everything that we need to start working with Grunt. The defau ```bash grunt -grunt --shopId 1 # optionally specify shopId +grunt --shopId=1 # optionally specify shopId ``` ![Grunt Screenshot](grunt-screenshot.png) + +### Development Task +Since Shopware 5.5.4 there is a development grunt task, which will compile LESS and Javascript files without starting to watch them afterwards. + +```bash +grunt development +``` + +## New in Shopware 5.3 - LiveReload & modularized grunt tasks + +In Shopware 5.3 we added a couple of new features to our Grunt integration. First of all we've added a LiveReload mode which automatically reloads your browser when the Grunt compilation is successful which should speed up your workflow quite a bit. +We changed the structure of our Grunt tasks too. We're now auto-loading the grunt plugins and it's now possible to add your own configurations & tasks without changing our core files. + +### LiveReload mode + +The Grunt `watch` command now supports live reloading of your browser. Here's how you can use the feature: + +#### Requirements + +Please install the Chrome Add-On called ["LiveReload"](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei). It will provide you with a simple button in your browser chrome, we need it later on. + +#### How to use it + +1. Start up Grunt as usual using the command `grunt` in your "themes" directory. +2. When the process is done you'll see the message "Waiting for changes..." popping up in your terminal. +3. Now press the LiveReload button in your browser chrome and start working and changing your LESS styles for example. +4. After you save your changes, the LiveReload plugin automatically requests the compiled file and injects it into your shop. + +### Modularized Grunt tasks +The next big improve are the modularized Grunt tasks. We're using auto-loading for Grunt plugins and a JIT (Just in Time) plugin loader which allows us to separate the different tasks and configurations into separate files. + +![Grunt Screenshot](grunt-modularized.png) + +The advantage of this approach is that you as a third party developer are able to register your own tasks and add new plugins like a FTP deployment for example without changing the core `Gruntfile.js`. + + +#### Adding your own tasks +Adding a new task is easy now. You just have to install the Grunt plugin you want using `npm / yarn` and create a new configuration for the tasks in the `grunt-tasks/config` directory. Please keep in mind the file has to be named along the task name. For example if you're installing the plugin [`grunt-contrib-clean`](https://github.com/gruntjs/grunt-contrib-clean) you create a new file `clean.js` in the `config` folder. + +Inside the file you're exporting the configuration of the plugin. To stick to our example the configuration would look like this: + +``` +module.exports = { + build: { + src: ['path/to/dir/one', 'path/to/dir/two'] + } +}; +``` + +Now it's possible to run the task using: +``` +grunt clean:build +``` + +Usually you want to run multiple tasks at the same time like clearing cache folders, compiling your LESS code, compressing your JavaScript and start watching your working directories for further changes. To do so, you just have to create your own task. Head over to the `grunt-config/tasks` and create a new JavaScript file with the name of the task you want. In our example we name the file `debug.js`. The content of the file can look like this: + +``` +module.exports = (grunt) => { + grunt.registerTask('debug', [ 'clean:build', 'less:development', 'uglify:development', 'chokidar' ]); +}; +``` diff --git a/source/designers-guide/browser-notice/index.md b/source/designers-guide/browser-notice/index.md index 42adcb273f..8aa57c4024 100644 --- a/source/designers-guide/browser-notice/index.md +++ b/source/designers-guide/browser-notice/index.md @@ -69,7 +69,7 @@ var $buoop = { ``` ### Edit the CSS -The following CSS rules are applied by the script. You can overwrite them in your own CSS. To do so you need to add some more specifity to the css-rules: +The following CSS rules are applied by the script. You can overwrite them in your own CSS. To do so you need to add some more specificity to the css-rules: ``` .buorg { diff --git a/source/designers-guide/configuration-using-theme-php/index.md b/source/designers-guide/configuration-using-theme-php/index.md index 15b5727d82..318de0b45f 100644 --- a/source/designers-guide/configuration-using-theme-php/index.md +++ b/source/designers-guide/configuration-using-theme-php/index.md @@ -70,6 +70,51 @@ protected $css = array( ); ``` +### Discard JavaScript/CSS from other Themes + +
+Since Shopware 5.4, it's possible to manipulate the chain of inheritance by discarding Less/JavaScript, +defined by another theme. +
+ +When you want to develop a completely new theme, +it might be a good idea to discard the styling defined +by the Shopware Responsive theme. + +You can do so by adding the following code to your ```Theme.php```: + +```php + protected $discardedLessThemes = [\Shopware\Themes\Responsive\Theme::class]; +``` + +It's also possible to discard Javascript files: + +```php + protected $discardedJavascriptThemes = [\Shopware\Themes\Responsive\Theme::class]; +``` + +To have an unique identifier, you have to use the full class name of the Theme, +you would like to discard. You get this by navigating to the theme folder +located at themes/{the-theme-name}/ and opening the Theme.php. Locate the namespace at the top of the file. + +For the Bare theme the namespace looks like this: + +```php + namespace Shopware\Themes\Bare; +``` + +Add the name of the class (always `Theme`) to it to get the complete name. +You will have to use: + +```php +\Shopware\Themes\Bare\Theme::class +``` + +in your own Theme.php if you want to discard the Less or JavaScript of the Bare theme. + +Of course you can discard multiple themes. You have to recompile the themes +after changing these two configuration options. + ## Customizing the theme configuration It's possible to add custom configuration options to your theme. Using this method, the user can fully customize the theme without having to edit any CSS files. diff --git a/source/designers-guide/css-and-js-files-usage/index.md b/source/designers-guide/css-and-js-files-usage/index.md index 5fd10cc19f..e76d6c6cc4 100644 --- a/source/designers-guide/css-and-js-files-usage/index.md +++ b/source/designers-guide/css-and-js-files-usage/index.md @@ -1,16 +1,20 @@ --- layout: default -title: Using CSS and JavaScript files in themes. +title: Using CSS and JavaScript in themes github_link: designers-guide/css-and-js-files-usage/index.md indexed: true group: Frontend Guides subgroup: Developing Themes -menu_title: Using CSS and JavaScript files +menu_title: Using CSS and JavaScript menu_order: 50 --- -This quick tip shows off the best way on how to use CSS and JavaScript files for your custom themes, in order to enable them to be automatically compressed by the Shopware theme compiler. To use this feature you have to place your CSS and JavaScript files inside your theme directory under the subdirectories `frontend/_public`. This would be an example directory structure: +
+## The theme compiler +Shopware uses a compiler to concatenate and minify resource files like CSS and JavaScript to single compressed files. This reduces the file size and the amount of server requests. It is highly recommended to add your files to the compiler, too. The generated files are added to the template automatically, so you don't have to worry about including your files at all. + +Before you add your files to the theme compiler you have to place them in the correct directory structure inside your custom theme. Here is an example directory structure: ``` ExampleTheme └── frontend @@ -22,24 +26,52 @@ ExampleTheme └── example.js ``` -As the second step you will have to define the CSS or JavaScript files you would like to use inside your custom theme. This can be done by adding an array to your `Theme.php` file that contains the specific file paths, as the following examples shows: +As the second step you will have to define the CSS or JavaScript files you would like to use inside your custom theme. This can be done by defining the corresponding configuration variables in the `Theme.php` file, where you add the specific file paths. + +
+ Since Shopware 5.4, it's possible to manipulate the chain of inheritance by discarding + Less/JavaScript, defined by another theme. You can find detailed information + here. +
-#### Add CSS files: #### +### Add CSS files +Add the file paths to the `$css` variable in your `Theme.php` file, relative to the `_public` directory. ```php -protected $css = array( +protected $css = [ 'src/css/example.css' -); +]; ``` -#### Add JavaScript files: #### +### Add JavaScript files +Add the file paths to the `$javascript` variable in your `Theme.php` file, relative to the `_public` directory. ```php -protected $javascript = array( +protected $javascript = [ 'src/js/example.js' -); +]; ``` -After clearing the theme cache the changes should be displayed in the storefront. +### Compiling files +Every time you add changes to your LESS, CSS or JavaScript files the corresponding theme files have to be compiled. There are two ways of doing this. + +#### Production +During production mode you can start the compiler via the administration panel in the cache and performance module. Go to `Configuration` -> `Caches & Performance` -> `Caches` and check the `Compile themes` option. + +#### Development +For development you have the option to disable the compiling in `Configuration` -> `Theme Manager` -> `Settings` -> `Disable compiler caching`. +But the **best way for developing** is to use the [grunt file watcher](/designers-guide/best-practice-theme-development/). + +## Asynchronous JavaScript + +Since Shopware 5.3 we are loading the concatenated JavaScript file asynchronously. This improves the first rendering of the page also known as page speed. If you are using the compiler you should not worry about a thing, because your script is loaded together with all other Shopware scripts. + +If there is a reason for you to implement your script in a different way, please be aware of possible race conditions that could occur. When you need some parts from the main script as a dependency (for example jQuery) there is a new callback method which you can use to wait for the main script to load. + +```javascript +document.asyncReady(function() { + // do your magic here +}); +``` \ No newline at end of file diff --git a/source/designers-guide/custom-detail-page/index.md b/source/designers-guide/custom-detail-page/index.md index a3804a830f..631f9a2f73 100644 --- a/source/designers-guide/custom-detail-page/index.md +++ b/source/designers-guide/custom-detail-page/index.md @@ -1,11 +1,11 @@ --- layout: default -title: Example: Custom detail page +title: Example - Custom detail page github_link: designers-guide/custom-detail-page/index.md indexed: true group: Frontend Guides subgroup: Tutorials -menu_title: Example: Custom Detail page +menu_title: Example - Custom Detail page menu_order: 40 --- @@ -204,7 +204,7 @@ Using the Shopware default components, you should get the following button appea ## Styling Because of the changes we made to the structure, we need to adjust the styling. To keep things organized, we will create a new file named `custom-detail.less`: @@ -486,3 +486,17 @@ The code to remove the image zoom is pretty simple. We check if the main `conten If you want to take a closer look at all the code we have written in this tutorial you can download this plugin: Plugin download + +The theme is packed inside a plugin. This is a requirement to distribute themes via our store. +The structure of the theme is exactly the same as before, it is just placed a few layers deeper in the directory hierarchy: + +``` +SwagCustomDetailTheme +└── Resources + └── Themes + └── Frontend + └── *The theme folder* +``` + +In addition, the root directory of the plugin contains a plugin base file and a `plugin.xml` with meta information. +For more information on the Shopware plugin system, please head over to our [developer documentation](https://developers.shopware.com/plugin-guide/). diff --git a/source/designers-guide/custom-listing-page/index.md b/source/designers-guide/custom-listing-page/index.md index 0fab84cb7e..fe0f938328 100644 --- a/source/designers-guide/custom-listing-page/index.md +++ b/source/designers-guide/custom-listing-page/index.md @@ -1,11 +1,11 @@ --- layout: default -title: Example: Custom listing page +title: Example - Custom listing page github_link: designers-guide/custom-listing-page/index.md indexed: true group: Frontend Guides subgroup: Tutorials -menu_title: Example: Custom listing page +menu_title: Example - Custom listing page menu_order: 30 --- @@ -107,6 +107,10 @@ We will simply overwrite the default listing block and add our own iteration thr {/block} ``` + + ### Custom product box The new product box is separated into its own `box-custom.tpl` file and extends the default `box-basic` product box of the bare theme. When we added the `{extends}` command of Smarty and chose the correct path, we can now overwrite every part of the product box inside our newly created file: @@ -147,7 +151,7 @@ After defining the block we are ready to add some markup to our template file. W The structural changes that we added require a styling adjustment in the frontend in order to work and look correctly. We will add two new `less` files to our theme, first the required `all.less` file and a `custom-listing.less` which will contain our style adjustments. The `all.less` file is automatically recognized by Shopware and all we need to do is import our new created file. ``` @@ -258,7 +262,7 @@ In the last step we will add styling to the product box itself, add the hover ef width: 100%; .info--name { - .unitize(font-size, 30px); + .unitize(font-size, 30); color: #000; display: block; font-weight: 300; @@ -266,7 +270,7 @@ In the last step we will add styling to the product box itself, add the hover ef } .info--price { - .unitize(font-size, 22px); + .unitize(font-size, 22); display: block; text-align: center; } @@ -278,7 +282,7 @@ In the last step we will add styling to the product box itself, add the hover ef transform: translate(0, -50%); .image--media img { - .unitize(padding, 30px); + .unitize(padding, 30); height: 100%; } } @@ -290,3 +294,17 @@ In the last step we will add styling to the product box itself, add the hover ef If you want to take a closer look at all the code we have written in this tutorial you can download this plugin: Plugin download + +The theme is packed inside a plugin. This is a requirement to distribute themes via our store. +The structure of the theme is exactly the same as before, it is just placed a few layers deeper in the directory hierarchy: + +``` +SwagCustomListingTheme +└── Resources + └── Themes + └── Frontend + └── *The theme folder* +``` + +In addition, the root directory of the plugin contains a plugin base file and a `plugin.xml` with meta information. +For more information on the Shopware plugin system, please head over to our [developer documentation](https://developers.shopware.com/plugin-guide/). diff --git a/source/designers-guide/custom-templates/custom_product_box_layout.jpg b/source/designers-guide/custom-templates/custom_product_box_layout.jpg new file mode 100644 index 0000000000..d92657c123 Binary files /dev/null and b/source/designers-guide/custom-templates/custom_product_box_layout.jpg differ diff --git a/source/designers-guide/custom-templates/index.md b/source/designers-guide/custom-templates/index.md index b22aabc89a..63e494b0a4 100644 --- a/source/designers-guide/custom-templates/index.md +++ b/source/designers-guide/custom-templates/index.md @@ -38,3 +38,41 @@ Go to *Configuration* -> *Basic settings* -> *Frontend* -> *Shopping cart / item Add the template file and the name for the label to the string in the input field. In our example it is `;custom_detail.tpl:My custom page`. After clearing the configuration cache you can select your new template in the product settings. +## Custom product box layouts + +
+ +You can add custom product box layouts by extending the `ProductBoxLayout` store with an ExtJS Plugin. By extending this store it is possible to select an additional item from the Product Layout dropdown at the category's detail view. +This could look like this: + +![Custom product box layout](custom_product_box_layout.jpg) + +To add a new product layout you have to override the `createLayoutData` method in `themes/Backend/ExtJs/backend/base/store/product_box_layout.js` and push your new product layout item to the `data` array. This could look as following: +```javascript +//{namespace name=backend/base/product_box_layout} +//{block name="backend/base/store/product_box_layout" append} +Ext.override(Shopware.apps.Base.store.ProductBoxLayout, { + + createLayoutData: function(config) { + var me = this, + data = me.callParent(arguments); + + data.push({ + key: 'shopware', + label: '{s name=box_layout_shopware_label}Shopware{/s}', + description: '{s name=box_layout_shopware_description}This is the custom Shopware box layout{/s}', + image: '{link file="backend/_resources/images/category/layout_box_basic.png"}' + }); + + return data; + } +}); +//{/block} +``` + +The chosen key is important as you have to create a new template file that contains your key (e.g. `box-shopware.tpl` corresponding to the previous lines of code) in `frontend/listing/product-box/`. This template-file contains the new product box layout and is displayed in the frontend. + +For creating new product layouts easily, we provide the `SwagCustomProductBoxLayout`-Plugin which can be downloaded here. +A default product layout has already been created. You can change the existing layout or create new ones within this plugin. Just **check the front- and backend resources**. diff --git a/source/designers-guide/datepicker/datepicker-multiselect.jpg b/source/designers-guide/datepicker/datepicker-multiselect.jpg new file mode 100644 index 0000000000..d01bac653d Binary files /dev/null and b/source/designers-guide/datepicker/datepicker-multiselect.jpg differ diff --git a/source/designers-guide/datepicker/index.md b/source/designers-guide/datepicker/index.md new file mode 100644 index 0000000000..374ea4b474 --- /dev/null +++ b/source/designers-guide/datepicker/index.md @@ -0,0 +1,88 @@ +--- +layout: default +title: Datepicker +github_link: designers-guide/datepicker/index.md +indexed: true +group: Frontend Guides +subgroup: General Resources +menu_title: Datepicker +menu_order: 50 +--- + +
+ +## Introduction + +The datepicker jQuery plugin was added in Shopware version 5.3 and allows you to easily add a functional datepicker to your themes and plugins, it is based on [flatpickr](https://github.com/chmln/flatpickr) with custom styling added to it. + +
+ Multiselection datepicker +
+ +## Basic usage + +To add a datepicker to your template, this piece of html will suffice: + +```html + +``` + +The inputs `type` is set to `"text"` because we wouldn't be able to set a placeholder value for an input of type `"date"`. Also, every browser shows slightly different native behaviour for date inputs and this way we're circumventing that. The `name` and `id` attributes are important for form submission and you're free to choose any value here. The `placeholder` attribute lets you define a placeholder string. `data-datepicker="true"` is what makes the datepicker actually work: via this statement our jQuery plugin is able to identify this input as a datepicker and will then apply various classes to it, as well as generate the necessary markup for the calendar overlay. + +## Configuration + +### Template +Like most of our jQuery plugins, the datepicker may be configured through data attributes: + +```html + +``` + +Every data attribute overwrites one of the default values in [`themes/Frontend/Responsive/frontend/_public/src/js/jQuery.datepicker.js`](https://github.com/shopware5/shopware/blob/5.3/themes/Frontend/Responsive/frontend/_public/src/js/jquery.datepicker.js), which alter the behaviour of the datepicker plugin. + +Option | Default value | Possible values | Explanation +--- | --- | --- | --- +**mode** | `'single'` | single, multiple, range | single: Select a single date
multiple: Select multiple dates in one picker
range: Select a range of dates in one picker +**utc** | `false` | boolean | If true, dates will be parsed, formatted, and displayed in UTC.
Pre loading date strings with timezones is recommended but not necessary. +**wrap** | `false` | boolean | See [https://chmln.github.io/flatpickr/options/](https://chmln.github.io/flatpickr/options/) +**static** | `false` | boolean | Position the calendar inside the wrapper and next to the input element. +**weekNumbers** | `false` | boolean | Enables / Disables week numbers +**allowInput** | `false` | boolean | Enables / Disables manual input via keyboard. +**clickOpens** | `true` | boolean | Clicking on input opens the date picker.
Disable if you wish to open the calendar manually with the open() method. +**time_24hr** | `true` | boolean | Enables / Disables time picker 24 hour mode. +**enableTime** | `false` | boolean | Enables / Disables the time picker functionality. +**noCalendar** | `false` | boolean | Set to true to hide the calendar. Use for a time picker along with enableTime. +**dateFormat** | `'Y-m-d'` | string | More format chars at [https://chmln.github.io/flatpickr/formatting/](https://chmln.github.io/flatpickr/formatting/) +**timeFormat** | `' H:i:S'` | string | Is added to dateFormat when enableTime option is set to true.
More formats at [https://chmln.github.io/flatpickr/formatting/](https://chmln.github.io/flatpickr/formatting/) +**altInput** | `true` | boolean | If set to true, the original input is hidden, and a new one displaying a differently formatted time is created. The used format can be specified via the `altFormat` option. +**rangeStartInput** | `null` | string | The name attribute of an additional input field for storing the single start value of a range.
Only working with mode 'range'. +**rangeEndInput** | `null` | string | The name attribute of an additional input field for storing the single end value of a range.
Only working with mode 'range'. +**altInputClass** | `'flatpickr-input form-control input'` | string | If `altInput` is set to true, the created element will have this class. +**altFormat** | `'F j, Y'` | string | Used when altInput is set to true.
More date format chars at [https://chmln.github.io/flatpickr/formatting/](https://chmln.github.io/flatpickr/formatting/) +**altTimeFormat** | `' - H:i'` | string | Used when altInput is set to true.
More date format chars at [https://chmln.github.io/flatpickr/formatting/](https://chmln.github.io/flatpickr/formatting/) +**multiDateSeparator** | `null` | char | Defines the symbol which is used to separate multiple dates.
Only necessary for mode 'multiple'.
The default separator of flatpickr.js is ';'. +**defaultDate** | `null` | string / date object | Define the symbol which is used to separate multiple dates.
Only necessary for mode 'multiple'.
The default separator of flatpickr.js is ';'. +**minDate** | `null` | string / date object | The minimum date that a user should be able to pick (inclusive). +**maxDate** | `null`| string / date object | The minimum date that a user should be able to pick (inclusive). +**enabledDates** | `null` | array | Define an array of dates which should be selectable.
You may also pass a comma separated list via data attribute.
All unlisted dates will be disabled. +**parseDate** | `null` | string | Date parser that transforms a given string to a date object. +**autoSubmit** | `false` | boolean | Set to true, to submit the parent form of the date picker input on date change. + +### Global + +Apart from the element-specific configuration, theres also a global configuration file: [`themes/Frontend/Bare/frontend/index/datepicker-config.tpl`](https://github.com/shopware5/shopware/blob/5.3/themes/Frontend/Bare/frontend/index/datepicker-config.tpl). The values in this file are set via Smarty snippets, so you may easily edit them in the Shopware Backend. + +## Structure + +The datepicker plugin itself is a wrapper around the [flatpickr.js](https://github.com/chmln/flatpickr/blob/master/src/flatpickr.js) dependency. Through this plugin we're able to expose the configuration options listed above and use the Shopware [state manager](https://developers.shopware.com/designers-guide/javascript-statemanager-and-pluginbase/#the-state-manager) and [jQuery plugin base](https://developers.shopware.com/designers-guide/javascript-statemanager-and-pluginbase/#plugin-base-class). Those again provide functions to, for example, bind a flatpickr event to some Shopware event, or enabling/disabling the plugin altogether on certain viewports. diff --git a/source/designers-guide/edit-newsletter-and-document-templates/index.md b/source/designers-guide/edit-newsletter-and-document-templates/index.md index 1a5fc9e056..ffa9d2e983 100644 --- a/source/designers-guide/edit-newsletter-and-document-templates/index.md +++ b/source/designers-guide/edit-newsletter-and-document-templates/index.md @@ -16,7 +16,10 @@ menu_order: 50 In this guide you will learn how to modify the default appearance of Shopware's newsletter and document templates.
-Note: Before you start modifying your newsletter or document templates please keep in mind that you shouldn't apply any changes to the default Bare theme directory. This can cause issues during the next update process and your changes could be overwritten. You should always include your own newsletter and document modifications within your own Shopware theme. If your don't have a custom Shopware theme you should create one for newsletter and document editing. +Note: Before you start modifying your newsletter or document templates please keep in mind that you shouldn't apply any changes to the default Bare theme directory. +This can cause issues during the next update process and your changes could be overwritten. +You should always include your own newsletter and document modifications within your own Shopware theme. +If your don't have a custom Shopware theme you should create one for newsletter and document editing.
In the following examples we will use a newly created theme called `NewsDocExample`: @@ -27,7 +30,8 @@ In the following examples we will use a newly created theme called `NewsDocExamp ## Edit newsletter templates -In the following steps you will learn how to edit Shopware's default newsletter template. If you want an introduction in general usage of the newsletter module please have a look at our [newsletter wiki tutorial](http://wiki.shopware.com/Newsletter_detail_933.html). +In the following steps you will learn how to edit Shopware's default newsletter template. +If you want an introduction in general usage of the newsletter module please have a look at our [newsletter wiki tutorial](https://docs.shopware.com/en/shopware-5-en/marketing-and-shopping-worlds/newsletter). ### Newsletter directory structure Within the `newsletter` directory you can find all necessary files to edit the template: @@ -37,7 +41,6 @@ themes ├── Backend ├── Frontend │   ├── Bare - │ ├── Responsive │ │ ├── documents │ │ ├── frontend │ │ ├── newsletter @@ -45,25 +48,32 @@ themes │ │ │ ├── container │ │ │ └── index │ │ └── widgets + │ ├── Responsive ``` -There are two different types of newsletter templates: A default newsletter template and a merchants template. The `index.tpl` contains the default template and the `indexh.tpl` contains the merchants template. +There are two different types of newsletter templates: A default newsletter template and a merchant template. +The `index.tpl` contains the default template and the `indexh.tpl` contains the merchant template. -The newsletter templates are written in HTML containing a table structure to keep compatibility with the most email clients. Because of the email clients the usage of external css files is not possible and the overall appearance is managed by inline styles within the template files. +The newsletter templates are written in HTML containing a table structure to keep compatibility with the most email clients. +Because of the email clients the usage of external css files is not possible and the overall appearance is managed by inline styles within the template files. -If the newsletter recipient cannot display HTML emails, Shopware will use the newsletter templates within the `alt` directory. Those contain all important variables to display the newsletter as plain text. +If the newsletter recipient cannot display HTML emails, Shopware will use the newsletter templates within the `alt` directory. +Those contain all important variables to display the newsletter as plain text. ![Newsletter preview](img-newsletter-preview.jpg) ### Change the newsletter logo -To change the **newsletter logo (1)** navigate to the index directory of the Shopware default newsletter theme: `themes/Frontend/Bare/newsletter/index`. Now copy the `header.tpl` file into the matching directory of your created theme: `themes/Frontend/NewsDocExample/newsletter/index`. Within the copied file you can see the following code on line 4: +To change the **newsletter logo (1)** navigate to the index directory of the Shopware default newsletter theme: `themes/Frontend/Bare/newsletter/index`. +Now copy the `header.tpl` file into the matching directory of your created theme: `themes/Frontend/NewsDocExample/newsletter/index`. +Within the copied file you can see the following code on line 4: ```html ``` -If you don't want to display the default shop logo you can simply replace the path with a path to your desired image. In this example we copied our new logo into the `frontend/_public/src/img/logos/` directory of our `NewsDocExample` theme: +If you don't want to display the default shop logo you can simply replace the path with a path to your desired image. +In this example we copied our new logo into the `frontend/_public/src/img/logos/` directory of our `NewsDocExample` theme: ```html @@ -71,7 +81,9 @@ If you don't want to display the default shop logo you can simply replace the pa ### Change the newsletter headline -In this example we will change the color of the **newsletter headline (2)** in the right top corner. To change the color you can also edit your copied `header.tpl` file from the logo example. You can find the desired code in line 7: +In this example we will change the color of the **newsletter headline (2)** in the right top corner. +To change the color you can also edit your copied `header.tpl` file from the logo example. +You can find the desired code in line 7: ```html NEWSLETTER @@ -85,7 +97,8 @@ Now you can modify the color property with another value. In this example we rep ### Change the newsletters general appearance -At first navigate into the default newsletter theme again: `themes/Frontend/Bare/newsletter/index`. Now copy the `index.tpl` file into the matching directory of your created theme: `themes/Frontend/NewsDocExample/newsletter/index`. +At first navigate into the default newsletter theme again: `themes/Frontend/Bare/newsletter/index`. +Now copy the `index.tpl` file into the matching directory of your created theme: `themes/Frontend/NewsDocExample/newsletter/index`. If you want to edit the **general appearance of the newsletter (3)** you can e.g. modify the styling of the wrapping newsletter table on line 37: @@ -101,7 +114,8 @@ In our example we will change the `bgcolor` attribute to a grey tone as well as ### Change the newsletter footer -To change the **newsletter footer (4)** you copy the `footer.tpl` within the `themes/Frontend/Bare/newsletter/index` directory into your matching theme directory as in the examples before. In our example we will edit the copyright snippet in line 21 and change its color to red: +To change the **newsletter footer (4)** you copy the `footer.tpl` within the `themes/Frontend/Bare/newsletter/index` directory into your matching theme directory as in the examples before. +In our example we will edit the copyright snippet in line 21 and change its color to red: ```html @@ -111,15 +125,18 @@ To change the **newsletter footer (4)** you copy the `footer.tpl` within the `th ### Newsletter snippets -The newsletter elements like copyright information or the unsubscribe text are snippets which are namespaced in `newsletter/index/footer`. You can edit those snippets with the backend snippet module. +The newsletter elements like copyright information or the "unsubscribe" text are snippets which are namespaced in `newsletter/index/footer`. +You can edit those snippets with the backend snippet module. ## Edit document templates -In this quick example you will learn how to edit Shopware's default document template for invoices, delivery notes etc. The PDF document creation backend module provides you already with many customization features. If you'd like more information about the general usage please have a look at our [PDF document creation wiki tutorial](http://wiki.shopware.com/PDF-Belegerstellung_detail_1086.html). +In this quick example you will learn how to edit Shopware's default document template for invoices, delivery notes etc. +The PDF document creation backend module provides you already with many customization features. +If you'd like more information about the general usage please have a look at our [PDF document creation wiki tutorial](https://docs.shopware.com/en/shopware-5-en/settings/documents#creating-or-adjusting-a-pdf-document). ### Documents directory structure -You can find the document templates within the documents directory of the `Bare` theme. +You can find the document templates within the `documents` directory of the `Bare` theme. ``` themes @@ -146,17 +163,22 @@ There are four types of document templates: ### Adding an article description -In our example we want to add a short article description to the invoice template. The default invoice template comes only with the article names without descriptions. To edit the `index.tpl` (invoice template) you create a new `index.tpl` within your own theme `NewsDocExample`. Now you can extend from the original invoice template just like in other Shopware templates: +In our example we want to add a short article description to the invoice template. +The default invoice template comes only with the article names without descriptions. +To edit the `index.tpl` (invoice template) you create a new `index.tpl` within your own theme `NewsDocExample`. +Now you can extend from the original invoice template just like in other Shopware templates: ``` {extends file='parent:documents/index.tpl'} ``` -Within the extended `index.tpl` you can override the `document_index_table_name` block to add the product description. In our example we have added a truncating with 70 characters to keep the width of the table layout: +Within the extended `index.tpl` you can override the `document_index_table_name` block to add the product description. +In our example we have added a truncating with 70 characters to keep the width of the table layout: ``` {$position.meta.description_long|truncate:70:""} ``` -The `$position` variable provides you with even more article variables. If you need more variables you can read our wiki tutorial about [additional variables in PDF document templates](http://wiki.shopware.com/pdf-Belegerstellung-Zus%C3%A4tzliche-Variablen_detail_1220.html). +The `$position` variable provides you with even more article variables. +If you need more variables you can read our wiki tutorial about [additional variables in PDF document templates](https://docs.shopware.com/en/shopware-5-en/settings/documents#available-data). You can also set the article name to `` for better appearance: ```html diff --git a/source/designers-guide/eslint-for-plugins/index.md b/source/designers-guide/eslint-for-plugins/index.md index fafa988074..fbd14260fd 100644 --- a/source/designers-guide/eslint-for-plugins/index.md +++ b/source/designers-guide/eslint-for-plugins/index.md @@ -25,7 +25,7 @@ Use the following command to install ESLint, the standardjs configuration & nece npm install -g eslint eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard ``` -#### Installating the dependencies per plugin +#### Installing the dependencies per plugin If you're not feeling comfortable installing the dependencies globally, you can also create a `package.json` file in your plugin directory (e.g. `SwagBundle`). You can create this file using the command `npm init`. After the file was successfully created, run the following command to install the dependencies as `devDependencies`: @@ -58,7 +58,7 @@ To add a new script, open up the newly created `package.json` in your plugin dir { // ... "scripts": { - "lint": "./node_modules/eslint/bin/eslint.js -c ../../../../../../themes/.eslintrc.js Views/frontend/_public/src/js" + "lint": "./node_modules/eslint/bin/eslint.js -c ../../../../../../themes/.eslintrc.js Views/frontend/_public/src/js" }, //.... } @@ -74,7 +74,7 @@ npm run lint Using ESLint can cause trouble at first. The following tips & tricks should help you out. #### Using global variables in your plugin -You're usually working with external libaries or configurations which are defined in a template file. As mentioned before ESLint is a static code analyses tool, therefore you have declare any used globals at the top at of the file which is using the library / configuration: +You're usually working with external libraries or configurations which are defined in a template file. As mentioned before ESLint is a static code analyses tool, therefore you have declare any used globals at the top at of the file which is using the library / configuration: ``` /* global jQuery */ @@ -94,7 +94,7 @@ If you're using a `package.json` file in your plugin, we highly recommend adding // ... "scripts": { "lint": "..." - "fix": "./node_modules/eslint/bin/eslint.js -c ../../../../../../themes/.eslintrc.js Views/frontend/_public/src/js" + "fix": "./node_modules/eslint/bin/eslint.js -c ../../../../../../themes/.eslintrc.js Views/frontend/_public/src/js" }, //.... } diff --git a/source/designers-guide/find-smarty-blocks/index.md b/source/designers-guide/find-smarty-blocks/index.md index 7493304fb1..38390ba5b2 100644 --- a/source/designers-guide/find-smarty-blocks/index.md +++ b/source/designers-guide/find-smarty-blocks/index.md @@ -32,7 +32,7 @@ Apart from the frontend, a theme can also include modules such as newsletter and ├── documents ├── frontend ├── newsletter -├── documents +├── widgets └── Theme.php ``` *Directory structure of a typical theme* diff --git a/source/designers-guide/general-resources.html b/source/designers-guide/general-resources.html index f46925080e..bf35154ccd 100644 --- a/source/designers-guide/general-resources.html +++ b/source/designers-guide/general-resources.html @@ -10,15 +10,18 @@ --- \ No newline at end of file + diff --git a/source/designers-guide/getting-started/index.md b/source/designers-guide/getting-started/index.md index 9e7fe03364..01467cbd52 100644 --- a/source/designers-guide/getting-started/index.md +++ b/source/designers-guide/getting-started/index.md @@ -146,7 +146,7 @@ If you wish to add your content __after__ the initial block content you can do i {/block} ``` -Of cause the other way around will also work, if you wish to add your content __before__ the content of the initial block: +Of course the other way around will also work, if you wish to add your content __before__ the content of the initial block: ```smarty {block name='frontend_index_checkout_actions'} // place your new element here diff --git a/source/designers-guide/google-pagespeed-best-practise/index.md b/source/designers-guide/google-pagespeed-best-practise/index.md index 7fd9a8eb6a..aed196e884 100644 --- a/source/designers-guide/google-pagespeed-best-practise/index.md +++ b/source/designers-guide/google-pagespeed-best-practise/index.md @@ -14,7 +14,7 @@ menu_order: 55 ## Introduction -PageSpeed is a tool by Google to indicate the performance of a website. It checks the site for some of the best practice techniques in web development. Google wants to ensure that every site offers a great experience for the user. It is not clear, if the ranking of the tool realy affects the visibility of the site in the Google search algorithm. Although the performance of your website has an affect at the bounce rate and the user experience. So a better ranking in the PageSpeed tool is always a good goal you should work on. +PageSpeed is a tool by Google to indicate the performance of a website. It checks the site for some of the best practice techniques in web development. Google wants to ensure that every site offers a great experience for the user. It is not clear, if the ranking of the tool really affects the visibility of the site in the Google search algorithm. Although the performance of your website has an affect at the bounce rate and the user experience. So a better ranking in the PageSpeed tool is always a good goal you should work on. This guide will cover some key metrics which have a great impact on the score and how to optimize them. @@ -42,7 +42,7 @@ If your server has tools available like `optipng` or `jpegtran`, they already op ### Responsive images -Most images in Shopware provide the full range of the defined thumbnail sizes so the browser can choose which one to display. But to select the suitablein addition, the browser needs to know, how big the image that will be displayed is. +Most images in Shopware provide the full range of the defined thumbnail sizes so the browser can choose which one to display. But to select the suitable addition, the browser needs to know, how big the image that will be displayed is. **Recommendation for the best score** @@ -84,4 +84,4 @@ In case you can't add your files to the compile process, you should include your * [Responsive Images](/designers-guide/responsive-images/) * [PageSpeed Rules and Recommendations](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/page-speed-rules-and-recommendations) * [Critical Rendering Path](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/) -* [Render Blocking CSS](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-blocking-css) \ No newline at end of file +* [Render Blocking CSS](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-blocking-css) diff --git a/source/designers-guide/header.jpg b/source/designers-guide/header.jpg deleted file mode 100644 index 020101d7be..0000000000 Binary files a/source/designers-guide/header.jpg and /dev/null differ diff --git a/source/designers-guide/header.svg b/source/designers-guide/header.svg new file mode 100644 index 0000000000..ddcbd42517 --- /dev/null +++ b/source/designers-guide/header.svg @@ -0,0 +1,82 @@ + + +Styleguide-doppel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/designers-guide/html-smarty-coding-guidelines/index.md b/source/designers-guide/html-smarty-coding-guidelines/index.md index 110a6e0df2..eeadb51779 100644 --- a/source/designers-guide/html-smarty-coding-guidelines/index.md +++ b/source/designers-guide/html-smarty-coding-guidelines/index.md @@ -22,7 +22,7 @@ We're using a component based design and the goal was to create components which ## Naming Smarty blocks correctly -Definiting the names of Smarty block seems harder than it is. We're using a pretty straight forward pattern here: +Defining the names of Smarty block seems harder than it is. We're using a pretty straight forward pattern here: ``` [module]_[controller]_[action]_[functionality / purpose] diff --git a/source/designers-guide/index.html b/source/designers-guide/index.html index f686784c56..67dd776226 100644 --- a/source/designers-guide/index.html +++ b/source/designers-guide/index.html @@ -1,49 +1,92 @@ --- -layout: default +layout: guides title: Frontend Guides github_link: designers-guide/index.html menu_title: Frontend Guides menu_order: 30 menu_style: bullet --- -Hero unit image - -

Developing Themes

- - -

General Resources

- - -

Tutorials

- + +
+ +
+ + {% if page.title is defined %} +

+ {{ page.title }} +

+ {% endif %} + +
+ + {% if page.shopware_version %} + + as of version + {{ page.shopware_version }} + + {% endif %} + +
+ + {# START AND UDEMY #} +
+
+
+
+ "Use the Startup Guide to learn how to create awesome new themes!"
whizzymind, designer @shopware
+ +
+ +
+
+
+

Video Template Training

+ +
+
+ +
+
+ + {# DESIGNER MENUE #} + + + {# STYLETILE #} +
+ +
+

Shopware style tile

+

Your guide for the Shopware Responsive Theme - learn more.

+
+ +
+ +
+
+
+ +
+
+
+

Preview

Get a quick and visual overview of every component that Shopware introduces.
+
+
+
+

Cheatsheet

View all the regular classes and variables that Shopware introduces on one page.
+
+
+ + + +
+ +
+ + +
{# END START GUIDE #} diff --git a/source/designers-guide/javascript-statemanager-and-pluginbase/index.md b/source/designers-guide/javascript-statemanager-and-pluginbase/index.md index d1fe59e477..6ab0372862 100644 --- a/source/designers-guide/javascript-statemanager-and-pluginbase/index.md +++ b/source/designers-guide/javascript-statemanager-and-pluginbase/index.md @@ -12,39 +12,39 @@ menu_order: 70
## Introduction -The javascript development can be painful especially when you have to deal on responsive websites where you have to adjust the behavior of the code based on the available screen real estate. Therefore we came up with a component called *StateManager*, which provides you with the ability to define states and triggers *callback* function, if a state was entered or left. +Javascript development can be painful especially when you have to deal with responsive websites where you have to adjust the behavior of the code based on the available screen real estate. Therefore we came up with a component called *StateManager*, which provides you with the ability to define states and triggers a *callback* function, if a state was entered or left. -On the other hand we have our lovely jQuery plugins which are not always a pleasure to built. To simplify the process we implemented a plugin base class which features the best practices of the jQuery plugin development and flawlessly integrate with the *StateManager*. +On the other hand, we have our lovely jQuery plugins which are not always a pleasure to build. To simplify the process, we've implemented a plugin base class which features the best practices of jQuery plugin development that flawlessly integrates with the *StateManager*. In the following document we want to give you a general overview of the provided functionality, which can come in handy for your next theme. ## Plugin base class -As mentioned, the jQuery plugin base class was built up with the best practice of the jQuery plugin development. Here's feature set at a glance: +As mentioned, the jQuery plugin base class was built up with the best practices of jQuery plugin development. Here's feature set at a glance: * Default configuration + ability to override it with a user configuration * Ability to use HTML5 ```data``` attributes to configure the plugin * Support for jQuery's method chaining * Namespacing of events * Built-in functionality to remove event listeners -* Preventing multiple instanciation on the same element +* Preventing multiple instantiation on the same element * Custom expression to check if an element uses a specific plugin * Automatically binding the plugin to the element using jQuery's ```data```-method -As you can see, we put a lot of effort in the provided feature set to provide you an easy to use class for your next jQuery plugin. +As you can see, we've put a lot of effort in the provided feature set to provide you with an easy to use class for your next jQuery plugin. ### Getting started -Now it's time to take a look on the actual implementation process of a jQuery plugin using the plugin base class. Here's a commented example of a generic plugin: +Now it's time to take a look at the actual implementation process of a jQuery plugin using the plugin base class. Here's a commented example of a generic plugin: ```javascript /** * Example jQuery plugin using the base class * - * The $.plugin method binded to the globally available jQuery + * The $.plugin method bound to the globally available jQuery * object. The method needs two parameters, the first one is * simply the name of the plugin which will be used to bind * the plugin to jQuery's $.fn namespace. The second parameter - * is a object which provides the default configuration and - * the actucal implementation of the plugin. + * is an object which provides the default configuration and + * the actual implementation of the plugin. */ $.plugin('example', { @@ -52,7 +52,7 @@ $.plugin('example', { * The default configuration object of the plugin. The * user can provide custom settings which will be automatically * merged into a new object which can be accessed using "this.opts" - * in any plugin method which scope is on the plugin. + * in any plugin method which scope is in the plugin. */ defaults: { activeCls: 'js--is-active' @@ -71,7 +71,7 @@ $.plugin('example', { * Calling the "applyDataAttributes" method the base class * automatically reads out the all "data" attributes from * the element and overrides the configuration. It's especially - * useful if you want to configure your plugin using the HTML + * useful if you want to configure your plugin using HTML * markup instead of providing a configuration object. * * For example, we call this plugin on the following element: @@ -90,7 +90,7 @@ $.plugin('example', { * plugin specific event collection. The collection will be * automatically iterated and removes the registered event listeners * from the element. - * Additionally the event name will be namespaced on the fly which + * Additionally, the event name will be namespaced on the fly which * provides us with a safe way to remove a specific event listener from * an element and doesn't affect other plugins which are listening on * the same event. @@ -102,7 +102,7 @@ $.plugin('example', { * In the condition we're using the custom expression of the plugin * to terminate if the element uses our plugin. * Additionally you see that we're using the variable "this.$el" which - * is the element that has instanciated the plugin. + * is the element that has instantiated the plugin. */ if(me.$el.is('plugin-example')) { @@ -118,7 +118,7 @@ $.plugin('example', { /** * The destroy method can either be called programmically from outside the plugin * or automatically using the "StateManager" when the defined states are left. - * Usually you remove classes which were added by your plugin to the element and + * Usually, you remove classes which were added by your plugin to the element and * removes the event listeners from the element. */ destroy: function() { @@ -130,7 +130,7 @@ $.plugin('example', { * Calling the "_destroy" method will remove all event listeners which were * registered using the "_on" method of the plugin base. * You can access the collection of the events in the plugin using the variable - * "this._events" if you wanna iterate over the event listeners yourself. + * "this._events" if you want to iterate over the event listeners yourself. */ me._destroy(); } @@ -142,9 +142,9 @@ $.plugin('example', { * ```_name : String``` * Name of the plugin. * ```$el : jQuery``` - * The HTMLElement which instanciated the plugin as a jQuery object. + * The HTMLElement which instantiated the plugin as a jQuery object. * ```opts : Object``` - * Result of the default configuration and the provides user configuration. Keep in mind that calling the ```this.applyDataAttributes()``` method overrides the property values in the object. + * Result of the default configuration and the provided user configuration. Keep in mind that calling the ```this.applyDataAttributes()``` method overrides the property values in the object. * ```_events : Array``` * Collection, which contains all registered event listener which are added using the ```_on``` method. @@ -152,11 +152,11 @@ $.plugin('example', { * ```init()``` * Template method which acts as the constructor of the plugin where you can cache the necessary HTML elements and set up the event listeners. * ```destroy()``` - * Template method which destroyes the plugin. Usually you remove classes and event listeners which you're added to the element. The method should be implemented in your plugin especially when you plan to provide the plugin functionality only for certain states. + * Template method which destroys the plugin. Usually, you remove classes and event listeners which you've added to the element. The method should be implemented in your plugin, especially when you plan to provide the plugin functionality only for certain states. * ```update()``` - * Template method which will be called when a certain state was entered / left to update the behavior of the plugin. This method is only necessary when you use the StateManager to instanciate the plugin. + * Template method which will be called when a certain state was entered / left to update the behavior of the plugin. This method is only necessary when you use the StateManager to instantiate the plugin. * ```_destroy()``` - * Private method which iterates over the registered event listeners in the ```_events``` property of the plugin. Additionally the method removes the in-memory binding of the plugin to the element using the jQuery's ```removeData()``` method and fires an event on the globally available observer. + * Private method which iterates over the registered event listeners in the ```_events``` property of the plugin. Additionally, the method removes the in-memory binding of the plugin to the element using jQuery's ```removeData()``` method and fires an event on the globally available observer. * ```_on()``` * **Arguments** * ```element : jQuery | HTMLElement``` - The event target for the specified event listener. @@ -174,7 +174,7 @@ $.plugin('example', { * ```event : String | Array``` - One or more space-separated event types * Applies the event namespace to the provided event types. * ```getElement()``` - * Getter method for the element which instanciate the plugin. + * Getter method for the element which instantiate the plugin. * ```getOptions()``` * Getter method for the merged configuration object. * ```getOption()``` @@ -187,14 +187,25 @@ $.plugin('example', { * ```value : Mixed``` - Value for the provided key * Setter method which overrides the value of the provided key with the provided value. * ```applyDataAttributes()``` - * Fetches the provided configuration keys and overrides the values based on the elements ```data``` attributes. + * **Arguments** + * ```[shouldDeserialize : Boolean = undefined]``` - Tries to parse the + given string values and returns the right value if its successful. + Supports boolean, null, number, json, string. This feature is enabled by default. + Pass `false` to deactivate parsing. + * ```[ignoreList : Array = []]``` - A list of options which will be excluded + when applying the data attributes to the corresponding options. Introduced with Shopware 5.3.7 + * Fetches the provided configuration keys and overrides the values based on + the elements ```data``` attributes. Hint: You don't need to convert + ([camel|pascal](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles))-case + java script variable names to ([dash|hyphend|kebab](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles))-case + html attribute names. ## Global jQuery event observer -We added a global event server into Shopware 5 too. It provides us with the ability to define events globally on the jQuery object and therefor every plugin can listen to this events: +We've added a global event observer into Shopware 5 as well. It provides us with the ability to define events globally in the jQuery object and therefore every plugin can listen to these events: ```javascript -// Register new event +// Register a new event $.publish('plugin/some-plugin/onInit', me); // Listen for an event @@ -202,7 +213,7 @@ $.subscribe('plugin/some-plugin/onInit', function() { console.log('onInit'); }) -// Remove event listener +// Remove an event listener $.unsubscribe('plugin/some-plugin/onInit'); ``` @@ -211,13 +222,13 @@ Please keep in mind to register your event listeners with a namespace, otherwise ```javascript $.subscribe('plugin/some-plugin/onInit.my-plugin', function() {}); -// Remove event listener +// Remove an event listener $.unsubscribe('plugin/some-plugin/onInit.my-plugin'); ``` -## The state manager -The state manager helps you master different behaviors for different screen sizes. +## The StateManager +The StateManager helps you master different behaviors for different screen sizes. It provides you with the ability to register different states that are handled by breakpoints. @@ -230,13 +241,13 @@ functions will be called. This way you can register callbacks that will be called on entering / exiting the defined state. -The manager provides you multiple helper methods and polyfills which help you +The StateManager provides you with multiple helper methods and polyfills which help you master responsive design. -## Using the state manager -The state manager is self-containing and globally available in the global javascript scope in the storefront. +## Using the StateManager +The StateManager is self-containing and globally available in the global javascript scope in the storefront. -It has been initialized with the following breakpoints: +It's initialized with the following breakpoints: * State XS * Range between ```0``` and ```479``` pixels @@ -255,7 +266,7 @@ It has been initialized with the following breakpoints: * Usually used for desktop PCs with a high resolution monitor ### Adding an event listener -Registering or removing an event listener which uses the state manager is as easy as doing it in pure javascript. +Registering or removing an event listener which uses the StateManager, is as easy as doing it in pure javascript. The following example shows how to register an event listener: @@ -280,16 +291,20 @@ StateManager.registerListener([{ ### Register additional breakpoints The default breakpoints can be extended using the ```registerBreakpoint()``` method of the StateManager. -**Note:** Breakpoint ranges are not allowed to overlap with other existing ones. +**Note:** Breakpoint ranges are not allowed to overlap with other existing ranges. ```javascript StateManager.registerBreakpoint({ state: 'xxl', - enter: 78.75 // = 1260px + enter: 78.75, // = 1260px exit: 90 // = 1440px }); ``` +### Class properties +* ```EventEmitter``` +    * Class constructor for an [`EventEmitter`](#the-eventemitter) + ### Class methods * ```init()``` * **Arguments** @@ -298,10 +313,10 @@ StateManager.registerBreakpoint({ * ```registerBreakpoint()``` * **Arguments** * ```breakpoints : Array | Object``` - The states, which should be available on start up - * Registers an additional breakpoint to the State Manager. + * Registers an additional breakpoint to the StateManager. * ```removeBreakpoint()``` * **Arguments** - * ```state : String``` - State which should be removed e.g. "xs" or "l" + * ```state : String``` - State which should be removed, e.g. "xs" or "l" * Removes the provided state from the StateManager. * ```registerListener()``` * **Arguments** @@ -313,13 +328,13 @@ StateManager.registerBreakpoint({ * ```pluginName : String``` - Name of the plugin which should be added to the selector. * ```config : Object (optional)``` - Custom configuration for the plugin. Can be omitted. * ```viewport: Array | String``` - The states where the plugin should be active. - * Registers a jQuery stateful to the StateManager. This functionality is especially useful when you want to provide a certian behavior only for specific states. + * Registers a jQuery plugin to the StateManager. This functionality is especially useful when you want to provide certain behavior only for specific states. * ```removePlugin()``` * **Arguments** * ```selector : String | HTMLElement | jQuery``` - Element selector * ```pluginName : String``` - Name of the plugin which should be removed from the selector. * ```viewport: Array | String``` - A state where the plugin should be removed. - * Removes a previously added plugin from a element for a certain state. + * Removes a previously added plugin from an element for a certain state. * ```updatePlugin()``` * **Arguments** * ```selector : String | HTMLElement | jQuery``` - Element selector @@ -329,7 +344,7 @@ StateManager.registerBreakpoint({ * **Arguments** * ```selector : String | HTMLElement | jQuery``` - Element selector * ```pluginName : String``` - Name of the plugin which should be destroyed. - * The method removes the plugin from the StateManager. Unlike to the ```removePlugin()``` method, the method calls the ```destroy()``` method of the provided plugin. + * The method that removes the plugin from the StateManager. Unlike the ```removePlugin()``` method, the method calls the ```destroy()``` method of the provided plugin. * ```getViewportWidth()``` * Getter method which returns the current width of browser window. * ```getViewportHeight()``` @@ -338,13 +353,13 @@ StateManager.registerBreakpoint({ * Returns the previous state. This can be either a ```String``` or ```null``` when no previous state was active. * ```isPreviousState()``` * **Arguments** - * ```state : String``` - State which should be checked e.g. "xs" or "l" + * ```state : String``` - State which should be checked, e.g. "xs" or "l" * Determine if the argument passed was the previous active state. * ```getCurrentState()``` * Getter method which returns the currently active state. * ```isCurrentState()``` * **Arguments** - * ```state : String``` - State which should be checked e.g. "xs" or "l" + * ```state : String``` - State which should be checked, e.g. "xs" or "l" * Determine if the argument passed is the currently active state. * ```isPortraitMode()``` * Determine if the device is in portrait mode. @@ -354,10 +369,10 @@ StateManager.registerBreakpoint({ * Determine the pixel device ratio of the device. * ```isBrowser()``` * **Arguments** - * ```browser : String``` - Browser name to test e.g. "firefox" or "safari" - * Determine if the argument passed is the current browser of the user. + * ```browser : String``` - Browser name to test, e.g. "firefox" or "safari" + * Determines if the argument passed is the current browser of the user. * ```getScrollBarHeight()``` - * Returns the default scroll bar width of the browser. + * Returns the default scrollbar width of the browser. * ```matchMedia()``` * ```matchMedia``` polyfill, which provides the ability to test CSS media queries in javascript. * ```requestAnimationFrame()``` @@ -370,10 +385,48 @@ StateManager.registerBreakpoint({ * ```softError : Boolean``` - Truthy to return the provided property when no vendor was found, otherwise the method returns ```null``` * Tests the provided CSS style property on an empty div with all vendor properties. +## The EventEmitter +The EventEmitter is a utility class, offering a simple base-class for event driven architecture. Currently the EventEmitter is used as the basis for the StateManager, but you can use it to let your own objects emit events. + +An object which inherits from the EventEmitter exposes the functionality of subscribing and listening to events. Much like the [global event observer](#global-jquery-event-observer). + +### Class methods +* ```on()``` + * **Arguments** + * ```eventName : String``` - The name of the event which to listen to + * ```callback : Function``` - The function which should be called, when the event is triggered + * ```context : Any``` - An optional context, which to bind the callback to. e.g. `this` + * Add an event listener + * Chainable +* ```once()``` + * **Arguments** + * ```eventName : String``` - The name of the event which to listen to + * ```callback : Function``` - The function which should be called, when the event is triggered + * ```context : Any``` - An optional context, which to bind the callback to. e.g. `this` + * Behaves exactly like on, expect that the listener gets removed after the callback has been called once + * Chainable +* ```off()``` + * **Arguments** + * ```eventName : String``` - Optional, the name used for subscribing + * ```callback : Function``` - Optional, the callback used for subscribing + * ```context : Any``` - Optional, the context used for subscribing +    * Removes an event listener. It tries to find the listener you added by the parameters you've given. E.g. if there's a listener matching the `eventName` and the `callback` you've given, but not the `context` (if you've given one) the listener won't be removed. If you only supply the eventName, all listeners of that event will be removed. If you supply no parameters all event listeners will be removed. +    * Chainable +* ```trigger()``` + * **Arguments** + * ```eventName : String``` - The name of the event to trigger +    * Triggers an event. +    * Chainable +* ```destroy()``` +    * Can be called to clean up. +    * Chainable + + + ## Working with stateful jQuery plugins -The combination of the StateManager paired with the jQuery plugin base class provides an easy-to-use way to register jQuery plugins for certain state. That provides us with the ability to provide a different behavior of components based on the current active state. For example the Offcanvas menu plugin is only active on mobile devices (states "xs" and "s") and is disabled on tablets and desktop pc's. +The combination of the StateManager paired with the jQuery plugin base class provides an easy-to-use way to register jQuery plugins for certain state. That provides us with the ability to provide different behavior for components based on the current active state. For example the Offcanvas menu plugin is only active on mobile devices (states "xs" and "s") and is disabled on tablets and desktop pc's. -The state manager is available in the global javascript scope of the storefront. To register your plugin, simply can call the addPlugin() method of the state manager. +The StateManager is available in the global javascript scope of the storefront. To register your plugin, simply call the addPlugin() method of the StateManager. In the following example we register our own jQuery plugin for the XS and S states. The name of the plugin is "myPlugin" and we will bind it to the HTML DOM nodes which have the class .my-selector: @@ -381,7 +434,7 @@ In the following example we register our own jQuery plugin for the XS and S stat StateManager.addPlugin('.my-selector', 'myPlugin', [ 'xs', 's' ]); ``` -### Passing a user configuration to the jQuery plugin +### Passing user configuration to the jQuery plugin It's also possible to pass user configuration options to the plugin, which will be merged with the plugin's default configuration. The merged configuration is accessible using the this.opts object in your plugin. ```javascript @@ -397,8 +450,26 @@ StateManager.addPlugin('.my-selector', 'myPlugin', { 'speed': 2000 }, [ 'xs', 's' ]); ``` +Or pass the configuration via html `data` attributes and call the `applyDataAttributes()` method in our `init()` method. + +```html +
+``` + +```javascript +$.plugin('myPlugin', { + defaults: { + myStringVar: 'myStandardValue', + myBooleanVar: false + }, + init: function() { + var me = this; + me.applyDataAttributes(); + } +}); +``` -If you need to pass a modified configuration to your plugin for a specific viewport, you can use the following pattern: +If you need to pass modified configuration to your plugin for a specific viewport, you can use the following pattern: ```javascript StateManager.addPlugin('.my-selector', 'myPlugin', { @@ -410,9 +481,9 @@ StateManager.addPlugin('.my-selector', 'myPlugin', { ## Adding javascript files to your theme -Working with compressors isn't always as easy as adding the files to your HTML structure using ```script``` tags. The built-in javascript compressor is as easy as this and perfectly suited your workflow as a web developer. +Working with compressors isn't always as easy as adding the files to your HTML structure using ```script``` tags. The built-in javascript compressor is just as easy as this and perfectly suited to your workflow as a web developer. -Simply place your javascript files in the ```frontend/_public``` directory and add their paths to the ```$javascript``` array in your ```Theme.php```, and you're good to go. +Simply place your javascript files in the ```frontend/_public``` directory and add their paths to the ```$javascript``` array in your ```Theme.php``` and you're good to go. ```php /** @var array Defines the files which should be compiled by the javascript compressor */ diff --git a/source/designers-guide/preparing-themes-for-the-community-store/SwagTutorialTheme.zip b/source/designers-guide/preparing-themes-for-the-community-store/SwagTutorialTheme.zip deleted file mode 100644 index 4db9028010..0000000000 Binary files a/source/designers-guide/preparing-themes-for-the-community-store/SwagTutorialTheme.zip and /dev/null differ diff --git a/source/designers-guide/preparing-themes-for-the-community-store/index.md b/source/designers-guide/preparing-themes-for-the-community-store/index.md index 1c7bf34a37..9852aea46f 100644 --- a/source/designers-guide/preparing-themes-for-the-community-store/index.md +++ b/source/designers-guide/preparing-themes-for-the-community-store/index.md @@ -1,11 +1,11 @@ --- layout: default -title: Preparing themes for the community store +title: Preparing themes for the Community Store github_link: designers-guide/preparing-themes-for-the-community-store/index.md indexed: true group: Frontend Guides subgroup: Developing Themes -menu_title: Preparing themes for the community store +menu_title: Preparing themes for the Community Store menu_order: 90 --- @@ -25,24 +25,87 @@ The plugin directory has to have a specific structure in order to work inside Sh [developer prefix][plugin name] ``` -Themes in the Shopware Community Store have to be wrapped inside plugins in order to be installable with the plugin manager. The plugin requires a `Bootstrap.php` file in the root directory and the custom theme, that was previously created. It has to be located inside the `Themes/Frontend` directory (just as it would be inside the normal Shopware installation). +## 5.2 plugin system + +Themes in the Shopware Community Store have to be wrapped inside plugins in order to be installable with the plugin manager. The plugin requires a *plugin base file* and a `plugin.xml` file in the root directory and the custom theme, that was previously created. It has to be located inside the `Resources/Themes/Frontend` directory (just as it would be inside the normal Shopware installation). **Attention: the directory and file names are case sensitive.** +##### Plugin directory +``` +SwagTutorialTheme + ├── Resources + │ ├── Themes + │ │ ├── Frontend + │ │ │ ├──TutorialTheme + │ │   │ │ ├── preview.png + │ │   │ │ ├── Theme.php + │ │ │ │ └── frontend + ├── SwagTutorialTheme.php + └── plugin.xml +``` + +### Creating the plugin + +Just create a plain *plugin base file*. Because the `TutorialTheme` directory is located inside the `Resources/Themes/Frontend`, the plugin automatically detects its content. + +##### SwagTutorialTheme.php - *the plugin base file* +```php + + + + + 1.0.0 + (c) by shopware AG + MIT + http://store.shopware.com + shopware AG + + +``` + +The plugin should now be displayed inside the Shopware plugin manager, where it can be installed. Once the plugin is activated, the theme will be available and selectable in the theme manager, along with all other existing themes. If this is the case, the plugin is ready to be published in the Shopware Community Store. +![Inside the plugin manager](img-pm.jpg) + +### Result +The example plugin as download: + ++ [Final store ready plugin - Download](/exampleplugins/SwagTutorialTheme.zip) + + +## Legacy plugin system + +Themes in the Shopware Community Store have to be wrapped inside plugins in order to be installable with the plugin manager. The plugin requires a `Bootstrap.php` file in the root directory and the custom theme, that was previously created. It has to be located inside the `Themes/Frontend` directory (just as it would be inside the normal Shopware installation). + +**Attention: the directory and file names are case sensitive.** ##### Plugin directory ``` SwagTutorialTheme ├── Themes - │ ├──Frontend - │ │ ├──TutorialTheme - │   │ │ ├── preview.png - │   │ │ ├── Theme.php - │ │ │ └── frontend + │ ├── Frontend + │ │ ├──TutorialTheme + │   │ │ ├── preview.png + │   │ │ ├── Theme.php + │ │ │ └── frontend └── Bootstrap.php ``` -## Creating the plugin +### Creating the plugin The only requirements the `Bootstrap.php` file has, in this case, are the plugin label (name that is displayed in the plugin manager later on) and the version number. So, with that in mind, you would create the file and add the 2 required functions to it. Because the `TutorialTheme` directory is located inside the `Themes/Frontend`, the plugin automatically detects its content. @@ -65,10 +128,7 @@ class Shopware_Plugins_Frontend_SwagTutorialTheme_Bootstrap extends Shopware_Com } ``` -The plugin should now be displayed inside the Shopware plugin manager, where it can be installed. Once the plugin is enabled, the theme will be available and selectable in the theme manager, along with all other existing themes. If this is the case, the plugin is ready to be published in the Shopware Community Store. -![Inside the plugin manager](img-pm.jpg) - -## Result +### Result The example plugin as download: -+ [Final store-ready plugin - Download](SwagTutorialTheme.zip) ++ [Legacy plugin system plugin - Download](/exampleplugins/SwagLegacyTutorialTheme.zip) diff --git a/source/designers-guide/responsive-images/index.md b/source/designers-guide/responsive-images/index.md index 8120c174d7..241125b0ed 100644 --- a/source/designers-guide/responsive-images/index.md +++ b/source/designers-guide/responsive-images/index.md @@ -50,14 +50,14 @@ The `picture` element also contains a default `img` tag, in cases the `media` at ```html + srcset="product-small@2x.jpg 400w, product-medium@2x.jpg 1200w, product-large@2x.jpg 2560w" + media="(min-resolution: 192dpi)"> + srcset="product-small.jpg 200w, product-medium.jpg 600w, product-large.jpg 1280w"> Product picture ``` -To learn more about the `srcset` and `sizes` attribute, please refer to the [Srcset and sizes blog post](https://ericportis.com/posts/2014/srcset-sizes/) by Eric Portis. \ No newline at end of file +To learn more about the `srcset` and `sizes` attribute, please refer to the [Srcset and sizes blog post](https://ericportis.com/posts/2014/srcset-sizes/) by Eric Portis. diff --git a/source/designers-guide/responsive-theme-default-components/index.md b/source/designers-guide/responsive-theme-default-components/index.md index 442414a2a7..0a4f8f2f3a 100644 --- a/source/designers-guide/responsive-theme-default-components/index.md +++ b/source/designers-guide/responsive-theme-default-components/index.md @@ -64,7 +64,7 @@ Creates a webfont icon. ``` -The Shopware 5 Responsive theme provides you with a large amount of webfont icons. You can find a list of all Icons in our [Shopware UI components overview](https://developers.shopware.com/styletile/_components-icon-set.html). +The Shopware 5 Responsive theme provides you with a large amount of webfont icons. You can find a list of all Icons in our [Shopware UI components overview](https://developers.shopware.com/styletile/components.html#icon-set). You can also use icons within buttons by adding the positioning classes from the [Buttons example](#buttons): diff --git a/source/designers-guide/smarty-plugins/index.md b/source/designers-guide/smarty-plugins/index.md new file mode 100644 index 0000000000..bcd1446b9d --- /dev/null +++ b/source/designers-guide/smarty-plugins/index.md @@ -0,0 +1,222 @@ +--- +layout: default +title: Smarty Plugins +github_link: designers-guide/smarty-plugins/index.md +indexed: true +group: Frontend Guides +subgroup: General Resources +menu_title: Smarty Plugins +menu_order: 55 +--- + +
+ +# Smarty plugins # + +Several more Smarty plugins are available in the current Shopware version. This makes it even easier to deal with currencies, data (times), configurations, links and paths in the template. Below you will find all the information needed to use these plugins in your Smarty template. + +## Currency plugin ## + +The currency plugin allows you to control how prices are formatted and displayed in your shop. So you have the possibility to apply different predefined standard formats and to determine where, for example, the currency symbol will appear. + +To activate the plugin, please use the following syntax: + +Example: *currency syntax* +``` +{* Syntax *} +{[Price]|currency:[FORMAT]:[CURRENCY POSITION} + +{* Examplecall - Output: 49,95 EUR *} +{$sArticle.price|currency:use_shortname:right} +``` + +The following formats are available in the menu: + +- no_symbol - price without specifying currency, e.g., 49.95. Here the position is ignored. +- use_symbol - price with currency symbol, e.g., $ 49.95 +- use_shortname - price with indication of currency as short, e.g., 49.95 USD +- use_name - price with indication of currency as long as the form, e.g., 49.95 dollars + +The following positions are available in the menu: + +- left - displays the currency symbol to the left of the price as USD 49.95 +- right - displays the currency symbol to the right of the price as 49.95 USD +- standard - displays the currency symbol in the default location, which is on the right e.g., 49,95 USD + +
+ Note: Shopware, by default, formats prices in the frontend as follows: + {$sArticle.price|currency:use_symbol:right}. + If you wish to have certain areas of your shop in a different format, this plugin will allow you to change the format. +
+ +## Config plugin ## + +The config plugin grants you the ability to access and read the various features in the backend. + +To do so, use the following syntax: + +Example: *config syntax* +``` +{* Syntax *} +{config name=[NAME OF PROPERTY]} + +{* Examplecall "Disable vote" *} +{config name=VoteDisable} +``` + +## Link Plugin ## + +The link plugin helps you to specify file paths, for example, when loading images or style sheets. We'll assume that you've installed your shop in a subdirectory shopware on your server (domain = http://www.meinshop.de) and would like to load a style sheet my_styles.css from the directory frontend/_resources/styles into your template my_template. + +Without the plugin, the call looks like this: + +Example: *normal style sheet call* +``` + +``` + + +By using the link plugin you only have to define the path relative to your own theme directory. + +Example: *Calling a style sheet via link plugin* +``` + +``` + +The syntax of the plugin looks like this: + +``` +{* Syntax *} +{link file="[PATH TO FILE]"} +``` + +## Media plugin ## + +With Shopware 5.2 the new media plugin was added. + +In your Smarty templates you may use the {media path=...} expression to get the fully qualified URL. + +Example: *Calling an image via media plugin* +``` + +``` + +{media} evaluates the given path at template's compile time, so you cannot use runtime variables for its path argument (generally you will use a constant path as in the example above). + +[Shopware 5 Media Service Documentation](https://developers.shopware.com/developers-guide/shopware-5-media-service/#url-generation) + +## Url plugin ## + +The url plugin gathers the URLS together from throughout the frontend and passes those specified on to the appropriate controller and action. Let's assume that you have a link that should connect to the wish list, then the call would look like this: + + +Example: *example of a call to the wish list* +``` +{* A link to notes *} + + Show notes + +``` + +The index action is always the default action. If you wish to call, for example, instant downloads in the account area, then the action must be passed on accordingly. + +Example: *example of a call to instant downloads* +``` +{* A link to instant downloads *} + + Open instant downloads + +``` + +You also have the option to pass on to the controller parameters. For this example, we'll extend the call to instant downloads with the parameter sParam with the value test. + +Example: *call to instant download with parameter* +``` +{* A link to instant downloads *} + + Open instant downloads with parameter + +``` + +The syntax of the url plugin is as follows: + + +Example: *Syntax url plugin* +``` +{* Syntax *} +{url module='[FRONTEND/WIDGETS]' controller='[CONTROLLERNAME]' action='[ACTIONNAME]' [MORE PARAMETERS='PARAMETERVALUE']} +``` + +The plugin also automatically builds up the SEO links if the corresponding plugin is installed. + +## Date plugin ## + +The date plugin is used to format date and time information. To this end, you have the ability to use a variety of formats. The syntax of the date plugin is as follows: + +Example: *date plugin syntax* +``` +{* Syntax *} +{[VALUE]|date:[FORMAT]:[TYPE]} +``` + +The plugin has the following format types, which are based on the Zend Framework: + +
+ Note: Due to localization, the following date formats may differ slightly from the examples given. +
+ +Date: + +- DATE_FULL - full date e.g., "Thursday, November 4, 2010"' +- DATE_LONG - DATE_LONG - long date e.g., "4th of November 2010" +- DATE_MEDIUM - normal date e.g., "04/11/2010" +- DATE_SHORT - abbreviated date e.g., "11.04.10"' + +Time: + +- TIME_FULL - full time e.g., "1:55:52 pm Europe/Berlin" +- TIME_LONG - long time e.g., "1:55:52 pm CET" +- TIME_MEDIUM - normal time e.g., "1:55:52 pm" +- TIME_SHORT - abbreviated time e.g., "1:55 pm" + +Date/time: + +- DATETIME_FULL - full date with time e.g., "Thursday, November 4, 2010 1:55:52 pm Europe / Berlin" +- DATETIME_LONG - long date with time e.g., "November 4, 2010 1:55:52 pm CET" +- DATETIME_MEDIUM - normal date with time e.g., "11/04/2010 1:55:52 pm" +- DATETIME_SHORT - abbreviated date with time e.g., "11/04/10 1:55 pm" + +Miscellaneous: + +- ISO_8601 - date according to ISO 8601 e.g., "2010-11-04T13:55:52+01:00" +- RFC_2822 - date according to RFC 2822 e.g., "Thu, 04 Nov 2010 13:55:52 +0100" +- TIMESTAMP - UNIX time e.g., "1288875352" +- ATOM - date according to ATOM e.g., "2010-11-04T13:55:52+01:00" +- RSS - date for RSS feeds e.g., "Thu, 04 Nov 2010 13:55:52 +0100" +- COOKIE - date for cookies e.g., "Thursday, 04-Nov-10 1:55:52 p.m. Europe / Berlin" +- W3C - date for HTML or HTTP W3C e.g., "2010-11-04T13:55:52+01:00" + +The following types can be defined for the plugin: + +- ISO - ISO format for date formatting +- PHP - PHP's date() function for date formatting + +## Action plugin ## + +The plugin uses widgets to embed templates. Widgets are self-contained parts of the frontend, such as the shopping worlds. + +``` smarty +{* Syntax *} +{action module=widgets controller=[CONTROLLERNAME] action=[ACTIONNAME] [[MORE PARAMETER]]} + +{* Examplecall - Topseller *} +{action module=widgets controller=listing action=top_seller sCategory=$sCategoryContent.id} +``` + +In this case, an HTTP request is triggered internally within the system, resulting in widgets being completely dynamic elements which are not cached. + +For further information read this blog: On action tags + + +## Custom Smarty plugins +To register custom smarty plugins please see register custom smarty plugins diff --git a/source/designers-guide/smarty/index.md b/source/designers-guide/smarty/index.md index 62f1ff960d..ea84e28bb8 100644 --- a/source/designers-guide/smarty/index.md +++ b/source/designers-guide/smarty/index.md @@ -39,7 +39,7 @@ To start with, we use Smarty to output the data we receive from the shop system. **Example:** *Variable output* ```html -

{$sArticle.name}

+

{$sArticle.articleName}

``` This short example will output the content of the variable in a normal `

` element. The dividing `.` in the variable name is used for accessing sub-values. As shown in this example, a template variable may not only be of a simple `string` or `number` type, but also of a large set of data, a so called `array`. In our example we have a template variable called `sArticle`, which is an `array` containing several fields, including the field `name`. Template variables can be nested even deeper. You may find something like `{$sArticle.image.thumbnails[0].source}` in our template code. All fields of an `array` can also be accessed with the `[]` syntax you may know from PHP. @@ -90,7 +90,7 @@ To handle a larger set of data, like a list of products, we can create dynamic o ```html
    {foreach $sArticles as $item} -
  • {$item.name}
  • +
  • {$item.articleName}
  • {/foreach}
``` @@ -199,6 +199,49 @@ In Shopware 5 we added the ability to register your own custom Smarty plugins in **Example:** *Path to custom Smarty plugins* ``` -/_private/smarty/function.markdown.php -/_private/smarty/modifier.picture.php +themes/Frontend/ThemeName/_private/smarty/function.markdown.php +themes/Frontend/ThemeName/_private/smarty/modifier.picture.php +``` +**Notice:** Smarty plugins are not working inside mail templates + +### Register custom smarty plugins in a Shopware plugin + +To specify your custom smarty plugin directory inside your Shopware plugin you can use the `CompilerPass` to add your own directory: + +```php +getDefinition('template'); + $template->addMethodCall('addPluginsDir', ['directory/name']); + } +} +``` + +Register CompilerPass class in `build` method in plugin boostrap class: + +```php +/** + * @param ContainerBuilder $container + */ +public function build(ContainerBuilder $container) +{ + parent::build($container); + + $container->addCompilerPass(new AddTemplatePluginDirCompilerPass()); +} ``` diff --git a/source/designers-guide/snippets/index.md b/source/designers-guide/snippets/index.md index 3f2507445c..fe4e5d5c74 100644 --- a/source/designers-guide/snippets/index.md +++ b/source/designers-guide/snippets/index.md @@ -45,7 +45,7 @@ If you use this feature, your snippets will no longer require an explicit declar You can optionally still declare the namespace in that snippet. The namespace declared in the snippets takes precedence over the file's global namespace. Use this if you need to reuse, in your current namespace, a snippet from another namespace. Keep in mind that this is not recommended, as later changes to that snippet will impact multiple points of your plugin, potentially causing undesired consequences. -Remember that the namespaces of snippets that are defined by using `.ini` files in your theme's `_private` directory will have the `theme/` prefix automatically added to them. +Remember that the namespaces of snippets that are defined by using `.ini` files in your theme's `_private` directory will have the `themes/` prefix automatically added to them. ### Using snippets @@ -59,6 +59,60 @@ While snippets are mostly used to translate plain text, they are flexible enough {s name="frontend/checkout/cart/separate_dispatch"}bold example text{/s} ``` +### Using smarty variables and functions in snippets + +With smarty you have access to config variables, view variables and you can even use modifiers and functions. With this you have the possibility to make your snippets more adaptive. + +Here are some examples what's possible: + +Instead of changing the name in each snippet make use of the config variable: +``` +Hello to the new {config name=shopName} Shop! +``` + +Instead of changing the year of the copyright every year, make use of a smarty modifier: +``` +Copyright {"%Y"|strftime} {config name=shopName} +``` + +Determine the output of your snippets with "if-queries": +``` +{if $sArticle.length}L{$sArticle.length} {/if}{if $sArticle.width}B{$sArticle.width} {/if}{if $sArticle.height}H{$sArticle.height} {/if}cm +``` + + +### Using dynamic snippets + +You are able to create snippets which use dynamic snippet names. This could be useful if you only have a value/name stored in variable, but you need a translation of that value/name. + +Example: You have an attribute on your product (attr1, combobox) with the following contents selectable (value => content): +* light => Light +* medium => Medium +* hard => Hard + +But those values in the combobox are not translatable. For this purpose you can generate dynamic snippets depending on the value of your attribute. + +The following smarty code shows how it works. +``` +{$name = "DetailDataHardness"|cat:$sArticle.attr1} +{$namespace = "frontend/detail/data"} +{$sArticle.attr1|snippet:$name:$namespace} +``` + +DetailDataHardness is the name of the snippet in your snippet manager and will be concatinated with the value of your attribute and will be stored as variable in `$name`. + +In our example we will get three new snippets called: +* DetailDataHardnesslight +* DetailDataHardnessmedium +* DetailDataHardnesshard + +As with every snippet, we need to select the namespace, stored in `$namespace`. + +After this we use the smarty modifier `snippet` and set the `$name` and `$namespace`. + +In your snippet manager you can now edit the translations of the dynamically generated snippets. + + ### Understanding snippet handling Snippet handling is configurable, so you can decide exactly how snippets are loaded and saved by Shopware. This can be changed in your `config.php` file, inside the `snippet` section: @@ -81,9 +135,9 @@ array( When handling snippets while rendering templates, the following workflow is used: + If `readFromDb` is `true`, Shopware will look for your snippet value in the database -+ If the snippet is not present in the database, has a default value set and `writeToDb` is `true`, Shopware will write that value into the database. ++ If the snippet is not present in the database, has set a default value and `writeToDb` is `true`, Shopware will write that value into the database. + If `readFromIni` is `true`, Shopware will look for your snippet value in your .ini files. -+ If the snippet is not present in the .ini file, has a default value set and `writeToIni` is `true`, Shopware will write that value into an .ini file. ++ If the snippet is not present in the .ini file, has set a default value and `writeToIni` is `true`, Shopware will write that value into an .ini file. The additional `showSnippetPlaceholder` (introduced in Shopware 5.0.2) option allows you to specify how you want Shopware to display empty and undefined snippets (snippets that are declared in your template files, but are not defined or defined as empty in your database and/or .ini files). By default, these snippets are displayed as an empty string, which is recommended for production environments. If you set this option to `true`, these snippets will be rendered as their name wrapped in hash signs. This makes it easier for you to identify and handle missing or empty snippets during development. @@ -94,8 +148,9 @@ The above example configuration values represent the default values that are use While developing with snippets, you need to declare them in your template files and, later on, assign them values besides the default ones. These values can be set inside .ini files:
-Note: When you create .ini files in the _private directory of your theme, the namespace will not match the default namespace of your template files, to avoid namespace collision. The namespace of all snippets from the .ini files will be prefixed with themes//.
-Example: _private/snippets/frontend/index/shop-navigation.ini will require the themes//frontend/index/shop-navigation namespace in order to work properly inside your theme. The namespace can either be set for the whole file (affecting the namespaces of all snippets, which will possibly break the default Shopware snippets) or can be set manually for each snippet tag. +Note: When you create .ini files in the _private directory of your theme (/themes/Frontend//_private), the namespace will not match the default namespace of your template files, to avoid namespace collision. The namespace of all snippets from the .ini files will be prefixed with themes/THEMENAME/.
+Example: _private/snippets/frontend/index/shop-navigation.ini will require the themes/THEMENAME/frontend/index/shop-navigation namespace in order to work properly inside your theme.

+In this example the snippet tag has to be {s name="SnippetName" namespace="themes/THEMENAME/frontend/index/shop-navigation"}.

The namespace can either be set for the whole file by using the {namespace} tag as described above (affecting the namespaces of all snippets, which will possibly break the default Shopware snippets) or can be set manually for each snippet tag.
``` @@ -169,6 +224,22 @@ A few things to keep in mind when using this approach: If your plugin/theme uses snippets, they should be placed inside the corresponding directory in your plugin/theme. If that is done correctly, when the plugin is installed in another Shopware installation, those snippets will be automatically imported from the .ini file into the database. This minimizes the number of file reads in production environments, maximizing performance. +For plugins, place the .ini files below the following directory: + +``` +PluginDirectory + Resources + snippets +``` + +For themes, place the .ini files below the following directory: + +``` +ThemeDirectory + _private + snippets +``` + ## Snippets during installation/production phases ![Backend snippet administration](admin.jpg) diff --git a/source/designers-guide/theme-startup-guide/index.md b/source/designers-guide/theme-startup-guide/index.md index 935634836e..3e03e3a9c9 100644 --- a/source/designers-guide/theme-startup-guide/index.md +++ b/source/designers-guide/theme-startup-guide/index.md @@ -70,7 +70,7 @@ As part of the restructuring of the theme, we updated the list of browsers which * Firefox version 29 or above * Safari, Mac OS X only. Support for the windows version has been discontinued * Opera version 15 with Blink engine or above -* Internet Explorer version 9 or above +* Internet Explorer version 9 or above (with Shopware 5.3 Internet Explorer version 11) Please keep in mind that older browsers don't support all available HTML5 and CSS3 features. @@ -135,6 +135,13 @@ class Theme extends \Shopware\Components\Theme } ``` +
+ Since Shopware 5.4, it's possible to manipulate the chain of inheritance by discarding + Less/JavaScript, defined by another theme. You can find detailed information + here. +
+ + ### Adding javascript files to your theme Working with compressors isn't always as easy as adding the files to your HTML structure using ```script``` tags. The built-in javascript compressor is as easy as this and perfectly suited your workflow as a web developer. @@ -369,7 +376,7 @@ The structure of a Shopware 5 theme is very similar to the one already existing ``` ### Differences between the Shopware 4 and Shopware 5 structure -Shopware 5 themes, like in Shopware 4, are still divided in great sections, with multiple subsections each. In addition we've divided the template files even smaller parts to increase the reusability and maintainability. +Shopware 5 themes, like in Shopware 4, are still divided in great sections, with multiple subsections each. In addition, we've divided the template files even smaller parts to increase the re-usability and maintainability. For example, we splitted the product box template file ```box_article.tpl``` in smaller parts which can be found in the ```listing/product-box``` directory. diff --git a/source/designers-guide/tutorials.html b/source/designers-guide/tutorials.html index 76c2336086..2362c31f26 100644 --- a/source/designers-guide/tutorials.html +++ b/source/designers-guide/tutorials.html @@ -15,6 +15,7 @@
  • In-depth example: Custom listing page
  • In-depth example: Custom detail page
  • Edit newsletter and document templates
  • +
  • Google PageSpeed
  • +
  • Browser notification for visitors with an outdated browser
  • Choosing the right range slider algorithm
  • -
  • Browser notification for visitors with a outdated browser
  • - \ No newline at end of file + diff --git a/source/developers-guide/address-management-guide/index.md b/source/developers-guide/address-management-guide/index.md index 728448f023..7c0ab9a8ea 100644 --- a/source/developers-guide/address-management-guide/index.md +++ b/source/developers-guide/address-management-guide/index.md @@ -13,19 +13,24 @@ menu_title: Address Management menu_order: 80 --- -The address management allows a customer to manage more than only one address which gets changed with every order. The customer is now able to create more addresses, e.g. for home and work, and use them later on in an order without losing all existing address data. He can just change the reference to the default billing address, instead of changing it entirely. +The address management allows a customer to manage more than only one address which gets changed with every order. +The customer is now able to create more addresses, e.g. for home and work, and use them later on in an order without losing all existing address data. +He can just change the reference to the default billing address, instead of changing it entirely.
    ## Address Service -The address service is used to manage all address entities in Shopware. It only works with models which makes it easy to comprehend, which properties are available and can be used. To know which properties are available, please refer to the model `\Shopware\Models\Customer\Address` in the source code. +The address service is used to manage all address entities in Shopware. +It only works with models which makes it easy to comprehend, which properties are available and can be used. +To know which properties are available, please refer to the model `\Shopware\Models\Customer\Address` in the source code. Please don't handle the persisting using Doctrine ORM yourself as you might risk data inconsistency. ### Create an address -Like said above, you have to pass an already complete address model and an existing customer to the address service. In case of an error, you'll get an exception. +Like said above, you have to pass an already complete address model and an existing customer to the address service. +In case of an error, you'll get an exception. ```php $address = new \Shopware\Models\Customer\Address(); @@ -38,7 +43,8 @@ $this->get('shopware_account.address_service')->create($address, $customer); ### Update an address -Updating an address is almost the same as creating one. The only difference is, that you don't have to provide the customer since the address is already associated with it. +Updating an address is almost the same as creating one. +The only difference is, that you don't have to provide the customer since the address is already associated with it. Pretending that you already fetched an address in `$address`, your update call might look like this: @@ -49,7 +55,8 @@ $this->get('shopware_account.address_service')->update($address); ### Delete an address -The deletion of an address also requires an existing model. In case of errors, you'll get an exception. +The deletion of an address also requires an existing model. +In case of errors, you'll get an exception. Pretending that you already fetched an address in `$address`, your delete call might look like this: @@ -59,7 +66,6 @@ $this->get('shopware_account.address_service')->delete($address); This call might throw an exception if you are trying to delete an address, which is associated with a default billing or shipping address of a customer. - ### Set as default billing or shipping address The service includes two methods for setting an address as new default billing or shipping address. @@ -73,11 +79,14 @@ $this->get('shopware_account.address_service')->setDefaultShippingAddress($addre ## Extending the address form -The addresses are now validated by a symfony form `\Shopware\Bundle\AccountBundle\Form\Account\AddressFormType`. If you want to add your own fields to it, you can subscribe to the `Shopware_Form_Builder` event and create or modify fields inside the `additional` field. +The addresses are now validated by a symfony form `\Shopware\Bundle\AccountBundle\Form\Account\AddressFormType`. +If you want to add your own fields to it, you can subscribe to the `Shopware_Form_Builder` event and create or modify fields inside the `additional` field. ### Attributes -The address form also supports attributes. They will automatically be mapped to the attribute model if you follow the input naming conventions. This is an example for providing attributes with an HTML input field: +The address form also supports attributes. +They will automatically be mapped to the attribute model if you follow the input naming conventions. +This is an example for providing attributes with an HTML input field: ```html @@ -85,7 +94,9 @@ The address form also supports attributes. They will automatically be mapped to ### Custom data -If you don't want to use attributes, e.g. for temporary data transfer or non-persistent data, you can use the `additional` field. The address model now contains a new property `additional` which is declared as an array, a key/value store to be exact. This array will be filled with these form fields, you've added earlier using the event (see Example #1 below). +If you don't want to use attributes, e.g. for temporary data transfer or non-persistent data, you can use the `additional` field. +The address model now contains a new property `additional` which is declared as an array, a key/value store to be exact. +This array will be filled with these form fields, you've added earlier using the event (see Example #1 below). To correctly map the submitted data to your new fields, you have to follow the convention of using the additional array as field name like: @@ -93,22 +104,37 @@ To correctly map the submitted data to your new fields, you have to follow the c ``` -You have access to your data by using `$address->getAdditional()`. The first example will show how to add fields to the address form. +You have access to your data by using `$address->getAdditional()`. +The first example will show how to add fields to the address form. #### Example #1: Adding a new field This example will add a new field named `neighboursName` and it should not be empty. -##### Add to install() method in Bootstrap.php -```php -$this->subscribeEvent('Shopware_Form_Builder', 'onFormBuild'); +##### Add the subscriber to the service.xml + +```xml + + + ``` -##### Create method onFormBuild() in Bootstrap.php +##### Create the Subscriber file + ```php +// PluginFolder/Subscriber/FormExtenderSubscriber.php + +public static function getSubscribedEvents() +{ + return [ + 'Shopware_Form_Builder' => 'onFormBuild', + ]; +} + public function onFormBuild(\Enlight_Event_EventArgs $event) { - if ($event->getReference() !== \Shopware\Bundle\AccountBundle\Form\Account\AddressFormType::class) { + /** @see \Shopware\Bundle\AccountBundle\Form\Account\AddressFormType::getBlockPrefix */ + if ($event->getReference() !== 'address') { return; } @@ -126,15 +152,20 @@ public function onFormBuild(\Enlight_Event_EventArgs $event) #### Example #2: Adding multiple fields at once -The example will add multiple fields with different validation options. If you don't provide a list of constraints to a field, it will be optional. +The example will add multiple fields with different validation options. +If you don't provide a list of constraints to a field, it will be optional. -##### Add to install() method in Bootstrap.php -```php -$this->subscribeEvent('Shopware_Form_Builder', 'onFormBuild'); +##### Add the subscriber to the service.xml +```xml + + + ``` -##### Create method onFormBuild() in Bootstrap.php +##### Create the Subscriber file and the onFormBuild() method + ```php +// PluginFolder/Subscriber/FormExtenderSubscriber.php public function onFormBuild(\Enlight_Event_EventArgs $event) { if ($event->getReference() !== \Shopware\Bundle\AccountBundle\Form\Account\AddressFormType::class) { return; @@ -161,7 +192,8 @@ public function onFormBuild(\Enlight_Event_EventArgs $event) { #### Example #3: Handle the additional data in the AddressService -This example will decorate the address service in order to handle additional data. First, we need a new class, which implements the `AddressServiceInterface`. +This example will decorate the address service in order to handle additional data. +First, we need a new class, which implements the `AddressServiceInterface`. ##### MyAddressService.php @@ -233,21 +265,17 @@ class MyAddressService implements AddressServiceInterface } ``` -Now that we have created the class, we need to decorate the existing service with our new service. For this, subscribe to a new event in your `Bootstrap.php` file. - -##### Add to install() method in Bootstrap.php -```php -$this->subscribeEvent('Enlight_Bootstrap_AfterInitResource_shopware_account.address_service', 'decorateService'); -``` - -##### Create method decorateService() in Bootstrap.php -```php -public function decorateService() -{ - $coreService = $this->get('shopware_account.address_service'); - $newService = new MyAddressService($coreService); - Shopware()->Container()->set('shopware_account.address_service', $newService); -} +Now that we have created the class, we need to decorate the existing service with our new service. + +##### Decorate the service in /Resources/service.xml +```xml + + + ``` You are now set and your service takes over the work. diff --git a/source/developers-guide/attribute-system/img/account.jpg b/source/developers-guide/attribute-system/img/account.jpg new file mode 100644 index 0000000000..63cd92b253 Binary files /dev/null and b/source/developers-guide/attribute-system/img/account.jpg differ diff --git a/source/developers-guide/attribute-system/img/customer.jpg b/source/developers-guide/attribute-system/img/customer.jpg new file mode 100644 index 0000000000..084511c06e Binary files /dev/null and b/source/developers-guide/attribute-system/img/customer.jpg differ diff --git a/source/developers-guide/attribute-system/index.md b/source/developers-guide/attribute-system/index.md index 8049f2535e..0b33859995 100644 --- a/source/developers-guide/attribute-system/index.md +++ b/source/developers-guide/attribute-system/index.md @@ -36,7 +36,7 @@ Following types are supported: | Unified type | SQL type | Backend view | | ------------- |:-------------:| -----:| -| string | VARCHAR(500) | Ext.form.field.Text +| string | TEXT | Ext.form.field.Text | text | TEXT | Ext.form.field.TextArea | html | MEDIUMTEXT | Shopware.form.field.TinyMCE | integer | INT(11) | Ext.form.field.Number @@ -108,6 +108,26 @@ class SwagAttribute extends Plugin } ``` +### Set a default value +```php +container->get('shopware_attribute.crud_service'); + $service->update('s_articles_attributes', 'my_integer', 'integer', [], null, false, 3); + } +} +``` +Creates a new attribute `my_integer` with the default value `3`. Please notice that default values will only be shown in the backend if you're using SW 5.5.4 or higher. In addition to that, please keep in mind that MySQL allows default values for none text/blob columns only. + ### Delete an existing attribute ``` getSubject()->View(); - $view->addTemplateDir($this->getPath() . '/Views/'); + $view->addTemplateDir($this->getPath() . '/Resources/views/'); $view->extendsTemplate('backend/swag_attribute/Shopware.attribute.Form.js'); } @@ -369,7 +391,7 @@ Ext.define('Shopware.attribute.Form-SwagAttribute', { ``` This example only shows a small validation to `allowBlank: false` and defines a minimum string length of 10. ExtJS supports different validation functions for an `Ext.form.field.Base`, for more information see: -[ExtJs Docs](http://docs.sencha.com/extjs/4.1.3/#!/api/Ext.form.field.VTypes) +[ExtJs Docs](http://docs.sencha.com/extjs/4.1.1/#!/api/Ext.form.field.VTypes) ### Define own backend view In some cases it is required to define an own view for the backend attribute which is not kind of the default view elements. @@ -657,6 +679,19 @@ class SwagAttribute } } ``` +
    +Note: The model must contain minimum one of the following fields to correctly display the item in the backend. + +* label +* name +* title +* number +* description +* value + +If this is not done, the entry is displayed with the value `null` and works not properly. +
    + If no individual view defined, shopware uses the `Shopware.form.field.Grid` class for multi selections. In case the attribute is configured as single selection type, the `Shopware.form.field.SingleSelection` class is used. If it is necessary to define which data has to be displayed in the selection elements, it is simply possible to extend the `Shopware.attribute.AbstractEntityFieldHandler` class to handle the attribute and extend the `Shopware.form.field.Grid` to modify the displayed data: @@ -819,7 +854,7 @@ class SwagShoeSize extends Plugin ``` ### Adding an input element for the attribute to the registration form -We create the template `Resources/Views/frontend/register/personal_fieldset.tpl` and extend the block where we want the input to show up. The attribute is persisted automatically along with the registered customer. +We create the template `Resources/views/frontend/register/personal_fieldset.tpl` and extend the block where we want the input to show up. The attribute is persisted automatically along with the registered customer. ``` {extends file="parent:frontend/register/personal_fieldset.tpl"} @@ -837,7 +872,7 @@ We create the template `Resources/Views/frontend/register/personal_fieldset.tpl` **Attention**: Although the field names are defined in snake_case when created using the CRUD-service, you need to use camelCase in name attributes. This is necessary due to the way the internally used FormBuilder works. ### Show attributes in the frontend -Attributes are loaded automatically with the entity they belong to. To display the shoesize in the account we create `Resources/Views/frontend/account/index.tpl`: +Attributes are loaded automatically with the entity they belong to. To display the shoesize in the account we create `Resources/views/frontend/account/index.tpl`: ``` {extends file="parent:frontend/account/index.tpl"} @@ -853,3 +888,127 @@ Attributes are loaded automatically with the entity they belong to. To display t Plugin Download: [SwagAttribute.zip](/exampleplugins/SwagAttribute.zip) + +### Attribute label translations + +With Shopware 5.3 it is possible to translate the different labels for an attribute (help, support, label) via snippets. +For example, if you create a new attribute in table `s_articles_attributes` named `attr1`, you can specify three translations: + +| ExtJS field | Snippet name | +|-------|--------------| +| `label` | s_articles_attributes_attr1_label | +| `support text` | s_articles_attributes_attr1_supportText | +| `help text` | s_articles_attributes_attr1_helpText | + +### Example: Translation for `s_article_attributes.my_column` +``` +container->get('shopware_attribute.crud_service'); + + $service->update('s_articles_attributes', 'my_column', TypeMapping::TYPE_STRING, [ + 'displayInBackend' => true, + 'label' => 'My column default label', + 'supportText' => 'My column default support text', + 'helpText' => 'My column default help text' + ]); + } +} +``` + +The snippets are stored in `SwagAttribute/Resources/snippets/backend/attribute_columns.ini`: +``` +[en_GB] +s_articles_attributes_my_column_label = "English label" +s_articles_attributes_my_column_supportText = "English support text" +s_articles_attributes_my_column_helpText = "English help text" + +[de_DE] +s_articles_attributes_my_column_label = "Deutsches label" +s_articles_attributes_my_column_supportText = "Deutscher support text" +s_articles_attributes_my_column_helpText = "Deutscher help text" +``` + +## Product slider based on attributes +__As of Shopware 5.3__ it is easily possible to display additional product sliders or box listing with the help of attributes and the listing widgets. +The following functions allows to display product templates based on a product stream or a selection of product numbers: +* `Shopware/Controllers/Widgets/Listing.php::streamAction` +* `Shopware/Controllers/Widgets/Listing.php::productsAction` + +The following example displays additional products in a customer account. +First the s_user_attributes table get two new columns: +- `recommendedVariants` - "Multi selection - Variants` - which should be displayed +- `recommendedStream` - "Single selection - Product Stream" - which should be displayed + +customer details + +In case of a plugin, use the following code: +``` +/** @var CrudService $crud */ +$crud = $this->container->get('shopware_attribute.crud_service'); + +$crud->update('s_user_attributes', 'recommendedVariants', 'multi_selection', [ + 'displayInBackend' => true, + 'label' => 'Recommended variants', + 'entity' => 'Shopware\Models\Article\Detail', +]); +$crud->update('s_user_attributes', 'recommendedStream', 'single_selection', [ + 'displayInBackend' => true, + 'label' => 'Recommended stream', + 'entity' => 'Shopware\Models\ProductStream\ProductStream', +]); +``` + +To display the data in the account section, add the following source code to a template which extends the `frontend/account/index.tpl`: + +``` +{extends file="parent:frontend/account/index.tpl"} +{block name="frontend_account_index_welcome"} + {$smarty.block.parent} + + {$data = $sUserData.additional.user} + +

    Recommended variants for you

    + + {action module=widgets controller=listing action=products numbers=$data.recommendedvariants type=slider} + +

    Recommended stream

    + + {action module=widgets controller=listing action=stream streamId=$data.recommendedstream type=slider} + +{/block} +``` +To display a list of product boxes, the `type=slider` property has to be removed: + +``` +{extends file="parent:frontend/account/index.tpl"} +{block name="frontend_account_index_welcome"} + {$smarty.block.parent} + + {$data = $sUserData.additional.user} + +

    Recommended variants for you

    + + {action module=widgets controller=listing action=products numbers=$data.recommendedvariants productBoxLayout='list'} + +

    Recommended stream

    + + {action module=widgets controller=listing action=stream streamId=$data.recommendedstream productBoxLayout='image'} +{/block} +``` + +Within the template file the widgets actions are called with the given customer attribute data. +Thats it. Now choose individual products or streams which will be shown to the customer. + +### Download plugin +The whole plugin can be downloaded here. diff --git a/source/developers-guide/backend-and-extjs.html b/source/developers-guide/backend-and-extjs.html index 811c6c0258..af6c5eef1e 100644 --- a/source/developers-guide/backend-and-extjs.html +++ b/source/developers-guide/backend-and-extjs.html @@ -17,6 +17,7 @@
  • Backend Components - Associations
  • Backend Components - Listing extensions
  • Backend Components - Batch processes
  • +
  • Extension of statistics module
  • Lightweight backend modules
  • Backend icon set overview
  • Backend escaping
  • diff --git a/source/developers-guide/backend-components/associations/index.md b/source/developers-guide/backend-components/associations/index.md index 9dcf2481f0..43f1f39326 100644 --- a/source/developers-guide/backend-components/associations/index.md +++ b/source/developers-guide/backend-components/associations/index.md @@ -42,17 +42,16 @@ Shopware uses Doctrine 2 to manage models and their corresponding associations. However, as these associations are required before we can continue our Backend module development, you should copy the following model code into your plugin (or, as an alternative, replace your `Models` directory with the corresponding content from [SwagProductAssoc.zip](/exampleplugins/SwagProductAssoc.zip)) -### The Product model (`Models\Product\Product.php`) +### The Product model (`\Models\Product.php`) ```php setOneToOne( $attribute, - '\Shopware\CustomModels\Product\Attribute', + Attribute::class, 'attribute', 'product' ); } /** - * @return \Shopware\CustomModels\Product\Variant[] + * @return Variant[] */ public function getVariants() { @@ -324,14 +323,14 @@ class Product extends ModelEntity } /** - * @param \Shopware\CustomModels\Product\Variant[] $variants + * @param Variant[] $variants * @return \Shopware\Components\Model\ModelEntity */ public function setVariants($variants) { return $this->setOneToMany( $variants, - '\Shopware\CustomModels\Product\Variant', + Variant::class, 'variants', 'product' ); @@ -355,17 +354,15 @@ class Product extends ModelEntity } ``` -### The Product Attribute model (`Models\Product\Attribute.php`) +### The Product Attribute model (`Models\Attribute.php`) ```php attr5; } - - - } ``` -### The Product Variant model (`Models\Product\Variant.php`) +### The Product Variant model (`Models\Variant.php`) ```php weight; } - - - } ``` @@ -752,9 +741,13 @@ Both queries have their own method for generating a query builder. They are name As a rule of thumb, you should always call the parent method and then extend the query: ```php -class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Backend_Application +leftJoin('product.tax', 'tax') - ->leftJoin('product.attribute', 'attribute') - ->leftJoin('product.categories', 'categories') - ->leftJoin('product.variants', 'variants'); + ->leftJoin('product.attribute', 'attribute'); - $builder->addSelect(array('tax', 'categories', 'variants', 'attribute')); + $builder->addSelect(array('tax', 'attribute')); return $builder; } @@ -786,9 +777,13 @@ class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Back The detail query might grow fast in different scenarios, therefore it's not recommended to select `@ORM\ManyToMany` and `@ORM\OneToMany` associations in the same query. But since the `getDetailQuery()` method always has to return a query builder, this isn't possible. For those cases you can use the `getAdditionalDetailData()` which gets the result of the query builder as an array. You can now modify the array to your needs and return it: ```php -class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Backend_Application +leftJoin('product.tax', 'tax') - ->leftJoin('product.attribute', 'attribute') - ->leftJoin('product.variants', 'variants'); + ->leftJoin('product.attribute', 'attribute'); - $builder->addSelect(array('tax', 'variants', 'attribute')); + $builder->addSelect(array('tax', 'attribute')); return $builder; } @@ -817,6 +811,7 @@ class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Back protected function getAdditionalDetailData(array $data) { $data['categories'] = $this->getCategories($data['id']); + $data['variants'] = array(); return $data; } @@ -824,10 +819,10 @@ class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Back { $builder = $this->getManager()->createQueryBuilder(); $builder->select(array('products', 'categories')) - ->from('Shopware\CustomModels\Product\Product', 'products') - ->innerJoin('products.categories', 'categories') - ->where('products.id = :id') - ->setParameter('id', $productId); + ->from(Product::class, 'products') + ->innerJoin('products.categories', 'categories') + ->where('products.id = :id') + ->setParameter('id', $productId); $paginator = $this->getQueryPaginator($builder); @@ -836,55 +831,90 @@ class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Back return $data['categories']; } } + ``` ### Table Generation Now, the data is going to be selected and returned to the backend application. To create the database table for the variant and attribute models, you have to modify the `install()` and `uninstall()` methods to tell doctrine what to do. You have to add the class metadata of the models to an array and use `createSchema()` to create a table or `dropSchema()` to drop a table respectively: ```php -class Shopware_Plugins_Backend_SwagProduct_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - ... +registerCustomModels(); + $this->createDatabase(); - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); + $this->addDemoData(); + } - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product'), - $em->getClassMetadata('Shopware\CustomModels\Product\Variant'), - $em->getClassMetadata('Shopware\CustomModels\Product\Attribute') - ); + /** + * {@inheritdoc} + */ + public function activate(ActivateContext $activateContext) + { + $activateContext->scheduleClearCache(ActivateContext::CACHE_LIST_DEFAULT); + } - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore + /** + * {@inheritdoc} + */ + public function uninstall(UninstallContext $uninstallContext) + { + if (!$uninstallContext->keepUserData()) { + $this->removeDatabase(); } - $tool->createSchema($classes); + } - $this->addDemoData(); + private function createDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); + + $classes = $this->getClasses($modelManager); + + $tool->updateSchema($classes, true); // make sure use the save mode } - public function uninstall() + private function removeDatabase() { - $this->registerCustomModels(); + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); + $classes = $this->getClasses($modelManager); - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product'), - $em->getClassMetadata('Shopware\CustomModels\Product\Variant'), - $em->getClassMetadata('Shopware\CustomModels\Product\Attribute') - ); $tool->dropSchema($classes); - - return true; } + + /** + * @param ModelManager $modelManager + * @return array + */ + private function getClasses(ModelManager $modelManager) + { + return [ + $modelManager->getClassMetadata(Product::class), + $modelManager->getClassMetadata(Variant::class), + $modelManager->getClassMetadata(Attribute::class) + ]; + } + ... } ``` @@ -894,16 +924,47 @@ class Shopware_Plugins_Backend_SwagProduct_Bootstrap extends Shopware_Components Last, you have to add some demo data to the tables. For this task, append the `addDemoData()` method with the following code: ```php -class Shopware_Plugins_Backend_SwagProduct_Bootstrap extends Shopware_Components_Plugin_Bootstrap +container->get('dbal_connection'); + + $this->createProductDemoData($connection); + $this->createProductVariantDemoData($connection); + $this->createProductAttributeDemoData($connection); + $sql = " - INSERT IGNORE INTO s_product - (id, name, active, description, descriptionLong, lastStock, createDate, tax_id) + SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_categories (product_id, category_id) + SELECT + a.articleID as product_id, + a.categoryID as category_id + FROM s_articles_categories a + "; + + $connection->exec($sql); + } + + private function createProductDemoData(Connection $connection) + { + $sql = 'INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate, tax_id) SELECT a.id, a.name, @@ -914,13 +975,15 @@ class Shopware_Plugins_Backend_SwagProduct_Bootstrap extends Shopware_Components a.datum as createDate, a.taxID as tax_id FROM s_articles a - "; - Shopware()->Db()->query($sql); + '; - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_variant - (id, product_id, number, additionalText, active, inStock, stockMin, weight) + $connection->exec($sql); + } + + private function createProductVariantDemoData(Connection $connection) + { + $sql = 'SET FOREIGN_KEY_CHECKS = 0; + INSERT IGNORE INTO s_product_variant (id, product_id, number, additionalText, active, inStock, stockMin, weight) SELECT a.id, a.articleID, @@ -931,11 +994,14 @@ class Shopware_Plugins_Backend_SwagProduct_Bootstrap extends Shopware_Components a.stockmin, a.weight FROM s_articles_details a - "; - Shopware()->Db()->query($sql); + '; - $sql = " - SET FOREIGN_KEY_CHECKS = 0; + $connection->exec($sql); + } + + private function createProductAttributeDemoData(Connection $connection) + { + $sql = 'SET FOREIGN_KEY_CHECKS = 0; INSERT IGNORE INTO s_product_attribute SELECT a.id, @@ -946,21 +1012,13 @@ class Shopware_Plugins_Backend_SwagProduct_Bootstrap extends Shopware_Components a.attr4, a.attr5 FROM s_articles_attributes a - "; - Shopware()->Db()->query($sql); - - $sql = " - SET FOREIGN_KEY_CHECKS = 0; - INSERT IGNORE INTO s_product_categories (product_id, category_id) - SELECT - a.articleID as product_id, - a.categoryID as category_id - FROM s_articles_categories a - "; - Shopware()->Db()->query($sql); + '; + $connection->exec($sql); } + ... + } ``` @@ -973,10 +1031,10 @@ Since we return the product including its associations, you now have to define t In the following sections of the tutorial, multiple models and views will be created. You always have to register them in the `app.js` file. Below you'll find the final `app.js` with some commented lines so we don't have to show you the whole file every time we create a component. To enable a component, just uncomment the appropriate line and reload the application: ```javascript -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductAssocAssoc', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductAssocAssoc', loadPath: '{url action=load}', bulkLoad: true, @@ -1019,24 +1077,24 @@ To implement associations in the detail window, you first have to understand how Such definition has already been implemented in the first tutorial of our Shopware Backend Components series, in the detail window of the product model: ```php -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + detail: 'Shopware.apps.SwagProductAssoc.view.detail.Product' }; }, ... }); ``` -Here you defined that the detail window should always display the `Shopware.app.SwagProduct.view.detail.Product` component. If you provide a `Shopware.apps.SwagProduct.model.Product` entry to the `Shopware.window.Detail` component, the detail window will recognize the entry as a main entry and check the `detail` parameter of the model. +Here you defined that the detail window should always display the `Shopware.app.SwagProduct.view.detail.Product` component. If you provide a `Shopware.apps.SwagProductAssoc.model.Product` entry to the `Shopware.window.Detail` component, the detail window will recognize the entry as a main entry and check the `detail` parameter of the model. Associations work in a similar way, but you first have to look up which type of association is configured. To keep things simple, there is a simplified model with four possible associations below: ```php -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', ... @@ -1045,13 +1103,13 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { model: 'Shopware.apps.Base.model.Tax', }, { relation: 'ManyToMany', - model: 'Shopware.apps.SwagProduct.model.Category', + model: 'Shopware.apps.SwagProductAssoc.model.Category', }, { relation: 'OneToOne', - model: 'Shopware.apps.SwagProduct.model.Attribute' + model: 'Shopware.apps.SwagProductAssoc.model.Attribute' }, { relation: 'OneToMany', - model: 'Shopware.apps.SwagProduct.model.Variant' + model: 'Shopware.apps.SwagProductAssoc.model.Variant' }] }); ``` @@ -1065,7 +1123,7 @@ Every association type exists with their appropriate configuration option / view If the `ManyToOne` association from the example above is displayed in a detail window, the detail window will check which component has been configured using the `field` property of the `Shopware.apps.Base.model.Tax` component. -If the `ManyToMany` association is displayed in a detail window, the detail window will check which component has been configured using the `related` property of the `Shopware.apps.SwagProduct.model.Category` component. +If the `ManyToMany` association is displayed in a detail window, the detail window will check which component has been configured using the `related` property of the `Shopware.apps.SwagProductAssoc.model.Category` component. Because associations may be displayed in the same way, the `Shopware.data.Model` already has some views pre-configured. For example, `ManyToOne` associations (e.g. product & tax) always display a combobox: @@ -1100,7 +1158,7 @@ The `ManyToOne` association is the easiest type to configure. This association i To link a product to a tax rate, you have to extend the product model: ```php -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', configure: function() { @@ -1175,7 +1233,7 @@ First, the product model will be extended with the category association. At the **SwagProduct/Views/backend/swag_product/model/product.js** ```php -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', configure: function() { ... }, fields: [ ... ], @@ -1184,7 +1242,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { relation: 'ManyToMany', type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Category', + model: 'Shopware.apps.SwagProductAssoc.model.Category', name: 'getCategory', associationKey: 'categories' } ... ] @@ -1193,13 +1251,13 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { **SwagProduct/Views/backend/swag_product/model/category.js** ```php -Ext.define('Shopware.apps.SwagProduct.model.Category', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Category', { extend: 'Shopware.apps.Base.model.Category', configure: function() { return { - related: 'Shopware.apps.SwagProduct.view.detail.Category' + related: 'Shopware.apps.SwagProductAssoc.view.detail.Category' } } }); @@ -1212,7 +1270,7 @@ Because you are using the `ManyToMany` association, we have to define the `relat This option has a new view component assigned to it, which is implemented in `Views/backend/swag_product/view/detail/category.js`: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Category', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Category', { extend: 'Shopware.grid.Association', alias: 'widget.product-view-detail-category', height: 300, @@ -1220,7 +1278,7 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Category', { configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductAssoc', columns: { name: {} } @@ -1241,7 +1299,7 @@ Both places have the same API to assign associations. For this, the `association To display the category view as a new tab, you have to modify the detail window (`Views/backend/swag_product/view/detail/window.js`) with the following code: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', @@ -1264,14 +1322,14 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { The category view can also be displayed right inside the product container. For this, you have to extend the product container (`Views/backend/swag_product/view/detail/product.js`) with the following code: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', padding: 20, configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductAssoc', associations: [ 'categories' ] }; } @@ -1309,7 +1367,7 @@ First, you have to extend the product model by adding the association and create **SwagProduct/Views/backend/swag_product/model/product.js** ```php -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', configure: function() { ... }, fields: [ ... ], @@ -1318,7 +1376,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { relation: 'OneToOne', type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Attribute', + model: 'Shopware.apps.SwagProductAssoc.model.Attribute', name: 'getAttribute', associationKey: 'attribute' } ... ] @@ -1327,12 +1385,12 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { **SwagProduct/Views/backend/swag_product/model/attribute.js** ```php -Ext.define('Shopware.apps.SwagProduct.model.Attribute', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Attribute', { extend: 'Shopware.data.Model', configure: function() { return { - detail: 'Shopware.apps.SwagProduct.view.detail.Attribute' + detail: 'Shopware.apps.SwagProductAssoc.view.detail.Attribute' }; }, @@ -1354,7 +1412,7 @@ Because the attribute model is not part of Shopware's base models, you cannot ex The attribute model has been linked with a `OneToOne` association and therefore the configuration option `detail` needs to be implemented. The attribute view will be a new file in `Views/backend/swag_product/view/detail/attribute.js`: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Attribute', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Attribute', { extend: 'Shopware.model.Container', padding: 20, @@ -1383,7 +1441,7 @@ Lastly, you have to define, where the attribute data in the detail window should To display the attribute data as a new tab, you have to modify the detail window (`Views/backend/swag_product/view/detail/window.js`) with the following code: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', @@ -1406,14 +1464,14 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { Alternatively, you can display the attribute view in a `Shopware.model.Container` and you would be able to display the attribute view right inside of the product container. For this you just have to move the association from the detail window into the product container (`Views/backend/swag_product/view/detail/product.js`): ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', padding: 20, configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductAssoc', associations: [ 'attribute' ] }; } @@ -1438,11 +1496,11 @@ The following tasks are necessary to use the `OneToMany` association using the S * Definition of the variant view * Definition of where the variant view should be displayed -First, you have to extend the product model by adding the variant association and create a new file containing the varaint model in `Views/backend/swag_product/model/variant.js`: +First, you have to extend the product model by adding the variant association and create a new file containing the variant model in `Views/backend/swag_product/model/variant.js`: **SwagProduct/Views/backend/swag_product/model/product.js** ```php -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', configure: function() { ... }, @@ -1452,7 +1510,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { relation: 'OneToMany', type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Variant', + model: 'Shopware.apps.SwagProductAssoc.model.Variant', name: 'getVariants', associationKey: 'variants' } ...] @@ -1461,12 +1519,12 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { **SwagProduct/Views/backend/swag_product/model/variant.js** ```php -Ext.define('Shopware.apps.SwagProduct.model.Variant', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Variant', { extend: 'Shopware.data.Model', configure: function() { return { - listing: 'Shopware.apps.SwagProduct.view.detail.Variant' + listing: 'Shopware.apps.SwagProductAssoc.view.detail.Variant' }; }, @@ -1486,7 +1544,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Variant', { Since the variants have been implemented as an `OneToMany` association, you have to set `listing` property. The variant view component will be created in `Views/backend/swag_product/view/detail/variant.js`: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Variant', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Variant', { extend: 'Shopware.grid.Panel', alias: 'widget.shopware-product-variant-grid', title: 'Variant', @@ -1494,14 +1552,14 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Variant', { }); ``` -By default, a `Shopware.grid.Panel` will be created in order to display `OneToMany` associations. The `Shopware.grid.Panel` component can then implement their own detail view in order to edit the variants. Alternatively, you can implement a [Ext.grid.plugin.RowEditing](http://docs.sencha.com/extjs/4.1.3/#!/api/Ext.grid.plugin.RowEditing) component to edit the data directly inside of the grid. +By default, a `Shopware.grid.Panel` will be created in order to display `OneToMany` associations. The `Shopware.grid.Panel` component can then implement their own detail view in order to edit the variants. Alternatively, you can implement a [Ext.grid.plugin.RowEditing](http://docs.sencha.com/extjs/4.1.1/#!/api/Ext.grid.plugin.RowEditing) component to edit the data directly inside of the grid. Like the `OneToMany` and `OneToOne` associations, you have to define where the variant view should be displayed. Like seen in previous section, you can display the view in a new tab or inside of a model container. To display the variant view as a new tab, you have to modify the detail window (`Views/backend/swag_product/view/detail/window.js`) with the following code: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', @@ -1524,14 +1582,14 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { As an alternative, you can display the association right inside of the product container. For this, you have to move the association from the detail window to the product container in `Views/backend/swag_product/view/detail/product.js`: ```php -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', padding: 20, configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductAssoc', associations: [ 'variants' ] }; } @@ -1560,7 +1618,7 @@ To load the variant grid data as the tab gets active, you have to do the followi At first, you have to extend the variant association in the product model: ```php -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.model.Product', { extend: 'Shopware.data.Model', configure: function() { ... }, @@ -1568,11 +1626,11 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { associations: [{ relation: 'OneToMany', - storeClass: 'Shopware.apps.SwagProduct.store.Variant', + storeClass: 'Shopware.apps.SwagProductAssoc.store.Variant', lazyLoading: true, type: 'hasMany', - model: 'Shopware.apps.SwagProduct.model.Variant', + model: 'Shopware.apps.SwagProductAssoc.model.Variant', name: 'getVariants', associationKey: 'variants' } ...] @@ -1584,12 +1642,12 @@ The option `lazyLoading` defines, that the association should only be loaded, if The option `storeClass` defines, which store component should be used. By default, ExtJS creates an `Ext.data.Store` for every association, but without an proxy configuration which is needed to load the data from the controller. The store will be implemented in `Views/backend/swag_product/store/variant.js`: ```php -Ext.define('Shopware.apps.SwagProduct.store.Variant', { +Ext.define('Shopware.apps.SwagProductAssoc.store.Variant', { extend: 'Shopware.store.Association', - model: 'Shopware.apps.SwagProduct.model.Variant', + model: 'Shopware.apps.SwagProductAssoc.model.Variant', configure: function() { return { - controller: 'SwagProduct' + controller: 'SwagProductAssoc' }; } }); @@ -1602,7 +1660,7 @@ Because the data is now lazy loaded, you have to remove the selection from the ` ```php class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Backend_Application { - protected $model = 'Shopware\CustomModels\Product\Product'; + protected $model = Product::class; protected $alias = 'product'; protected function getListQuery() diff --git a/source/developers-guide/backend-components/basics/index.md b/source/developers-guide/backend-components/basics/index.md index 22745183fb..328f1e101d 100644 --- a/source/developers-guide/backend-components/basics/index.md +++ b/source/developers-guide/backend-components/basics/index.md @@ -24,9 +24,9 @@ We also recommend the following articles: **ExtJS Documentation** -* [ExtJS API](http://docs.sencha.com/extjs/4.1.3/#!/api) -* [ExtJS Guides](http://docs.sencha.com/extjs/4.1.3/#!/guide) -* [ExtJS Examples](http://docs.sencha.com/extjs/4.1.3/#!/example) +* [ExtJS API](http://docs.sencha.com/extjs/4.1.1/#!/api) +* [ExtJS Guides](http://docs.sencha.com/extjs/4.1.1/#!/guide) +* [ExtJS Examples](http://docs.sencha.com/extjs/4.1.1/#!/example) **Shopware Plugin Concept** @@ -45,16 +45,22 @@ These tutorials are built upon each other and you will find a zip file with the To do so, you can add the following configuration to your `config.php`: ```php -'front' => array( 'throwExceptions' => true ), -'template' => array( 'forceCompile' => true ), -'model' => array( 'cacheProvider' => 'Array' ), -'cache' => array( +'front' => [ + 'throwExceptions' => true + ], +'template' => [ + 'forceCompile' => true + ], +'model' => [ + 'cacheProvider' => 'Array' + ], +'cache' => [ 'backend' => 'Black-Hole', - 'backendOptions' => array(), - 'frontendOptions' => array( + 'backendOptions' => [], + 'frontendOptions' => [ 'write_control' => false - ), -), + ] +], ``` **Important: This configuration should *not* be used in production.** @@ -69,75 +75,68 @@ The backend contains some Shopware specific components, which are based on diffe We will start by writing our own small backend application with standard components. First we have to create a plugin which provides us with some basic features. They are: * A menu item in the backend -* Your own backend controller * Your own Doctrine model +* Your own backend controller To speed things up, our plugin is expected to create a new product list to cover all sections of our backend components. Since the doctrine models are an essential part of the backend development and modules, this tutorial will rebuild and explain some parts of the `Shopware\Models\Article` namespace. -### The Plugin Bootstrap - `Bootstrap.php` -First, you have to create a directory called `SwagProduct` with a new file called `Bootstrap.php` in it. The following features will be implemented in the bootstrap class: +### The Plugin Base File +First, you have to create a directory in `..\custom\plugins` called `SwagProductBasic` with a new file called `SwagProductBasic.php` in it. The following features will be implemented in the plugin file class: -* Register your own backend controller * Create a backend menu item which calls the controller ```php 'Shopware Product Overview' - ); - } +namespace SwagProductBasic; - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Dispatcher_ControllerPath_Backend_SwagProduct', - 'getBackendController' - ); - - $this->createMenuItem(array( - 'label' => 'Shopware Product overview', - 'controller' => 'SwagProduct', - 'class' => 'sprite-application-block', - 'action' => 'Index', - 'active' => 1, - 'parent' => $this->Menu()->findOneBy(['label' => 'Marketing']) - )); - return true; - } +use Shopware\Components\Plugin; - public function getBackendController(Enlight_Event_EventArgs $args) +class SwagProductBasic extends Plugin +{ + /** + * {@inheritdoc} + */ + public function install(InstallContext $installContext) { - // Register the template directory to not have to provide it - // in the controller every time - $this->Application()->Template()->addTemplateDir( - $this->Path() . 'Views/' - ); + parent::install($installContext); + } +} +``` - // Register the models to have access to them in the controller - $this->registerCustomModels(); +### Create a menu item in the backend +Create a `/../SwagProductBasic/Resources/menu.xml` file. +```xml + + + + + + SwagBasicProduct + + + SwagProductBasic + index + sprite-application-block + Article + + + - return $this->Path() . '/Controllers/Backend/SwagProduct.php'; - } ``` -### The Doctrine Model - `Models/Product/Product.php` +### The Doctrine Model - `/Models/Product.php` -Afterwards we create a new product model in `Models/Product/Product.php`. +Afterwards we create a new product model in `/Models/Product.php`. ```php id; } + /** * @param int $active */ @@ -300,9 +300,10 @@ class Product extends ModelEntity return $this->lastStock; } } + ``` -The product model shown above contains just a fraction of the original Shopware article model, but is sufficient for our purpose. Please notice the namespace at the top. The `Shopware\CustomModels` namespace is used exclusively for plugin models. To create the associated database table, you should use Doctrine's schema tool, which can be found in `\Doctrine\ORM\Tools\SchemaTool`. The following features will now be implemented in the plugin bootstrap: +The product model shown above contains just a fraction of the original Shopware article model, but is sufficient for our purpose. Please notice the namespace at the top. The `PluginNameSpace/Models namespace is used exclusively for plugin models. To create the associated database table, you should use Doctrine's schema tool, which can be found in `\Doctrine\ORM\Tools\SchemaTool`. The following features will now be implemented in the plugin base file: * Create the database tables in the `install()` method * Add demo data to the new database table @@ -311,65 +312,78 @@ The product model shown above contains just a fraction of the original Shopware ```php subscribeEvent( ... ); - - $this->createMenuItem( ... ); + $this->createDatabase(); - $this->updateSchema(); - - return true; + $this->addDemoData(); } - protected function updateSchema() + /** + * {@inheritdoc} + */ + public function activate(ActivateContext $activateContext) { - $this->registerCustomModels(); + $activateContext->scheduleClearCache(InstallContext::CACHE_LIST_ALL); + } - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); + public function uninstall(UninstallContext $uninstallContext) + { + if (!$uninstallContext->keepUserData()) { + $this->removeDatabase(); + } + } - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); + private function createDatabase() + { + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); - try { - $tool->dropSchema($classes); - } catch (Exception $e) { - //ignore - } - $tool->createSchema($classes); + $classes = $this->getClasses($modelManager); - $this->addDemoData(); + $tool->updateSchema($classes, true); // make sure to use the save mode } - public function uninstall() + private function removeDatabase() { - $this->registerCustomModels(); + $modelManager = $this->container->get('models'); + $tool = new SchemaTool($modelManager); - $em = $this->Application()->Models(); - $tool = new \Doctrine\ORM\Tools\SchemaTool($em); + $classes = $this->getClasses($modelManager); - $classes = array( - $em->getClassMetadata('Shopware\CustomModels\Product\Product') - ); $tool->dropSchema($classes); - - return true; + } + + /** + * @param ModelManager $modelManager + * @return array + */ + private function getClasses(ModelManager $modelManager) + { + return [ + $modelManager->getClassMetadata(Product::class) + ]; } - protected function addDemoData() + private function addDemoData() { $sql = " - INSERT IGNORE INTO s_product (id, name, active, description, descriptionLong, lastStock, createDate) + INSERT IGNORE INTO s_product (`name`, active, description, descriptionLong, lastStock, createDate) SELECT - a.id, a.name, a.active, a.description, @@ -378,7 +392,8 @@ class Shopware_Plugins_Backend_SwagProduct_Bootstrap extends Shopware_Components a.datum as createDate FROM s_articles a "; - Shopware()->Db()->query($sql); + + $this->container->get('dbal_connection')->exec($sql); } } ``` @@ -392,39 +407,39 @@ Finally, we implement our own PHP controller in `Controllers/Backend/SwagProduct ```php The `model` property of your PHP controller is not configured! The property `$alias` will be the query alias used in every query with the root model (`$model` property). ## ExtJS Implementation -Now, we go on to the proper usage of the backend components. First, we implement the following components in our directory `Views/backend`: +Now, we go on to the proper usage of the backend components. First, we implement the following components in our directory `Resources/views/backend`: -* `swag_product/app.js` -* `swag_product/controller/main.js` -* `swag_product/model/product.js` -* `swag_product/store/product.js` -* `swag_product/view/list/window.js` -* `swag_product/view/list/product.js` +* `swag_product_basic/app.js` +* `swag_product_basic/controller/main.js` +* `swag_product_basic/model/product.js` +* `swag_product_basic/store/product.js` +* `swag_product_basic/view/list/window.js` +* `swag_product_basic/view/list/product.js` -### The Subapplication - `swag_product/app.js` -To start, we define the starting point of our backend application in `swag_product/app.js`. +### The Subapplication - `swag_product_basic/app.js` +To start, we define the starting point of our backend application in `swag_product_basic/app.js`. ```javascript -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductBasic', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductBasic', loadPath: '{url action=load}', bulkLoad: true, @@ -451,11 +466,11 @@ The `launch()` method will instantiate the main controller and return its `mainW
    Keep in mind that the naming of the classes in subapplications is important. It should always match the PHP controller name.
    -### The Main Controller - `swag_product/controller/main.js` +### The Main Controller - `swag_product_basic/controller/main.js` The main controller is responsible for starting the application by creating and displaying the listing window: ```javascript -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductBasic.controller.Main', { extend: 'Enlight.app.Controller', init: function() { @@ -467,11 +482,11 @@ Ext.define('Shopware.apps.SwagProduct.controller.Main', { There is not much to say about the main controller. The only important thing is to set the property `mainWindow`, in order to ensure that your application can be closed by clicking the footer button. -### The Listing Window - `swag_product/view/list/window.js` +### The Listing Window - `swag_product_basic/view/list/window.js` The listing window derives from the `Shopware.window.Listing` component and will be the first backend component that will be used in our application. ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductBasic.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 450, @@ -479,8 +494,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product' + listingGrid: 'Shopware.apps.SwagProductBasic.view.list.Product', + listingStore: 'Shopware.apps.SwagProductBasic.store.Product' }; } }); @@ -488,7 +503,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { The `Shopware.window.Listing` component has 2 requirements, which have to be met in the `configure()` method. The `configure()` method is meant to be your starting point to set Shopware related configurations. This method is available in every Shopware backend component. -* `listingGrid` - Should be the class name of the `Shopware.grid.Panel` which will be displayed inside the window. We'll define that class in `swag_product/view/list/product.js` file in the next step. +* `listingGrid` - Should be the class name of the `Shopware.grid.Panel` which will be displayed inside the window. We'll define that class in `swag_product_basic/view/list/product.js` file in the next step. * `listingStore` - Should be the class name of the `Shopware.store.Listing` which will be loaded when starting the application. This store will be the store used in the `Shopware.grid.Panel`. It is important to set property `alias`, otherwise all component events can't be determined by the ExtJS Component Query. @@ -497,11 +512,11 @@ It is important to set property `alias`, otherwise all component events can't be This tutorial already contains some of the Shopware configuration options. You'll find an extended list of the configuration options in the upcoming tutorials. -### The Grid Panel - `swag_product/view/list/product.js` -Next, we implement the `Shopware.grid.Panel` in `swag_product/view/list/product.js`. +### The Grid Panel - `swag_product_basic/view/list/product.js` +Next, we implement the `Shopware.grid.Panel` in `swag_product_basic/view/list/product.js`. ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductBasic.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center' @@ -514,13 +529,13 @@ Keep in mind that you still have to provide an alias for the component. Since the `Shopware.grid.Panel` can be used everywhere, you have to define `region: 'center'` in the `Shopware.window.Listing` because a border layout has been applied to the panel. -### The Data Model - `swag_product/model/product.js` +### The Data Model - `swag_product_basic/model/product.js` Next, we implement the `Shopware.data.Model` which reflects the data structure of `Shopware\CustomModels\Product\Product`. -For this, we implement the following code in `swag_product/model/product.js`: +For this, we implement the following code in `swag_product_basic/model/product.js`: ```javascript -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductBasic.model.Product', { extend: 'Shopware.data.Model', configure: function() { @@ -545,13 +560,13 @@ The only required property here is the `controller` property, but only if this i In case you want to do CRUD operations on your others models, you may set the `controller` property in those too. You will then be able to perform operations like `create`, `update` and `delete`. -The property `controller` expects the individual name of your `Shopware_Controllers_Backend_Application` controller. Given that your controller is called `Shopware_Controllers_Backend_SwagProduct`, the individual name would be `SwagProduct`. +The property `controller` expects the individual name of your `Shopware_Controllers_Backend_Application` controller. Given that your controller is called `Shopware_Controllers_Backend_SwagProductBasic`, the individual name would be `SwagProductBasic`. -### The Data Store - `swag_product/store/product.js` -Finally, we have to define the `Shopware.store.Listing` store, which will be used for the `Shopware.grid.Panel`. For this, we implement the following code in `swag_product/store/product.js`: +### The Data Store - `swag_product_basic/store/product.js` +Finally, we have to define the `Shopware.store.Listing` store, which will be used for the `Shopware.grid.Panel`. For this, we implement the following code in `swag_product_basic/store/product.js`: ```javascript -Ext.define('Shopware.apps.SwagProduct.store.Product', { +Ext.define('Shopware.apps.SwagProductBasic.store.Product', { extend:'Shopware.store.Listing', configure: function() { @@ -559,7 +574,7 @@ Ext.define('Shopware.apps.SwagProduct.store.Product', { controller: 'SwagProduct' }; }, - model: 'Shopware.apps.SwagProduct.model.Product' + model: 'Shopware.apps.SwagProductBasic.model.Product' }); ``` @@ -579,19 +594,19 @@ The application now supports the following operations: ## The last 25% to complete our application Until now, we have implemented 75% of the whole application. What's still missing is the detail window. This should be opened by clicking the pencil icon at the end of a line. Right now you'll get the following error message in the debug console: -
    Uncaught Shopware configuration error: Shopware.apps.SwagProduct.view.list.Product: Component requires the `detailWindow` property in the `configure()` function
    +
    Uncaught Shopware configuration error: Shopware.apps.SwagProductBasic.view.list.Product: Component requires the `detailWindow` property in the `configure()` function
    To make the application complete, we have to implement the following components: -* `swag_product/view/detail/window.js` -* `swag_product/view/detail/product.js` +* `swag_product_basic/view/detail/window.js` +* `swag_product_basic/view/detail/product.js` -### The Detail Window - `swag_product/view/detail/window.js` -Here, you have two tasks to do. First, implement a new `Shopware.window.Detail` component and second, declare it in our `Shopware.grid.Panel`. But first, implement the detail window by putting the following code in `swag_product/view/detail/window.js`: +### The Detail Window - `swag_product_basic/view/detail/window.js` +Here, you have two tasks to do. First, implement a new `Shopware.window.Detail` component and second, declare it in our `Shopware.grid.Panel`. But first, implement the detail window by putting the following code in `swag_product_basic/view/detail/window.js`: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductBasic.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', @@ -604,9 +619,9 @@ The only requirement for this component is that you have to provide a property n We now have to define the detail window at two places. -1. **Define the component in our app in `swag_product/app.js`:** +1. **Define the component in our app in `swag_product_basic/app.js`:** ```javascript -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductBasic', { extend: 'Enlight.app.SubApplication', views: [ ... @@ -621,25 +636,25 @@ Ext.define('Shopware.apps.SwagProduct', { }); ``` -2. **Define the `detailWindow` in our `Shopware.grid.Panel` (`swag_product/view/list/product.js`) to let the application know which component should be called by clicking on the pencil:** +2. **Define the `detailWindow` in our `Shopware.grid.Panel` (`swag_product_basic/view/list/product.js`) to let the application know which component should be called by clicking on the pencil:** ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductBasic.view.list.Product', { extend: 'Shopware.grid.Panel', ... configure: function() { return { - detailWindow: 'Shopware.apps.SwagProduct.view.detail.Window' + detailWindow: 'Shopware.apps.SwagProductBasic.view.detail.Window' }; } }); ``` -### The Product Detail Window - `swag_product/view/detail/product.js` -The last thing to do is implement the `Shopware.model.Container` which can be used for the representation of a model. The following code needs to be implemented in `swag_product/view/detail/product.js`: +### The Product Detail Window - `swag_product_basic/view/detail/product.js` +The last thing to do is implement the `Shopware.model.Container` which can be used for the representation of a model. The following code needs to be implemented in `swag_product_basic/view/detail/product.js`: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductBasic.view.detail.Product', { extend: 'Shopware.model.Container', padding: 20, @@ -656,7 +671,7 @@ The `Shopware.model.Container` has two requirements. A proper configured `contro Like above, you have to register your component in your `app.js`: ```javascript -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductBasic', { extend: 'Enlight.app.SubApplication', views: [ ... @@ -670,16 +685,16 @@ Ext.define('Shopware.apps.SwagProduct', { }); ``` -In addition, you have to update your `Shopware.data.Model` to make it know about your detail window. For that, add the following code to `swag_product/model/product.js`: +In addition, you have to update your `Shopware.data.Model` to make it know about your detail window. For that, add the following code to `swag_product_basic/model/product.js`: ```javascript -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductBasic.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { controller: 'SwagProduct', - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + detail: 'Shopware.apps.SwagProductBasic.view.detail.Product' }; }, fields: [ @@ -689,12 +704,14 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { }); ``` -The Shopware configuration property `detail` defines which component should be created when the detail window for `Shopware.apps.SwagProduct.model.Product` is requested. Now, the detail window will always look the same in the whole backend. +The Shopware configuration property `detail` defines which component should be created when the detail window for `Shopware.apps.SwagProductBasic.model.Product` is requested. Now, the detail window will always look the same in the whole backend. -## Plugin Download - [SwagProductBasics.zip](/exampleplugins/SwagProductBasics.zip) +## Plugin Download Congratulations! You have just created your first backend application used the Shopware backend components. The application is completely usable and provides you with all features you can see in other backend applications. You are now able to create and edit records, too. +You can download the plugin here: SwagProductBasic.zip + ![Product detail](img/backend_1.png) ## Further Tutorials diff --git a/source/developers-guide/backend-components/batch-processes/index.md b/source/developers-guide/backend-components/batch-processes/index.md index 5e3a9783a3..cda10157dc 100644 --- a/source/developers-guide/backend-components/batch-processes/index.md +++ b/source/developers-guide/backend-components/batch-processes/index.md @@ -40,9 +40,9 @@ To help you process large data sets, Shopware provides you with the `Shopware.wi Our goal is to extend the existing product listing with the batch process component. But before we start, you have to implement the basis to make it work. For this, you have to implement the following methods in your PHP controller: ```php -class Shopware_Controllers_Backend_SwagProduct extends Shopware_Controllers_Backend_Application +class Shopware_Controllers_Backend_SwagProductAssoc extends Shopware_Controllers_Backend_Application { - protected $model = 'Shopware\CustomModels\Product\Product'; + protected $model = Product::class; protected $alias = 'product'; ... @@ -106,14 +106,14 @@ Both methods are quite simple and should only show you a simple implementation o In ExtJS, you need to add a new toolbar button to the listing window. This button will trigger the appropriate batch process. Put the following code in `Views/backend/swag_product/view/list/product.js`: ```php -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductAssoc.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', configure: function() { return { - detailWindow: 'Shopware.apps.SwagProduct.view.detail.Window' + detailWindow: 'Shopware.apps.SwagProductAssoc.view.detail.Window' }; }, @@ -155,7 +155,7 @@ First, you have to know how the `Shopware.window.Progress` works. Basically, th Now, to implement the progress window in the product listing window, you have to modify your main controller like this: ```php -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductAssoc.controller.Main', { extend: 'Enlight.app.Controller', init: function() { @@ -225,7 +225,7 @@ tasks: [{ Now you have to implement the actual task logic in the main controller: ```php -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductAssoc.controller.Main', { extend: 'Enlight.app.Controller', init: function() { @@ -244,7 +244,7 @@ Ext.define('Shopware.apps.SwagProduct.controller.Main', { onDeactivateProducts: function (task, record, callback) { Ext.Ajax.request({ - url: '{url controller=SwagProduct action=deactivateProducts}', + url: '{url controller=SwagProductAssoc action=deactivateProducts}', method: 'POST', params: { productId: record.get('id') @@ -285,7 +285,7 @@ Different from the previous components, the `Shopware.window.Progress` component To illustrate this, the example below now sends an additional AJAX request, which should set the creation date of the product to the current day. Modify your main controller to match the following changes: ```php -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductAssoc.controller.Main', { extend: 'Enlight.app.Controller', init: function() { @@ -305,7 +305,7 @@ Ext.define('Shopware.apps.SwagProduct.controller.Main', { onChangeCreateDate: function (task, record, callback) { Ext.Ajax.request({ - url: '{url controller=SwagProduct action=changeCreateDate}', + url: '{url controller=SwagProductAssoc action=changeCreateDate}', method: 'POST', params: { productId: record.get('id') diff --git a/source/developers-guide/backend-components/detail/index.md b/source/developers-guide/backend-components/detail/index.md index 16b6bb72fe..46864b3fb9 100644 --- a/source/developers-guide/backend-components/detail/index.md +++ b/source/developers-guide/backend-components/detail/index.md @@ -17,9 +17,9 @@ This tutorial is part of a series that covers the Shopware Backend Components. I We will take the plugin result from the last tutorial as basis for this tutorial. If you don't have it already, you can download this plugin here: [SwagProductListing.zip](/exampleplugins/SwagProductListing.zip) -The `Shopware.window.Detail` for the detail window was implemented in `Views/backend/swag_product/view/detail/window.js`. +The `Shopware.window.Detail` for the detail window was implemented in `Resources/views/backend/swag_product_detail/view/detail/window.js`. -The `Shopware.model.Container` for more detailed configuration of the detail window in `Views/backend/swag_product/view/list/window.js`. +The `Shopware.model.Container` for more detailed configuration of the detail window in `Resources/views/backend/swag_product_detail/view/list/window.js`.
    @@ -27,12 +27,12 @@ The `Shopware.model.Container` for more detailed configuration of the detail win The `Shopware.window.Detail` component, hereinafter referred to as detail window, is the entry to manage a record in detail and is defined as `detailWindow` in the `Shopware.grid.Panel`. The `Shopware.grid.Panel` calls the detail window with a single record. This is the only requirement for this component to work. The detail window will then create the view based on the `Shopware.data.Model`. That's why we create a relation in the `configure()` method right inside of the `Shopware.data.Model`. ```javascript -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductDetail.model.Product', { extend: 'Shopware.data.Model', configure: function() { return { - controller: 'SwagProduct', - detail: 'Shopware.apps.SwagProduct.view.detail.Product' + controller: 'SwagProductDetail', + detail: 'Shopware.apps.SwagProductDetail.view.detail.Product' }; }, fields: [ @@ -46,7 +46,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { The event handling of the `Shopware.window.Detail` and `Shopware.model.Container` components are managed by the `Shopware.detail.Controller`. This controller will be created and mapped by the `Shopware.window.Detail` automatically. To prevent duplicated event names, every event will be prefixed. The prefix will be determined by the class name of the given record. **Example**: -The class name of the given record is `Shopware.apps.SwagProduct.model.Product`. The event prefix will then be `product`. +The class name of the given record is `Shopware.apps.SwagProductDetail.model.Product`. The event prefix will then be `product`. All events are now prefixed like this: @@ -63,7 +63,7 @@ In this section of the tutorial, we'll focus on the `Shopware.model.Container`, The base for the form generation is the `Shopware.data.Model` instance provided in the `record` property. By default, you have to create every field manually except for the `id` property. This should result in a faster application. Furthermore, only one field set will be created by default. ```javascript -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductDetail.model.Product', { extend: 'Shopware.data.Model', configure: { ... }, fields: [ @@ -87,12 +87,12 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { Based on the field types, different default Shopware form fields will be created. Because a model can contain many more fields than those seen above, you can decide whether a field should be displayed alone or inside a field set. You can use the `fieldSets` configuration option to group them according to your needs. The `fieldSets` parameter is an array of objects, each containing a `title` and `fields`. The `title` is the title of the field set whereas the property `fields` is an object of the desired fields defined as its keys. ```javascript -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductDetail.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductDetail', fieldSets: [{ title: 'Product data', fields: { @@ -122,14 +122,14 @@ Like in the previous tutorial, the order of the field sets and fields is importa The `fields` property may not only be used for limitation - it can also be used to configure the field even more precisely. You can also specify additional configuration parameters for the filed: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductDetail.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', padding: 20, configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductDetail', fieldSets: [{ title: 'Product data', fields: { @@ -168,7 +168,7 @@ Ext.define('...view.detail.Product', { alias: 'widget.product-detail-container', configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductDetail', fieldSets: [{ title: 'Product data', fields: { @@ -213,7 +213,7 @@ Ext.define('...view.detail.Product', { alias: 'widget.product-detail-container', configure: function() { return { - controller: 'SwagProduct', + controller: 'SwagProductDetail', fieldSets: [{ title: 'Product data', fields: { @@ -256,7 +256,7 @@ In this section of this tutorial you will learn how to easily extend the `Shopwa * through override of the methods * through the ExtJS event system -The following examples will show you both ways. To use the ExtJS event system, you need your own ExtJS Controller. Here, we use our main controller in `swag_product/controller/main.js`. +The following examples will show you both ways. To use the ExtJS event system, you need your own ExtJS Controller. Here, we use our main controller in `swag_product_detail/controller/main.js`. ### Create a Toolbar Button To add a new button to the toolbar, you have to extend the toolbar elements of the `Shopware.window.Detail`. The toolbar will be created by the `createToolbar()` method. The actual elements will be created by the `createToolbarItems()` method. @@ -281,7 +281,7 @@ You can now choose to extend them by either overriding the method or using the e **Through method overriding** ```javascript -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductDetail.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', @@ -307,7 +307,7 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { **Through the event system** ```javascript -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductDetail.controller.Main', { extend: 'Enlight.app.Controller', init: function() { var me = this; @@ -335,7 +335,7 @@ Ext.define('Shopware.apps.SwagProduct.controller.Main', { ### Implement a sidebar -To implement a sidebar, you have to extend the product's `Shopware.model.Container` component in `swag_product/view/detail/product.js`. The elements of the `Shopware.model.Container` will be created by the `createItems()` method: +To implement a sidebar, you have to extend the product's `Shopware.model.Container` component in `swag_product_detail/view/detail/product.js`. The elements of the `Shopware.model.Container` will be created by the `createItems()` method: ```javascript createItems: function() { @@ -361,7 +361,7 @@ createItems: function() { }, ``` -Now, to implement the actual sidebar, you have either to override the `createItems()` method in `Views/backend/swag_product/view/detail/product.js` or subscribe to the event `product-after-create-items` in the main controller. Additionally, you should change the layout of the product container to `hbox`: +Now, to implement the actual sidebar, you have either to override the `createItems()` method in `Resources/views/backend/swag_product_detail/view/detail/product.js` or subscribe to the event `product-after-create-items` in the main controller. Additionally, you should change the layout of the product container to `hbox`: ```javascript Ext.define('...view.detail.Product', { @@ -379,7 +379,7 @@ In the following example, we've removed the `configure()` method, since you don' **Through method overriding** ```javascript -Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { +Ext.define('Shopware.apps.SwagProductDetail.view.detail.Product', { extend: 'Shopware.model.Container', alias: 'widget.product-detail-container', ... @@ -420,7 +420,7 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Product', { **Through the event system** ```javascript -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductDetail.controller.Main', { extend: 'Enlight.app.Controller', init: function() { @@ -518,7 +518,7 @@ To complete the implementation, you can override the `createTabItems()` method a **Through method overriding** ```javascript -Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { +Ext.define('Shopware.apps.SwagProductDetail.view.detail.Window', { extend: 'Shopware.window.Detail', alias: 'widget.product-detail-window', title : '{s name=title}Product details{/s}', @@ -545,7 +545,7 @@ Ext.define('Shopware.apps.SwagProduct.view.detail.Window', { **Through the event system** ```javascript -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductDetail.controller.Main', { extend: 'Enlight.app.Controller', init: function() { diff --git a/source/developers-guide/backend-components/listing-extensions/index.md b/source/developers-guide/backend-components/listing-extensions/index.md index 8842d9607c..893771ea68 100644 --- a/source/developers-guide/backend-components/listing-extensions/index.md +++ b/source/developers-guide/backend-components/listing-extensions/index.md @@ -39,10 +39,10 @@ In the following example you have to create new components which you have regist To keep this tutorial simple, you'll find the final `app.js` below. After each section, you can uncomment the appropriate component. ```php -Ext.define('Shopware.apps.SwagProduct', { +Ext.define('Shopware.apps.SwagProductListingExtension', { extend: 'Enlight.app.SubApplication', - name:'Shopware.apps.SwagProduct', + name:'Shopware.apps.SwagProductListingExtension', loadPath: '{url action=load}', bulkLoad: true, @@ -90,17 +90,17 @@ You have to make the following changes to your application: * Configuration of the info panel in the listing window * Registration of the info panel in the `app.js` -First, you have to implement the new view component. Put the following code into `Views/backend/swag_product/view/list/extensions/info.js`: +First, you have to implement the new view component. Put the following code into `Resources/views/backend/swag_product/view/list/extensions/info.js`: ```php -Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Info', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.extensions.Info', { extend: 'Shopware.listing.InfoPanel', alias: 'widget.product-listing-info-panel', width: 270, configure: function() { return { - model: 'Shopware.apps.SwagProduct.model.Product' + model: 'Shopware.apps.SwagProductListingExtension.model.Product' }; } }); @@ -111,7 +111,7 @@ The only prerequisite for the `Shopware.listing.InfoPanel` component is the conf Through this configuration, the info panel is able to create the appropriate template for every field, which will then be displayed in a `Ext.view.View`. Afterwards, you have to add the info panel extension to the listing window. For this, you have to implement the `extensions` option, containing the alias of the info panel component as `xtype`, within the `configure()` method: ```php -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 340, @@ -146,13 +146,13 @@ The `Shopware.listing.InfoPanel` extension creates a display element for each fi Inside of the `configure()` method, you can control in which way and in which order a field is displayed. Use the `fields` option like in the `Shopware.grid.Panel` or the form fields in the `Shopware.model.Container`: ```php -Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Info', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.extensions.Info', { extend: 'Shopware.listing.InfoPanel', ... configure: function() { return { - model: 'Shopware.apps.SwagProduct.model.Product', + model: 'Shopware.apps.SwagProductListingExtension.model.Product', fields: { name: null, description: null @@ -182,7 +182,7 @@ Ext.define('...view.list.extensions.Info', { ... configure: function() { return { - model: 'Shopware.apps.SwagProduct.model.Product', + model: 'Shopware.apps.SwagProductListingExtension.model.Product', fields: { name: '

    ' + 'Der Produktname lautet: ' + @@ -209,7 +209,7 @@ Ext.define('...view.list.extensions.Info', { var me = this; return { - model: 'Shopware.apps.SwagProduct.model.Product', + model: 'Shopware.apps.SwagProductListingExtension.model.Product', fields: { name: '...', description: me.createDescriptionField @@ -240,7 +240,7 @@ Ext.define('...view.list.extensions.Info', { configure: function() { return { - model: 'Shopware.apps.SwagProduct.model.Product' + model: 'Shopware.apps.SwagProductListingExtension.model.Product' }; }, @@ -269,7 +269,7 @@ Ext.define('...view.list.extensions.Info', { -You can find a more detailed list of all available `Ext.XTemplates` features in the official [ExtJS documentation](http://docs.sencha.com/extjs/4.1.3/#!/api/Ext.XTemplate). +You can find a more detailed list of all available `Ext.XTemplates` features in the official [ExtJS documentation](http://docs.sencha.com/extjs/4.1.1/#!/api/Ext.XTemplate). ## Shopware.listing.FilterPanel @@ -281,18 +281,18 @@ The implementation of the filter panel is identical to the info panel. You have * Configuration of the filter panel in the listing window * Registration of the filter panel in the `app.js` -First, you have to implement the new view component. Put the following code into `Views/backend/swag_product/view/list/extensions/filter.js`: +First, you have to implement the new view component. Put the following code into `Resources/iews/backend/swag_product/view/list/extensions/filter.js`: ```php -Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Filter', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.extensions.Filter', { extend: 'Shopware.listing.FilterPanel', alias: 'widget.product-listing-filter-panel', width: 270, configure: function() { return { - controller: 'SwagProduct', - model: 'Shopware.apps.SwagProduct.model.Product' + controller: 'SwagProductListingExtension', + model: 'Shopware.apps.SwagProductListingExtension.model.Product' }; } }); @@ -306,7 +306,7 @@ The `Shopware.listing.FilterPanel` requires the following two parameters: After you've registered the component in the `app.js`, you can enable the extension in the `extensions` option using the alias of the component as `xtype`: ```php -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 340, @@ -315,8 +315,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product', + listingGrid: 'Shopware.apps.SwagProductListingExtension.view.list.Product', + listingStore: 'Shopware.apps.SwagProductListingExtension.store.Product', extensions: [ { xtype: 'product-listing-filter-panel' } @@ -351,15 +351,15 @@ The generated fields are similar to the generated fields in the detail window. T Sometimes you don't want to filter every field of the model. Because of that, the `Shopware.listing.FilterPanel` allows you to configure and limit the displayed fields. ```php -Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Filter', { +Ext.define('Shopware.apps.SwagProductListingExtension.view.list.extensions.Filter', { extend: 'Shopware.listing.FilterPanel', alias: 'widget.product-listing-filter-panel', width: 270, configure: function() { return { - controller: 'SwagProduct', - model: 'Shopware.apps.SwagProduct.model.Product', + controller: 'SwagProductListingExtension', + model: 'Shopware.apps.SwagProductListingExtension.model.Product', fields: { name: {}, taxId: 'Steuersatz', @@ -382,6 +382,33 @@ Ext.define('Shopware.apps.SwagProduct.view.list.extensions.Filter', { The configuration of the filter fields is similar to the column of the `Shopware.grid.Panel` and form fields of the `Shopware.model.Container`. +#### Configure SQL comparison + +Since Shopware 5.3.0 it is possible to define your own SQL query +expression for comparison. This configuration option is available when +using the `Shopware.listing.FilterPanel`. An implementation for the `>=` +operator could look like this: + +```php +Ext.define('Shopware.apps.Vote.view.list.extensions.Filter', { + extend: 'Shopware.listing.FilterPanel', + alias: 'widget.vote-listing-filter-panel', + configure: function() { + return { + controller: 'Vote', + model: 'Shopware.apps.Vote.model.Vote', + fields: { + points: { + expression: '>=', + } + } + }; + } +}); +``` + +The default expression is `=`. + ## Plugin Download - [SwagProductListingExtension.zip](/exampleplugins/SwagProductListingExtension.zip)

    @@ -394,4 +421,4 @@ The configuration of the filter fields is similar to the column of the `Shopware The next tutorial will cover the batch processing of large data sets. -Proceed to [Backend Components - Batch Processes](/developers-guide/backend-components/batch-processes/) \ No newline at end of file +Proceed to [Backend Components - Batch Processes](/developers-guide/backend-components/batch-processes/) diff --git a/source/developers-guide/backend-components/listing/index.md b/source/developers-guide/backend-components/listing/index.md index d0c9a8e342..5db5a900e6 100644 --- a/source/developers-guide/backend-components/listing/index.md +++ b/source/developers-guide/backend-components/listing/index.md @@ -15,11 +15,11 @@ menu_order: 30 This tutorial is part of a series that covers the Shopware Backend Components. In the last tutorial [Backend Components - Basics](/developers-guide/backend-components/basics/) we covered the implementation of a simple product listing. In this tutorial, you'll learn the basics of the listing and get a little example of it. For this, the `Shopware.grid.Panel` and `Shopware.window.Listing` components will be explained in more detail. -We will take the plugin result from the last tutorial as basis for this tutorial. If you don't have it already, you can download this plugin here: [SwagProductBasics.zip](/exampleplugins/SwagProductBasics.zip) +We will take the plugin result from the last tutorial as basis for this tutorial. If you don't have it already, you can download this plugin here: [SwagProductBasic.zip](/exampleplugins/SwagProductBasic.zip) -The `Shopware.grid.Panel` for the listing was implemented in `Views/backend/swag_product/view/list/product.js`. +The `Shopware.grid.Panel` for the listing was implemented in `Resources/views/backend/swag_product_listing/view/list/product.js`. -The `Shopware.window.Listing` in `Views/backend/swag_product/view/list/window.js`. +The `Shopware.window.Listing` in `Resources/views/backend/swag_product_listing/view/list/window.js`.
    @@ -27,7 +27,7 @@ The `Shopware.window.Listing` in `Views/backend/swag_product/view/list/window.js The `Shopware.window.Listing` component, hereinafter referred to as listing window, does not contain many configuration options and therefore it's quickly explained. The listing window is usually used as startup window of an application and has been defined in our main controller last time. As requirements you have the `listingGrid` and `listingStore` property. Here you have to define the class names of our `Shopware.grid.Panel` and `Shopware.store.Listing`. ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Window', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Window', { extend: 'Shopware.window.Listing', alias: 'widget.product-list-window', height: 340, @@ -36,8 +36,8 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { configure: function() { return { - listingGrid: 'Shopware.apps.SwagProduct.view.list.Product', - listingStore: 'Shopware.apps.SwagProduct.store.Product' + listingGrid: 'Shopware.apps.SwagProductListing.view.list.Product', + listingStore: 'Shopware.apps.SwagProductListing.store.Product' }; } }); @@ -45,11 +45,11 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Window', { The defined listing store will be created while instantiating the listing window using the `createListingStore()` method. If you don't define any, you'll get the following error message: -
    Uncaught Shopware configuration error: Shopware.apps.SwagProduct.view.list.Window: Component requires the configured `listingStore` property in the configure() function.
    +
    Uncaught Shopware configuration error: Shopware.apps.SwagProductListing.view.list.Window: Component requires the configured `listingStore` property in the configure() function.
    The `Shopware.grid.Panel` will be created by the `createGridPanel()` method and will be added to the `items` property of the listing window. In addition, the created `Shopware.grid.Panel` will also be available in the listing window as a property called `gridPanel`. This makes it easier to access the component later on. If there is no `listingGrid` defined, you'll get the following error message: -
    Uncaught Shopware configuration error: Shopware.apps.SwagProduct.view.list.Window: Component requires the configured `listingGrid` property in the configure() function.
    +
    Uncaught Shopware configuration error: Shopware.apps.SwagProductListing.view.list.Window: Component requires the configured `listingGrid` property in the configure() function.
    Further configuration options of the `Shopware.window.Listing` will be covered in the upcoming tutorials. @@ -60,7 +60,7 @@ In this part of the tutorial we'll cover the basics of how the `Shopware.grid.Pa The `Shopware.grid.Panel` expects a `Ext.data.Store` which contains `Ext.data.Model` instances. The model will be the basis for the generation of the column. By default, you should create every field manually except for the `id` property. This should result in a faster application: ```javascript -Ext.define('Shopware.apps.SwagProduct.model.Product', { +Ext.define('Shopware.apps.SwagProductListing.model.Product', { fields: [ { name : 'id', type: 'int', useNull: true }, { name : 'name', type: 'string' }, @@ -82,7 +82,7 @@ Ext.define('Shopware.apps.SwagProduct.model.Product', { Based on the field types of your model, different default Shopware columns will be created. Because a model can contain many more fields than those seen above, you can decide whether a certain field should be displayed or not. To do so, you can set the property `columns` within the `configure()` method. As soon as the property `columns` is defined, only the provided fields will be created and displayed. The `columns` property is defined as an object based on a key/value schema where the key represents the field name like `name`: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -103,7 +103,7 @@ The `columns` property may not only be used for limitation - it can also be used The first configuration option is the sorting of the shown columns. The `Shopware.grid.Panel` creates the columns in the order of their declaration. Second, you can define field specific configurations, like a renderer method or translations, by providing an object like seen below: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -124,7 +124,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { But this is not the only way to configure a column. You can also provide a function which returns the column: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -148,7 +148,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { In the configuration above, the `header` property of the column `name` has been modified to change the name of the column header. There is also a shorthand for that. Instead of providing an object, you can provide a string which will be the column header. You can even use that shorthand for translations: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -166,7 +166,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { The `Shopware.grid.Panel` also has features like a toolbar and their children elements. This feature can be activated or deactivated. Every `Shopware.grid.Panel` feature has its own activation parameter: ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -182,7 +182,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { #### Example: Disable add button and hide search field ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -199,7 +199,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { #### Example: Hide action column ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -215,7 +215,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { #### Example: Hide delete column ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', configure: function() { return { @@ -253,7 +253,7 @@ You can extend the `Shopware.grid.Panel` with either one of the following ways: * through override of the methods * through the ExtJS event system -The following examples will show you both ways. To use the ExtJS event system, you need your own ExtJS Controller. Here, we use our main controller, `swag_product/controller/main.js`. +The following examples will show you both ways. To use the ExtJS event system, you need your own ExtJS Controller. Here, we use our main controller, `swag_product_listing/controller/main.js`. ### Add custom action column The action column of the `Shopware.grid.Panel` will be created by the `createActionColumn()` method. The actual elements of the action column will be created by the `createActionColumnItems()` method: @@ -277,12 +277,12 @@ createActionColumnItems: function () { }, ``` -To add a new column, you can easily just override the method of the `Shopware.apps.SwagProduct.view.list.Product` component or subscribe to the `product-after-create-action-column-items` event in the main controller: +To add a new column, you can easily just override the method of the `Shopware.apps.SwagProductListing.view.list.Product` component or subscribe to the `product-after-create-action-column-items` event in the main controller: **Through method overriding** ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', @@ -306,7 +306,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { **Through the event system** ```php -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductListing.controller.Main', { extend: 'Enlight.app.Controller', init: function() { var me = this; @@ -368,7 +368,7 @@ The event system example will use the `product-before-create-right-toolbar-items **Through method overriding** ```php -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', @@ -396,7 +396,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { **Through the event system** ```php -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductListing.controller.Main', { extend: 'Enlight.app.Controller', init: function() { var me = this; @@ -477,7 +477,7 @@ To implement this, you have to either override the `createColumns()` method or u **Through method overriding** ```javascript -Ext.define('Shopware.apps.SwagProduct.view.list.Product', { +Ext.define('Shopware.apps.SwagProductListing.view.list.Product', { extend: 'Shopware.grid.Panel', alias: 'widget.product-listing-grid', region: 'center', @@ -512,7 +512,7 @@ Ext.define('Shopware.apps.SwagProduct.view.list.Product', { **Through the event system** ```javascript -Ext.define('Shopware.apps.SwagProduct.controller.Main', { +Ext.define('Shopware.apps.SwagProductListing.controller.Main', { extend: 'Enlight.app.Controller', init: function() { var me = this; diff --git a/source/developers-guide/backend-extension/img/numberfield.png b/source/developers-guide/backend-extension/img/numberfield.png deleted file mode 100644 index 0c39972395..0000000000 Binary files a/source/developers-guide/backend-extension/img/numberfield.png and /dev/null differ diff --git a/source/developers-guide/backend-extension/img/replace.png b/source/developers-guide/backend-extension/img/replace.png new file mode 100644 index 0000000000..84af7c7536 Binary files /dev/null and b/source/developers-guide/backend-extension/img/replace.png differ diff --git a/source/developers-guide/backend-extension/index.md b/source/developers-guide/backend-extension/index.md index 71ecb0bbb8..8068288531 100644 --- a/source/developers-guide/backend-extension/index.md +++ b/source/developers-guide/backend-extension/index.md @@ -51,11 +51,13 @@ names are in CamelCase, backend paths are in snake_case. ### Bootstrapping When clicking the menu item, Shopware will trigger the following request to the configured controller's `index` action: -```http://localhost/training/backend/Customer?file=app``` +```text +http://localhost/training/backend/Customer?file=app +``` The controller will then respond with the content of the `app.js` file from the path `backend/customer/app.js`: -``` +```js //{block name="backend/customer/application"} Ext.define('Shopware.apps.Customer', { name:'Shopware.apps.Customer', @@ -95,8 +97,8 @@ Ext.define('Shopware.apps.Customer', { //{/block} ``` -This `app.js` is basically a definition of dependencies, as you can see in the `view`, `controllers`, `stores` and `models` -array. The `app.js` is then executed by the JavaScript framework and will lazy load the dependencies. +This `app.js` is basically a definition of dependencies, as you can see in the `views`, `controllers`, `stores` and `models` +arrays. The `app.js` is then executed by the JavaScript framework and will lazy load the dependencies. ### Loading dependencies This is done by performing another Ajax request to the configured controller. As Shopware uses some minification here, usually @@ -122,40 +124,76 @@ you will need to insert your JavaScript in the `index` action (see description a components (e.g. a grid in the customer module), you will need to insert your overwrite in the `load` action - as this is where all the components are loaded. - - ### Example #1: Simple extension + + In this example, an extension for Shopware's default customer module is implemented. First of all, an existing free -text field should be changed to only allow entering numbers. +text field should be changed ComboBox with preset titles. In order to do so, we need to know where the original text field is created. This happens in -`themes/Backend/ExtJs/backend/customer/view/detail/billing.js` in the method `createBillingFormRight`. This method -will return all fields for the right side of the billing info overview. In order to modify this method, we subscribe to -the `PostDispatchSecure` event of the customer controller: - +`themes/Backend/ExtJs/backend/customer/view/detail/window.js` in the method `createPersonalFieldSet`. This method +will return all fields for the right side of the billing info overview as a FieldSet. In order to modify this method, we subscribe to +the `PostDispatchSecure` event of the customer controller in the service.xml: + +```xml + + + + + %swag_extend_customer.plugin_dir% + + + + ``` -public function install() -{ - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_Customer', - 'onCustomerPostDispatch' - ); - return true; -} +```php +getSubject(); - $view = $controller->View(); - $request = $controller->Request(); + /** + * @var string + */ + private $pluginDirectory; + + /** + * @param $pluginDirectory + */ + public function __construct($pluginDirectory) + { + $this->pluginDirectory = $pluginDirectory; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Customer' => 'onCustomerPostDispatch' + ]; + } - $view->addTemplateDir(__DIR__ . '/Views'); + public function onCustomerPostDispatch(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $controller */ + $controller = $args->getSubject(); - if ($request->getActionName() == 'load') { - $view->extendsTemplate('backend/swag_extend_customer/view/detail/billing.js'); + $view = $controller->View(); + $request = $controller->Request(); + + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); + + if ($request->getActionName() == 'load') { + $view->extendsTemplate('backend/swag_extend_customer/view/detail/window.js'); + } } } ``` @@ -163,23 +201,55 @@ public function onCustomerPostDispatch(Enlight_Event_EventArgs $args) So in the `load` action, where all the components are loaded, we inject our modification template. In this example, this file looks like this: -``` -//{block name="backend/customer/view/detail/billing"} +```js +//{block name="backend/customer/view/detail/window"} // {$smarty.block.parent} -Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Billing', { - override:'Shopware.apps.Customer.view.detail.Billing', +Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Window', { + override: 'Shopware.apps.Customer.view.detail.Window', /** - * This extjs override will call the original method first - * and then change the xtype of the 3rd field + * Replace the textBox for field "title" with a comboBox + * + * @returns { Ext.form.FieldSet } */ - createBillingFormRight: function() { + createPersonalFieldSet: function() { var me = this, - result = me.callParent(arguments); + fieldSet = me.callParent(arguments), + titelField = fieldSet.down('[name="title"]'), + titleContainer = titelField.up('container'); - result[2].xtype = 'numberfield'; + titelField.destroy(); - return result; + titleContainer.insert(0, Ext.create('Ext.form.field.ComboBox', { + labelWidth: 155, + anchor: '95%', + name: 'title', + displayField: 'name', + valueField: 'name', + store: me.createTitleStore(), + fieldLabel: 'Title' + })); + + return fieldSet; + }, + + /** + * @returns { Ext.data.Store } + */ + createTitleStore: function() { + return Ext.create('Ext.data.Store', { + fields: [ + { name: 'name' } + ], + data: [ + { name: 'Sir' }, + { name: 'Madame' }, + { name: 'Lord' }, + { name: 'Dr.' }, + { name: 'Prof.' }, + { name: 'Prof. Dr.' } + ] + }) } }); //{/block} @@ -187,72 +257,107 @@ Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Billing', { As you can see, the block of the original customer module is extended using Smarty: -``` -{block name="backend/customer/view/detail/billing"} +```smarty + {block name="backend/customer/view/detail/window"} {$smarty.block.parent} ``` -Then a ExtJS class is defined. The class name `Shopware.apps.SwagExtendCustomer.view.detail.Billing` originates +Then a ExtJS class is defined. The class name `Shopware.apps.SwagExtendCustomer.view.detail.Window` originates from the path of that template file. In the next line ExtJS is instructed to override the original customer billing class: -``` -override:'Shopware.apps.Customer.view.detail.Billing' +```js +override:'Shopware.apps.Customer.view.detail.Window' ``` -Now the method `createBillingFormRight` can just be re-implemented. Calling `me.callParent(arguments)` will call the -original (overridden) method and return its result - in our case the array of fields we want to modify. This array can -be modified as needed - in this example the xtype of the 3rd field is changed to `numberfield`: `result[2].xtype = 'numberfield';`. +Now the method `createPersonalFieldSet` can just be re-implemented. Calling `me.callParent(arguments)` will call the +original (overridden) method and return its result - in this case we get a Ext.form.FieldSet as result and modify the items of the FieldSet -At the end the modified array is returned - this way our extension is rendered into the window instead of the original +At the end the modified FieldSet is returned - this way our extension is rendered into the window instead of the original one. - +### Example #2: Custom components -### Example #2: Custom components + In some cases, you also want to add whole new components to a backend module. This example might look similar to the first one, but there are subtle changes. In this example, a new tab is introduced to the customer module, which will just contain a simple "hello world" message - but you could easily extend it to show custom data. -The `Bootstrap` looks similar, it subscribes to `Enlight_Controller_Action_PostDispatchSecure_Backend_Customer` and +The `Subscriber` looks similar, it subscribes to `Enlight_Controller_Action_PostDispatchSecure_Backend_Customer` and registers a callback method. In the callback, there is a distinction between `index` and `load` actions. The `index` action is the right place to introduce the new component, so we know that it's available before all other components are loaded. The `load` action - just as in the first example - is used to modify the original module, so that the new tab is actually displayed: +```xml + + + + + %swag_extend_customer.plugin_dir% + + + + ``` -public function install() -{ - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_Customer', - 'onCustomerPostDispatch' - ); - return true; -} +```php +getSubject(); - $view = $controller->View(); - $request = $controller->Request(); +namespace SwagExtendCustomer\Subscriber; - $view->addTemplateDir(__DIR__ . '/Views'); +use Enlight\Event\SubscriberInterface; - if ($request->getActionName() == 'index') { - $view->extendsTemplate('backend/swag_extend_customer/app.js'); +class ExtendCustomer implements SubscriberInterface +{ + /** + * @var string + */ + private $pluginDirectory; + + /** + * @param $pluginDirectory + */ + public function __construct($pluginDirectory) + { + $this->pluginDirectory = $pluginDirectory; } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Customer' => 'onCustomerPostDispatch' + ]; + } + + public function onCustomerPostDispatch(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $controller */ + $controller = $args->getSubject(); - if ($request->getActionName() == 'load') { - $view->extendsTemplate('backend/swag_extend_customer/view/detail/window.js'); + $view = $controller->View(); + $request = $controller->Request(); + + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); + + if ($request->getActionName() == 'index') { + $view->extendsTemplate('backend/swag_extend_customer/app.js'); + } + + if ($request->getActionName() == 'load') { + $view->extendsTemplate('backend/swag_extend_customer/view/detail/window.js'); + } } } ``` As the template file `backend/swag_extend_customer/app.js` just needs to include the new component, its rather simple: -``` +```js //{block name="backend/customer/application"} // {$smarty.block.parent} // {include file="backend/swag_extend_customer/view/detail/my_own_tab.js"} @@ -261,7 +366,7 @@ As the template file `backend/swag_extend_customer/app.js` just needs to include It appends the original `app.js` file of the customer module and includes the new tab component `backend/swag_extend_customer/view/detail/my_own_tab.js`: -``` +```js Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.MyOwnTab', { extend: 'Ext.container.Container', padding: 10, @@ -283,7 +388,7 @@ Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.MyOwnTab', { In this case, the component inherits from `Ext.container.Container` - a container which organizes other elements. Here we only have a simple label: -``` +```js me.items = [{ xtype: 'label', html: '

    Hello world

    ' @@ -292,7 +397,7 @@ me.items = [{ In order to bring the new tab into play, the `Bootstrap` described above also extends the `load` action: -``` +```js if ($request->getActionName() == 'load') { $view->extendsTemplate('backend/swag_extend_customer/view/detail/window.js'); } @@ -303,10 +408,14 @@ The mechanism is the same as in the first example: in this case the `getTabs` me Also, instead of modifying an existing array element, a new one is pushed - which makes use of the new tab component that we introduced before: -``` +```js //{block name="backend/customer/view/detail/window"} // {$smarty.block.parent} Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Window', { + /** + * Override the customer detail window + * @string + */ override: 'Shopware.apps.Customer.view.detail.Window', getTabs: function() { @@ -321,5 +430,39 @@ Ext.define('Shopware.apps.SwagExtendCustomer.view.detail.Window', { //{/block} ``` +#### Add custom ExtJS controller + +To register a custom controller in an existing backend ExtJS module, first you need to add the file to the app.js: + +```js +//{block name="backend/customer/application"} +// {$smarty.block.parent} +// {include file="backend/swag_extend_customer/controller/my_own_controller.js"} +// {include file="backend/swag_extend_customer/view/detail/my_own_tab.js"} +//{/block} +``` + +After adding it to the app.js file, the file will be loaded, but the controller will not be instanced. + +To instantiate your controller, you need to override the existing controller (depends on the module you want to extend). In this file you can override existing functions of the parent controller. You can and mostly need to call the parent function with the line `me.callParent(arguments)`: + +```js +Ext.define('Shopware.apps.SwagExtendCustomer.controller.MyOwnController', { + + /** + * Override the customer main controller + * @string + */ + override: 'Shopware.apps.Customer.controller.Main', + + init: function () { + var me = this; + + // me.callParent will execute the init function of the overridden controller + me.callParent(arguments); + } +}); +``` + ## Download The full example can be [downloaded here](/exampleplugins/SwagExtendCustomer.zip). diff --git a/source/developers-guide/backend-statistics-extension/index.md b/source/developers-guide/backend-statistics-extension/index.md new file mode 100644 index 0000000000..d1341396a7 --- /dev/null +++ b/source/developers-guide/backend-statistics-extension/index.md @@ -0,0 +1,309 @@ +--- +layout: default +title: Extension of the statistics module +github_link: developers-guide/backend-statistics-extension/index.md +tags: + - extjs + - plugin + - extend + - override + - backend +indexed: true +group: Developer Guides +subgroup: Backend and ExtJS +menu_title: Statistics extension +menu_order: 75 +--- + +This tutorial shows the extension of the statistics module with own statistics. + +
    + +## PHP implementation + +### Plugin base class + +SwagCustomStatistics/SwagCustomStatistics.php + +```php + 'onPostDispatchBackendAnalytics', + ]; + } + + /** + * @param ActionEventArgs $args + */ + public function onPostDispatchBackendAnalytics(ActionEventArgs $args) + { + $request = $args->getRequest(); + $view = $args->getSubject()->View(); + + $view->addTemplateDir($this->getPath() . '/Resources/views/'); + + if ($request->getActionName() === 'index') { + $view->extendsTemplate('backend/analytics/swag_custom_statistics/app.js'); + } + + if ($request->getActionName() === 'load') { + $view->extendsTemplate('backend/analytics/swag_custom_statistics/store/navigation.js'); + } + } +} + +``` + +Subscribe to the event which is fired, when the backend analytics controller is called. +In the callback method, add your plugin template directory. +Load your custom ExtJs files by using the different actions. +Have a look at [this article](/developers-guide/backend-extension/#extending), to learn more about that. + +### Backend controller + +SwagCustomStatistics/Controllers/Backend/SwagCustomStatistics.php + +```php +container->get('dbal_connection'); + $query = $connection->createQueryBuilder(); + $query->select(['COUNT(codes.cashed) as amount', 'vouchers.description as name']) + ->from('s_emarketing_voucher_codes', 'codes') + ->innerJoin('codes', 's_emarketing_vouchers', 'vouchers', 'vouchers.id = codes.voucherID') + ->where('codes.cashed = 1') + ->groupBy('vouchers.id'); + + $idList = (string) $this->Request()->getParam('selectedShops'); + if (!empty($idList)) { + $selectedShopIds = explode(',', $idList); + + foreach ($selectedShopIds as $shopId) { + $query->addSelect('SUM(IF(vouchers.subshopID = ' . $connection->quote($shopId) . ', codes.cashed, 0)) as amount' . $shopId); + } + } + + $data = $query->execute()->fetchAll(); + + $this->View()->assign([ + 'success' => true, + 'data' => $data, + 'count' => count($data) + ]); + } +} +``` + +Select the used individual vouchers in the `getVoucherStatisticsAction`. +As it is possible to select different shops in the analytics module, consider the selected shops from the request. They are separated by commas. +The action is called by the [ExtJs](#custom-statistics-store) store to gather the data. + +## ExJs implementation + +### Custom statistics store + +SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation/voucher.js + +```javascript +Ext.define('Shopware.apps.Analytics.swagCustomStatistics.store.navigation.Voucher', { + extend: 'Ext.data.Store', + alias: 'widget.analytics-store-voucher', + remoteSort: true, + + fields: [ + 'amount', + 'name' + ], + + proxy: { + type: 'ajax', + + url: '{url controller=SwagCustomStatistics action=getVoucherStatistics}', + + reader: { + type: 'json', + root: 'data', + totalProperty: 'total' + } + }, + + constructor: function(config) { + var me = this; + config.fields = me.fields; + + if (config.shopStore) { + config.shopStore.each(function(shop) { + config.fields.push('amount' + shop.data.id); + }); + } + + me.callParent(arguments); + } +}); +``` + +In this store you can define the fields, which you want to access later in the grid/chart. +Also define the proxy. It defines from where the data comes. Use your [backend controller](#backend-controller) with its action for that. + +### Navigation store extension + +SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/store/navigation.js + +```javascript +// {block name="backend/analytics/store/navigation/items"} +// {$smarty.block.parent} +{ + id: 'voucher', + text: 'Gutscheine', + store: 'analytics-store-voucher', + iconCls: 'sprite-ticket', + comparable: true, + leaf: true, + multiShop: true +}, +// {/block} +``` + +Extends the navigation store of the statistics module. +This is not only used for displaying a new menu item, but also for the linking to the right grid/chart via the ID. + +### Custom statistics grid + +SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/table/voucher.js + +```javascript +Ext.define('Shopware.apps.Analytics.swagCustomStatistics.view.table.Voucher', { + extend: 'Shopware.apps.Analytics.view.main.Table', + alias: 'widget.analytics-table-voucher', + + initComponent: function() { + var me = this; + + me.columns = { + items: me.getColumns(), + defaults: { + flex: 1, + sortable: false + } + }; + + me.initStoreIndices('amount', 'Anzahl: [0]'); + + me.callParent(arguments); + }, + + getColumns: function() { + return [ + { + dataIndex: 'name', + text: 'Name' + }, + { + dataIndex: 'amount', + text: 'Anzahl' + } + ]; + } +}); +``` + +This grid shows us our data in a table. +As we extend from a ready-made Shopware component, we don't have to write anything on our own. +Just configure the columns and the data fields defined in the [store](#custom-statistics-store). +To set the linking correct, mind the right alias `widget.analytics-table-`. +The last part is the ID of the menu item you defined [here](#navigation-store-extension) + +### Custom statistics chart + +SwagCustomStatistics/Resources/views/backend/analytics/swag_custom_statistics/view/chart/voucher.js + +```javascript +Ext.define('Shopware.apps.Analytics.swagCustomStatistics.view.chart.Voucher', { + extend: 'Shopware.apps.Analytics.view.main.Chart', + alias: 'widget.analytics-chart-voucher', + animate: true, + shadows: true, + + legend: { + position: 'right' + }, + + initComponent: function() { + var me = this; + + me.series = []; + + me.axes = [ + { + type: 'Numeric', + position: 'left', + fields: me.getAxesFields('amount'), + title: 'Eingelöst', + grid: true, + minimum: 0 + }, + { + type: 'Category', + position: 'bottom', + fields: ['name'], + title: 'Gutscheine' + } + ]; + + this.series = [ + { + type: 'column', + axis: 'left', + gutter: 80, + xField: 'name', + yField: me.getAxesFields('amount'), + title: me.getAxesTitles('{s name=chart/country/sum}Total sales{/s}'), + stacked: true, + label: { + display: 'insideEnd', + field: 'amount', + orientation: 'horizontal', + 'text-anchor': 'middle' + }, + tips: { + trackMouse: true, + width: 300, + height: 60, + renderer: function(storeItem, barItem) { + var name = storeItem.get('name'), + field = barItem.yField; + + this.setTitle(name + ' : ' + storeItem.get(field)); + } + } + } + ]; + + me.callParent(arguments); + } +}); +``` + +The last part is the visualization of our data with a bar chart. +Again extend from the ready-made Shopware component. +Also check for the right alias here `widget.analytics-chart-`. + +## Download +The full example can be [downloaded here](/exampleplugins/SwagCustomStatistics.zip). diff --git a/source/developers-guide/cheat-sheet/index.md b/source/developers-guide/cheat-sheet/index.md new file mode 100644 index 0000000000..c79d82af2a --- /dev/null +++ b/source/developers-guide/cheat-sheet/index.md @@ -0,0 +1,322 @@ +--- +layout: default +title: Cheat-Sheet for developers +github_link: developers-guide/cheat-sheet/index.md +shopware_version: 5.6.0 +tags: + - cheat-sheet + - developers + - beginner +indexed: true +group: Developer Guides +subgroup: General Resources +menu_title: Cheat-Sheet +menu_order: 1 +--- + +
    + +## Introduction + +This article will give you brief information about often used functionalities, methods and good practises for developers, who are working on shopware. +
    +This list is not complete. If you miss something important, feel free to open a pull request with the GitHub link in the top. +
    + +## Templating + +Have a look here for a Smarty Cheat-Sheet + +### Disable Smarty Rendering + +```php +$this->Front()->Plugins()->ViewRenderer()->setNoRender(); +``` + +### Add json Rendering + +Useful for ajax calls +```php +$this->Front()->Plugins()->Json()->setRenderer(); +``` + +### Dynamic snippet names / namespaces +```smarty +{"Snippet Content"|snippet:$dynamicName:$dynamicNamespace} +``` + +## Events and hooks + +Use your main plugin class or a subscriber class which implements the `\Enlight\Event\SubscriberInterface` to register new events or hooks + +### Events + +```php +public static function getSubscribedEvents() +{ + return [ + 'EVENT_NAME1' => 'EVENT_LISTENER1', + 'EVENT_NAME2' => ['EVENT_LISTENER2', POSITION], + 'EVENT_NAME3' => [ + ['EVENT_LISTENER3_0'], + ['EVENT_LISTENER3_1', POSITION], + ], + ]; +} +``` +The `POSITION` defines the execution order of the event listener methods. The default value is `0`. The higher the number, the later the event listener method is executed. For example a position like `-10` will be executed quite early. On the contrary a position like `10` quite late. + +### Hooks + +types: before / after / replace +```php +public static function getSubscribedEvents() +{ + return [ + 'CLASS::FUNCTION::TYPE' => 'LISTENER', + ]; +} +``` + +## Attributes + +### Creating a new attribute / update an existing attribute + +```php +$attributeCrudService = $this->container->get('shopware_attribute.crud_service'); +$attributeCrudService->update( + 's_articles_attributes', + 'swag_test_attribute', + \Shopware\Bundle\AttributeBundle\Service\TypeMapping::TYPE_STRING, + [ + 'displayInBackend' => true, + 'position' => 10, + 'custom' => true, + 'defaultValue' => 'test' + ] + +); +``` + +### Naming - Attribute generation + +If you get the `Error: Unrecognized field: my_field` when querying for your attribute it could be that you forgot to generate the attribute models: +```php +$this->container()->get('models')->generateAttributeModels(); +``` +or ran into naming issues: +* the field name gets __lower cased__ before added to the database +* when using underscores(`_`) in field names they must be queried in __camel case__ + +### Delete existing attribute + +```php +$attributeCrudService = $this->container->get('shopware_attribute.crud_service'); +$attributeCrudService->delete('s_articles_attributes', 'swag_test_attribute'); +``` + +### Translate label, help text, support text + +create `SwagTest\Resources\snippets\backend\attribute_columns.ini` +``` +[en_GB] +s_articles_attributes_swag_test_attribute_label = "English label" +s_articles_attributes_swag_test_attribute_supportText = "English support text" +s_articles_attributes_swag_test_attribute_helpText = "English help text" + +[de_DE] +s_articles_attributes_swag_test_attribute_label = "Deutsches Label" +s_articles_attributes_swag_test_attribute_supportText = "Deutscher Supporttext" +s_articles_attributes_swag_test_attribute_helpText = "Deutscher Hilfetext" +``` + +### Load attributes when they are missing in the template + +Sometimes the attributes from an Entity are missing in the template on certain actions. To load them, just register a `PostDispatch` event on the controller or module in question to add your custom logic. You can then extract the id of the entity you are interested in, load it's attributes and assign them to the view. + +Say you want to load the attributes of an order on the account's order page: + +```php + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Account' => 'onFrontendPostDispatchAccount' + ]; + } + + public function onFrontendPostDispatchAccount(\Enlight_Controller_ActionEventArgs $args) + { + // We only need to load the attributes if we're on the 'order' page + if ($args->getRequest()->getActionName() !== 'orders') { + return; + } + + // Retrieve the controller-object from the event arguments to access the view parameters + $controller = $args->get('subject'); + $view = $controller->View(); + + // Fetch the order information from the template + $orders = $view->getAssign('sOpenOrders') + + // This service allows easy loading of attributes + $service = $this->container->get('shopware_attribute.data_loader'); + + $attributes = []; + foreach ($orders as $order) { + // We use the service to load the attributes of each order by the order's id from the table 's_order_attributes' and store it in an array + $attributes[$order['id']] = $service->load('s_order_attributes', $order['id']); + } + + $view->assign('order_attributes', $attributes); + } +} +``` + +## Plugin configuration + +see [this article](/developers-guide/plugin-system/#plugin-configuration-/-forms) + +## DI container configuration + +see [this article](/developers-guide/plugin-system/#container-configuration) + +## Create menu item + +see [this article](/developers-guide/plugin-system/#backend-menu-items) + +## Database queries + +### Select with queryBuilder + +```php +$queryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); +$queryBuilder->select('*') + ->from('s_articles') + ->where('active = :active') + ->setParameter('active', true); + +$data = $queryBuilder->execute()->fetchAll(); +``` + +with fetch mode +```php +$queryBuilder = $this->container->get('dbal_connection')->createQueryBuilder(); +$queryBuilder->select('variants.ordernumber') + ->from('s_articles_details', 'variants') + ->where('variants.kind = :kind') + ->setParameter('kind', 1); + +$data = $queryBuilder->execute()->fetchAll(\PDO::FETCH_COLUMN); +``` + +### Select with plain SQL + +```php +$connection = $this->container->get('dbal_connection'); +$sql = 'SELECT * FROM s_articles WHERE active = :active'; +$data = $connection->fetchAll($sql, [':active' => true]); +``` + +## Doctrine + +### Custom model + +create `SwagTest\Models\TestCustomModel` +```php +id; + } + + /** + * @param string $testName + */ + public function setName($testName) + { + $this->testName = $testName; + } + + /** + * @return string + */ + public function getName() + { + return $this->testName; + } +} +``` + +### Create database table from model + +Make sure that you use the same database collation as Shopware in your custom model, if you have relations to Shopware default tables +```php +$em = $this->container->get('models'); +$tool = new \Doctrine\ORM\Tools\SchemaTool($em); +$classes = [$em->getClassMetadata(\SwagTest\Models\TestCustomModel::class)]; +$tool->createSchema($classes); +``` + +### Delete database table + +```php +$em = $this->container->get('models'); +$tool = new \Doctrine\ORM\Tools\SchemaTool($em); +$classes = [$em->getClassMetadata(\SwagTest\Models\TestCustomModel::class)]; +$tool->dropSchema($classes); +``` + +### QueryBuilder + +Select some data +```php +$builder = $this->container->get('models')->createQueryBuilder(); +$builder->select(['product', 'mainVariant']) + ->from(\Shopware\Models\Article\Article::class, 'product') + ->innerJoin('product.mainDetail', 'mainVariant') + ->where('product.id = :productId') + ->setParameter('productId', 2); + +// Array with \Shopware\Models\Article\Article objects +$objectData = $builder->getQuery()->getResult(); + +// Array with arrays +$arrayData = $builder->getQuery()->getArrayResult(); +``` + +## Activate completion in shell + +```sh +source <(bin/console _completion --generate-hook) +``` + +For details on implementing completion for custom commands see the [CLI commands page](/developers-guide/shopware-5-cli-commands/#add-completion-for-commands). diff --git a/source/developers-guide/coding-standards/index.md b/source/developers-guide/coding-standards/index.md index bcf9ad046a..4991123a37 100644 --- a/source/developers-guide/coding-standards/index.md +++ b/source/developers-guide/coding-standards/index.md @@ -5,7 +5,8 @@ github_link: developers-guide/coding-standards/index.md indexed: true menu_title: Coding Standards menu_order: 30 -group: Contributing +group: Developer Guides +subgroup: General Resources --- The shopware team uses the following coding standards. All contributions to Shopware as well as plugins and customizations should also use these standards. @@ -24,9 +25,11 @@ For PHP Code all contributions should use the [PSR-1: Basic Coding Standard](htt You can automatically check and fix the coding style with [php-cs-fixer](http://cs.sensiolabs.org/): ```bash -php-cs-fixer fix -v --level=psr2 /path/to/files +./vendor/bin/php-cs-fixer fix -v ``` +The configuration for php-cs-fixer can be found in `/.php_cs.dist`. + ## JavaScript Coding Standard Please see our dedicated page for the [JavaScript Coding Standard](/designers-guide/javascript-coding-style/). diff --git a/source/developers-guide/concept-cart-bundle/img/img.001.png b/source/developers-guide/concept-cart-bundle/img/img.001.png deleted file mode 100644 index 0dac698c12..0000000000 Binary files a/source/developers-guide/concept-cart-bundle/img/img.001.png and /dev/null differ diff --git a/source/developers-guide/concept-cart-bundle/img/img.002.png b/source/developers-guide/concept-cart-bundle/img/img.002.png deleted file mode 100644 index bf913abcab..0000000000 Binary files a/source/developers-guide/concept-cart-bundle/img/img.002.png and /dev/null differ diff --git a/source/developers-guide/concept-cart-bundle/index.md b/source/developers-guide/concept-cart-bundle/index.md deleted file mode 100644 index 468bc193fd..0000000000 --- a/source/developers-guide/concept-cart-bundle/index.md +++ /dev/null @@ -1,424 +0,0 @@ ---- -layout: default -title: Cart Concept -github_link: developers-guide/concept-cart-bundle/index.md -shopware_version: X -indexed: true -tags: - - cart - - refactoring -group: Developer Guides -subgroup: General Resources -menu_title: Cart Concept -menu_order: 400 ---- - -## 2016/12/09 - First concept -Today we released a first concept for a new cart bundle. You can see the development process on Github, where we created a new repository which allows the community to create pull requests and issues. The new repository contains a new bundle in `/engine/Shopware/Bundle/CartBundle` which contains a first proof of concept for a new cart process. -This article documents the current implementation and how it can be used. At the moment, the cart bundle isn't implemented in Shopware's frontend, rather it is only used for testing different calculations and processes. - -## Usage - -### Add an line item -Lets start with a simple example: *Add a product to cart* - -``` -public function addProductAction() -{ - /** @var CartBundle\Infrastructure\StoreFrontCartService $cartService */ - $cartService = $this->container->get('shopware_cart.store_front_cart_service'); - - $cartService->add( - new CartBundle\Domain\LineItem\LineItem( - 'SW10239', //number - CartBundle\Domain\Product\ProductProcessor::TYPE_PRODUCT, - 1 // quantity - ) - ); -} -``` - -### Remove a line item -Next we remove this item again using the cart identifier (see above `SW10239`) -``` -public function removeAction() -{ - /** @var CartBundle\Infrastructure\StoreFrontCartService $cartService */ - $cartService = $this->container->get('shopware_cart.store_front_cart_service'); - - $cartService->remove('SW10239'); -} -``` - -### Get line items -To get access of all line items in cart, the `StoreFrontCartService` allows access on the calculated cart over `getCalculated()`. - -``` -public function showLineItemsAction() -{ - /** @var CartBundle\Infrastructure\StoreFrontCartService $cartService */ - $cartService = $this->container->get('shopware_cart.store_front_cart_service'); - - $cartService->add( - new CartBundle\Domain\LineItem\LineItem( - $number = 'SW10239', - CartBundle\Domain\Product\ProductProcessor::TYPE_PRODUCT, - $quantity = 10 - ) - ); - $cartService->add( - new CartBundle\Domain\LineItem\LineItem( - $number = 'SW10009', - CartBundle\Domain\Product\ProductProcessor::TYPE_PRODUCT, - $quantity = 10 - ) - ); - - /** @var CartBundle\Domain\Cart\CalculatedCart $cart */ - $cart = $cartService->getCalculated(); - - /** @var CartBundle\Domain\LineItem\CalculatedLineItemInterface $lineItem */ - foreach ($cart->getLineItems() as $lineItem) { - echo "\n\n line item: " . $lineItem->getIdentifier(); - echo "\n unit price: " . $lineItem->getPrice()->getUnitPrice(); - echo "\n quantity: " . $lineItem->getPrice()->getQuantity(); - echo "\n price : " . $lineItem->getPrice()->getPrice(); - echo "\n taxes : " . $lineItem->getPrice()->getCalculatedTaxes()->getAmount(); - - /** @var CartBundle\Domain\Tax\CalculatedTax $tax */ - foreach ($lineItem->getPrice()->getCalculatedTaxes() as $tax) { - echo "\n tax " . $tax->getTaxRate() . "% : " . $tax->getTax(); - } - } -} -``` - -### Get cart amount -The cart amount is stored inside the `CalculatedCart` and can be accessed over `getPrice()`. -```php -public function showAmountAction() -{ - /** @var CartBundle\Infrastructure\StoreFrontCartService $cartService */ - $cartService = $this->container->get('shopware_cart.store_front_cart_service'); - - $cartService->add( - new CartBundle\Domain\LineItem\LineItem( - $number = 'SW10239', - CartBundle\Domain\Product\ProductProcessor::TYPE_PRODUCT, - $quantity = 10 - ) - ); - $cartService->add( - new CartBundle\Domain\LineItem\LineItem( - $number = 'SW10009', - CartBundle\Domain\Product\ProductProcessor::TYPE_PRODUCT, - $quantity = 5 - ) - ); - - /** @var CartBundle\Domain\Cart\CalculatedCart $cart */ - $cart = $cartService->getCalculated(); - - echo "\n amount: " . $cart->getPrice()->getTotalPrice(); - echo "\n amount net: " . $cart->getPrice()->getNetPrice(); - echo "\n tax amount: " . $cart->getPrice()->getCalculatedTaxes()->getAmount(); - - /** @var CartBundle\Domain\Tax\CalculatedTax $tax */ - foreach ($cart->getPrice()->getCalculatedTaxes() as $tax) { - echo "\n tax " . $tax->getTaxRate() . "% : " . $tax->getTax(); - } -} -``` - -### Get deliveries -Each cart contains a collection of deliveries, in case that the customer is logged in (requires a delivery address). -This deliveries can be accessed over `getDeliveries()`. -``` -public function showDeliveriesAction() -{ - /** @var CartBundle\Infrastructure\StoreFrontCartService $cartService */ - $cartService = $this->container->get('shopware_cart.store_front_cart_service'); - $cartService->add( - new CartBundle\Domain\LineItem\LineItem( - $number = 'SW10239', - CartBundle\Domain\Product\ProductProcessor::TYPE_PRODUCT, - $quantity = 50 - ) - ); - $cartService->add( - new CartBundle\Domain\LineItem\LineItem( - $number = 'SW10009', - CartBundle\Domain\Product\ProductProcessor::TYPE_PRODUCT, - $quantity = 25 - ) - ); - - /** @var CartBundle\Domain\Cart\CalculatedCart $cart */ - $cart = $cartService->getCalculated(); - - /** @var CartBundle\Domain\Delivery\Delivery $delivery */ - foreach ($cart->getDeliveries() as $index => $delivery) { - echo "\n\n ---------- \n delivery #" . $index; - - $price = $delivery->getPositions()->getPrices()->getTotalPrice(); - echo "\n amount of delivery: " . $price->getPrice(); - echo "\n tax amount of delivery: " . $price->getCalculatedTaxes()->getAmount(); - - echo "\n\n address: "; - echo ' ' . $delivery->getAddress()->getFirstname(); - echo ' ' . $delivery->getAddress()->getLastname(); - echo ' ' . $delivery->getAddress()->getStreet(); - echo ' ' . $delivery->getAddress()->getZipcode(); - echo ' ' . $delivery->getAddress()->getCity(); - - echo "\n delivery date: "; - echo $delivery->getDeliveryDate()->getEarliest()->format('Y-m-d'); - echo ' - '; - echo $delivery->getDeliveryDate()->getLatest()->format('Y-m-d'); - - /** @var CartBundle\Domain\Delivery\DeliveryPosition $position */ - foreach ($delivery->getPositions() as $i => $position) { - echo "\n\n position " . $i; - echo "\n quantity " . $position->getQuantity(); - echo "\n price in delivery: " . $position->getPrice()->getPrice(); - } - - } -} -``` - -## Architecture - -### Cart layers -The cart passes through different states during the calculation process. In order to provide a valid state for each service layer, the states are reflected in different classes: -* `CartBundle\Domain\Cart\Cart` - * Defines which line items have to be calculated inside the process -* `CartBundle\Domain\Cart\ProcessorCart` - * Defines which line items have already been calculated and which deliveries have been generated -* `CartBundle\Domain\Cart\CalculatedCart` - * Contains a list of all calculated line items - * Contains a collection of all generated deliveries - * Has a calculated price with total tax amounts, tax rules and net or gross prices - -### Processor concept -The following diagram shows the architecture behind the cart process for product calculation: - -![image](img/img.001.png) - -The cart calculation is done in the `CartBundle\Domain\Cart\CartCalculator` class. -This class contains a list of `CartBundle\Domain\Cart\CartProcessorInterface`, which are the access points for the Shopware core and third party developers in the cart process. -```php -interface CartProcessorInterface -{ - public function process( - CartBundle\Domain\Cart\Cart $cart, - CartBundle\Domain\Cart\ProcessorCart $processorCart, - CartContextInterface $context - ); -} - -``` - -### Domain and Infrastructure layers -For the architecture design, we took great care to separate business logic from the database and Shopware dependencies. -That means all Shopware-specific operations such as database access, delivery information or price selections (with graduated prices or price groups) are separated into individual gateways that can be replaced with other data sources. -These layers are named **Domain** and **Infrastructure** and are placed on the first level of the `CartBundle`. -The domain layer should not have any dependencies to the Shopware core. That's the infrastructure/bundle layer. -Interactions with Shopware are defined over gateways. Applying this concept to the product processor described above leads to the following architecture: - -![image](img/img.002.png) - - -## Price calculations -At the moment, the `CartBundle` contains the following calculation classes: -* `\Shopware\Bundle\CartBundle\Domain\Price\PriceCalculator` - * Calculates a total price for a provided `PriceDefinition` - * Calculates the gross/net unit price and total price - * Uses tax calculation services for including/excluding taxes - -* `\Shopware\Bundle\CartBundle\Domain\Price\PercentagePriceCalculator` - * Calculates a percentage price based on a provided collection of prices (`PriceCollection`) - * Sums all prices to a total amount and calculates a percentage price value - * Calculates the percentage share of tax rules inside the provided prices and calculates the taxes percentage - * Example: - * 100.00 € with 19% and 100.00€ with 7% - * 10% should be calculated - * 200€ (price amount) * 10% => 20.00% - * 50% of the price is based on 19% tax calculation - * 50% of the price is based on 7% tax calculation - -And the following tax calculation services: -* `\Shopware\Bundle\CartBundle\Domain\Tax\TaxRuleCalculator` - * Tax calculation is based on a price with a simple tax rate - * Example - * 100.00 € should be calculated with a 19% tax rate - -* `\Shopware\Bundle\CartBundle\Domain\Tax\PercentageTaxRuleCalculator` - * Tax calculation is based on a percentage price value - * Example: - * total price: 100.00 € - * 90.00€ should be calculated with a 19% tax rate - * 10.00€ should be calculated with a 7% tax rate - -## Extensibility concept -All services in the CartBundle defined inside the service container, which means each service can be replaced or decorated. - -### Example - Discount for new customers -The following examples shows one possible solution for creating dynamic discounts for new customers. -``` -connection = $connection; - $this->percentagePriceCalculator = $percentagePriceCalculator; - } - - /** - * Access point for cart process - * @param Cart $cart - * @param ProcessorCart $processorCart - * @param CartContextInterface $context - */ - public function process( - Cart $cart, - ProcessorCart $processorCart, - CartContextInterface $context - ) { - //no logged in state? - if (!$context->getCustomer()) { - return; - } - - //validate if customer should get discount - if (!$this->isNewCustomer($context->getCustomer()->getId())) { - return; - } - - //get access to all goods inside the cart - $goods = $processorCart->getLineItems()->filterGoods(); - - //use core calculator for percentage price calculation with all goods prices - $discount = $this->percentagePriceCalculator->calculatePrice( - -10, - $goods->getPrices(), - $context - ); - - //add calculated discount to cart - $processorCart->getLineItems()->add( - new CalculatedLineItem('new-customer-discount', $discount) - ); - } - - /** - * Validates if the provided customer id is a new customer in the system - * and should get the `new customer discount` - * - * @param int $customerId - * @return bool - */ - private function isNewCustomer($customerId) - { - $hasOrder = $this->connection->createQueryBuilder() - ->select('1') - ->from('s_order', 'orders') - ->where('orders.userID = :id') - ->andWhere('orders.ordernumber > 0') - ->setMaxResults(1) - ->setParameter(':id', $customerId) - ->execute() - ->fetch(\PDO::FETCH_COLUMN); - - return !$hasOrder; - } -} -``` -The first two conditions validate if a customer is logged in and if the customer has already made an order. -After the validation passes that the customer is a new customer, the processor first collects all calculated goods in the cart `$goods = $processorCart->getLineItems()->filterGoods();`. -To calculate the percentage discount for the `new customer discount` the processor uses the Shopware core calculator `\Shopware\Bundle\CartBundle\Domain\Price\PercentagePriceCalculator`. - -The processor has to be registered over the `cart_processor` container tag. The priority defines at which position the calculator has to be executed (after product calculation, before voucher, ...). -``` - - - - - -``` - -### Example - Blacklisted products -The following examples shows a possible solution for preventing some products from entering the cart: - -``` -getLineItems()->filterType(ProductProcessor::TYPE_PRODUCT); - - /** @var LineItem $product */ - foreach ($products as $product) { - if (in_array($product->getIdentifier(), $this->blackList)) { - $cart->getLineItems()->remove($product->getIdentifier()); - } - } - } -} -``` - -The service is registered as follow: -``` - - - -``` -Using a high priority defines an early position inside the cart calculation for this processor. The `\Shopware\Bundle\CartBundle\Domain\Product\ProductProcessor` is registered with priority 30000, which means it is executed after this blacklist processor. diff --git a/source/developers-guide/content-types/index.md b/source/developers-guide/content-types/index.md new file mode 100644 index 0000000000..8a680b1a31 --- /dev/null +++ b/source/developers-guide/content-types/index.md @@ -0,0 +1,300 @@ +--- +layout: default +title: Content Types +github_link: developers-guide/content-types/index.md +shopware_version: 5.6.0 +indexed: true +menu_title: Content-Types +menu_order: 100 +group: Developer Guides +subgroup: General Resources +--- + +Content Types allows users to create own simple entities with a crud, own api and frontend controller. + +## General + +Content Types can be created using an interface in the administration in "Settings" => "Content Types" or with a 'contenttypes.xml' in the Resources folder of your Plugin. If a Content Type is created using a plugin, it can't be modified in the administration. In this guide we will create a Content Type using a plugin. + +## Creating a Content Type + +A plugin Content Type can be created with a 'Resources/contenttypes.xml' in your plugin directory. A XML file can contain multiple Content Types. + +```xml + + + + + + +``` + +Now we add a Content Type in the `types` tag. + +A Content Type needs as a requirement a typeName (technical name, will be used for the controllers, table, etc. with the Prefix 'Custom'), a name for the Menu and Storefront if enabled. + +In this example we will create a Content Type with the technical name 'AwesomeRecipeContentType', display name 'My most favorite recipes by name' with a single field 'name' as textfield. + +```xml + + + + + AwesomeRecipeContentType + My most favorite recipes by name + +
    + + + true + +
    +
    +
    +
    +
    +``` + +After the plugin installation and activation, will be a new menu entry in the 'Content' section. +Alternatively, this command can be used to update the configuration for an installed plugin: +`bin/console sw:content:type:sync` + +### Default elements + +| Name | DBAL Type | ExtJs Type | +|---------------|-----------|--------------------------------------------| +| textarea | text | Ext.form.field.TextArea | +| text | string | Ext.form.field.Text | +| aceeditor | text | Shopware.form.field.AceEditor | +| integer | int | Ext.form.field.Number | +| tinymce | text | Shopware.form.field.TinyMCE | +| media | int | Shopware.form.field.Media | +| combobox | string | Ext.form.field.Combobox | +| checkbox | int | Ext.form.field.Checkbox | +| date | date | Ext.form.field.Date | +| media-grid | string | Shopware.form.field.MediaGrid | +| product-field | int | Shopware.form.field.ProductSingleSelection | +| product-grid | string | Shopware.form.field.ProductGrid | +| shop-field | int | Shopware.form.field.SingleSelection | +| shop-grid | string | Shopware.form.field.ShopGrid | + +### Dynamic elements + +Content types can also reference each other. For this reason we have dynamic elements. + +With '[technicalName]-field' can you create an association to a single selection of content type 'technicalName' + +With '[technicalName]-grid' can you create an association to a multi selection of content type 'technicalName' + +### Possible tags in the '<type>' tag + +| Name | Description | +|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| typeName | Technical name of the content type | +| name | Display name | +| showInFrontend | Create a frontend controller, allow usage of emotion element. Requires viewTitleFieldName, viewDescriptionFieldName, viewImageFieldName | +| menuIcon | Menu icon | +| menuPosition | Menu position | +| menuParent | Parent controller name | +| viewTitleFieldName | Fieldname for title fields in storefront | +| viewDescriptionFieldName | Fieldname for description fields in storefront | +| viewImageFieldName | Fieldname for image fields in storefront | +| seoUrlTemplate | SEO URL template for the URL generation | +| sortField | Fieldname to sort on | +| sortDirection | Sorting direction | + +### Possible tags in '<field>' +| Name | Description | +|---------------|---------------------------------------------------------------| +| name | Technical fieldname in table | +| label | Label for the user | +| type | Field type | +| helpText | Field helpText | +| description | Field description | +| translateable | Field is translatable? | +| required | Field is required? | +| options | Can be used to pass variables to extjs | +| custom | Can be used to store custom variables | +| store | Options for combobox selection | +| showListing | Show the field in the extjs listing window | +| searchAble | Field should be searchable in the extjs listing window search | + +## Access using the API + +All Content Types have an API endpoint generated automatically. It will be accessible using the route '/api/Custom[TechnicalName]' (e.g '/api/CustomAwesomeRecipeContentType') and follows the default Shopware API schema. + +In default, it reads the data in raw format, without resolving the associated data and translation. + +To resolve the associations, you can pass a GET parameter '?resolve=1'. To load the translations, can you pass a GET parameter '?loadTranslations=1' to the list and get one call. + +The data being passed to create or update an entity has to be in the raw format. Multi selection fields values have to been split by pipe (e.g '|1|2|'). + +## Usage internal in PHP + +### Getting the Content Type configuration +The Content Type configuration will be saved as struct of type 'Shopware\Bundle\ContentTypeBundle\Structs\Type'. To get the configuration of your Content Type you can use the service 'shopware.bundle.content_type.type_provider' and call the method 'getType' with your technical name. + +### Fetching the Content Type data + +Every Content Types has an own Repository which implements the interface 'Shopware\Bundle\ContentTypeBundle\Services\RepositoryInterface'. These repositories are registered dynamically in the DI with following naming scheme 'shopware.bundle.content_type.[technicalName]'. + +#### Example usages + +```php + +/** @var \Shopware\Bundle\ContentTypeBundle\Services\RepositoryInterface $repository */ +$repository = $this->container->get('shopware.bundle.content_type.store'); + +$criteria = new \Shopware\Bundle\ContentTypeBundle\Structs\Criteria(); +$criteria->limit = 5; +$criteria->loadTranslations = true; +$criteria->loadAssociations = true; +$criteria->calculateTotal = true; + +/** @var \Shopware\Bundle\ContentTypeBundle\Structs\SearchResult $result */ +$result = $repository->findAll($criteria); + +var_dump($result->total); // 5 +var_dump($result->type); // Type struct +var_dump($result->items); // Fetched data + +// Delete a record +$repository->delete($id); + +// If an id is passed in $data, it will be updated, otherwise it will create a new record +$repository->save($data); +``` + +## Displaying in Frontend + +To enable the Frontend controller, you have to set 'showInFrontend' in the 'type' to 'true' and fill the fields 'viewTitleFieldName', 'viewDescriptionFieldName', 'viewImageFieldName'. + +These view fields will be used for SEO information in the '<head>'-tag, as well as in the listing of the contents and in the emotion world. + +The controller name is generated, like in the API, with the same schema 'Custom[TechnicalName]'. +By default the controller tries to load the template in the default directory structure (`frontend/controller/action.tpl`) and if that template is missing, it will fall back to the folder `frontend/content_type/action.tpl`). + +In the default template, only fields are visible in the frontend, which implements the interface 'Shopware\Bundle\ContentTypeBundle\Field\TemplateProvidingFieldInterface' + +## Translation in Backend + +To translate the extjs field names can you create a new snippet file in namespace 'backend/custom[technicalName]/main'. + +| Snippet-Name | Description | +|-------------------------|------------------------------------------| +| name | content type name in backend | +| [fieldName]_label | field label for the frontend and backend | +| [fieldName]_helpText | field helpText for the backend | +| [fieldName]_description | field description for the backend | + +## Translation in Frontend + +To translate the extjs field names can you create a new snippet file in namespace 'frontend/custom[technicalName]/index'. + +| Snippet-Name | Description | +|----------------------|------------------------------| +| IndexMetaDescription | meta description in '<head>' | +| IndexMetaImage | meta image in '<head>' | +| IndexMetaTitle | meta title in '<head>' | + +## Creating a new Field + +To create a new field, you have to create a new class which implements the 'Shopware\Bundle\ContentTypeBundle\Field\FieldInterface'. + +Here we have an example field MediaField. + +```php + +use Doctrine\DBAL\Types\Type; +use Shopware\Bundle\ContentTypeBundle\Structs\Field; + +class MediaField implements FieldInterface +{ +public static function getDbalType(): string +{ +return Type::INTEGER; +} + +public static function getExtjsField(): string +{ +return 'shopware-media-field'; +} + +public static function getExtjsType(): string +{ +return 'int'; +} + +public static function getExtjsOptions(Field $field): array +{ +return []; +} + +public static function isMultiple(): bool +{ +return false; +} +} +``` + +Overview of the methods + +| Method | Description | +|-----------------|--------------------------------------------------------| +| getDbalType | Returns the DBAL Type for the column | +| getExtjsField | Returns the extjs xtype for the field | +| getExtjsType | Returns the extjs model type | +| getExtjsOptions | Returns an array of options for the extjs configuration | +| isMultiple | Returns that this field holds multiple values | + +After the creating of the class, we have to register our new field in the 'services.xml' with the tag 'shopware.bundle.content_type.field' and a 'fieldName'. + +```xml + + + +``` + +The 'fieldName' is the unique identifier of your new field and can be used in the 'contenttypes.xml'. This also extends the selection in the extjs interface for the creation of a Content Type. + +### ResolveableFieldInterface + +If you want to populate the data after it has been read from the database, can you implement the interface 'Shopware\Bundle\ContentTypeBundle\Field\ResolveableFieldInterface' in your field . + +This interface requires that you have to implement the method `getResolver`. In the `getResolver` method have you to return a service id of your resolver. The resolver will handle the processing of the saved data. + +```php +public static function getResolver(): string +{ +return MediaResolver::class; +} +``` + +### Resolver + +A resolver has to extend 'Shopware\Bundle\ContentTypeBundle\FieldResolver\AbstractResolver' and needs to be registered in the DI container. The abstract class requires that you implement the method 'resolve'. The Content Type's repository reads all information from the database and then adds all fields that have to be resolved in the resolver using the 'add' method, defined in the 'AbstractResolver'. After all IDs are added, it will call the 'resolve()' method where the Resolver should fetch and store the data by the added IDs. + +Here we have an example for the 'MediaField': + +```php +public function resolve(): void +{ +$medias = $this->mediaService->getList($this->resolveIds, $this->contextService->getShopContext()); + +foreach ($medias as $id => $media) { +$this->storage[$id] = $this->structConverter->convertMediaStruct($media); +} + +$this->resolveIds = []; +} +``` + +The 'resolveIds' property contains all requested IDs of the values. After fetching these, we write the data back into the 'storage' property with the keys we had in 'propertyIds'. +In the last step the Repository loads the values back from the 'get' method implemented by the 'AbstractResolver' with the values in the 'storage' property. +This concept also has a simple cache inside, if the requested ID is already in the 'storage' proeprty, it won't be added to the 'resolveIds' property. + +### TemplateProvidingFieldInterface + +With the 'TemplateProvidingFieldInterface' interface can you mark your field as frontend ready with a specific template. This template will be loaded with the default template of the detail page. diff --git a/source/developers-guide/controller/index.md b/source/developers-guide/controller/index.md index bbcb6453d6..7f451beead 100644 --- a/source/developers-guide/controller/index.md +++ b/source/developers-guide/controller/index.md @@ -34,15 +34,14 @@ For that reason, the calls "http://my-shop.com/" and "http://my-shop.com/fronten controller action. ### Module -A module is actually a namespace for a controller. Technically it does not matter where you create your controller, but -it should be quite easy to decide: +A module is actually a namespace for a controller. Controllers are automatically registered under `PluginDirectory/Controllers/{Frontend|Backend|Api|Widgets}` like below: -* `frontend`: Namespace for controllers related to the store front - e.g. cart, account, listing... -* `widgets`: Namespace for widgets, i.e. reusable, cache compatible `{action}` blocks that will render ESI tags -* `backend`: Namespace for backend controllers, usually protected by the Shopware backend authentication system -* `api`: Namespace for REST API related controllers. Usually protected by the API authentication +* `Frontend`: Namespace for controllers related to the store front - e.g. cart, account, listing... +* `Widgets`: Namespace for widgets, i.e. reusable, cache compatible `{action}` blocks that will render ESI tags +* `Backend`: Namespace for backend controllers, usually protected by the Shopware backend authentication system +* `Api`: Namespace for REST API related controllers. Usually protected by the API authentication -Each namespace can be found in `engine/Shopware/Controllers/{frontend|backend|api|widgets}`. +Each namespace can be found in `engine/Shopware/Controllers/{Frontend|Backend|Api|Widgets}`. ### Controller A controller is a specific class within one of the controller namespaces (see above). It will usually take care of one specific @@ -54,10 +53,10 @@ output (headers, cookies, session, template). Depending on the namespace of the controller, it will usually have another class name and another base class: -* `frontend`: `class Shopware_Controllers_Frontend_NAME extends Enlight_Controller_Action` -* `backend`: `class Shopware_Controllers_Backend_NAME extends Shopware_Controllers_Backend_Application` -* `widgets`: `class Shopware_Controllers_Widgets_NAME extends Enlight_Controller_Action` -* `api`: `class Shopware_Controllers_Api_NAME extends Shopware_Controllers_Api_Rest` +* `Frontend`: `class Shopware_Controllers_Frontend_NAME extends Enlight_Controller_Action` +* `Backend`: `class Shopware_Controllers_Backend_NAME extends Shopware_Controllers_Backend_Application` +* `Widgets`: `class Shopware_Controllers_Widgets_NAME extends Enlight_Controller_Action` +* `Api`: `class Shopware_Controllers_Api_NAME extends Shopware_Controllers_Api_Rest` This is not as cumbersome as it might look: `Enlight_Controller_Action` is the generic base controller, while `Shopware_Backend_Application` and `Shopware_Controllers_Api_Rest` add authentication, JSON and input parameter handling. @@ -77,14 +76,8 @@ public function indexAction() This makes it easy for you to tell apart public available endpoints from methods that should not be callable by URL. ## Plugin controllers -Creating controllers from a plugin is quite easy: Starting from Shopware 4.2.0 there is a `registerController` convenience method you can use in your `install` method: - -```php -public function install() -{ - $this->registerController('frontend', 'test'); -} -``` +Creating controllers from a plugin is quite easy: +Create the controller in the correct directory `PluginDirectory/Controllers/{Frontend|Backend|Api|Widgets}` and the controller will automatically registered. For this to work, you must put a file called `Test.php` in the path `Controllers/Frontend` of your plugin directory. Shopware will do the rest for you automatically. @@ -106,11 +99,11 @@ After (re)installing the plugin, you can call `http://my-shop.com/frontend/test/ "Hello World". ### Template -If you remove the `die("Hello World");` call, Shopware will raise an exception. By default Shopware +If you remove the `die('Hello World');` call, Shopware will raise an exception. By default Shopware will automatically try to find a template for your controller. In this case, this template should be called `frontend/test/index.tpl`. As this template does not exists, yet, an exception is raised. This is easy to fix: -Create the file `Views/frontend/test/index.tpl` in your plugin and add the following code: +Create the file `Resources/views/frontend/test/index.tpl` in your plugin and add the following code: ``` {extends file="parent:frontend/index/index.tpl"} @@ -128,11 +121,11 @@ Shopware with a "Hello World" message in the main context section. You can access the Shopware DI container from every controller: ```php -$this->get('db'); +$this->get('dbal_connection'); $this->get('templatemail'); ``` -These two lines will return an instance of the Shopware PDO object (1) and the Shopware template mailer service (2). +These two lines will return an instance of the DBAL\Connection object (1) and the Shopware template mailer service (2). #### $this->Request() The request object will give you access to all kind of request related variables as: @@ -143,7 +136,7 @@ The request object will give you access to all kind of request related variables * cookies #### $this->Response() -The response object will allow you to manipulate the response that will be generated by Shopware for this request: +The response object will allow you to manipulate the response that will be generated by Shopware for these requests: * set cookies * set HTTP status code @@ -180,32 +173,53 @@ event. The same applies to the `PostDispatchSecure` events. In the callback method of your event, you have complete access to the original controller, so that you can e.g. modify the view or some input parameters: +Add the subscriber to the services.xml +```xml + + + +``` + +Create the subscriber *.php file ```php -/** - * The install method of your plugin bootstrap - */ -public function install() -{ - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Listing_Index', - 'onListingIndex' - ); -} +getSubject(); - $request = $controller->Request(); - $view = $controller->View(); - $response = $controller->Response(); + /** + * The install method of your plugin base file + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Listing_Index' => 'onListingIndex' + ]; + } + + /** + * Event callback for the event registered above + */ + public function onListingIndex(\Enlight_Event_EventArgs $args) + { + /** @var $controller \Enlight_Controller_Action */ + $controller = $args->getSubject(); + $request = $controller->Request(); + $view = $controller->View(); + $response = $controller->Response(); + + // DO SOMETHING + } } ``` +Since Shopware 5.6 it is also possible to extend or exchange Shopware controllers over the DI container. You can find a example for decorating a service [here](https://developers.shopware.com/developers-guide/plugin-quick-start/#decorate-a-service). + + In addition to that, you can also extend `public` and `protected` methods in any controller using the Shopware hook system. ## Controllers and caching @@ -222,7 +236,7 @@ Additional in detail information about the HTTP cache can be found [here](https: ## SEO Technical controller URLs like `http://my-shop.com/frontend/test/index` are, in most cases, not what you want to have. -For that reason, Shopware supports the definition of [custom SEO URLs](http://en.community.shopware.com/SEO-guide_detail_1424.html#Using_the_.22Special_SEO_URL.22_field). +For that reason, Shopware supports the definition of [custom SEO URLs](https://developers.shopware.com/blog/2017/07/24/seo-urls-in-plugins/). ## Example You can find a simple controller plugin example here. diff --git a/source/developers-guide/cookie-consent-manager/index.md b/source/developers-guide/cookie-consent-manager/index.md new file mode 100644 index 0000000000..47c7a1f563 --- /dev/null +++ b/source/developers-guide/cookie-consent-manager/index.md @@ -0,0 +1,243 @@ +--- +layout: default +title: Register a cookie to the cookie consent manager +github_link: developers-guide/cookie-consent-manager/index.md +shopware_version: 5.6.3 +indexed: true +tags: + - cookie + - consent + - manager +group: Developer Guides +subgroup: General Resources +menu_title: Register a cookie to the cookie consent manager +menu_order: 300 +--- + +Starting with Shopware 5.6.3, a cookie consent manager has been integrated. +This enables the shop user to configure in detail which cookies he wants to accept or decline by providing an easy to use overlay. + +**But what happens now when a plugin introduces a new cookie in Shopware?**
    +If you're using the "Technically necessary cookies only" mode, each cookie that is not known to Shopware 5.6.3, +will automatically be deleted with every server response. + +The following guide will explain, how you can register your plugin's cookies successfully, +so Shopware knows about it and can deal with it properly. + +## Registering your own cookie + +The very first step is to make sure, that Shopware knows about your cookie and can deal with its presence. +This will show your cookie in the cookie consent manager automatically. If the user decides not to allow your cookie, +Shopware will also automatically try to delete your cookie with every response sent by the server. + +Registering your cookie is done using PHP, and only PHP. We wanted to have a single point of truth, +but don't you worry, the necessary changes are very easy. +Also, if you need to do stuff in javascript, this guide also has you covered! + +Let's start with an example, before we get into it in detail - but don't worry, it's very simple! + +```php + 'addComfortCookie' + ]; + } + + public function addComfortCookie(): CookieCollection + { + $collection = new CookieCollection(); + $collection->add(new CookieStruct( + 'comfort', + '/^comfort$/', + 'Matches with only "comfort"', + CookieGroupStruct::COMFORT + )); + + return $collection; + } +} +``` + +That's actually everything necessary to let Shopware know of your cookie! +Yet, let's have a look at it. + +This code is basically a simple plugin, which is listening to an event called `CookieCollector_Collect_Cookies`, +which is the main event to register your cookie. +In the event listener, in this case called `addComfortCookie`, you have to create an instance of a `\Shopware\Bundle\CookieBundle\CookieCollection`. +Afterwards, you'll have to add at least one instance of a `\Shopware\Bundle\CookieBundle\Structs\CookieStruct`. + +The `CookieStruct` requires three parameters, we highly suggest to use all four of them though. +The first parameter represents the cookie's technical name, which will be used for saving the current 'active' state of your cookie. + +Very important for you is the second parameter, which is the RegEx to match your cookie. +This way you can deal with dynamic cookie names like `session-1`. Just try to be as precise as possible here, +since you **probably** do not want to match multiple cookies with just one RegEx. +In the example mentioned above, only cookies with the exact name `comfort` would be matched, neither `comfortCookie` nor `myComfort`. + +If you really want to match multiple cookies with a single `CookieStruct`, go ahead and use a more open RegEx: +```php +new CookieStruct( + 'comfort', + '/^swag/', + 'Every cookie prefixed with "swag"', + CookieGroupStruct::COMFORT +) +``` + +Even when there's no common prefix, you could a RegEx capturing group to group up your cookies: +```php +new CookieStruct( + 'comfort', + '/^(foo|bar)/', + 'All cookies starting with either "foo" or "bar"', + CookieGroupStruct::COMFORT +) +``` + +The third parameter represents the label being shown in the cookie consent manager for your cookie. +Make sure to add translations here, e.g. like this: +```php +public function addComfortCookie(): CookieCollection +{ + $pluginNamespace = $this->container->get('snippets')->getNamespace('my_plugins_snippet_namespace'); + + $collection = new CookieCollection(); + $collection->add(new CookieStruct( + 'comfort', + '/^comfort$/', + $pluginNamespace->get('my_cookie_label'), + CookieGroupStruct::COMFORT + )); + + return $collection; +} +``` + +The fourth parameter is optional and represents your cookie's group. If none is applied, the "Others" group is used. +You can find all default groups as constants in the `\Shopware\Bundle\CookieBundle\Structs\CookieGroupStruct`. +Also, have a look at the next headline to figure out how to register your cookie group, if necessary. + +The last line of your method then returns the `CookieCollection` - and that's it, you've successfully registered your own cookie. + +## Registering an own cookie group + +Normally, you wouldn't want to do this, but there might be a use case forcing to create your own cookie group. +This is just as simple as registering your own cookie. + +Once again, let's first start with the example, only to explain it in short then afterwards. + +```php +public static function getSubscribedEvents(): array +{ + return [ + 'CookieCollector_Collect_Cookie_Groups' => 'addCookieGroup', + ]; +} + +public function addCookieGroup(\Enlight_Event_EventArgs $args): CookieGroupCollection +{ + $pluginNamespace = $this->container->get('snippets')->getNamespace('my_plugins_snippet_namespace'); + + $collection = new CookieGroupCollection(); + $collection->add(new CookieGroupStruct( + 'custom', + $pluginNamespace->get('custom_label'), + $pluginNamespace->get('custom_description') + )); + + return $collection; +} +``` + +As already mentioned, this looks very similar to registering your custom cookie. +First of all, the event being used is named `CookieCollector_Collect_Cookie_Groups` this time. + +It expects a `\Shopware\Bundle\CookieBundle\CookieGroupCollection` as the return, thus you need to create it in your event listener. +It then has to be filled using a `\Shopware\Bundle\CookieBundle\Structs\CookieGroupStruct`, which accepts four parameters, +two of them being required. + +The first one is the technical name of the group, which is also used by a cookie later on. Maybe add a constant for this. + +The second parameter is the actual label to be shown with this group. Once more, make sure to add your translations just like shown in the example +above. + +The third parameter is optional and represents a description, which will also be shown when the group is expanded. + +The last parameter marks your group as "required", which will prevent the customer to disallow this group and its cookies.
    +**Do not use this, unless you're a hundred percent sure what you're doing! This might come with legal issues!** + +Afterwards, only return your new collection, and that's it! + +
    +Note: Empty groups without a single assigned cookie are hidden by default! +
    + +## Reacting upon changes in javascript + +We're aware of the fact, that some plugins need to react properly once their cookie got activated or de-activated. +For this reason, we also implemented a javascript event, which can be used to react on changes made to the customer's preferences. + +For this we're making use of our publish / subscribe system in our jQuery plugins. +The event you need to subscribe to is called `plugin/swCookieConsentManager/onBuildCookiePreferences`. +It gets fired every time the customer clicks on the "Save" button in the cookie consent manager, no matter if actual changes were made or not. + +Here is a short example on how to register your custom logic here: +```js +$.subscribe('plugin/swCookieConsentManager/onBuildCookiePreferences', function (event, plugin, preferences) { + console.log("Do something like removing a cookie or displaying some warning regarding possible issues!"); +}); +``` + +The first parameter supplied represents an instance of the Event object, the second parameter being the `swCookieConsentManager` jQuery plugin +and the last one is an object containing all the necessary data, such as the groups, their active state, the cookies and their respective active state. + +As already said, this event is only fired when the customer changes his preferences, or sets them for the very first time. +There might be the need to check for your cookie state with every single page-reload though, but we also got that issue covered. + +You can actually check the active state of your cookie in javascript at any given point in time, using the global method `$.getCookiePreference(cookieName)`. +This will return either `true` or `false`, depending on your cookie's active state. If your cookie is unknown yet and thus not saved in the preferences, +`false` will be returned as well. + +## Dealing with the LocalStorage + +If your plugin also saves data into the LocalStorage, this might be a legal issue as well. +The consent manager itself neither prevents saving data into the LocalStorage nor does it delete or modify data being saved there, at least not with Shopware 5.6.3. + +You can still use the consent manager to deal with this issue by simply registering another dummy cookie consisting of a random RegEx, so it matches no actual cookie, and a proper label. + +```php +public function addComfortCookie(): CookieCollection +{ + $pluginNamespace = $this->container->get('snippets')->getNamespace('my_plugins_snippet_namespace'); + + $collection = new CookieCollection(); + $collection->add(new CookieStruct( + 'allow_local_storage', + '/^match_no_cookie_DJ7ra9WMy8$/', + 'Saving statistical data into the LocalStorage', + CookieGroupStruct::COMFORT + )); + + return $collection; +} +``` + +This way, your customers will be able to allow saving data into the LocalStorage and you can check for its active status in your javascript code. + +```js +if ($.getCookiePreference('allow_local_storage')) { + // Save data into LocalStorage +} +``` \ No newline at end of file diff --git a/source/developers-guide/csrf-protection/index.md b/source/developers-guide/csrf-protection/index.md index ca1ce4669a..36e0392e67 100644 --- a/source/developers-guide/csrf-protection/index.md +++ b/source/developers-guide/csrf-protection/index.md @@ -109,16 +109,21 @@ In some cases, you might want to disable the protection for the backend or front ```php ... -'csrfProtection' => [ +'csrfprotection' => [ 'frontend' => false, 'backend' => false ], ... ``` -## Plugin compatibility for older versions +### Plugin compatibility for older versions -Since Shopware 5.2, your plugin needs to whitelist an action in order to e.g. transfer files or show a page within a window or iframe. For Shopware versions prior to 5.2, the interface `CSRFWhitelistAware` isn't available and you'll receive an exception. In this case, you have to create a dummy interface which will only be loaded if the original one does not exist. +Since Shopware 5.2, your plugin needs to whitelist an action in order to +e.g. transfer files or show a page within a window or iframe. +For Shopware versions prior to 5.2, the interface `CSRFWhitelistAware` +isn't available and you'll receive an exception. In this case, you have to +create a dummy interface which will only be loaded if the original one does +not exist. #### 1. Create a new file `Components/CSRFWhitelistAware.php` @@ -137,3 +142,63 @@ if (!interface_exists('\Shopware\Components\CSRFWhitelistAware')) { ``` require_once __DIR__ . '/Components/CSRFWhitelistAware.php'; ``` + +The same approach is applicable if you have a plugin which relies on +the `CSRFGetProtectionAware` interface which was introduced in 5.2.22: + +## Protect GET requests via CSRF tokens + +Since Shopware 5.2.22 you are able to protect your AJAX JSONP requests via CSRF tokens. There are two main steps to enable CSRF support for your Shopware Plugin. + +The first step is to implement the `CSRFGetProtectionAware` interface for the controller which contains the action you want to protect via CSRF. The interface requires a method called `getCSRFProtectedActions` which returns all actions that will be validated. Your controller should now look like this: + +```php +
    ## Registering a new element ## -For creating custom shopping world elements Shopware provides some helper functions which can be used in the `Bootstrap.php` of a [Shopware plugin](/developers-guide/plugin-quick-start/). So all you have to do is to create a simple plugin where you can register one ore more elements via the `createEmotionComponent()` method. As an example for this tutorial we will create a Vimeo element for adding videos to the shopping world. +For creating custom shopping world elements Shopware provides some helper functions which can be used where the DependencyInjectionContainer is available. So all you have to do is to get the `shopware.emotion_component_installer` service in a simple plugin where you can register one ore more elements via the `createOrUpdate()` method. As an example for this tutorial we will create a Vimeo element for adding videos to the shopping world. ```php -$vimeoElement = $this->createEmotionComponent([ - 'name' => 'Vimeo Video', - 'xtype' => 'emotion-components-vimeo', - 'template' => 'emotion_vimeo', - 'cls' => 'emotion-vimeo-element', - 'description' => 'A simple vimeo video element for the shopping worlds.' -]); +$this->emotionComponentInstaller = $this->container->get('shopware.emotion_component_installer'); + +$vimeoElement = $this->emotionComponentInstaller->createOrUpdate( + $this->pluginName, + 'SwagVimeoElement', + [ + 'name' => 'Vimeo Video', + 'template' => 'emotion_vimeo', + 'cls' => 'emotion-vimeo-element', + 'description' => 'A simple vimeo video element for the shopping worlds.' + ] +); ``` -In the `install()` method of our plugin we register a new element and save it in the variable `$vimeoElement` for later use. The `createEmotionComponent()` method expects a configuration array with the following properties: +In the `install()` you use a helper class in the install method which creates the new element. The `createOrUpdate()` method expects the plugin name, a element name and a configuration array with the following properties: @@ -359,10 +364,10 @@ $emotionElement->createMediaField([
    ## Creating a frontend template for the element ## -template directory structure -After registering the element and creating all the configuration fields we already see a full functional shopping world element in the backend module which can be placed on the design canvas. All we have to do now is to provide a frontend template to define the layout in the store. In the `Views` directory of our plugin we create the necessary directory structure to the file. Template files for shopping world elements can automatically be added by creating the hierarchy structure in the special directory called `emotion_components`. The full path to the template file would be `Views/emotion_components/widgets/emotion/components/{name}.tpl`. +template directory structure +After registering the element and creating all the configuration fields we already see a full functional shopping world element in the backend module which can be placed on the design canvas. All we have to do now is to provide a frontend template to define the layout in the store. In the `Resources/views` directory of our plugin we create the necessary directory structure to the file. Template files for shopping world elements can automatically be added by creating the hierarchy structure in the special directory called `emotion_components`. The full path to the template file would be `Resources/views/emotion_components/widgets/emotion/components/{name}.tpl`. -The name of the file has to match the definition in your `createEmotionComponent()` method. You can access your configuration fields inside the template file as properties of the `$Data` smarty variable. Let's create the embed code for displaying the Vimeo video. +The name of the file has to match the definition in the `createOrUpdate()` method. You can access your configuration fields inside the template file as properties of the `$Data` smarty variable. Let's create the embed code for displaying the Vimeo video. ``` {block name="widgets_emotion_components_vimeo_element"} @@ -400,47 +405,272 @@ The name of the file has to match the definition in your `createEmotionComponent {/block} ``` +To load your template you have to register the emotion component view subscriber in your dependency injection container. This subscriber registers all necessary view paths for your frontend template and custom components. + +```xml + + %swag_vimeo_element.plugin_dir% + + +``` + ## Process the element data before output ## + When you have to process the saved element data before it is passed to the frontend, you have the possibility to register to the `Shopware_Controllers_Widgets_Emotion_AddElement` controller event. Here you get the original data to manipulate the output. +### Shopware 5.3 and above + +Create a new component handler class for your element and implement the `\Shopware\Bundle\EmotionBundle\ComponentHandler\ComponentHandlerInterface`. + ```php -public function install() +use Shopware\Bundle\EmotionBundle\ComponentHandler\ComponentHandlerInterface; +use Shopware\Bundle\EmotionBundle\Struct\Collection\PrepareDataCollection; +use Shopware\Bundle\EmotionBundle\Struct\Collection\ResolvedDataCollection; +use Shopware\Bundle\EmotionBundle\Struct\Element; +use Shopware\Bundle\StoreFrontBundle\Struct\ShopContextInterface; + +class VimeoComponentHandler implements ComponentHandlerInterface { - // ... - - $this->subscribeEvent( - 'Shopware_Controllers_Widgets_Emotion_AddElement', - 'onEmotionAddElement' - ); + public function supports(Element $element) + { + return $element->getComponent()->getTemplate() === 'emotion_vimeo'; + } + + public function prepare(PrepareDataCollection $collection, Element $element, ShopContextInterface $context) + { + // do some prepare logic, e.g. requesting articles for rendering + } + + public function handle(ResolvedDataCollection $collection, Element $element, ShopContextInterface $context) + { + // do some handle logic and fill the element data, which will be available in your template under $Data.key + $element->getData()->set('key', 'value'); + } } +``` + +Because the handler is called for every element we have to do a check if the handler supports the executed element before processing the data. + +Now register the handler in the dependency injection container to make it available when rendering the component. + +```xml + + + +``` -public function onEmotionAddElement(Enlight_Event_EventArgs $args) +
    Hint: Existing plugins using the old method below will still work in Shopware 5.3.
    + +#### Requesting items in ComponentHandler + +To make use of the performance improvement, you have to split your logic into a prepare step and handle step. The prepare step collects product numbers or criteria objects which will be resolved across all elements at once. The handle step provides a collection with resolved products and can be filled into your element. + +```php +public function prepare(PrepareDataCollection $collection, Element $element, ShopContextInterface $context) { - $element = $args->get('element'); + $productNumber = $element->getConfig()->get('selected_product_number'); + $collection->getBatchRequest()->setProductNumbers('my-unique-request', [$productNumber]); +} - if ($element['component']['xType'] !== 'emotion-components-vimeo') { - return; - } +public function handle(ResolvedDataCollection $collection, Element $element, ShopContextInterface $context) +{ + $product = current($collection->getBatchResult()->get('my-unique-request')); + $element->getData()->set('product', $product); +} +``` - $data = $args->getReturn(); - - // Do some stuff with the element data +Keep in mind to use a unique key for requesting and getting products. For best practise, use the element's id in your key (`$element->getId()`). + +## Adding a custom component handler for export +backend emotion component handler directory structure +Since Shopware 5.3 it is possible to export and import shopping worlds including all settings and +assets. Shopware delivers component handlers for all standard shopping world elements which use assets. + +Due to the fact every element is storing its assets slightly different, it is required +to have component handlers which "know" their component and know where to extract assets from and where to put back data on import. + +The component handlers are using a special service tag to be registered by the `PresetDataSynchronizer` Service which is used to prepare the export and +process the import for single elements. + +```xml + + + + + + + +``` + +If you want to register your own component handler you have to add the tag `shopware.emotion.preset_component_handler` to it, to assure that your handler +will be recognized by the import and export process. + +Your handler should implement the `ComponentHandlerInterface`. Please take care of the +namespace because there is another ComponentHandlerInterface available via the `EmotionBundle` which differs. + +The interface requires three methods to be implemented. The `supports` method needs to return a boolean +to give a signal if the handler can handle the current component. The `import` and `export` methods are responsible for the business logic of the import and +export process. The `import` and `export` methods have to return the processed element at the end. + +During export and import processing, the `PresetDataSynchronizer` loops through +all elements of a shopping world and checks if there is a handler which can handle +the component. + +Please have look at the Shopware component handlers like the `BannerComponentHandler`. The purpose of a handler is to identify assets used in the component +and to store information about them in the `$syncData ParameterBag`. + +The handler creates an `md5hash` of the media +id which is used to identify double assets inside one shopping world. During the +import process and after importing one asset the information of the imported asset is +also stored in the `ParameterBag` and stored in the preset data, because element import is +handled as a single process for each element, but the elements need information about +already imported assets if they are using the same. + +So if an element is using an already imported +asset, it can gather the information from the `ParameterBag` or otherwise has to store new information after import. + +```php +namespace Shopware\Components\Emotion\Preset\ComponentHandler; + +use Symfony\Component\HttpFoundation\ParameterBag; + +class BannerComponentHandler extends AbstractComponentHandler +{ - $args->setReturn($data); + const COMPONENT_TYPE = 'emotion-components-banner'; // Components unique name + + const ELEMENT_DATA_KEY = 'file'; // Data field with the asset information + + /** + * {@inheritdoc} + */ + public function supports($componentType) + { + return $componentType === self::COMPONENT_TYPE; // Is the passed componentType the one this handler can handle? + } + + /** + * {@inheritdoc} + */ + public function import(array $element, ParameterBag $syncData) + { + if (!isset($element['data'])) { + return $element; + } + + return $this->processElementData($element, $syncData); + } + + /** + * {@inheritdoc} + */ + public function export(array $element, ParameterBag $syncData) + { + if (!isset($element['data'])) { + return $element; + } + + return $this->prepareElementExport($element, $syncData); + } + + /** + * @param array $element + * @param ParameterBag $syncData + * + * @return array + */ + private function processElementData(array $element, ParameterBag $syncData) + { + $data = $element['data']; + $assets = $syncData->get('assets', []); // Contains info about the assets like actual path + $importedAssets = $syncData->get('importedAssets', []); // Contains info about already imported assets + + foreach ($data as &$elementData) { + if ($elementData['key'] === self::ELEMENT_DATA_KEY) { + if (!array_key_exists($elementData['value'], $assets)) { + break; + } + if (!array_key_exists($elementData['value'], $importedAssets)) { // Already imported? + $assetPath = $assets[$elementData['value']]; + + $media = $this->doAssetImport($assetPath); // Import new/unknown asset + $importedAssets[$elementData['value']] = $media->getId(); + } else { + $media = $this->getMediaById($importedAssets[$elementData['value']]); // Gather info about already imported asset + } + + $elementData['value'] = $media->getPath(); // Set the asset path as value on element data + + break; + } + } + unset($elementData); + + $syncData->set('importedAssets', $importedAssets); // Store info about handled asset imports + $element['data'] = $data; + + return $element; // Return processed element + } + + /** + * @param array $element + * @param ParameterBag $syncData + * + * @return array + */ + private function prepareElementExport(array $element, ParameterBag $syncData) + { + $assets = $syncData->get('assets', []); + $data = $element['data']; + + foreach ($data as &$elementData) { + if ($elementData['key'] === self::ELEMENT_DATA_KEY) { + $assetPath = $elementData['value']; + $media = $this->getMediaByPath($assetPath); + + if ($media) { + $assetHash = md5($media->getId()); // Create hash as unique identifier + $assets[$assetHash] = $this->mediaService->getUrl($assetPath); + $elementData['value'] = $assetHash; // Save hash as value + + break; + } + } + } + unset($elementData); + + $syncData->set('assets', $assets); // Store asset info globally + $element['data'] = $data; + + return $element; + } } ``` -Because the event is called for every element we have to do a check before processing the data. You can get the element info from the event arguments with `$args->get('element')`. To test for a specific element we can validate the defined `xType`. When the element is the right one we can get the data of the configuration form with `$args->getReturn()`. After processing the data we have to set the new output for the frontend with `$args->setReturn($data)`. - ## Advanced: Adding a custom emotion component in ExtJS ## backend component directory structure If you want to go a little further by creating custom configuration fields for your element you have the possibility to create your own ExtJS component for the element. Here you have full access to the configuration form in ExtJS. You can manipulate existing fields or add new fields which are more complex than the standard form elements. -The file for the component is also located in the `emotion_components` directory, so it will be detected automatically. The complete path to the file is `Views/emotion_components/backend/{name}.js`. +The file for the component is also located in the `emotion_components` directory. The complete path to the file is `Resources/views/emotion_components/backend/{name}.js`. For the Vimeo example we use the custom ExtJS component to make a call to the Vimeo api for receiving information about the preview image of the video and save it in the hidden input we already created via the helper functions. +First of all you have to define a xtype in the createEmotionComponent() method for your element. + +```php +$vimeoElement = $this->emotionComponentInstaller->createOrUpdate( + $this->pluginName, + 'SwagVimeoElement', + [ + 'xtype' => 'emotion-components-vimeo', + // the config of your component + ] +); ``` + +Now we can implement the custom javascript. + +```js //{block name="emotion_components/backend/vimeo_video"} Ext.define('Shopware.apps.Emotion.view.components.VimeoVideo', { @@ -448,6 +678,13 @@ Ext.define('Shopware.apps.Emotion.view.components.VimeoVideo', { alias: 'widget.emotion-components-vimeo', + snippets: { + 'vimeo_interface_color': { + 'fieldLabel': '{s name=interfaceColorFieldLabel}{/s}', + 'supportText': '{s name=interfaceColorSupportText}{/s}' + } + }, + initComponent: function () { var me = this; @@ -497,32 +734,64 @@ The component always has to extend the base class `Shopware.apps.Emotion.view.co In the component you can get access to the fields which you already created by using the `findField()` method on the `form` object which can be received by `this.getForm()`. +If you want to translate the field label, support and help texts, just add the property `snippets`. Using the name which which was set in the emotion component installer, define every input field which you want to translate. + ## Advanced: Adding a custom designer component in ExtJS ## grid elements Since Shopware 5.2 you are able to create a custom ExtJS component for the designer elements. Here you have the possibility to add an icon and a preview template for the element, which gets shown in the grid of the designer. For extending the designer components we have to do a classic template extension of the backend files. So we create a new file in the necessary template hierarchy `Views/backend/emotion/{pluginName}/view/detail/elements`. -Otherwise than the custom emotion component we have to register the template manually by extending the template inheritance system with our new file. We can subscribe to the `PostDispatch` event of the emotion module in the `install()` method of our plugin to do so. - +Otherwise than the custom emotion component we have to register the template manually by extending the template inheritance system with our new file. We can subscribe to the `PostDispatch` event of the emotion module. +```xml + + + %swag_vimeo_element.plugin_dir% + + +``` ```php -public function install() -{ - // ... - - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_Emotion', - 'onPostDispatchBackendEmotion' - ); -} +getSubject(); - $view = $controller->View(); + /** + * @var string + */ + private $pluginDirectory; + + /** + * @param $pluginDirectory + */ + public function __construct($pluginDirectory) + { + $this->pluginDirectory = $pluginDirectory; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Emotion' => 'onPostDispatchBackendEmotion' + ]; + } - $view->addTemplateDir($this->Path() . 'Views/'); - $view->extendsTemplate('backend/emotion/vimeo_element/view/detail/elements/vimeo_video.js'); + /** + * @param \Enlight_Controller_ActionEventArgs $args + */ + public function onPostDispatchBackendEmotion(\Enlight_Controller_ActionEventArgs $args) + { + $view = $args->getSubject()->View(); + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); + $view->extendsTemplate('backend/emotion/swag_vimeo_element/view/detail/elements/vimeo_video.js'); + } } ``` @@ -609,6 +878,9 @@ In the component we can extend the base class of the designer elements `Shopware +## Removing a custom element ## +There's no need to delete your custom element in the plugin's uninstall method on your own. Shopware will remove your element and all the occurrences in emotion worlds on the uninstallation process for you. + ## Downloads ## You can download the complete example plugin with documented code here: diff --git a/source/developers-guide/customer-streams-extension/index.md b/source/developers-guide/customer-streams-extension/index.md new file mode 100644 index 0000000000..bd4410a3f6 --- /dev/null +++ b/source/developers-guide/customer-streams-extension/index.md @@ -0,0 +1,354 @@ +--- +layout: default +title: Customer - Search and Streams +github_link: developers-guide/customer-streams-extension/index.md +indexed: true +group: Developer Guides +subgroup: General Resources +menu_title: Customer - Search & Streams +menu_order: 500 +--- + +This article explains how Customer Streams work and how to extend the +customer stream and customer search module in Shopware. In most cases, +the purpose of a plugin is to provide more conditions or add additional data to +the search. The examples below describe how to achieve this. + +Shopware 5.3 introduced a new feature called Customer Streams. +Customer Streams are similar to Product Streams and +allow you to create a subgroup of customers by using a combination of over 20 +default filters (more can be added with plugins). +Customer Streams can be used for: + +* Newsletters: Define the newsletter recipients +* Shopping worlds: Only display shopping worlds if the user is part of the +specified Customer Stream +* Vouchers +* Exports +* Advanced Promotions Suite + +## Introduction +In order to create a new Customer Stream, all customers need to be analysed +before. This process is required to ensure high performance. +During the analysis, all data which is required for the filters is collected and +saved into the `s_customer_search_index` database table. + +If you create a new Customer Stream based on some filters, the module will +check which of the previously analysed customers match these filters and adds +them to your Customer Stream. + +The mapping between customers and Customer Streams is saved +in the `s_customer_streams_mapping` table. The stream definition +(e.g. name, filters) is saved in the database table `s_customer_streams`. +To improve the performance even further, only new customers are analysed. + +While the process described above is very efficient, you have to keep in mind, +that the customer related data can change (e.g. an order gets canceled). +All theses changes are not reflected immediately since all the Customer Stream +data is basically cached. Therefore you can not rely on a Customer Stream if you +need to be sure that the data is still up to date. + + +## Add own condition + +Since the customer search is based on the same functions as the product search, +the way of extending the search is pretty similar. The customer search gets a +criteria object in which the different search conditions are summarized. +The first step is to define your own condition: + +``` +active = $active; + } + + public function getName() + { + return 'ActiveCondition'; + } + + public function onlyActive() + { + return $this->active; + } +} +``` + +The condition above only describes on an abstract level +what is to be searched for. The actual processing of the condition happens +in the corresponding implementation of the search. Currently the customer search +in Shopware is only executed in SQL. The concept is based on SearchBundleDBAL +and SearchBundleES. An actual handler class which handles this condition +in SQL could look like this: + +``` +andWhere('customer.active = :active'); + + /** @var ActiveCondition $condition */ + $query->setParameter(':active', $condition->onlyActive()); + } +} +``` + +The handler can be registered with a compiler tag, +named `customer_search.condition_handler`: + +``` + + + + + + + + + + + + +``` + +## Backend Module extension + +To support the condition in the backend, it is necessary to extend the +customer module via ExtJS. The module can be extended over the +PostDispatch event of the backend customer controller: + +``` + 'extendCustomerStream' + ]; + } + + public function extendCustomerStream(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $subject */ + $subject = $args->getSubject(); + + $subject->View()->addTemplateDir(__DIR__ . '/Resources/views'); + + $subject->View()->extendsTemplate('backend/customer/swag_customer_stream_extension.js'); + } +} +``` + +The extended `swag_customer_stream_extension.js` contains all overrides for the +backend module: + +``` +// {block name="backend/customer/view/customer_stream/condition_panel"} + +// {$smarty.block.parent} + +Ext.define('Shopware.apps.Customer.SwagCustomerStreamExtension', { + override: 'Shopware.apps.Customer.view.customer_stream.ConditionPanel', + + registerHandlers: function() { + var me = this, + //fetch original handlers + handlers = me.callParent(arguments); + + //push own handler into + handlers.push(Ext.create('Shopware.apps.Customer.swag_customer_stream_extension.ActiveCondition')); + + //return modified handlers array + return handlers; + } +}); + + +//definition of your own condition +Ext.define('Shopware.apps.Customer.swag_customer_stream_extension.ActiveCondition', { + + getLabel: function() { + return 'My active condition'; + }, + + supports: function(conditionClass) { + return (conditionClass == 'SwagCustomerSearchExtension\\Bundle\\CustomerSearchBundle\\ActiveCondition'); + }, + + create: function(callback) { + callback(this._create()); + }, + + load: function(conditionClass, items, callback) { + callback(this._create()); + }, + + _create: function() { + return { + title: this.getLabel(), + conditionClass: 'SwagCustomerSearchExtension\\Bundle\\CustomerSearchBundle\\ActiveCondition', + items: [{ + xtype: 'checkbox', + name: 'active', + boxLabel: 'Activate for active customers, deactivate for inactive customers', + inputValue: true, + uncheckedValue: false + }] + }; + } +}); + +// {/block} +``` + +The first part hooks into the customer stream condition panel and registers the +plugin condition. The second part contains the whole logic to handle the condition +for load and create actions. + +The create and load function have to return an object with the following data: + +* `title` - Used for the panel title +* `conditionClass` - Used for class generation - Aside, used for singleton detection +* `items` - Contains a list of parameters, which used for `__construct` call + +## Search Indexing + +The `CustomerSearchBundleDBAL` uses an aggregated table which allows fast filtering +and sorting, even on large data sets. This table is generated by +the `Shopware\Bundle\CustomerSearchBundleDBAL\Indexing\SearchIndexer` class. +If a plugin wants to filter and sort additional aggregated data, it can hook into +the indexing process to collect additional data. +The following `services.xml` shows how to decorate the `customer_search.dbal.indexing.indexer`. + +``` + + + + + + + + + + + + +``` + +Shopware expects that a class is found under this service name, which +implements the interface `SearchIndexerInterface`: + +``` +coreIndexer = $coreIndexer; + $this->connection = $connection; + } + + public function populate(array $ids) + { + $this->coreIndexer->populate($ids); + + //fetch data + $rows = $this->connection->createQueryBuilder()->execute()->fetchAll(); + + //create prepared statement for fast inserts + $statement = $this->connection->prepare("INSERT INTO test-table"); + + //iterate rows and insert data + foreach ($rows as $row) { + $statement->execute($row); + } + } + + public function clearIndex() + { + $this->coreIndexer->clearIndex(); + $this->connection->executeUpdate("DELETE FROM test-table"); + } + + public function cleanupIndex() + { + $this->coreIndexer->cleanupIndex(); + } +} +``` + +The best way to index additional data is to aggregate the data beforehand +and save it into a separate table, which is in a 1:1 relation to the +original search_index table. The condition handler can access these +indexed data quickly to allow a fluent experience. +You download the example plugin above +here. + + +## REST API +We also offer a complete REST API for Customer Streams. +For a detailed documentation click **[here](https://developers.shopware.com/developers-guide/rest-api/customer-streams/)**. diff --git a/source/developers-guide/debugging/index.md b/source/developers-guide/debugging/index.md index bed41e1c5a..3853b98805 100644 --- a/source/developers-guide/debugging/index.md +++ b/source/developers-guide/debugging/index.md @@ -10,27 +10,34 @@ indexed: true group: Developer Guides subgroup: General Resources menu_title: Debugging Shopware -menu_order: 30 +menu_order: 35 --- -Writing and extending software is only a part of a developer's daily work. Debugging and bug fixing is another relevant part one needs to take care of. +Writing and extending software is only a part of a developer's daily work. +Debugging and bug fixing is another relevant part one needs to take care of. So what should you do if something does not work as it is supposed to?
    -Note: All the suggested changes in this page are exclusively recommended for development environments. They might expose sensitive information about your system and shop, and should not be performed in public or production systems. +Note: All the suggested changes in this page are exclusively recommended for development environments. +They might expose sensitive information about your system and shop, and should not be performed in public or production systems.
    ## Default log output -First of all you should check if Shopware already logged the error message you are looking for. For that reason you should check the webserver's `error.log` file, as well as Shopware's `logs` directory. Shopware creates a log file per day (if there was something to log). +First you should check if Shopware already logged the error message you are looking for. +For that reason you should check the webserver's `error.log` file, as well as Shopware's `logs` directory. +Shopware creates a log file per day (if there was something to log). -As Shopware frequently uses AJAX queries in the frontend and backend, you should also open an instance of your browser's developer tools. You might find error messages in the Javascript console or in the network tab. +As Shopware frequently uses AJAX queries in the frontend and backend, you should also open an instance of your browser's developer tools. +You might find error messages in the Javascript console or in the network tab. -By default, Shopware hides exceptions from your customers, in order to not expose private and/or technical data. If you are experiencing problems with your shop installation, you might want to re-enable the error output while debugging, by pasting this snippet into your `config.php` file: +By default, Shopware hides exceptions from your customers, in order to not expose private and/or technical data. +If you are experiencing problems with your shop installation, you might want to re-enable the error output while debugging, +by pasting this snippet into your `config.php` file: ```php -array( +[ 'db' => [ // your database configuration ], @@ -40,18 +47,22 @@ array( 'phpsettings' => [ 'display_errors' => 1, ], -) +] ``` ## PHP ### Xdebug -Xdebug is a very common and convenient way to debug your PHP applications. It will allow you to debug a request step by step and inspect variables and object values at any point. +Xdebug is a very common and convenient way to debug your PHP applications. +It will allow you to debug a request step by step and inspect variables and object values at any point. -It can be found in all common linux distributions, e.g. in Ubuntu as `php5-xdebug`. After installing the extension, you will need to configure the xdebug php extension, e.g. in the file `/etc/php5/apache2/conf.d/20-xdebug.ini` (this might vary depending on your distribution and PHP setup). Using a local setup, your configuration might look like this: +It can be found in all common linux distributions, e.g. in Ubuntu as `php-xdebug`. +After installing the extension, you will need to configure the xdebug php extension, +e.g. in the file `/etc/php/7.4/apache2/conf.d/20-xdebug.ini` (this might vary depending on your distribution and PHP setup). +Using a local setup, your configuration might look like this: -``` +```ini zend_extension=xdebug.so xdebug.remote_enable=on @@ -60,34 +71,48 @@ xdebug.remote_port=9000 xdebug.idekey=PhpStorm ``` -After restarting the web server, Xdebug should already be available. Now you should set up Xdebug in your IDE, (e.g. [PhpStorm](https://www.jetbrains.com/phpstorm/help/configuring-xdebug.html)). -In order to comfortably switch Xdebug on and off, you might use a browser extension like [Xdebug helper for chrome](https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc?utm_source=chrome-app-launcher-info-dialog). Equivalent extensions exist for other browsers. Should your browser not have or support these extensions, you might still use Xdebug by appending a specific query argument to your URL. PhpStorm will do this automatically for you if you configure the debug environment, but this feature is not exclusive to this IDE. Refer to Xdebug and your IDE's documentation for more info on this feature. +After restarting the web server, Xdebug should already be available. +Now you should set up Xdebug in your IDE, (e.g. [PhpStorm](https://www.jetbrains.com/help/phpstorm/configuring-xdebug.html)). +In order to comfortably switch Xdebug on and off, you might use a browser extension like [Xdebug helper for chrome](https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc). +Equivalent extensions exist for other browsers. +Should your browser not have or support these extensions, you might still use Xdebug by appending a specific query argument to your URL. +PhpStorm will do this automatically for you if you configure the debug environment, but this feature is not exclusive to this IDE. +Refer to Xdebug and your IDE's documentation for more info on this feature. This might be very useful, as Xdebug might reduce the overall performance of your request. -If you are debugging a CLI command, you can also use Xdebug. Use `export XDEBUG_CONFIG="idekey=PHPSTORM"` prior to running your PHP CLI command and make sure that PhpStorm is listening for Xdebug connections. Again, this feature is not exclusive to PhpStorm, and might be supported in other IDEs. +If you are debugging a CLI command, you can also use Xdebug. +Use `export XDEBUG_CONFIG="idekey=PHPSTORM"` prior to running your PHP CLI command and make sure that PhpStorm is listening for Xdebug connections. +Again, this feature is not exclusive to PhpStorm, and might be supported in other IDEs. ### Monolog -Shopware makes use of the Monolog logger, which allows you to log into files, databases, emails or FirePHP. By default a `CoreLogger` and a `PluginLogger` are set up for usage: +Shopware makes use of the Monolog logger, which allows you to log into files, databases, emails or FirePHP. +By default, a `CoreLogger` and a `PluginLogger` are set up for usage: -``` -Shopware()->PluginLogger()->info("my info"); -Shopware()->PluginLogger()->warning("my warning"); -Shopware()->PluginLogger()->error("my error"); +```php +Shopware()->Container()->get('pluginlogger')->info("my info"); +Shopware()->Container()->get('pluginlogger')->warning("my warning"); +Shopware()->Container()->get('pluginlogger')->error("my error"); ``` -These calls will render the messages "my info", "my warning" and "my error" to the file `logs/plugin_production-YYY-MM-DD.log`. Depending on the logger configuration, you could force monolog to only show info messages if a warning or error occurs later (two fingers crossed handler), which might also be a huge benefit in productive environments. If multiple plugins write to the "PluginLogger", creating own loggers with other persistence backends is also an option. +These calls will render the messages "my info", "my warning" and "my error" to the file `logs/plugin_production-YYY-MM-DD.log`. +Depending on the logger configuration, you could force monolog to only show info messages if a warning or error occurs later (two fingers crossed handler), +which might also be a huge benefit in productive environments. +If multiple plugins write to the "PluginLogger", creating own loggers with other persistence backends is also an option. ### error_log -Setting up Xdebug might not always be possible (e.g. you don't have full admin access over a server) or appropriate for a quick output check. The `error_log` function is useful in those cases. It allows you to write output to the webserver's error log file: +Setting up Xdebug might not always be possible (e.g. you don't have full admin access over a server) or appropriate for a quick output check. +The `error_log` function is useful in those cases. +It allows you to write output to the webserver's error log file: ```php error_log('Hello world'); ``` -In addition to that, `error_log` also allows you to define a file to write to. If you don't have access to the server's log file, or you don't want to spam it with debug messages, this call might be useful to you: +In addition to that, `error_log` also allows you to define a file to write to. +If you don't have access to the server's log file, or you don't want to spam it with debug messages, this call might be useful to you: ```php error_log(print_r(array('hello', 'world'), true)."\n", 3, Shopware()->DocPath() . '/error.log'); @@ -102,7 +127,8 @@ tail -f error.log ### Debugging complex objects / Doctrine -Dumping complex object trees (such as Doctrine models) might cause your browser or server to freeze. For this reason, things like this will not work in most cases: +Dumping complex object trees (such as Doctrine models) might cause your browser or server to freeze. +For this reason, things like this will not work in most cases: ```php // bad example @@ -113,13 +139,13 @@ exit(); Instead, you should use the Doctrine debug helper to print / log complex objects: -``` -$result = \Doctrine\Common\Util\Debug::dump(Shopware()->Shop()) +```php +$result = \Doctrine\Common\Util\Debug::dump(Shopware()->Shop(), 2, true, false); // now safely log $result with your preferred logger ``` ### shopware-profiler -The [shopware-profiler](https://github.com/shyim/shopware-profiler) is a plugin that adds a developer toolbar to your shop and provides some useful debugging features as: +The [FroshProfiler](https://github.com/FriendsOfShopware/FroshProfiler) is a plugin that adds a developer toolbar to your shop and provides some useful debugging features as: * showing registered and called events * viewing of database operations and their results * providing various template information such as variables and which files were loaded @@ -132,9 +158,9 @@ Writing frontend templates will confront you with questions like "what was that ### Smarty -For these kind of questions, Smarty offers the handy `{debug}` tag. You can just put it in any template block of your -plugin's template, or even the core template (it's just temporary). You should just make sure that the block you -are putting it into is actually rendered. +For this kind of questions, Smarty offers the handy `{debug}` tag. +You can just put it in any template block of your plugin's template, or even the core template (it's just temporary). +You should just make sure that the block you are putting it into is actually rendered. In this example, the `{debug}` tag was put in the `themes/Frontend/Bare/frontend/index/index.tpl` file, inside the `frontend_index_html` block. @@ -153,7 +179,8 @@ As you can see, you have a nice overview of all variables and assignments. ### Debug Plugin -Shopware also ships with a plugin called "debug" which will allow you to print out template assignments to the `console` tab of your developer tools window. Just install the plugin using Shopware's plugin manager, configure it to your needs and reload the page. +Shopware also ships with a plugin called "debug" which will allow you to print out template assignments to the `console` tab of your developer tools window. +Just install the plugin using Shopware's plugin manager, configure it to your needs and reload the page. As you can restrict the plugin to your own IP address, this is also suitable for production environments. @@ -161,48 +188,65 @@ As you can restrict the plugin to your own IP address, this is also suitable for ## ExtJS -Debugging ExtJS errors during development can be very time consuming. Errors like `c is not a constructor` are often not helpful. To address this, you can include `ext-all-debug.js` instead of the default `ext-all.js` file: -Edit the themes/Backend/ExtJs/backend/base/header.tpl` file `and replace `ext-all.js` with `ext-all-debug.js` in the block `backend/base/header/javascript`. -After clearing the cache and reloading the backend, the "debug" ExtJS file is included, which displays more helpful error messages. +Debugging ExtJS errors during development can be very time-consuming. +Errors like `c is not a constructor` are often not helpful. +To address this, you can include `ext-all-dev.js` instead of the default `ext-all.js` file. +Add the following part to your `config.php` file. -In many cases, you will have no alternative but to debug using `console.log()` calls in your Javascript code. The following list should help you narrow down the error: +```php +[ + // ... + 'extjs' => [ + 'developer_mode' => true, + ], + // ... +] +``` -* Invalid class names: The name of your ExtJS class (in the `define` call) must match your directory path. E.g. `Views/backend/my_plugin/view/window` should be `Shopware.apps.MyPlugin.view.Window` -* Referencing a wrong xtype: Whenever you use `xtype` to reference a ExtJS class, you should double check if the referenced xtype actually exists. -* Not registering the components: As ExtJS must actually know your components, you need to either register them in the `app.js` file or (when extending pre-existing modules) include them using Smarty and extending the original original applications `app.js` block. -* Missing call to `callParent(arguments);`: When implementing your own components in ExtJS, you will overwrite base-components a lot. When you are implementing a constructor like `initComponent` or `init` you should call `callParent(arguments);` so that ExtJS can handle the base component's logic. -* Smarty errors: Remember that Smarty parses the Javascript backend files. For that reason, Javascript objects always need to have whitespaces before and after the opening and closing curly brace. This also applies for your comments! So if your IDE generates a DocBlock like this: +After clearing the cache and reloading the backend, the "development" ExtJS file is included, which displays more helpful error messages. + +In many cases, you will have no alternative but to debug using `console.log()` calls in your Javascript code. +The following list should help you narrow down the error: + +* Invalid class names: The name of your ExtJS class (in the `define` call) must match your directory path. +E.g. `Resources/views/backend/my_plugin/view/window` should be `Shopware.apps.MyPlugin.view.Window` +* Referencing a wrong xtype: Whenever you use `xtype` to reference a ExtJS class, you should double-check if the referenced xtype actually exists. +* Not registering the components: As ExtJS must actually know your components, you need to either register them in the `app.js` file +or (when extending pre-existing modules) include them using Smarty and extending the original applications `app.js` block. +* Missing call to `callParent(arguments);`: When implementing your own components in ExtJS, you will overwrite base-components a lot. +When you are implementing a constructor like `initComponent` or `init` you should call `callParent(arguments);` so that ExtJS can handle the base component's logic. +* Smarty errors: Remember that Smarty parses the Javascript backend files. +For that reason, Javascript objects always need to have whitespaces before and after the opening and closing curly brace. +This also applies for your comments! So if your IDE generates a DocBlock like this: - ``` - // bad example - /** - * - * @returns {Array} - */ - function: test() { - return []; - } - ``` - Smarty will try to parse the snippet `{Array}` and raise an exception, as this is not a valid Smarty tag. The same applies for objects like this: - - ``` - // bad example - fields = [ - {name:"test"}, - {name:"another test"}, - ] - ``` - - A correct example might look like this: +```js +// bad example +/** +* +* @returns {Array} +*/ +function: test() { + return []; +} +``` + +Smarty will try to parse the snippet `{Array}` and raise an exception, as this is not a valid Smarty tag. +The same applies for objects like this: - ``` - // good example - fields = [ - { name:"test" }, - { name:"another test" }, - ] - ``` +```js +// bad example +fields = [ + {name:"test"}, + {name:"another test"}, +] +``` -### Tools +A correct example might look like this: -The Chrome extension [App Inspector for Sencha™](https://chrome.google.com/webstore/detail/app-inspector-for-sencha/pbeapidedgdpniokbedbfbaacglkceae) might also be very useful. +```js +// good example +fields = [ + { name: "test" }, + { name: "another test" }, +] +``` diff --git a/source/developers-guide/dic-tags/index.md b/source/developers-guide/dic-tags/index.md new file mode 100644 index 0000000000..d62613da20 --- /dev/null +++ b/source/developers-guide/dic-tags/index.md @@ -0,0 +1,260 @@ +--- +layout: default +title: Dependency Injection Tags +github_link: developers-guide/dic-tags/index.md +indexed: true +tags: + - dependency-injection + - container + - tags +group: Developer Guides +subgroup: General Resources +menu_title: Dependency Injection Tags +menu_order: 110 +--- + +Dependency Injection Tags are little strings that can be applied to a service to "flag" it to be used in some special way. For example, if you have a service that you would like to register as a subscriber, you can flag it with the `shopware.event_subscriber` tag. + +Below is information about all of the tags available inside Shopware. + +| Tag Name | Usage | +|------------------|------------------| +| [attribute_search_repository](#attribute_search_repository) | Add a custom entity repository | +| [condition_handler_dbal](#condition_handler_dbal) | Add SQL handler for a condition | +| [console.command](#console.command) | Add a command | +| [criteria_request_handler](#criteria_request_handler) | Add a criteria request handler modify the search | +| [customer_search.condition_handler](#customer_search.condition_handler) | Add a SQL handler for a customer condition | +| [customer_search.sorting_handler](#customer_search.sorting_handler) | Add a SQL handler for a customer sorting | +| [facet_handler_dbal](#facet_handler_dbal) | Add handler for a facet | +| [shopware.captcha](#shopware.captcha) | Add a captcha mechanism | +| [shopware_emotion.component_handler](#shopware_emotion.component_handler) | Process data for an emotion element | +| [shopware.emotion.preset_component_handler](#shopware.emotion.preset_component_handler) | Process element data on import / export | +| [shopware.event_subscriber](#shopware.event_subscriber) | To subscribe to a set of different events/hooks in Shopware | +| [shopware.event_listener](#shopware.event_listener) | Listen to different events/hooks in Shopware | +| [shopware_elastic_search.data_indexer](#shopware_elastic_search.data_indexer) | Add an Elasticsearch indexer | +| [shopware_elastic_search.mapping](#shopware_elastic_search.mapping) | Add an Elasticsearch field mapping | +| [shopware_elastic_search.settings](#shopware_elastic_search.settings) | Create Elasticsearch index settings | +| [shopware_elastic_search.synchronizer](#shopware_elastic_search.synchronizer) | Create an Elasticsearch index synchronizer | +| [shopware_search_es.search_handler](#shopware_search_es.search_handler) | Add an Elasticsearch handler for a condition | +| [shopware_media.adapter](#shopware_media.adapter) | Add a media adapter | +| [shopware_media.optimizer](#shopware_media.optimizer) | Add a media optimizer | +| [sorting_handler_dbal](#sorting_handler_dbal) | Add SQL handler for a sorting | + +## attribute_search_repository + +**Purpose**: Add a custom entity repository + +Custom attribute search repositories are services to search for entities in an optimized way. By default, the most entities are searched through a generic repository which applies the given filter term on every field in the entity. + +If you want to search through related entities, you have to add a custom search repository, which extends the filter to join the related entities. + +For example, the CustomerReader to search for addresses and customer group. + +```php +class CustomerReader extends GenericReader +{ + protected function createListQuery() + { + $query = $this->entityManager->createQueryBuilder(); + $query->select([ + 'entity.id', + 'entity.email', + 'entity.active', + 'billing.firstName', + 'billing.lastName', + 'billing.company', + 'entity.number', + 'grp.name as customerGroup', + ]); + $query->from(Customer::class, 'entity', $this->getIdentifierField()); + $query->innerJoin('entity.billing', 'billing'); + $query->innerJoin('entity.group', 'grp'); + + return $query; + } +} +``` + +And registered in the DIC: + +```xml + + + Shopware\Models\Customer\Customer + + + + +``` + +## shopware_media.adapter + +**Purpose**: Add a media adapter + +For details on registering a new media adapter, read [Media Service - Build your own adapter](/developers-guide/shopware-5-media-service/#build-your-own-adapter). + +## shopware_media.optimizer + +**Purpose**: Add a media optimizer + +For details on registering a new media optimizer, read [Media Optimizer - Create optimizer using a HTTP API](/developers-guide/media-optimizer/#example:-create-optimizer-using-a-http-api). + +## shopware_elastic_search.data_indexer + +**Purpose**: Add an Elasticsearch indexer + +After the data mapping is defined, the data can be indexed using the `Shopware\Bundle\ESIndexingBundle\DataIndexerInterface` interface. The `populate` method is responsible for loading all relevant data entries into Elasticsearch for the provided shop. + +For details on Elasticsearch, read [Elasticsearch development](/developers-guide/elasticsearch/). + +## shopware_elastic_search.mapping + +**Purpose**: Add an Elasticsearch field mapping + +The entity properties must be mapped to Elasticsearch fields using the `Shopware\Bundle\ESIndexingBundle\MappingInterface` interface. + +For details on Elasticsearch, read [Elasticsearch development](/developers-guide/elasticsearch/). + +## shopware_elastic_search.settings + +**Purpose**: Add custom Elasticsearch analyzers + +For details on Elasticsearch, read [Elasticsearch development](/developers-guide/elasticsearch/). + +## shopware_elastic_search.synchronizer + +**Purpose**: Create an Elasticsearch index synchronizer + +Handles backlog queue to synchronize entities which are added by the `ORMBacklogSubscriber`. + +For details on Elasticsearch, read [Elasticsearch development](/developers-guide/elasticsearch/). + +## shopware_search_es.search_handler + +**Purpose**: Add an Elasticsearch handler for a condition + +Analog to the DBAL condition handlers, you have to translate the abstract condition to an Elasticsearch query. + +For details on Elasticsearch, read [Elasticsearch development](/developers-guide/elasticsearch/). + +## criteria_request_handler + +**Purpose**: Add a criteria request handler modify the search + +To add conditions to the product listing search request, if a specific parameter is set. + +## facet_handler_dbal + +**Purpose**: Add handler for a product facet + +Generates the facet data for the passed query, criteria and context object. For details on facets, read [SearchBundle - Concept Facets](/developers-guide/shopware-5-search-bundle/#concept-facets). + +## condition_handler_dbal + +**Purpose**: Add SQL handler for a product condition + +Your handler must implement the `Shopware\Bundle\SearchBundleDBAL\ConditionHandlerInterface` interface and be registered in your `services.xml`. + +```xml + + + +``` + +For understanding the concept of conditions for products and customers, read [SearchBundle - Full implementation with condition](/developers-guide/shopware-5-search-bundle/#full-implementation-with-condition-(with-dbal)) + +## sorting_handler_dbal + +**Purpose**: Add SQL handler for a product sorting + +Each sorting class can be used for ascending or descending sorting. The direction is specified in the class constructor. Your handler must implement the `Shopware\Bundle\SearchBundleDBAL\SortingHandlerInterface` interface and be registered in your `services.xml`. + +You should use `addOrderBy()` on the query to prevent overwriting of other sortings. + +```xml + + + +``` + +For understanding the concept of sortings for products and customers, read [SearchBundle - List of conditions and sortings](/developers-guide/shopware-5-search-bundle/#list-of-conditions-and-sortings) + +## console.command + +**Purpose**: Add a command to the application + +For details on registering your own commands in the service container, read [How to Define Commands as Services](https://symfony.com/doc/2.8/console/commands_as_services.html). + +## shopware.event_subscriber + +**Purpose**: To subscribe to a set of different events/hooks in Shopware + +To enable a custom subscriber, add it as a regular service in your `services.xml` file and tag it with `shopware.event_subscriber`: + +```xml + + + +``` + +
    Hint! Your service must implement the EventSubscriberInterface interface.
    + +## shopware.event_listener + +**Purpose**: Listen to different events/hooks in Shopware + +During the execution of Shopware, different events are triggered and you can also dispatch custom events. This tag allows you to hook your own classes into any of those events. + +For a full example of this listener, read the [Shopware Events](/developers-guide/event-guide/) guide. + +## shopware.captcha + +**Purpose**: Add a captcha mechanism + +For details on creating a custom captcha mechanism, read [Implementing your own captcha](/developers-guide/implementing-your-own-captcha). + +## customer_search.sorting_handler + +**Purpose**: Add a SQL handler for a customer sorting + +Each sorting class can be used for ascending or descending sorting. The direction is specified in the class constructor. Your handler must implement the `Shopware\Bundle\CustomerSearchBundleDBAL\SortingHandlerInterface` interface and be registered in your `services.xml`. + +You should use `addOrderBy()` on the query to prevent overwriting of other sortings. + +```xml + + + +``` + +For understanding the concept of sortings for products and customers, read [SearchBundle - List of conditions and sortings](/developers-guide/shopware-5-search-bundle/#list-of-conditions-and-sortings) + +## customer_search.condition_handler + +**Purpose**: Add a SQL handler for a customer condition + +Your handler must implement the `Shopware\Bundle\CustomerSearchBundleDBAL\ConditionHandlerInterface` interface and be registered in your `services.xml`. + +```xml + + + +``` + +For understanding the concept of conditions for products and customers, read [SearchBundle - Full implementation with condition](/developers-guide/shopware-5-search-bundle/#full-implementation-with-condition-(with-dbal)) + +## shopware_emotion.component_handler + +**Purpose**: Process data for an emotion element + +The prepare step collects product numbers or criteria objects which will be resolved across all elements at once. The handle step provides a collection with resolved products and can be filled into your element for later usage. + +For details on creating your own emotion component handler, read [Custom shopping world elements](/developers-guide/custom-shopping-world-elements/#process-the-element-data-before-output). + +## shopware.emotion.preset_component_handler + +**Purpose**: Process element data on import / export + +During export and import processing, the `PresetDataSynchronizer` loops through all elements of a shopping world and checks if there is a handler which can handle the component. + +For details on creating your own preset handler, read [Custom shopping world elements](/developers-guide/custom-shopping-world-elements/#adding-a-custom-component-handler-for-export). diff --git a/source/developers-guide/digital-publishing-elements/img/file_structure.jpg b/source/developers-guide/digital-publishing-elements/img/file_structure.jpg deleted file mode 100644 index 4d33816238..0000000000 Binary files a/source/developers-guide/digital-publishing-elements/img/file_structure.jpg and /dev/null differ diff --git a/source/developers-guide/digital-publishing-elements/img/file_structure.png b/source/developers-guide/digital-publishing-elements/img/file_structure.png new file mode 100644 index 0000000000..319cbfa1a8 Binary files /dev/null and b/source/developers-guide/digital-publishing-elements/img/file_structure.png differ diff --git a/source/developers-guide/digital-publishing-elements/img/file_structure_backend.jpg b/source/developers-guide/digital-publishing-elements/img/file_structure_backend.jpg deleted file mode 100644 index d3dc58ae7f..0000000000 Binary files a/source/developers-guide/digital-publishing-elements/img/file_structure_backend.jpg and /dev/null differ diff --git a/source/developers-guide/digital-publishing-elements/img/file_structure_backend.png b/source/developers-guide/digital-publishing-elements/img/file_structure_backend.png new file mode 100644 index 0000000000..db21068766 Binary files /dev/null and b/source/developers-guide/digital-publishing-elements/img/file_structure_backend.png differ diff --git a/source/developers-guide/digital-publishing-elements/index.md b/source/developers-guide/digital-publishing-elements/index.md index 9816c00cc4..bb99f2941f 100644 --- a/source/developers-guide/digital-publishing-elements/index.md +++ b/source/developers-guide/digital-publishing-elements/index.md @@ -20,7 +20,7 @@ Digital Publishing is introduced in Shopware 5.1.0 as a new advanced feature. Th ## The basic plugin structure - + In this article we'll be focusing on the important parts to get started with the development of custom digital publishing elements. If you want to get more information about developing plugins for Shopware in general, please head over to the [plugin quick start guide](/developers-guide/plugin-quick-start). @@ -34,107 +34,173 @@ To add a new element to the Digital Publishing module we will need the following - **Frontend template** ## Registering the subscriber -First we register a new **subscriber** and the plugin namespace in the `Bootstrap.php` of our plugin. - -```php -public function install() -{ - $this->subscribeEvent('Enlight_Controller_Front_StartDispatch', 'registerSubscriber'); - $this->subscribeEvent('Shopware_Console_Add_Command', 'registerSubscriber'); - - return true; -} - -public function registerPluginNamespace() -{ - $this->Application()->Loader()->registerNamespace( - 'Shopware\SwagDigitalPublishingSample', - $this->Path() - ); -} - -public function registerSubscriber() -{ - $this->registerPluginNamespace(); - - $subscribers = array( - new Resources($this) - ); - - foreach ($subscribers as $subscriber) { - $this->Application()->Events()->addSubscriber($subscriber); - } -} +First we register new **subscribers** in the `service.xml` of our plugin. + +```xml + + + + + + %swag_digital_publishing_sample.plugin_dir% + + + + + + %swag_digital_publishing_sample.plugin_dir% + + + + + + + + + + + + + + + ``` -To complete this step we have to create the corresponding file in the `Subscriber` directory of our plugin. In our example, we will use a single subscriber called `Resources`. In most cases, you would create several subscribers, to split the tasks between backend and frontend, for example. But, for this little example, we will just use a single subscriber for all events. So let's take a look at our `Resources.php` file. +To complete this step we have to create the corresponding files in the `Subscriber` directory of our plugin. To split the tasks between backend and frontend, for example, we create the domain specific files. So let's take a look at our subscriber files. + +In the `getSubscribedEvents()` method we return all necessary events we want to subscribe to and the corresponding callback method. We have at least three callback methods which are called by our subscriber: ```php -namespace Shopware\SwagDigitalPublishingSample\Subscriber; +bootstrap = $bootstrap; + $this->pluginBaseDirectory = $pluginBaseDirectory; } + /** + * Returns an array of events you want to subscribe to + * and the names of the corresponding callback methods. + * + * @return array + */ public static function getSubscribedEvents() { - return array( + return [ 'Enlight_Controller_Action_PostDispatchSecure_Backend_SwagDigitalPublishing' => 'onPostDispatchBackend', - 'Enlight_Controller_Action_PostDispatchSecure_Widgets_SwagDigitalPublishing' => 'onPostDispatchFrontend', - 'Enlight_Controller_Action_PostDispatchSecure_Widgets_Emotion' => 'onPostDispatchFrontend', - 'Theme_Compiler_Collect_Plugin_Less' => 'onAddLessFiles', - ); + ]; } + /** + * Extends the backend templates with the necessary template files. + * + * @param \Enlight_Event_EventArgs $args + */ public function onPostDispatchBackend(\Enlight_Event_EventArgs $args) { + /** @var Enlight_Controller_Action $subject */ $subject = $args->getSubject(); $view = $subject->View(); - $view->addTemplateDir($this->bootstrap->Path() . 'Views/'); + $view->addTemplateDir($this->pluginBaseDirectory . '/Resources/views/'); $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/extension.js'); $view->extendsTemplate('backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js'); } +} +``` + +```php +getSubject(); - $view = $subject->View(); + $this->pluginBaseDirectory = $pluginBaseDirectory; + } - $view->addTemplateDir($this->bootstrap->Path() . 'Views/'); + /** + * Returns an array of events you want to subscribe to + * and the names of the corresponding callback methods. + * + * @return array + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Widgets_SwagDigitalPublishing' => 'onPostDispatchWidget', + 'Enlight_Controller_Action_PostDispatchSecure_Widgets_Emotion' => 'onPostDispatchWidget', + ]; } - public function onAddLessFiles() + /** + * Adds the template directory of the plugin to extend the frontend templates. + * + * @param \Enlight_Event_EventArgs $args + */ + public function onPostDispatchWidget(\Enlight_Event_EventArgs $args) { - $less = new LessDefinition(array(), array( __DIR__ . '/../Views/frontend/_public/src/less/all.less'), __DIR__); + /** @var \Enlight_Controller_Action $subject */ + $subject = $args->getSubject(); + $view = $subject->View(); - return new ArrayCollection(array($less)); + $view->addTemplateDir($this->pluginBaseDirectory . '/Resources/views/'); } } ``` -In the `getSubscribedEvents()` method we return all necessary events we want to subscribe to and the corresponding callback method. We have at least three callback methods which are called by our subscriber: - - `onPostDispatchBackend`: extends the backend with our ExtJS template files. -- `onPostDispatchFrontend`: Adds the *Views* directory, so we can automatically extend the templates of the storefront. -- `onAddLessFiles`: enables us to add new LESS files to the theme compiler for a little bit of styling. - +- `onPostDispatchWidget`: Adds the *Views* directory, so we can automatically extend the templates of the storefront. + + In the next step, we will look at the first event, which subscribes to the post dispatch of the backend. With it, we can extend the backend with our ExtJS template files in the `onPostDispatchBackend` callback. To add a new element to the Digital Publishing module, we have to extend the original `container.js` of the editor with our own `extension.js` file. All files should go in the `Views` directory of our plugin, in `backend/swag_digital_publishing_sample/view/editor`. We also create the custom handler for our element in the `elements` subdirectory called `youtube_element_handler.js`. These files are added to the backend template by the `extendsTemplate()` method. ## Creating the backend handler -Now that we extended the backend template and created the necessary ExtJS files, we can write some code. Let's start with the custom handler for our element: `Views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js` +Now that we extended the backend template and created the necessary ExtJS files, we can write some code. Let's start with the custom handler for our element: `Resources/views/backend/swag_digital_publishing_sample/view/editor/elements/youtube_element_handler.js` ``` //{block name="backend/swag_digital_publishing/view/editor/abstract_element_handler"} @@ -161,7 +227,7 @@ To extend the backend template, we're editing the Smarty `{block}` of the abstra - `iconCls`: the CSS class for the element icon - `createFormItems()`: the method which returns the configuration fields for this element -Now that we have created the handler, we have to add it to the list of available element handlers. Therefore, we created the `extension.js` to extend the original editor: `Views/backend/swag_digital_publishing_sample/view/editor/extension.js` +Now that we have created the handler, we have to add it to the list of available element handlers. Therefore, we created the `extension.js` to extend the original editor: `Resources/views/backend/swag_digital_publishing_sample/view/editor/extension.js` ``` //{block name="backend/swag_digital_publishing/view/editor/container"} @@ -285,7 +351,7 @@ We already implemented the functionality to translate the fields of an element i ## Creating the frontend template ## -Now that we have a fully functional element, we want to add the frontend template. In the subscriber we already added the `Views/` directory of our plugin via the `onPostDispatchFrontend` callback, so we can directly add a new template file. All element templates are located in the `widgets/swag_digital_publishing/components/` directory. We create the same structure for our custom element and create the corresponding template file `youtube.tpl`. It is important that the file has exactly the same name as we defined it in the ExtJS handler. The template will then be loaded automatically for our element. +Now that we have a fully functional element, we want to add the frontend template. In the subscriber we already added the `Resources/views/` directory of our plugin via the `onPostDispatchWidget` callback, so we can directly add a new template file. All element templates are located in the `widgets/swag_digital_publishing/components/` directory. We create the same structure for our custom element and create the corresponding template file `youtube.tpl`. It is important that the file has exactly the same name as we defined it in the ExtJS handler. The template will then be loaded automatically for our element. ```
    @@ -314,7 +380,7 @@ The rest is just little bit of frontend details and Smarty code. All element fie ## Styling the element ## -To style the element we already added a new LESS file called `all.less` in the subscriber, via the `onAddLessFiles()` callback. The file is located in the frontend directory under `Views/frontend/_public/src/less/`. Here we can put some LESS / CSS code to add a little bit of styling to our element. +To style the element we already added a new LESS file called `all.less` that will be loaded automatically. The file is located in the frontend directory under `Resources/views/frontend/less/`. Here we can put some LESS code to add a little bit of styling to our element. ``` .dig-pub--youtube { @@ -331,31 +397,7 @@ To style the element we already added a new LESS file called `all.less` in the s The YouTube element doesn't need much styling. We're using a combination of `width` and `max-width: 100%;` to let the YouTube frame not become bigger than the design layer. So, in a responsive layout, the frame would shrink accordingly when there is not enough space. -## Data handling with plugin version < 1.2.0 ## - -Not every element has a simple structure like the YouTube sample. Sometimes you want to manipulate the data of the element before it gets passed to the frontend. Therefore, we added a special event you can subscribe to. You could use another subscriber to do so. - -```php -public static function getSubscribedEvents() -{ - return array( - 'SwagDigitalPublishing_ContentBanner_FilterResult' => 'onContentBannerFilter' - ); -} - -public function onContentBannerFilter(\Enlight_Event_EventArgs $args) -{ - $banner = $args->getReturn(); - - // Do some magic data manipulation - - return $banner; -} -``` - -Via the event `$args` you can get the return value with `$args->getReturn()`. The return value contains the hole banner data with all layers and elements. You can do some data manipulation and return the data afterwards. For example, you could load some media data, if the user can select media files in your element. - -## Data handling with plugin version >= 1.2.0 ## +## Data handling ## Starting with the "SwagDigitalPublishing" version 1.2.0 we introduced a new way to handle your custom elements and their related data.
    Each element has to be handled by a so-called `ElementHandler` now. @@ -365,16 +407,11 @@ Those element handlers come in handy when you're trying to provide additional da In case you're using a custom element without a matching `ElementHandler`, you'll face a new error saying `Handler for element not found`. In order to register a new `ElementHandler` for our custom element, we'll have to add a new subscriber to our plugin: -```php -use Shopware\SwagDigitalPublishingSample\Subscriber\ElementHandler; - -[...] - -public function registerSubscriber() -{ - [...] - $this->Application()->Events()->addSubscriber(new ElementHandler()); -} +```xml + + + + ``` Now we'll have to create the new subscriber `ElementHandler`, which has to subscribe on a new event called `Shopware_DigitalPublishing_Collect_ElementHandler`. @@ -382,11 +419,13 @@ Now we'll have to create the new subscriber `ElementHandler`, which has to subsc This event is necessary to register our new `ElementHandler`, so our custom element `YouTube` can be properly handled. ```php -namespace Shopware\SwagDigitalPublishingSample\Subscriber; + 'collectElementHandler' - ); + ]; } /** @@ -406,7 +445,7 @@ class ElementHandler implements SubscriberInterface public function collectElementHandler() { return new ArrayCollection( - array(new YouTubeHandler()) + [new YouTubeHandler()] ); } } @@ -425,9 +464,12 @@ The interface requires the two methods `handle` and `canHandle`. The `handle` method provides additional data to our element, while the `canHandle` method decides if the given element is supported by this handler. ```php -namespace Shopware\SwagDigitalPublishingSample\Components; + 1.2.0**: Download.
    -**Sample Plugin < 1.2.0**: Download. +**Sample Plugin**: Download. diff --git a/source/developers-guide/elasticsearch/elasticsearch_system_log_detail.png b/source/developers-guide/elasticsearch/elasticsearch_system_log_detail.png new file mode 100644 index 0000000000..f000c39013 Binary files /dev/null and b/source/developers-guide/elasticsearch/elasticsearch_system_log_detail.png differ diff --git a/source/developers-guide/elasticsearch/index.md b/source/developers-guide/elasticsearch/index.md index 73a67a0760..1a8e063ec0 100644 --- a/source/developers-guide/elasticsearch/index.md +++ b/source/developers-guide/elasticsearch/index.md @@ -18,10 +18,11 @@ menu_order: 40 ## Introduction In this guide we introduce the Elasticsearch (ES) integration for Shopware. -Shopware uses two bundles for the ES implementation: +Shopware uses three bundles for the ES implementation: 1. ESIndexingBundle - Contains all components that index data from Shopware to ES 2. SearchBundleES - Implementation of the SearchBundle using ES +3. EsBackendBundle - Implementation ES in the Backend (Shopware 5.5) ## Libraries @@ -30,139 +31,142 @@ Shopware uses the official PHP low-level client for Elasticsearch [elasticsearch ## Public API The following list contains all relevant events, interfaces and public API calls for Elasticsearch: -| Console Command | Description -|------------------------------------|----------------------------- -| sw:es:analyze | Helper tool to test the integrated analyzers. -| sw:es:backlog:clear | Remove backlog entries that are already synchronized. -| sw:es:backlog:sync | Synchronize events from the backlog to the live index. -| sw:es:index:cleanup | Remove unused Elasticsearch indices. -| sw:es:index:populate | Reindex all shops into new indexes and switch the live system alias after the index process. -| sw:es:switch:alias | Switch live system aliases. - -| Interface | Description -|---------------------------------------------------|----------------------------- -| DataIndexerInterface | Required to add new data indexer -| MappingInterface | Required to add new data mappings -| SettingsInterface | Required to add new ES settings -| SynchronizerInterface | Required to add new data synchronizer -| ShopAnalyzerInterface | Defines which ES analyzer(s) is used in which shop -| SearchTermQueryBuilderInterface | Builds the search query for product search -| HandlerInterface | Allows handling criteria parts in ES number searches -| ResultHydratorInterface | Allows hydrating ES number search results - -| DI Container service | Description -|---------------------------------------------------|----------------------------- -| shopware_elastic_search.client | ES client for communication -| shopware_elastic_search.shop_indexer | Starts indexing process for all shops -| shopware_elastic_search.shop_analyzer | Defines which ES analyzer(s) is used in which shop -| shopware_elastic_search.backlog_processor | Process the backlog queue -| shopware_search_es.product_number_search | ProductNumberSearch using ES -| shopware_search_es.search_term_query_builder | Builds the search query for product searches - -| DI Container tag | Description -|---------------------------------------------------|----------------------------- -| shopware_elastic_search.data_indexer | Registers a new data indexer -| shopware_elastic_search.mapping | Registers a new data mapping -| shopware_elastic_search.synchronizer | Registers a new data synchronizer -| shopware_elastic_search.settings | Registers a new ES settings class -| shopware_search_es.search_handler | Registers a new search handler - -| Event | Description -|---------------------------------------------------|----------------------------- -| Shopware_ESIndexingBundle_Collect_Indexer | Registers a new data indexer -| Shopware_ESIndexingBundle_Collect_Mapping | Registers a new data mapping -| Shopware_ESIndexingBundle_Collect_Synchronizer | Registers a new data synchronizer -| Shopware_ESIndexingBundle_Collect_Settings | Registers a new ES settings class -| Shopware_SearchBundleES_Collect_Handlers | Registers a new search handler +| Console Command | Description | +|------------------------------|----------------------------------------------------------------------------------------------| +| sw:es:analyze | Helper tool to test the integrated analyzers. | +| sw:es:backlog:clear | Remove backlog entries that are already synchronized. | +| sw:es:backlog:sync | Synchronize events from the backlog to the live index. | +| sw:es:index:cleanup | Remove unused Elasticsearch indices. | +| sw:es:index:populate | Reindex all shops into new indexes and switch the live system alias after the index process. | +| sw:es:backend:index:populate | Reindex all documents for the backend. | +| sw:es:backend:sync | Synchronize events from the backend backlog to the live index. | +| sw:es:switch:alias | Switch live system aliases. | + +| Interface | Description | +|---------------------------------|------------------------------------------------------| +| DataIndexerInterface | Required to add new data indexer | +| MappingInterface | Required to add new data mappings | +| SettingsInterface | Required to add new ES settings | +| SynchronizerInterface | Required to add new data synchronizer | +| ShopAnalyzerInterface | Defines which ES analyzer(s) is used in which shop | +| SearchTermQueryBuilderInterface | Builds the search query for product search | +| HandlerInterface | Allows handling criteria parts in ES number searches | +| ResultHydratorInterface | Allows hydrating ES number search results | + +| DI container service | Description | +|----------------------------------------------|----------------------------------------------------| +| shopware_elastic_search.client | ES client for communication | +| shopware_elastic_search.client.logger | Logs specific ES requests and their responses | +| shopware_elastic_search.shop_indexer | Starts indexing process for all shops | +| shopware_elastic_search.shop_analyzer | Defines which ES analyzer(s) is used in which shop | +| shopware_elastic_search.backlog_processor | Process the backlog queue | +| shopware_search_es.product_number_search | ProductNumberSearch using ES | +| shopware_search_es.search_term_query_builder | Builds the search query for product searches | +| shopware_es_backend.indexer | Starts backend indexing process for all shops | + +| DI container tag | Description | +|--------------------------------------|-----------------------------------| +| shopware_elastic_search.data_indexer | Registers a new data indexer | +| shopware_elastic_search.mapping | Registers a new data mapping | +| shopware_elastic_search.synchronizer | Registers a new data synchronizer | +| shopware_elastic_search.settings | Registers a new ES settings class | +| shopware_search_es.search_handler | Registers a new search handler | + +| Event | Description | +|------------------------------------------------|-----------------------------------| +| Shopware_ESIndexingBundle_Collect_Indexer | Registers a new data indexer | +| Shopware_ESIndexingBundle_Collect_Mapping | Registers a new data mapping | +| Shopware_ESIndexingBundle_Collect_Synchronizer | Registers a new data synchronizer | +| Shopware_ESIndexingBundle_Collect_Settings | Registers a new ES settings class | +| Shopware_SearchBundleES_Collect_Handlers | Registers a new search handler | ## Indexing additional data -One common use case is to index additional data into ES and make it searchable. The following example shows which components are required to add new data sources to ES and keep it up to date. After data is indexed, the product number search will be extended to select additional data. The example indexes Shopware blog entries. To index additional data, the following class implementations are required: +One common use case is to index additional data into ES and make it searchable. +The following example shows which components are required to add new data sources to ES and keep it up to date. +After data is indexed, the product number search will be extended to select additional data. +The example indexes Shopware blog entries. To index additional data, the following class implementations are required: 1. DataIndexerInterface 2. MappingInterface 3. SynchronizerInterface 4. A class to record Doctrine events and write them into the backlog -You can find a installable ZIP package of this plugin here. - -### Plugin Bootstrap -The plugin's Bootstrap.php looks as follows: - -```php -subscribeEvent('Enlight_Bootstrap_InitResource_swag_es_blog_search.blog_indexer', 'registerIndexerService'); - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Indexer', 'addIndexer'); - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Mapping', 'addMapping'); - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Synchronizer', 'addSynchronizer'); - $this->subscribeEvent('Enlight_Controller_Front_StartDispatch', 'addBacklogSubscriber'); - return true; - } - - public function afterInit() - { - $this->get('Loader')->registerNamespace('ShopwarePlugins\SwagESBlog', $this->Path()); - } - - public function registerIndexerService() - { - return new BlogDataIndexer( - $this->get('dbal_connection'), - $this->get('shopware_elastic_search.client'), - new BlogProvider($this->get('dbal_connection')) - ); - } - - public function addIndexer() - { - return $this->get('swag_es_blog_search.blog_indexer'); - } - - public function addMapping() - { - return new BlogMapping($this->get('shopware_elastic_search.field_mapping')); - } - - public function addBacklogSubscriber() - { - $subscriber = new ORMBacklogSubscriber(Shopware()->Container()); - - /** @var ModelManager $entityManager */ - $entityManager = $this->get('models'); - $entityManager->getEventManager()->addEventSubscriber($subscriber); - } - - public function addSynchronizer() - { - return new BlogSynchronizer( - $this->get('swag_es_blog_search.blog_indexer'), - $this->get('dbal_connection') - ); - } -} - +You can find an installable ZIP package of this plugin here. + +### Register the services and subscriber +The plugin service.xml looks as follows: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ``` -The Bootstrap.php file contains only the events needed to register the new classes. The following classes are initialized and registered: +The service file contains only the events needed to register the new services and subscriber. +The following classes are initialized and registered: -| Class | Description -|---------------------------|-------------------------- -| BlogMapping | Defines how a blog data structure looks like -| BlogDataIndexer | Populates the blog data into the ES indices -| BlogProvider | Helper class which provides data for blog entries -| ORMBacklogSubscriber | Traces Doctrine events for the blog entity and insert changes into the backlog queue -| BlogSynchronizer | Handles backlog queue to synchronize blog entities which are added by the `ORMBacklogSubscriber` +| Class | Description | +|----------------------|--------------------------------------------------------------------------------------------------| +| BlogMapping | Defines how a blog data structure looks like | +| BlogDataIndexer | Populates the blog data into the ES indices | +| BlogProvider | Helper class which provides data for blog entries | +| ORMBacklogSubscriber | Traces Doctrine events for the blog entity and insert changes into the backlog queue | +| BlogSynchronizer | Handles backlog queue to synchronize blog entities which are added by the `ORMBacklogSubscriber` | To index data into ES, the `BlogMapping`, `BlogDataIndexer` and `BlogProvider` are required. @@ -172,7 +176,7 @@ Before data can be indexed, a data mapping has to be created. This is defined in ```php fieldMapping = $fieldMapping; } + /** + * @return string + */ public function getType() { return 'blog'; } + /** + * @param Shop $shop + * @return array + */ public function get(Shop $shop) { return [ @@ -209,26 +226,25 @@ class BlogMapping implements MappingInterface } ``` -`BlogMapping` uses the `FieldMappingInterface` to get definitions for language fields. It returns a string field definition with sub fields for configured shop analyzers. A language field is only required if a search term for this field has to be analyzed for different shop languages. For fields which shouldn't be searchable, it is more useful to define a simple string field. Example for shop with english locale: +`BlogMapping` uses the `FieldMappingInterface` to get definitions for language fields. +It returns a string field definition with sub-fields for configured shop analyzers. +A language field is only required if a search term for this field has to be analyzed for different shop languages. +For fields which shouldn't be searchable, it is more useful to define a simple string field. Example for shop with english locale: ```php -Array -( +[ [type] => string - [fields] => Array - ( - [english_analyzer] => Array - ( - [type] => string - [analyzer] => english - ) - - ) - -) + [fields] => [ + [english_analyzer] => [ + [type] => string + [analyzer] => english + ] + ] +] ``` -The `english_analyzer` field uses, at indexing and search time, a pre configured english analyzer of ES. The `getType` function defines the unique name for the data type, in this example `blog`. +The `english_analyzer` field uses, at indexing and search time, a pre-configured english analyzer of ES. +The `getType` function defines the unique name for the data type, in this example `blog`. ### Additional indexer After the data mapping is defined, the data can be indexed using the `BlogDataIndexer`, which looks as follows: @@ -236,7 +252,7 @@ After the data mapping is defined, the data can be indexed using the `BlogDataIn ```php execute()->fetchAll(\PDO::FETCH_COLUMN); } } - ``` The `populate` function is responsible for loading all blog entries into ES for the provided shop. -Since the shop indexer doesn't know how to iterate the `DataIndexer` rows, the iteration has to be inside the `populate` function. A progress can be displayed using the provided `ProgressHelperInterface`. First, the function selects all blog ids for the provided shop and passes them into the `index` function. To separate data indexing and data loading, the `BlogIndexer` loads blog data using his own `BlogProvider`. +Since the shop indexer doesn't know how to iterate the `DataIndexer` rows, the iteration has to be inside the `populate` function. +A progress can be displayed using the provided `ProgressHelperInterface`. +First, the function selects all blog ids for the provided shop and passes them into the `index` function. +To separate data indexing and data loading, the `BlogIndexer` loads blog data using his own `BlogProvider`. ```php $blog = $this->provider->get($ids); @@ -368,7 +386,8 @@ $this->client->bulk([ ]); ``` -Documents provided using the bulk API must be json serializable. In this case, the provider returns a `BlogStruct`, which implements the `JsonSerializable` interface. +Documents provided using the bulk API must be json serializable. +In this case, the provider returns a `BlogStruct`, which implements the `JsonSerializable` interface. The indexing process can be started using the `sw:es:index:populate` console command: ```bash $ php bin/console sw:es:index:populate @@ -391,7 +410,12 @@ Indexing products ``` ### Additional synchronisation @@ -402,7 +426,7 @@ The first step to synchronize blog entries is registering the `ORMBacklogSubscri ```php container = $container; } + /** + * {@inheritdoc} + */ public function getSubscribedEvents() { - return array(Events::onFlush, Events::postFlush); + return [ + Events::onFlush, + Events::postFlush + ]; } + /** + * @param OnFlushEventArgs $eventArgs + */ public function onFlush(OnFlushEventArgs $eventArgs) { /** @var $em ModelManager */ - $em = $eventArgs->getEntityManager(); + $em = $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); // Entity deletions @@ -483,6 +516,9 @@ class ORMBacklogSubscriber implements EventSubscriber } } + /** + * @param PostFlushEventArgs $eventArgs + */ public function postFlush(PostFlushEventArgs $eventArgs) { foreach ($this->inserts as $entity) { @@ -521,6 +557,10 @@ class ORMBacklogSubscriber implements EventSubscriber $this->queue = []; } + /** + * @param ModelEntity $entity + * @return Backlog + */ private function getDeleteBacklog($entity) { switch (true) { @@ -529,6 +569,10 @@ class ORMBacklogSubscriber implements EventSubscriber } } + /** + * @param ModelEntity $entity + * @return Backlog + */ private function getInsertBacklog($entity) { switch (true) { @@ -538,6 +582,10 @@ class ORMBacklogSubscriber implements EventSubscriber } } + /** + * @param ModelEntity $entity + * @return Backlog + */ private function getUpdateBacklog($entity) { switch (true) { @@ -548,26 +596,27 @@ class ORMBacklogSubscriber implements EventSubscriber } ``` -It separates between update, delete and insert actions of the Shopware blog model. This class can be used as reference for other implementations. +It separates between update, delete and insert actions of the Shopware blog model. +This class can be used as reference for other implementations. To prevent database operation inside a Doctrine flush event, the `postFlush` function registers a dynamic event listener for the `Enlight_Controller_Front_DispatchLoopShutdown` event, which inserts new backlogs entries at the end of the request. New backlogs can be easily added using the `shopware_elastic_search.backlog_processor` service. A `Backlog` struct contains the following data structure: - `event` > which can be defined manually - `payload` > data which will be saved as json string -Changes to blog entries are traced and stored in the `s_es_backlog` table. The synchronisation process will take place when the `sw:es:backlog:sync` command is called. +Changes to blog entries are traced and stored in the `s_es_backlog` table. +The synchronisation process will take place when the `sw:es:backlog:sync` command is called. To handle the backlog queue, it is required to register an additional `SynchronizerInterface`, which looks as follows: ```php subscribeEvent('Enlight_Bootstrap_AfterInitResource_shopware_search.product_search', 'decorateProductSearch'); - //... -} - -public function decorateProductSearch() -{ - $service = new BlogSearch( - $this->get('shopware_elastic_search.client'), - $this->get('shopware_search.product_search'), - $this->get('shopware_elastic_search.index_factory') - ); - Shopware()->Container()->set('shopware_search.product_search', $service); -} +Now the plugin has to [decorate](/developers-guide/shopware-5-core-service-extensions/) the ```ProductSearch``` to search for blog entries. +This is possible using following xml code: + +```xml + + + + + + ``` The `BlogSearch` looks as follows: @@ -659,7 +703,7 @@ The `BlogSearch` looks as follows: ```php coreService->search($criteria, $context); - if (!$criteria->hasCondition('search')) { - return $result; - } - - $blog = $this->searchBlog($criteria, $context); + if ($criteria->hasCondition('search')) { + $blog = $this->searchBlog($criteria, $context); - $result->addAttribute( - 'swag_blog_es_search', - new Struct\Attribute(['blog' => $blog]) - ); + $result->addAttribute( + 'swag_elastic_search', + new Struct\Attribute(['blog' => $blog]) + ); + } return $result; } @@ -725,26 +767,37 @@ class BlogSearch implements ProductSearchInterface { /**@var $condition SearchTermCondition*/ $condition = $criteria->getCondition('search'); - $query = new MultiMatchQuery( - ['title', 'shortDescription', 'longDescription'], - $condition->getTerm(), - ['operator' => 'AND'] - ); + $query = $this->createMultiMatchQuery($condition); $search = new Search(); $search->addQuery($query); $search->setFrom(0)->setSize(5); $index = $this->indexFactory->createShopIndex($context->getShop()); - $raw = $this->client->search([ + $params = [ 'index' => $index->getName(), 'type' => 'blog', 'body' => $search->toArray() - ]); + ]; + + $raw = $this->client->search($params); return $this->createBlogStructs($raw); } + /** + * @param SearchTermCondition $condition + * @return MultiMatchQuery + */ + private function createMultiMatchQuery(SearchTermCondition $condition) + { + return new MultiMatchQuery( + ['title', 'shortDescription', 'longDescription'], + $condition->getTerm(), + ['operator' => 'AND'] + ); + } + /** * @param $raw * @return array @@ -827,42 +880,36 @@ The `indexFactory` is used to get the index name based on the current shop of th As result, the client returns an array with all ES information of the search request: ```php -Array -( +[ [took] => 1 [timed_out] => - [_shards] => Array - ( - [total] => 5 - [successful] => 5 - [failed] => 0 - ) - [hits] => Array - ( - [total] => 1 - [max_score] => 1.0521741 - [hits] => Array - ( - [0] => Array - ( - [_index] => sw_shop2_20150707155554 - [_type] => blog - [_id] => 6 - [_score] => 1.0521741 - [_source] => Array - ( - [id] => 6 - [title] => The summer will be colorful - [shortDescription] => This summer is going to be colorful. Brightly colored clothes are the must-have for every style-conscious woman. - [longDescription] => This summer is going to be colorful. Brightly colored clothes are the must-have for every style-conscious woman. Whether lemon-yellow top, grass-green chinos or pink clutch – with these colors you will definitely be an eye catcher. And this year we even go one step further. - [metaTitle] => - [metaKeywords] => - [metaDescription] => - ) - ) - ) - ) -) + [_shards] => [ + [total] => 5 + [successful] => 5 + [failed] => 0 + ] + [hits] => [ + [total] => 1 + [max_score] => 1.0521741 + [hits] =>[ + [0] => [ + [_index] => sw_shop2_20150707155554 + [_type] => blog + [_id] => 6 + [_score] => 1.0521741 + [_source] => [ + [id] => 6 + [title] => The summer will be colorful + [shortDescription] => This summer is going to be colorful. Brightly colored clothes are the must-have for every style-conscious woman. + [longDescription] => This summer is going to be colorful. Brightly colored clothes are the must-have for every style-conscious woman. Whether lemon-yellow top, grass-green chinos or pink clutch – with these colors you will definitely be an eye catcher. And this year we even go one step further. + [metaTitle] => + [metaKeywords] => + [metaDescription] => + ] + ] + ] + ] +] ``` This data will be hydrated and converted to Blog structs using the `createBlogStructs` function: @@ -892,23 +939,16 @@ $result->addAttribute( ``` ### Additional ES analyzer -The basic blog data should be indexed and searchable with the custom ES analyzer. To add an additional analyzer, the `SettingsInterface` of the `ESIndexingBundle` can be used. -The plugin bootstrap registers an additional event to add the new `BlogSettings`: - -```php -use ShopwarePlugins\SwagESBlog\ESIndexingBundle\BlogSettings; -//... - -public function install() -{ - $this->subscribeEvent('Shopware_ESIndexingBundle_Collect_Settings', 'addSettings'); - //... -} - -public function addSettings() -{ - return new BlogSettings(); -} +The basic blog data should be indexed and searchable with the custom ES analyzer. +To add an additional analyzer, the `SettingsInterface` of the `ESIndexingBundle` can be used. +The plugin service.xml registers an additional service to add the new `BlogSettings`: + +```xml + + + + + ``` The BlogSettings class contains the following source code: @@ -916,7 +956,7 @@ The BlogSettings class contains the following source code: ```php subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_elastic_search.product_mapping', - 'decorateProductMapping' - ); - $this->subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_elastic_search.product_provider', - 'decorateProductProvider' - ); - return true; - } - - public function afterInit() - { - $this->get('Loader')->registerNamespace('ShopwarePlugins\SwagElasticSearch', $this->Path()); - } - - public function decorateProductMapping() - { - /** @var \Shopware\Bundle\ESIndexingBundle\MappingInterface $mapping */ - $mapping = $this->get('shopware_elastic_search.product_mapping'); - Shopware()->Container()->set( - 'shopware_elastic_search.product_mapping', - new ProductMapping($mapping) - ); - } - - public function decorateProductProvider() - { - /** @var ProductProviderInterface $provider */ - $provider = $this->get('shopware_elastic_search.product_provider'); - - Shopware()->Container()->set( - 'shopware_elastic_search.product_provider', - new ProductProvider($provider) - ); - } -} +The plugin service.xml looks as follows: + +```xml + + + + + + + + + ``` -Each event listener is registered on the `AfterInitResource` event to decorate the associated service. The new services has a dependency on the original service, otherwise the new service would override the original implementation. The `ProductMapping` class looks as follows: ```php subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_search_es.search_term_query_builder', - 'decorateSearchTermQueryBuilder' - ); - //... -} - -public function decorateSearchTermQueryBuilder() -{ - $searchTermQueryBuilder = new SearchTermQueryBuilder( - $this->get('shopware_search_es.search_term_query_builder') - ); - Shopware()->Container()->set('shopware_search_es.search_term_query_builder', $searchTermQueryBuilder); -} +Now it is time to extend the product search query, so it handles the new `my_name` field. +The product search query for ES is build using the `SearchTermQueryBuilderInterface` of the `SearchBundleES`, which can be decorated like all other services of the DI container: + +```xml + + + + ``` The `SearchTermQueryBuilder` contains only one `buildQuery` function that returns a `ONGR\ElasticsearchDSL\Query\BoolQuery`, which can contain different sub queries. @@ -1188,7 +1184,7 @@ The new `SearchTermQueryBuilder` contains the following source code: ```php decoratedQueryBuilder->buildQuery($context, $term); $matchQuery = new MultiMatchQuery( - ['attributes.properties.swag_es_product.my_name'], + ['attributes.swag_es_product.my_name'], $term ); $query->add($matchQuery, BoolQuery::SHOULD); @@ -1286,12 +1282,13 @@ ONGR\ElasticsearchDSL\Query\BoolQuery Object ) ``` -In this case, the query contains two `MultiMatchQuery` sub queries as a `SHOULD` query. In addition, the query contains a `minimum_should_match` parameter with a value of `1`, which means that one of the queries has to match. +In this case, the query contains two `MultiMatchQuery` sub queries as a `SHOULD` query. +In addition, the query contains a `minimum_should_match` parameter with a value of `1`, which means that one of the queries has to match. The new `SearchTermQueryBuilder` now adds an additional `MultiMatchQuery` for the new field `my_name` which is stored in `attributes.properties.swag_es_product`: ```php $matchQuery = new MultiMatchQuery( - ['attributes.properties.swag_es_product.my_name'], + ['attributes.swag_es_product.my_name'], $term ); $query->add($matchQuery, BoolQuery::SHOULD); @@ -1349,7 +1346,7 @@ ONGR\ElasticsearchDSL\Query\BoolQuery Object ( [fields] => Array ( - [0] => attributes.properties.swag_es_product.my_name + [0] => attributes.swag_es_product.my_name ) [query] => Spachtel @@ -1368,58 +1365,42 @@ ONGR\ElasticsearchDSL\Query\BoolQuery Object In the following example, the plugin adds a new `Facet`, `Sorting` and `Condition` for the product listing, which accesses the product sales field. This example can be used for each other field which is indexed for products. First, the plugin has to register his own criteria parts (`Facet`, `Sorting` and `Condition`) with a `CriteriaRequestHandler`. -The criteria part classes have no dependencies on any search implementation like DBAL or ES. They are defined as abstract and only describe how the search should be executed. +The criteria part classes have no dependencies on any search implementation like DBAL or ES. +They are defined as abstract and only describe how the search should be executed. The plugin bootstrap contains the additional source code: -```php -subscribeEvent('Shopware_SearchBundle_Collect_Criteria_Request_Handlers', 'addCriteriaRequestHandler'); - $this->subscribeEvent('Shopware_SearchBundleES_Collect_Handlers', 'addSearchHandlers'); - - return true; - } - - public function addCriteriaRequestHandler() - { - return new CriteriaRequestHandler(); - } - - public function addSearchHandlers() - { - return new ArrayCollection([ - new SalesFacetHandler(), - new SalesConditionHandler(), - new SalesSortingHandler() - ]); - } - - //... -} +```xml + + + + + + + + + + + + + + + ``` -The `Shopware_SearchBundle_Collect_Criteria_Request_Handlers` event allows you to register an additional `CriteriaRequestHandler`. -By registering the `Shopware_SearchBundleES_Collect_Handlers` it is possible to register additional handlers for the `SearchBundleES`. +The `criteria_request_handler` tag allows you to register an additional `CriteriaRequestHandler`. +By using the `shopware_search_es.search_handler` tag it is possible to register additional handlers for the `SearchBundleES`. Each criteria part should have its own handler class. -First, the plugin has to add the new criteria parts into the criteria for listings. This will be handled in the `CriteriaRequestHandler`, which is called if a Criteria class must be generated with the request parameters: +First, the plugin has to add the new criteria parts into the criteria for listings. +This will be handled in the `CriteriaRequestHandler`, which is called if a Criteria class must be generated with the request parameters: ```php getParam('minSales', null); @@ -1774,7 +1760,7 @@ This `SalesCondition` has its own `SalesConditionHandler` which looks as follows ```php Filters the result after the aggregations calculated 2. normal filter > Filters the result before the aggregations calculated -This can be checked using the criteria object, by using the `hasBaseCondition` function. If the `criteriaPart` is a `BaseCondition`, the filter has to be added as normal filter, otherwise as post filter. +This can be checked using the criteria object, by using the `hasBaseCondition` function. +If the `criteriaPart` is a `BaseCondition`, the filter has to be added as normal filter, otherwise as post filter. ```php if ($criteria->hasBaseCondition($criteriaPart->getName())) { @@ -1877,7 +1864,7 @@ This is handled by the `SalesSortingHandler`: ```php hasBaseCondition` statement: + +```php +public function handle( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context +) { + if ($criteria->hasBaseCondition($criteriaPart->getName())) { + $search->addFilter(new TermQuery('active', 1)); + } else { + $search->addPostFilter(new TermQuery('active', 1)); + } +} +``` + +This behavior is now controlled in the `\Shopware\Bundle\SearchBundleES\ProductNumberSearch`. +To support the new filter mode, each condition handler has to implement the `\Shopware\Bundle\SearchBundleES\PartialConditionHandlerInterface`. +It is possible to implement this interface alongside the original `\Shopware\Bundle\SearchBundleES\HandlerInterface`. + +```php +namespace Shopware\Bundle\SearchBundleES; +if (!interface_exists('\Shopware\Bundle\SearchBundleES\PartialConditionHandlerInterface')) { + interface PartialConditionHandlerInterface { } +} + +namespace Shopware\SwagBonusSystem\Bundle\SearchBundleES; + +class BonusConditionHandler implements HandlerInterface, PartialConditionHandlerInterface +{ + const ES_FIELD = 'attributes.bonus_system.has_bonus'; + + public function supports(CriteriaPartInterface $criteriaPart) + { + return ($criteriaPart instanceof BonusCondition); + } + + public function handleFilter( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context + ) { + $search->addFilter( + new TermQuery(self::ES_FIELD, 1) + ); + } + + public function handlePostFilter( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context + ) { + $search->addPostFilter(new TermQuery(self::ES_FIELD, 1)); + } + + public function handle( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context + ) { + if ($criteria->hasBaseCondition($criteriaPart->getName())) { + $this->handleFilter($criteriaPart, $criteria, $search, $context); + } else { + $this->handlePostFilter($criteriaPart, $criteria, $search, $context); + } + } +} +``` +## ElasticSearch Logging +In Shopware 5.5.5 we added an elasticsearch logger which logs several ES requests and their responses. +In addition to that, you'll get an evaluation of the indexing process after each step by default. +The following options have been added to the index populate command for both, frontend and backend indexing: + +| Option | Description | +|-----------------|-----------------------------------------------| +| --no-evaluation | Disable the evaluation | +| --stop-on-error | Abort the indexing process if an error occurs | + +An example indexing process that uses the `--stop-on-error` option could look like this: +```bash +$ sudo bin/console sw:es:index:populate --stop-on-error + + +## Indexing shop Deutsch ## +Indexing properties + 5/5 [============================] 100% < 1 sec/< 1 sec + + Evaluation: + Total: 5 items + Error: 0 items + Success: 5 items + + +Indexing products + +In ConsoleEvaluationHelper.php line 183: + + An error occured: + SW10002.3: mapper [calculatedPrices.EK_1.rule.unit.purchaseUnit] cannot be changed from type [long] to [float] + +``` + +As already mentioned do these options work with both, the `sw:es:index:populate` and the `sw:es:backend:index:populate` command. + +An occurred error can be viewed in detail via the system log backend view (Configuration > Logfile > System log) by selecting the correct ES logging file (something like `es_production-2018-12-06.log`). + +![elasticsearch_system_log_detail](elasticsearch_system_log_detail.png) + +The log files contain response and request data (in this order) for several ES requests. +In production environments only errors will be logged by default. +For development environments each request will be logged by default. +Please notice that if every ES request is logged, the log files will become accordingly large. +It is not recommended to log debug info in production environments. +You can customize the log level via your `config.php` from the default behaviour to fit your own needs (you can find the available log levels [here](https://github.com/Seldaek/monolog/blob/master/doc/01-usage.md#log-levels)): + +```php +// ... +'es' => [ + // ... + 'logger' => [ + 'level' => \Shopware\Components\Logger::DEBUG, + ], +], +// ... +``` diff --git a/source/developers-guide/emotion-preset-plugin/img/screen-presets.jpg b/source/developers-guide/emotion-preset-plugin/img/screen-presets.jpg new file mode 100644 index 0000000000..c0b3b84853 Binary files /dev/null and b/source/developers-guide/emotion-preset-plugin/img/screen-presets.jpg differ diff --git a/source/developers-guide/emotion-preset-plugin/img/screen-structure.jpg b/source/developers-guide/emotion-preset-plugin/img/screen-structure.jpg new file mode 100644 index 0000000000..4eeb3f003b Binary files /dev/null and b/source/developers-guide/emotion-preset-plugin/img/screen-structure.jpg differ diff --git a/source/developers-guide/emotion-preset-plugin/index.md b/source/developers-guide/emotion-preset-plugin/index.md new file mode 100644 index 0000000000..7f1aec6a6e --- /dev/null +++ b/source/developers-guide/emotion-preset-plugin/index.md @@ -0,0 +1,117 @@ +--- +layout: default +title: Create custom emotion preset plugin +github_link: developers-guide/emotion-preset-plugin/index.md +indexed: true +group: Developer Guides +tags: + - shopping worlds + - presets +subgroup: Tutorials +menu_title: Create custom emotion preset plugin +menu_order: 30 +--- + +
    + +Preset selection + +## Introduction +With __Shopware 5.3__ we introduced shopping world / emotion presets. This tutorial will point out how to create a custom emotion preset plugin to offer your created shopping worlds to other users. These kind of plugins +can be contributed to the Community Store like any other plugin. You just have to set a special flag in your Account settings for deployment, so that your plugin gets marked as emotion preset. + +### Qualification +Before getting started with this tutorial, it is recommended to become familiar with creating Shopware plugins first, since this guide will only point out the extras for creating a __custom emotion preset plugin__. For further information on developing plugins see [our plugin guides](/plugin-guide). + +## Plugin Structure +The structure of the example plugin is as follows: + +Directory structure + +The example plugin is based on the Shopware 5.2 plugin system. + +## Creating Preset Data +Creating preset data is really simple, because you can easily use the backend export function for shopping worlds. This function will +prepare your shopping worlds data as needed for a plugin. It will extract all assets from the shopping worlds and save all settings and data in the preset `*.json` file. + +### Create simple dataclasses for better maintainability +As you can see in the example plugin, which you can find at the end of this article, we created a simple class for our preset which implements the `Shopware\Components\Emotion\Preset\PresetMetaDataInterface`. This interface is +required for the preset import during plugin installation. + +We have some simple getters here that are required to be implemented. With this getters all information +of your preset will be collected during installation. + +The `getPresetData()` method uses a simple helper class that gets the json data from our export and replaces a path placeholder `__ASSETPATH__` we are using +with the actual path to the assets inside our plugin. + +```php +/** + * @return array + */ +public function getPresetData() +{ + $json = SwagEmotionPresetExample::getJsonData('MyCustomPreset.json'); + return json_decode($json, true); +} +``` + +The exported `json` data will have all information about the assets that are exported. + +```json +"syncData": { + "assets": { + "8b16ebc056e613024c057be590b542eb": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach_teaser5038874e87338.jpg", + "1728efbda81692282ba642aafd57be3a": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/deli_teaser503886c2336e3.jpg", + "db85e2590b6109813dafa101ceb2faeb": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/flip_teaser503886e4dd480.jpg", + "99c5e07b4d5de9d18c350cdf64c5aa3d": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/bienen_teaser.jpg", + "e995f98d56967d946471af29d7bf99f1": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach1503f8532d4648.jpg", + "6cd67d9b6f0150c77bda2eda01ae484c": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach2503f8535275aa.jpg", + "6bc24fc1ab650b25b4114e93a98f1eba": "file:\/\/___ASSETPATH___\/media\/MyCustomPreset\/beach3503f853820fa7.jpg" + } + } +``` +Here we changed the paths from `images\imagename.png` to `file://__ASSETPATH__/media/PRESETNAME/IMAGENAME.png`. Be aware that +the slashes have to be escaped because otherwise we would have illegal json data. + +We advise you to use an external source for image data because of the size limitation for plugins in our Community Store. + +## Installation +For the installation of your presets during plugin installation you can use the `PresetInstaller` service which is available via the service container. The following example shows how installation and +uninstallation are done by the example plugin. + +```php +public function install(InstallContext $context) +{ + /** @var PresetInstaller $presetInstallerService */ + $presetInstallerService = $this->container->get('shopware.emotion.preset_installer'); + + $presetInstallerService->installOrUpdate($this->getPresetInstances()); +} + +public function uninstall(UninstallContext $context) +{ + /** @var PresetInstaller $presetInstallerService */ + $presetInstallerService = $this->container->get('shopware.emotion.preset_installer'); + + $presetInstallerService->uninstall([ + 'my_custom_preset', + ]); +} + +/** + * @return PresetMetaDataInterface[] + */ +private function getPresetInstances() +{ + return [ + new MyCustomPreset() + ]; +} +``` + +During installation we pass an array of `PresetMetaDataInterfaces` to the `PresetInstaller`. + +For uninstallation we just use the __technical__ name of our emotion preset. + +## Download +The example plugin can be found here diff --git a/source/developers-guide/entity-relationship-model/index.md b/source/developers-guide/entity-relationship-model/index.md index 4d8b76a53f..9f1cd6d2ef 100644 --- a/source/developers-guide/entity-relationship-model/index.md +++ b/source/developers-guide/entity-relationship-model/index.md @@ -7,7 +7,7 @@ indexed: true tags: - ERD - ERM - - diagramm + - diagram - entity - model group: Developer Guides diff --git a/source/developers-guide/event-guide/index.md b/source/developers-guide/event-guide/index.md index d6cdc4a6ef..cef28c77a9 100644 --- a/source/developers-guide/event-guide/index.md +++ b/source/developers-guide/event-guide/index.md @@ -15,7 +15,6 @@ specifically
    - ## What is an "event system"? Event systems also known as [publish subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) are basically a pattern to layout software. They allow to emit an event at any point in the software - and let other pieces @@ -51,51 +50,79 @@ Generally when talking about events, three parties are involved: * the *event manager* dispatching the event #### Registering an event +First create an event subscriber class in `PluginName/Subscriber/` that implements the `Enlight\Event\SubscriberInterface`. +The interface determines that the `getSubscribedEvents` method must be implemented. +To register an event subscriber in the services.xml use the `shopware.event_subscriber` tag. +A very simple example could look like this: -A very naive example could look like this: - +```xml + + + ``` -Shopware()->Events()->addListener( - 'ORDER_FINISHED', - function($payload) { - echo "ORDER_FINISHED just occurred with an order amount of: $payload['amount']"; + +```php + 'onFrontendListing' + ]; } -); + + public function onFrontendListing(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Frontend_Listing $subject */ + $subject = $args->getSubject(); + + // Do some magic with the listing data + } +} ``` -It will basically tell the Shopware event manager: If an event with the name 'ORDER_FINISHED' occurs, please execute the given callback. There are **more sophisticated** ways to register an event in Shopware - but this is the general mechanism. -Technically this could already be a very simple plugin, that runs some code, when ORDER_FINISHED is emitted. +It will basically tell the Shopware event manager: If an event with the name 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Listing' occurs, please execute +the given callback. #### Emitting the event +Now how does Shopware emit those events? Consider this example of the `indexAction` of a controller, which runs, when you call the url `http://your-domain.com/SwagEvents/`: -Now how does Shopware emit those events? Consider this example of `Basket` class, which runs, when a customer finishes -an order: +```php +items); - $this->em->persist($basket); - $this->em->flush(); + // set no renderer is only for testing without creating a template. + $this->container->get('front')->Plugins()->ViewRenderer()->setNoRender(); + + // do some magic + + $this->container->get('events')->notify( + 'SwagEvent_Controller_indexAction', // give the event a unique name and add the payload + [ + 'payload' => 123, + 'payload2' => 'more Payload', + ] + ); - // emitting the ORDER_FINISHED event - Shopware()->Events()->notify('ORDER_FINISHED', ['amount' => 300, 'products' => $this->items]); + // do some magic } } ``` -In the first lines of the `buy` method the current basket is saved to the database. They are not relevant for the event. -The last line of the method will trigger the actual event: -``` -Shopware()->Events()->notify('ORDER_FINISHED', ['amount' => 300, 'products' => $this->items]) -``` +This will emit the `SwagEvent_Controller_indexAction` event. The event manager will now call any subscriber, who registered to this event. +The second parameter is the *payload* of that event - some additional context information. -This will emit the 'ORDER_FINISHED' event; the event manager will now call any subscriber, who registered to this event. -The second parameter is the *payload* of that event - some additional context information. In our example the basket amount -plus the items of the basket. Our event subscriber from the example above will print that amount. +The event subscriber will print out the $args object and the payload. ## Events in Shopware Events in Shopware do work pretty much as described above. There are some details, however, that will help you writing @@ -105,67 +132,120 @@ plugins. The *payload* of an event is passed to the *subscriber* with a simple context object called `Enlight_Event_EventArgs`. This is basically a container object, that will give you access to the payload. Given the example from above: -``` -Shopware()->Events()->notify('ORDER_FINISHED', ['amount' => 300, 'products' => $this->items]); +```php +$this->container->get('events')->notify( + 'SwagEvent_Controller_Index_After_Do_Some_Magic', + [ + 'payload' => 123, + 'payload2' => 'more Payload', + ] +); ``` You can access the *payload* like this: +```xml + + + ``` -public function myEventSubscriber(\Enlight_Event_EventArgs $args) + +```php +get('amount'); // 300 - $products = $args->get('products'); // the items of the basket - + public static function getSubscribedEvents() + { + return [ + 'SwagEvent_Controller_notifyAction' => 'onNotify' + ]; + } + + public function onNotify(\Enlight_Event_EventArgs $args) + { + $payload1 = $args->get('payload'); + $payload2 = $args->get('payload2'); + $payload3 = $args->get('yourPayload'); + } } ``` ### Event types -In the examples above, the `ORDER_FINISHED` event was emitted as a `notify` event: The subscriber was just informed about -the event - but there was no way to modify something. Shopware knows 4 different event types: `notify`, `notifyUntil`, -`filter` and `collect`. These 4 types behave differently and are useful in different ways. +In the examples above, the `SwagEvent_Controller_Index_After_Do_Some_Magic` event was emitted as a `notify` event: The subscriber was just informed about +the event - but there is no way to modify something. Shopware knows four different event types: + +* `notify` +* `notifyUntil` +* `filter` +* `collect` + +These four types behave differently and are useful in different ways. #### notify As described in the example above: -``` -Shopware()->Events()->notify('ORDER_FINISHED', ['amount' => 300, 'products' => $this->items]); +```php +$this->container->get('events')->notify( + 'SwagEvent_Controller_notifyAction', + [ + 'payload' => 123, + 'payload2' => 'more Payload', + 'yourPayload' => 'LoremIpsum', + ] +); ``` -This will emit the `ORDER_FINISHED` event and allow you to *read* `amount` and `products`. A modification is usually +This will emit the `SwagEvent_Controller_notifyAction` event and allow you to *read* `payload`, `payload2` and `yourPayload`. A modification is usually not possible, except an object is passed (those could be modified by reference). #### notifyUntil A `notifyUntil` event is usually used to allow you to *stop* Shopware from doing something: -``` -if (Shopware()->Events()->notifyUntil('ALLOW_ORDER', ['amount' => 300, 'userId' => 3])) { - echo 'failed'; - return false; +```php +public function notifyUntilAction() +{ + // set no renderer is only for testing without creating a template. + $this->container->get('front')->Plugins()->ViewRenderer()->setNoRender(); + + // do some magic + + $stop = $this->container->get('events')->notifyUntil( + 'SwagEvent_Controller_notifyUntilAction', + [ + 'someData' => $someData + ] + ); + + if ($stop) { + return; + } + + echo 'Do some magic'; } -echo 'success'; -``` -Now imagine, you subscribed to the `ALLOW_ORDER` event with the following event callback: +Now imagine, you subscribed to the `SwagEvent_Controller_Index_After_Do_Some_Magic_Notify_Until` event with the following event callback: -``` -public function myEventSubscriber(\Enlight_Event_EventArgs $args) +```php +public function onNotifyUntil(\Enlight_Event_EventArgs $args) { - $amount = $args->get('amount'); // 300 - $userId = $args->get('userId'); // 3 - - if ($userId == 3 && $amount > 400) { + $someData = $args->get('someData'); + + // Some condition + if ($someData === true) { + // if you return some result you stop the callStack return true; } - - return null; - } ``` -In this case, we disallow orders for the user with id `3` if the order amount is greater 400. The general rule here: +In this case, we disallow `echo 'Do some magic';` if the 'stop' payload is true. The general rule here: Return `null` if you want Shopware to proceed; return anything else to stop Shopware from proceeding. In the example above -Shopware will proceed and print `success`, as the the condition `$amount > 400` is false. +Shopware will proceed and print the results; Some real world examples for `notifyUntil` are: @@ -176,37 +256,52 @@ Some real world examples for `notifyUntil` are: #### filter The `filter` event allows you to *modify* certain data. It is often used to allow you to modify a computed result set: -``` -public function getArticlesForCategory($categoryId) +```php +public function filterAction() { - $result = $this->categoryService->fetch($categoryId); // $result = ['sw-123', 'sw-456', 'sw-789'] - - $result = Shopware()->Events()->filter('CATEGORY_ARTICLES', $result, ['categoryId' => $categoryId]); + $result = [ + ['id' => 1], + ['id' => 2], + ['id' => 3], + ]; + + $eventManager = $this->container->get('events'); - return $result; + $result = $eventManager->filter('SwagEvent_Controller_filterAction', 'some value', ['data' => $result]); + + echo '
    ';
    +    var_export($result);
    +    echo '
    '; + die('END'); } ``` -In this example a list of articles for a given category has been loaded. The `CATEGORY_ARTICLES` event would allow you +In this example a list of ids has been loaded. The `SwagEvent_Controller_Filter` event would allow you to modify this list and return a modified set with your subscriber: -``` -public function myEventSubscriber(\Enlight_Event_EventArgs $args) +```php +public function onFilter(\Enlight_Event_EventArgs $args) { - $amount = $args->get('categoryId'); - $result = $args->getReturn(); - - $result[] = 'sw-abc'; - - return $result; - + $return = $args->get('data'); + $value = $args->getReturn(); + + if ($value === 'some value') { + foreach ($return as $key => $value) { + if ($value['id'] === 2) { + $return[$key] = [ + 'id' => 178 + ]; + } + } + } + + return $return; } ``` -In this example, the method `getArticlesForCategory` would return three article numbers for the given categoryId: -`sw-123`, `sw-456` and `sw-789`. The event subscriber reads the original result using the call `$args->get('return')` and adds -the article number `sw-abc` to the result set. This way, every call to that method would include the article `sw-abc`, -no matter if it was included originally or not. +In this example, the method `filterAction` would return three ids for scenario: +`'id' => 1`, `'id' => 2` and `'id' => 3`. The event subscriber reads the original result using the call `$args->get('data')` +and removes the `'id' => 2` in the result set. This way, every call to that method would replace the id 2, Some real world examples for `filter` are: @@ -218,30 +313,68 @@ Some real world examples for `filter` are: The `collect` event is used in places, where Shopware wants to allow you to register e.g. handlers for certain situations. The following example would (by default) just print `hello`: -``` -$textPrinter = Shopware()->Events()->collect('TEXT_PRINTER', new ArrayCollection(['hello'])); -foreach($textPrinter as $text) { - echo $text . " "; -} +```php +public function collectAction() +{ + $collection = new ArrayCollection([ + new \SwagEvents\Components\NameClass1(), + new \SwagEvents\Components\NameClass2() + ]); -``` + $eventManager = $this->container->get('events'); + $eventManager->collect('SwagEvent_Controller_collectAction', $collection); -The following subscriber will add additional words to the `TEXT_PRINTER` event: + foreach ($collection->toArray() as $nameClass) { + echo $nameClass->getName(); + echo '
    '; + } + echo '
    ';
    +    \Doctrine\Common\Util\Debug::dump($collection->toArray());
    +    echo '
    '; + die('END'); +} ``` -public function myEventSubscriber(\Enlight_Event_EventArgs $args) + +The following subscriber will add additional classes to the `SwagEvent_Controller_collectAction` event: + +```php +public function onCollect(\Enlight_Event_EventArgs $args) { - return new ArrayCollection([ - 'world', - 'how', - 'are', - 'you' - ]); - + return new ArrayCollection( + [ + new NameClass3(), + new NameClass4() + ] + ); } ``` -Now the script would print `hello world how are you`. +Now the script would print: + +```text +SwagEvents\Components\NameClass1 +SwagEvents\Components\NameClass2 +SwagEvents\Components\NameClass3 +SwagEvents\Components\NameClass4 + +/var/www/html/doku/vendor/doctrine/common/lib/Doctrine/Common/Util/Debug.php:71: +array (size=4) + 0 => + object(stdClass)[1319] + public '__CLASS__' => string 'SwagEvents\Components\NameClass1' (length=32) + 1 => + object(stdClass)[1316] + public '__CLASS__' => string 'SwagEvents\Components\NameClass2' (length=32) + 2 => + object(stdClass)[1322] + public '__CLASS__' => string 'SwagEvents\Components\NameClass3' (length=32) + 3 => + object(stdClass)[1323] + public '__CLASS__' => string 'SwagEvents\Components\NameClass4' (length=32) + +END +``` Some real world examples for `collect` are: @@ -249,78 +382,78 @@ Some real world examples for `collect` are: * `Shopware_SearchBundleDBAL_Collect_Facet_Handlers`: Register a facet handler for the DBAL gateway * `Theme_Compiler_Collect_Plugin_Javascript`: Collect javascript files for the JS compiler -## Events and plugins -Usually you want to use events from within your plugins. There are generally two ways to do that: +You can download an example plugin here -### subscribeEvent -The default way (recommended for beginners) is the `subscribeEvent` method: +## Plugins use Subscribers -``` -class Shopware_Plugins_Frontend_SwagMyPlugin_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend', - 'onFrontendPostDispatch' - ); - - - return true; - } - - public function onFrontendPostDispatch(Enlight_Event_EventArgs $args) - { - /** @var \Enlight_Controller_Action $controller */ - $controller = $args->get('subject'); - $view = $controller->View(); - - $view->assign('day', date("l")); - - } -} -``` +The default way is to use subscriber classes that are registered in the `PluginBaseDirectory/Resources/services.xml`: +Subscribers are basically custom classes which register to Shopware events. This way, the plugin base class doesn't know about the +events, which can, in turn, be encapsulated in corresponding classes. -Using this approach, you can subscribe to events in the `install` method of your plugin using the `subscribeEvent` call. -For this call, you just pass the event name as first parameter and the name of your callback function as second parameter -(in this case it is `onFrontendPostDispatch`). The shopware event manager will now automatically call this callback -as soon as the event `Enlight_Controller_Action_PostDispatchSecure_Frontend` occurs. +First create an event subscriber class in `PluginName/Subscriber/` that implements the `Enlight\Event\SubscriberInterface`. +The interface determines that the method `getSubscribedEvents` must be implemented and must return a array. +For this call, you just pass the event name as array key and the name of your callback function as array value. -### Subscribers -The above example is good for beginners - but bad for bigger plugins: It will bloat the `Bootstrap.php` file with -a lot of event callbacks - and makes it hard to separate concerns. From our experience it will result in plugins that -are hard to maintain. For that reason we recommend the *subscriber pattern*: +```php +subscribeEvent('Enlight_Controller_Front_StartDispatch', 'onRegisterSubscriber'); - $this->subscribeEvent('Shopware_Console_Add_Command', 'onRegisterSubscriber'); + return [ + 'SwagEvent_Controller_collectAction' => 'onCollect', + ]; + } - return true; + /** + * @param \Enlight_Event_EventArgs $args + * + * @return ArrayCollection + */ + public function onCollect(\Enlight_Event_EventArgs $args) + { + return new ArrayCollection( + [ + new NameClass3(), + new NameClass4() + ] + ); } } ``` -In the `install` method of the plugin an early Shopware event called `Enlight_Controller_Front_StartDispatch` is subscribed. -It is also necessary to always subscribe to the `Shopware_Console_Add_Command`. -So still these events are needed to bring my subscribers into play - but afterwards, most other events can be -registered using subscribers. +The second step is to register the subscriber class in the `PluginBaseDirectory/Resources/services.xml`. +To tell Shopware that the registered service is a subscriber class add the `shopware.event_subscriber` tag. +```xml``` -The event callback `onRegisterSubscriber` usually looks like this: +```xml + + + + + + + ``` -public function onRegisterSubscriber(Enlight_Event_EventArgs $args) -{ - Shopware()->Events()->addSubscriber(new \Shopware\MyPlugin\MySubscriber()); -} -``` +(in this case it is `onCollect`). The shopware event manager will now automatically call this callback +as soon as the event `SwagEvent_Controller_collectAction` occurs. + +The services (subscriber) in the services.xml will be registered automatically by installing the plugin. What do the subscribers now look like? The `MySubscriber` subscriber gives a good example: @@ -464,8 +597,8 @@ After the actual controller method was run, the `postDispatch` method of the con Basically the same mechanics as for the `PreDispatch` event apply (so there is a MODULE_CONTROLLER and a MODULE suffix) as well as a "global" PostDispatch event without suffix. Notice that there are two types of PostDispatch event: `Enlight_Controller_Action_PostDispatchSecure*` will only be emitted, if a *template* is available and *no exception* -occured before; the `Enlight_Controller_Action_PostDispatch` event (without *secure* as suffix) will also be emitted -when an exception occured or no template was rendered for some reason. Usually the `Enlight_Controller_Action_PostDispatchSecure*` +occurred before; the `Enlight_Controller_Action_PostDispatch` event (without *secure* as suffix) will also be emitted +when an exception occurred or no template was rendered for some reason. Usually the `Enlight_Controller_Action_PostDispatchSecure*` events are recommended - if you are not using them, you need to perform the checks by yourself. ### Container events @@ -479,126 +612,55 @@ use this event to decorate / replace that SERVICE with your own one. The following examples should briefly show the usage of some common Shopware events. ### PostDispatch +```xml + + + ``` -class Shopware_Plugins_Frontend_SwagMyPlugin_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Checkout', - 'onFrontendPostDispatchSecure' - ); - - - return true; - } - - public function onFrontendPostDispatchSecure(Enlight_Event_EventArgs $args) - { - /** @var \Enlight_Controller_Action $controller */ - $controller = $args->get('subject'); - $view = $controller->View(); - $view->assign('hello', 'world'); - $view->addTemplateDir(__DIR__ . '/../Views'); +```php +subscribeEvent( - 'Enlight_Controller_Dispatcher_ControllerPath_Frontend_MyPlugin', - 'onRegisterMyPluginController' - ); - - - return true; - } - - public function onRegisterMyPluginController(Enlight_Event_EventArgs $args) + public static function getSubscribedEvents() { - Shopware()->Template()->addTemplateDir(__DIR__ . '/../Views'); - return __DIR__ . '/../Controllers/Frontend/MyController.php'; + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Listing' => 'onFrontendListing' + ]; } -} -``` -This will register a controller for the route `frontend/my_plugin`. Notice that in the event subscriber a template -directory is registered as well. This is useful, as it makes sure, that all your templates are available in the controller - -so the automatic template loading will work. Also notice, that the event subscriber needs to return the controller path. - -For a more convenient way to register a controller, see [the controller documentation](/developers-guide/controller/). - -### Registering a service -``` -class Shopware_Plugins_Frontend_SwagMyPlugin_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - public function install() + public function onFrontendListing(\Enlight_Event_EventArgs $args) { - $this->subscribeEvent( - 'Enlight_Bootstrap_InitResource_swag_myplugin.my_service', - 'onLoadMyService' - ); + /** @var \Shopware_Controllers_Frontend_Listing $subject */ + $subject = $args->getSubject(); - - return true; - } - - public function onLoadMyService(Enlight_Event_EventArgs $args) - { - return new MyService(); + // ... } } ``` -This example shows, how to register a service in Shopware. As soon as the service `swag_myplugin.my_service` is requested -from the DI container, Shopware will emit the event `Enlight_Bootstrap_InitResource_swag_myplugin.my_service` so you can -register to it and return an *instance* of your service. See [the service documentation](/developers-guide/services/) for -more details. - - -### Decorating / replacing a service -``` -class Shopware_Plugins_Frontend_SwagMyPlugin_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - public function install() - { - $this->subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_some_service', - 'onDecorateSomeService' - ); - - - return true; - } +This controller will subscribe to the `PostDispatchSecure_Frontend_Listing` event - so it will only be called *after* +the frontend listing controller and only if there was *no exception* and a *template is available*. Notice, that template +variables and the template directory `Resources/views` is registered. By registering the `Resources/views` directory +this way, you can overwrite / extend core templates, if you create them with the same name as in the core. See +the [plugin quick start guide](/developers-guide/plugin-quick-start/#template-extension) for more +infos. - public function onDecorateSomeService(Enlight_Event_EventArgs $args) - { - return new MyService( - Shopware()->Container()->get('some_service') - ); - } -} -``` -In this example the event `Enlight_Bootstrap_AfterInitResource_some_service` is used, to decorate the service `some_service` -with `MyService`. See the [core service extension documentation](/developers-guide/shopware-5-core-service-extensions/#your-extension-plugin) -for a more detailed explanation. +### Registering a controller +Controllers are loaded automatically if you use the directories `PluginBaseDirectory/Controllers/(Frontend | Backend | Api | Widgets)/YourController.php`. +Because of that a manual controller registration is not necessary. +Notice that when you create a controller you have to register a template directory  manually as well. +This is needed, as it makes sure, that all your templates are available in the controller - so the automatic template loading will work. +For more about controller, see [the controller documentation](/developers-guide/controller/). ## Hooks Hooks are the "last resort" for a programmer: If there is no good global or application event for your use case, hooks @@ -606,8 +668,9 @@ will allow you to literally "hook" into an existing function and execute your co Hooks are very powerful - but also very tightly bound to our internal code. As such, events should always be used whenever possible, and hooks should only be used as a last resort. For that same reason, we don't allow hooks on every -class, but only for controllers, core classes and repositories. Usually you will recognize hooks by the +class, but only for controllers, core classes and repositories. +Since Shopware 5.6 every hookable class implements the `Enlight_Hook` interface. Usually you will recognize hooks by the `FQN::METHOD::TYPE` syntax, e.g. `sBasket::sGetBasket::after`. Valid types are `before`, `replace` and `after`. There is a more detailed [blog post](https://developers.shopware.com/blog/2015/06/09/understanding-the-shopware-hook-system/) -about hooks in Shopware. +about hooks in Shopware and an [event list](/developers-guide/event-list/). diff --git a/source/developers-guide/event-list/index.md b/source/developers-guide/event-list/index.md new file mode 100644 index 0000000000..ab106df46c --- /dev/null +++ b/source/developers-guide/event-list/index.md @@ -0,0 +1,1421 @@ +--- +layout: default +title: Shopware event list +github_link: developers-guide/event-list/index.md +indexed: true +menu_title: Event list +menu_order: 55 +group: Developer Guides +subgroup: Developing plugins +--- + +If you want to hook onto Shopware you need to know the events. For more about events, see [the event guide](/developers-guide/event-guide/). The following list will give an overview of the existing events and how to subscribe to them. + +## Enlight_Controller_Front_StartDispatch + +### Definition +``` +public function dispatch() +{ + ... + $this->eventManager->notify( + 'Enlight_Controller_Front_StartDispatch', + $eventArgs + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Front_StartDispatch' => 'onEnlightControllerFrontStartDispatch' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerFrontStartDispatch(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Front */ + $enlightController = $arguments->getSubject(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Controller_EventArgs + [_processed] => + [_name] => Enlight_Controller_Front_StartDispatch + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_EnlightControllerFrontProxy + [plugins] => Enlight_Plugin_Namespace_Loader + [router] => + [dispatcher] => Enlight_Controller_Dispatcher_Default + [request] => + [response] => Enlight_Controller_Response_ResponseHttp + [throwExceptions] => + [returnResponse] => + [invokeParams] => Array(7) + [instances] => Array(0) + ) + ) +) +``` + +## Enlight_Controller_Front_RouteStartup + +### Definition +``` +public function dispatch() +{ + ... + $this->eventManager->notify( + 'Enlight_Controller_Front_StartDispatch', + $eventArgs + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Front_RouteStartup' => 'onEnlightControllerFrontRouteStartup' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerFrontRouteStartup(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Front */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Controller_EventArgs + [_processed] => + [_name] => Enlight_Controller_Front_RouteStartup + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_EnlightControllerFrontProxy + [plugins] => Enlight_Plugin_Namespace_Loader + [router] => Enlight_Controller_Router_Default + [dispatcher] => Enlight_Controller_Dispatcher_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [throwExceptions] => + [returnResponse] => + [invokeParams] => Array(7) + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/ + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => / + [_params] => Array(0) + [_rawBody] => + [_aliases] => Array(0) + [_dispatched] => + [_module] => + [_moduleKey] => module + [_controller] => + [_controllerKey] => controller + [_action] => + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(0) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Controller_Router_Route + +### Definition +``` +public function match($pathInfo, Context $context) +{ + ... + $event = $this->eventManager->notifyUntil('Enlight_Controller_Router_Route', [ + //'subject' => $router, @deprecated someone need it? + 'request' => $request, + 'context' => $context, + ] + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Router_Route' => 'onEnlightControllerRouteRoute' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerRouteRoute(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Router_Default */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Enlight_Controller_Router_Route + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Router_Default + [front] => Shopware_Proxies_EnlightControllerFrontProxy + [separator] => / + [globalParams] => Array(0) + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/ + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => / + [_params] => Array(0) + [_rawBody] => + [_aliases] => Array(0) + [_dispatched] => + [_module] => + [_moduleKey] => module + [_controller] => + [_controllerKey] => controller + [_action] => + [_actionKey] => action + ) + ) +) +``` + +## Enlight_Controller_Front_RouteShutdown + +### Definition +``` +public function dispatch() +{ + ... + $this->eventManager->notify( + 'Enlight_Controller_Front_RouteStartup', + $eventArgs + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Front_RouteShutdown' => 'onEnlightControllerFrontRouteShutdown' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerFrontRouteShutdown(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Front */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Controller_EventArgs + [_processed] => + [_name] => Enlight_Controller_Front_RouteShutdown + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_EnlightControllerFrontProxy + [plugins] => Enlight_Plugin_Namespace_Loader + [router] => Enlight_Controller_Router_Default + [dispatcher] => Enlight_Controller_Dispatcher_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [throwExceptions] => + [returnResponse] => + [invokeParams] => Array(7) + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/ + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => / + [_params] => Array(0) + [_rawBody] => + [_aliases] => Array(0) + [_dispatched] => + [_module] => + [_moduleKey] => module + [_controller] => + [_controllerKey] => controller + [_action] => + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(0) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Controller_Front_DispatchLoopStartup + +### Definition +``` +public function dispatch() +{ + ... + $this->eventManager->notify( + 'Enlight_Controller_Front_DispatchLoopStartup', + $eventArgs + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Front_DispatchLoopStartup' => 'onEnlightControllerFrontDispatchLoopStartup' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerFrontDispatchLoopStartup(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Front */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Controller_EventArgs + [_processed] => + [_name] => Enlight_Controller_Front_DispatchLoopStartup + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_EnlightControllerFrontProxy + [plugins] => Enlight_Plugin_Namespace_Loader + [router] => Enlight_Controller_Router_Default + [dispatcher] => Enlight_Controller_Dispatcher_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [throwExceptions] => + [returnResponse] => + [invokeParams] => Array(7) + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/ + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => / + [_params] => Array(0) + [_rawBody] => + [_aliases] => Array(0) + [_dispatched] => + [_module] => + [_moduleKey] => module + [_controller] => + [_controllerKey] => controller + [_action] => + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(0) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Controller_Front_PreDispatch + +### Definition +``` +public function dispatch() +{ + ... + $this->eventManager->notify( + 'Enlight_Controller_Front_PreDispatch', + $eventArgs + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Front_PreDispatch' => 'onEnlightControllerFrontPreDispatch' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerFrontPreDispatch(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Front */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Controller_EventArgs + [_processed] => + [_name] => Enlight_Controller_Front_PreDispatch + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_EnlightControllerFrontProxy + [plugins] => Enlight_Plugin_Namespace_Loader + [router] => Enlight_Controller_Router_Default + [dispatcher] => Enlight_Controller_Dispatcher_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [throwExceptions] => + [returnResponse] => + [invokeParams] => Array(7) + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/ + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => / + [_params] => Array(0) + [_rawBody] => + [_aliases] => Array(0) + [_dispatched] => 1 + [_module] => + [_moduleKey] => module + [_controller] => + [_controllerKey] => controller + [_action] => + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(0) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Controller_Action_PreDispatch* + +### Definition +``` +public function dispatch($action) +{ + ... + Shopware()->Events()->notify( + __CLASS__ . '_PreDispatch', + $args + ); + + Shopware()->Events()->notify( + __CLASS__ . '_PreDispatch_' . $moduleName, + $args + ); + + Shopware()->Events()->notify( + __CLASS__ . '_PreDispatch_' . $this->controller_name, + $args + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Action_PreDispatch_Frontend_Detail' => 'onEnlightControllerActionPreDispatchFrontendDetail' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerActionPreDispatchFrontendDetail(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Action */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Enlight_Controller_Action_PreDispatch_Frontend_Detail + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_ShopwareControllersFrontendDetailProxy + [front] => Shopware_Proxies_EnlightControllerFrontProxy + [view] => Enlight_View_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [controller_name] => Frontend_Detail + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/sommerwelten/accessoires/170/sonnenbrille-red + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => /sommerwelten/accessoires/170/sonnenbrille-red + [_params] => Array(4) + [_rawBody] => + [_aliases] => Array(1) + [_dispatched] => 1 + [_module] => frontend + [_moduleKey] => module + [_controller] => detail + [_controllerKey] => controller + [_action] => index + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(0) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Controller_Action_PostDispatch* and Enlight_Controller_Action_PostDispatchSecure* + +### Definition +``` +public function dispatch($action) +{ + ... + if ($this->Request()->isDispatched() + && !$this->Response()->isException() + && $this->View()->hasTemplate() + ) { + Shopware()->Events()->notify( + __CLASS__ . '_PostDispatchSecure_' . $this->controller_name, + $args + ); + + Shopware()->Events()->notify( + __CLASS__ . '_PostDispatchSecure_' . $moduleName, + $args + ); + + Shopware()->Events()->notify( + __CLASS__ . '_PostDispatchSecure', + $args + ); + } + + // fire non-secure/legacy-PostDispatch-Events + Shopware()->Events()->notify( + __CLASS__ . '_PostDispatch_' . $this->controller_name, + $args + ); + + Shopware()->Events()->notify( + __CLASS__ . '_PostDispatch_' . $moduleName, + $args + ); + + Shopware()->Events()->notify( + __CLASS__ . '_PostDispatch', + $args + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Action_PostDispatch_Frontend_Detail' => 'onEnlightControllerActionPostDispatchFrontendDetail' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerActionPostDispatchFrontendDetail(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Action */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Enlight_Controller_Action_PostDispatch_Frontend_Detail + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_ShopwareControllersFrontendDetailProxy + [front] => Shopware_Proxies_EnlightControllerFrontProxy + [view] => Enlight_View_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [controller_name] => Frontend_Detail + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/sommerwelten/accessoires/170/sonnenbrille-red + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => /sommerwelten/accessoires/170/sonnenbrille-red + [_params] => Array(4) + [_rawBody] => + [_aliases] => Array(1) + [_dispatched] => 1 + [_module] => frontend + [_moduleKey] => module + [_controller] => detail + [_controllerKey] => controller + [_action] => index + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(0) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Plugins_ViewRenderer_PreRender + +### Definition +``` +public function renderTemplate($template, $name = null) +{ + ... + $this->Application()->Events()->notify( + 'Enlight_Plugins_ViewRenderer_PreRender', + array( + 'subject' => $this, + 'template' => $template, + 'request' => $this->Action()->Request() + ) + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Plugins_ViewRenderer_PreRender' => 'onEnlightPluginsViewRendererPreRender' + ]; +} +``` + +### Listener +``` +public function onEnlightPluginsViewRendererPreRender(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightPlugin Enlight_Controller_Plugins_ViewRenderer_Bootstrap */ + $enlightPlugin = $arguments->getSubject(); + + /** @var $template Enlight_Template_Default */ + $template = $arguments->getTemplate(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Enlight_Plugins_ViewRenderer_PreRender + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Plugins_ViewRenderer_Bootstrap + [neverRender] => + [noRender] => + [front] => Shopware_Proxies_EnlightControllerFrontProxy + [action] => Shopware_Proxies_ShopwareControllersFrontendIndexProxy + [engine] => Enlight_Template_Manager + [name] => ViewRenderer + [collection] => Enlight_Plugin_Namespace_Loader + [instances] => Array(0) + ) + + [template] => stdClass Object + ( + [__CLASS__] => Enlight_Template_Default + [cache_id] => + [compile_id] => frontend_emotion_orange_de_DE_1 + [caching] => + [cache_lifetime] => 3600 + [template_resource] => + [mustCompile] => + [has_nocache_code] => + [properties] => Array(3) + [required_plugins] => Array(2) + [smarty] => Enlight_Template_Manager + [block_data] => Array(1) + [variable_filters] => Array(0) + [used_tags] => Array(0) + [allow_relative_path] => + [_capture_stack] => Array(1) + [template_class] => Smarty_Internal_Template + [tpl_vars] => Array(28) + [parent] => Enlight_Template_Manager + [config_vars] => Array(0) + ) + ) +) +``` + +## Enlight_Plugins_ViewRenderer_FilterRender + +### Definition +``` +public function renderTemplate($template, $name = null) +{ + ... + $render = $this->Application()->Events()->filter( + 'Enlight_Plugins_ViewRenderer_FilterRender', + $render, + array('subject' => $this, 'template' => $template) + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Plugins_ViewRenderer_FilterRender' => 'onEnlightPluginsViewRendererFilterRender' + ]; +} +``` + +### Listener +``` +public function onEnlightPluginsViewRendererFilterRender(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightPlugin Enlight_Controller_Plugins_ViewRenderer_Bootstrap */ + $enlightPlugin = $arguments->getSubject(); + + /** @var $template Enlight_Template_Default */ + $template = $arguments->getTemplate(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Enlight_Plugins_ViewRenderer_FilterRender + [_return] => + + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Plugins_ViewRenderer_Bootstrap + [neverRender] => + [noRender] => + [front] => Shopware_Proxies_EnlightControllerFrontProxy + [action] => Shopware_Proxies_ShopwareControllersWidgetsIndexProxy + [engine] => Enlight_Template_Manager + [name] => ViewRenderer + [collection] => Enlight_Plugin_Namespace_Loader + [instances] => Array(0) + ) + + [template] => stdClass Object + ( + [__CLASS__] => Enlight_Template_Default + [cache_id] => + [compile_id] => frontend_emotion_orange_de_DE_1 + [caching] => + [cache_lifetime] => 3600 + [template_resource] => widgets/index/menu.tpl + [mustCompile] => + [has_nocache_code] => + [properties] => Array(5) + [required_plugins] => Array(2) + [smarty] => Enlight_Template_Manager + [block_data] => Array(0) + [variable_filters] => Array(0) + [used_tags] => Array(0) + [allow_relative_path] => + [_capture_stack] => Array(1) + [template_class] => Smarty_Internal_Template + [tpl_vars] => Array(2) + [parent] => Enlight_Template_Manager + [config_vars] => Array(0) + ) + ) +) +``` + +## Enlight_Plugins_ViewRenderer_PostRender + +### Definition +``` +public function renderTemplate($template, $name = null) +{ + ... + $this->Application()->Events()->notify( + 'Enlight_Plugins_ViewRenderer_PostRender', + array('subject' => $this, 'template' => $template) + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Plugins_ViewRenderer_PostRender' => 'onEnlightPluginsViewRendererPostRender' + ]; +} +``` + +### Listener +``` +public function onEnlightPluginsViewRendererPostRender(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightPlugin Enlight_Controller_Plugins_ViewRenderer_Bootstrap */ + $enlightPlugin = $arguments->getSubject(); + + /** @var $template Enlight_Template_Default */ + $template = $arguments->getTemplate(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Enlight_Plugins_ViewRenderer_PostRender + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Plugins_ViewRenderer_Bootstrap + [neverRender] => + [noRender] => + [front] => Shopware_Proxies_EnlightControllerFrontProxy + [action] => Shopware_Proxies_ShopwareControllersWidgetsIndexProxy + [engine] => Enlight_Template_Manager + [name] => ViewRenderer + [collection] => Enlight_Plugin_Namespace_Loader + [instances] => Array(0) + ) + + [template] => stdClass Object + ( + [__CLASS__] => Enlight_Template_Default + [cache_id] => + [compile_id] => frontend_emotion_orange_de_DE_1 + [caching] => + [cache_lifetime] => 3600 + [template_resource] => widgets/index/menu.tpl + [mustCompile] => + [has_nocache_code] => + [properties] => Array(5) + [required_plugins] => Array(2) + [smarty] => Enlight_Template_Manager + [block_data] => Array(0) + [variable_filters] => Array(0) + [used_tags] => Array(0) + [allow_relative_path] => + [_capture_stack] => Array(1) + [template_class] => Smarty_Internal_Template + [tpl_vars] => Array(2) + [parent] => Enlight_Template_Manager + [config_vars] => Array(0) + ) + ) +) +``` + +## Enlight_Controller_Front_PostDispatch + +### Definition +``` +public function dispatch() +{ + ... + $this->eventManager->notify( + 'Enlight_Controller_Front_PostDispatch', + $eventArgs + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Front_PostDispatch' => 'onEnlightControllerFrontPostDispatch' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerFrontPostDispatch(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Front */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Controller_EventArgs + [_processed] => + [_name] => Enlight_Controller_Front_PostDispatch + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_EnlightControllerFrontProxy + [plugins] => Enlight_Plugin_Namespace_Loader + [router] => Enlight_Controller_Router_Default + [dispatcher] => Enlight_Controller_Dispatcher_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [throwExceptions] => + [returnResponse] => + [invokeParams] => Array(7) + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/ + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => / + [_params] => Array(0) + [_rawBody] => + [_aliases] => Array(0) + [_dispatched] => 1 + [_module] => frontend + [_moduleKey] => module + [_controller] => index + [_controllerKey] => controller + [_action] => index + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(1) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Controller_Front_DispatchLoopShutdown + +### Definition +``` +public function dispatch() +{ + ... + $this->eventManager->notify( + 'Enlight_Controller_Front_DispatchLoopShutdown', + $eventArgs + ); + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Front_DispatchLoopShutdown' => 'onEnlightControllerFrontDispatchLoopShutdown' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerFrontDispatchLoopShutdown(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Front */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); + + /** @var $response Enlight_Controller_Response_ResponseHttp */ + $response = $arguments->getResponse(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Controller_EventArgs + [_processed] => + [_name] => Enlight_Controller_Front_DispatchLoopShutdown + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Shopware_Proxies_EnlightControllerFrontProxy + [plugins] => Enlight_Plugin_Namespace_Loader + [router] => Enlight_Controller_Router_Default + [dispatcher] => Enlight_Controller_Dispatcher_Default + [request] => Enlight_Controller_Request_RequestHttp + [response] => Enlight_Controller_Response_ResponseHttp + [throwExceptions] => + [returnResponse] => + [invokeParams] => Array(7) + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/ + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => / + [_params] => Array(0) + [_rawBody] => + [_aliases] => Array(0) + [_dispatched] => 1 + [_module] => frontend + [_moduleKey] => module + [_controller] => index + [_controllerKey] => controller + [_action] => index + [_actionKey] => action + ) + + [response] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Response_ResponseHttp + [_cookies] => Array(0) + [_body] => Array(1) + [_exceptions] => Array(0) + [_headers] => Array(1) + [_headersRaw] => Array(0) + [_httpResponseCode] => 200 + [_isRedirect] => + [_renderExceptions] => + [headersSentThrowsException] => 1 + ) + ) +) +``` + +## Enlight_Controller_Dispatcher_ControllerPath_* + +### Definition +``` +public function getControllerPath(Enlight_Controller_Request_Request $request) +{ + ... + if ($event = Shopware()->Events()->notifyUntil( + 'Enlight_Controller_Dispatcher_ControllerPath_' . $moduleName . '_' . $controllerName, + ['subject' => $this, 'request' => $request] + ) + ) { + $path = $event->getReturn(); + } else { + $path = $this->curDirectory . $controllerName . '.php'; + } + ... +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Enlight_Controller_Dispatcher_ControllerPath_Frontend_Detail' => 'onEnlightControllerDispatcherControllerPathFrontendDetail' + ]; +} +``` + +### Listener +``` +public function onEnlightControllerDispatcherControllerPathFrontendDetail(\Enlight_Event_EventArgs $arguments) +{ + /** @var $enlightController Enlight_Controller_Dispatcher_Default */ + $enlightController = $arguments->getSubject(); + + /** @var $request Enlight_Controller_Request_RequestHttp */ + $request = $arguments->getRequest(); +} +``` + +### Arguments dump +``` +stdClass Object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Enlight_Controller_Dispatcher_ControllerPath_Frontend_Detail + [_return] => + [_elements] => Array + ( + [subject] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Dispatcher_Default + [curDirectory] => /var/www/sw406/engine/Shopware/Controllers/Frontend/ + [curModule] => frontend + [defaultAction] => index + [defaultController] => index + [defaultModule] => frontend + [frontController] => + [pathDelimiter] => _ + [wordDelimiter] => Array(2) + [controllerDirectory] => Array(4) + [front] => Shopware_Proxies_EnlightControllerFrontProxy + [response] => Enlight_Controller_Response_ResponseHttp + [instances] => Array(0) + ) + + [request] => stdClass Object + ( + [__CLASS__] => Enlight_Controller_Request_RequestHttp + [_paramSources] => Array(2) + [_requestUri] => /sw406/sommerwelten/accessoires/170/sonnenbrille-red + [_baseUrl] => /sw406 + [_basePath] => /sw406 + [_pathInfo] => /sommerwelten/accessoires/170/sonnenbrille-red + [_params] => Array(4) + [_rawBody] => + [_aliases] => Array(1) + [_dispatched] => 1 + [_module] => frontend + [_moduleKey] => module + [_controller] => detail + [_controllerKey] => controller + [_action] => + [_actionKey] => action + ) + ) +) +``` + +## Theme_Inheritance_Template_Directories_Collected + +### Definition +``` +public function getTemplateDirectories(Shop\Template $template) +{ + $directories = $this->getTemplateDirectoriesRecursive( + $template->getId(), + $this->fetchTemplates() + ); + + $directories = $this->eventManager->filter( + 'Theme_Inheritance_Template_Directories_Collected', + $directories, + ['template' => $template] + ); + + return $directories; +} +``` + +### Registration +``` +public static function getSubscribedEvents() +{ + return [ + 'Theme_Inheritance_Template_Directories_Collected' => 'onCollectTemplateDirectories', + ]; +} +``` + +### Listener +``` +public function onCollectTemplateDirectories(\Enlight_Event_EventArgs $args) +{ + /** @var $directories array */ + $directories = $args->getReturn(); + + /** @var $template \Shopware\Models\Shop\Template */ + $template = $args->get('template'); + + // adding own plugin view directory ($this->pluginDirectory contains plugin path) + $directories[] = $this->pluginDirectory . '/Resources/views'; + + $args->setReturn($directories); +} +``` + +### Arguments dump +``` +stdClass object +( + [__CLASS__] => Enlight_Event_EventArgs + [_processed] => + [_name] => Theme_Inheritance_Template_Directories_Collected + [_return] => Array ( + [0] => /var/www/html/themes/Frontend/Responsive + [1] => /var/www/html/themes/Frontend/Bare + ) + [_elements] => Array( + [template] => Shopware\Models\Shop\Template + ) +) +``` diff --git a/source/developers-guide/example-plugin/index.md b/source/developers-guide/example-plugin/index.md index ceb8ad051f..d16cc62632 100644 --- a/source/developers-guide/example-plugin/index.md +++ b/source/developers-guide/example-plugin/index.md @@ -1,9 +1,9 @@ --- layout: default -title: Storefront extension +title: Example plugin - storefront extension github_link: developers-guide/example-plugin/index.md indexed: true -menu_title: Storefront extension +menu_title: Example plugin menu_order: 20 group: Developer Guides subgroup: Developing plugins @@ -16,7 +16,6 @@ This guide shows how the full plugin system of Shopware 5 works. As part of this The plugin modifies the following parts of Shopware: -* Extends the `ListProductService` to append additional product data in listings or sliders. * Create a custom service, which is then injected into the DI container. * Extends the Shopware responsive template with custom plugin templates. * Implements a custom theme which overrides the plugin templates. @@ -28,181 +27,196 @@ This example gives answers to the following questions: * How to structure my plugin templates, so that they can be overwritten by local themes or other plugins? * How to override plugin templates inside my custom theme? -## Bootstrap +## Register template first -```php +
    +Always register your template directory first to prevent smarty security exceptions +
    +The plugin template directory should always be registered, even if it's not used in the current controller context. If your registration of the template depends on certain conditions, a Smarty Security error may occur. This could be confusing and annoying for other third-party developers, because it's not obvious where the error came from in the first place. +
    - + + %swag_plugin_system.plugin_dir% + + + +``` +The Subscriber class requires two parameter in the constructor. +1. The Plugin base path like `../shopware/custom/plugins/SwagPluginSystem`. +2. The Enlight_Template_Manager from the service container -class Shopware_Plugins_Frontend_SwagPluginSystem_Bootstrap - extends Shopware_Components_Plugin_Bootstrap -{ - public function getLabel() - { - return 'Shopware 5 - Big picture of the plugin system'; - } +Last but not least we add the `shopware.event_subscriber` tag. +With this tag it is pointed out that the class implements the Subscriber interface and could be handled by Shopware to register new handler for certain events. - public function install() - { - $this->subscribeEvent( - 'Enlight_Bootstrap_InitResource_shopware_storefront.seo_category_service', - 'registerSeoCategoryService' - ); +```php +subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service', - 'registerListProductService' - ); +namespace SwagPluginSystem\Subscriber; - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend', - 'addTemplateDir' - ); +use Enlight\Event\SubscriberInterface; - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Widgets', - 'addTemplateDir' - ); - - $this->subscribeEvent( - 'Theme_Compiler_Collect_Plugin_Less', - 'onCollectLessFiles' - ); +class TemplateRegistration implements SubscriberInterface +{ + /** + * @var string + */ + private $pluginDirectory; - return true; - } + /** + * @var \Enlight_Template_Manager + */ + private $templateManager; - public function afterInit() + /** + * @param $pluginDirectory + * @param \Enlight_Template_Manager $templateManager + */ + public function __construct($pluginDirectory, \Enlight_Template_Manager $templateManager) { - $this->get('Loader')->registerNamespace( - 'ShopwarePlugins\SwagPluginSystem', - $this->Path() - ); + $this->pluginDirectory = $pluginDirectory; + $this->templateManager = $templateManager; } /** - * @return \Shopware\Components\Theme\LessDefinition + * {@inheritdoc} */ - public function onCollectLessFiles() + public static function getSubscribedEvents() { - return new \Shopware\Components\Theme\LessDefinition( - [], - [__DIR__ . '/Views/frontend/_public/src/less/all.less'] - ); + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; } - public function addTemplateDir() + public function onPreDispatch() { - Shopware()->Container()->get('template')->addTemplateDir($this->Path() . 'Views/'); + $this->templateManager->addTemplateDir($this->pluginDirectory . '/Resources/views'); } +} +``` - public function registerSeoCategoryService() - { - $seoCategoryService = new SeoCategoryService( - Shopware()->Container()->get('dbal_connection'), - Shopware()->Container()->get('shopware_storefront.category_service') - ); - Shopware()->Container()->set('shopware_storefront.seo_category_service', $seoCategoryService); - } +The handler `onPreDispatch` registers the plugin's `/Resources/views` directory as a template directory for Shopware. +Attention: This event listener listens to the global pre dispatch event. The plugin shouldn't do some performance sensitive tasks here, otherwise each request will be slowed down. - public function registerListProductService() - { - Shopware()->Container()->set( - 'shopware_storefront.list_product_service', - new ListProductService( - Shopware()->Container()->get('shopware_storefront.list_product_service'), - Shopware()->Container()->get('shopware_storefront.seo_category_service') - ) - ); - } -} +## Register a service in your plugin +```xml + + + + + ``` -## Events -The plugin bootstrap registers the following events: +```php +connection = $connection; + $this->categoryService = $categoryService; + } + + /** + * @param ListProduct[] $listProducts + * @param ShopContextInterface $context + * @return Category[] indexed by product id + */ + public function getList($listProducts, ShopContextInterface $context) + { + $ids = array_map(function (ListProduct $product) { + return $product->getId(); + }, $listProducts); -* `Enlight_Bootstrap_InitResource_shopware_storefront.seo_category_service` - * The `Enlight_Bootstrap_InitResource_*` event fired when the suffixed service has to be initialized by the DI container. - * Because the `shopware_storefront.seo_category_service` is defined in this plugin, the plugin bootstrap has to handle the initialization of the service. -* `Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service` - * The `Enlight_Bootstrap_AfterInitResource_*` event is fired after the suffixed service was initialized by the DI container. - * The plugin wants to decorate the original `list_product_service`, so it needs to use the `AfterInitResource` event instead of the `InitResource` event, like in the `seo_category_service`. -* `Enlight_Controller_Action_PostDispatchSecure_Frontend` -* `Enlight_Controller_Action_PostDispatchSecure_Widgets` - * The `PostDispatchSecure_Frontend` and `PostDispatchSecure_Widgets` events are used to register the plugin `views` directory as a frontend and widgets template directory. -* `Theme_Compiler_Collect_Plugin_Less` - * The `Theme_Compiler_Collect_Plugin_Less` event is fired when Shopware compiles theme and plugin LESS files into one CSS file - * This event is used to add the plugin's LESS files to the compilation process. + //select all seo category ids, indexed by product id + $ids = $this->getCategoryIds($ids, $context); -Notice: The usage of the `Enlight_Bootstrap_InitResource` and `Enlight_Bootstrap_AfterInitResource` events has many benefits over other events, like `PreDispatch` or `Enlight_Controller_Front_StartDispatch`: + //now select all category data for the selected ids + $categories = $this->categoryService->getList($ids, $context); -* These events are only fired if the corresponding service is really required and used in the system. -* These events are also fired in console commands. + $result = []; + foreach ($ids as $productId => $categoryId) { + if (!isset($categories[$categoryId])) { + continue; + } + $result[$productId] = $categories[$categoryId]; + } + return $result; + } -## The event listener -The event listener contains the following sources: + /** + * @param $ids + * @param $context + * @return array + */ + private function getCategoryIds($ids, ShopContextInterface $context) + { + $query = $this->connection->createQueryBuilder(); + $query->select(['seoCategories.article_id', 'seoCategories.category_id']) + ->from('s_articles_categories_seo', 'seoCategories') + ->andWhere('seoCategories.article_id IN (:productIds)') + ->andWhere('seoCategories.shop_id = :shopId') + ->setParameter(':shopId', $context->getShop()->getId()) + ->setParameter(':productIds', $ids, Connection::PARAM_INT_ARRAY); -### Enlight_Bootstrap_InitResource_shopware_storefront.seo_category_service -```php -public function registerSeoCategoryService() -{ - $seoCategoryService = new SeoCategoryService( - Shopware()->Container()->get('dbal_connection'), - Shopware()->Container()->get('shopware_storefront.category_service') - ); - Shopware()->Container()->set('shopware_storefront.seo_category_service', $seoCategoryService); + return $query->execute()->fetchAll(\PDO::FETCH_KEY_PAIR); + } } ``` -Creates a new instance of the `SeoCategoryService` class, which is defined inside the plugin. As constructor parameters, the DBAL connection and the category service of Shopware are provided to the new service instance. The category service is a central service that loads category data identified by its id. After the service is initialized, it is set into the DI container via `Shopware()->Container()->set()`. -### Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service -```php -public function registerListProductService() -{ - Shopware()->Container()->set( - 'shopware_storefront.list_product_service', - new ListProductService( - Shopware()->Container()->get('shopware_storefront.list_product_service'), - Shopware()->Container()->get('shopware_storefront.seo_category_service') - ) - ); -} -``` -This event listener also creates a new instance of the `ListProductService`, which is also defined inside the plugin. In contrast with the previous event listener, this event listener is called after the original initialisation of a service. This means that the original `shopware_storefront.list_product_service` is already initialized, and can be loaded via `Shopware()->Container()->get()`. The new service expects the original `shopware_storefront.list_product_service` service and the `shopware_storefront.seo_category_service` as constructor parameters. +After plugin installation the service is available in the service container: +For example: -### Enlight_Controller_Action_PostDispatchSecure_Frontend/Widgets ```php -public function addTemplateDir() -{ - Shopware()->Container()->get('template')->addTemplateDir($this->Path() . 'Views/'); -} +$seoService = $this->container->get('shopware_storefront.seo_category_service'); ``` -Registers the plugin's `Views` directory as a template directory for Shopware. -Attention: This event listener listens to the global frontend and widgets post dispatch secure event. The plugin shouldn't do some performance sensitive tasks here, otherwise each post dispatch event in the store front will be slowed down. -### Theme_Compiler_Collect_Plugin_Less -```php -public function onCollectLessFiles() -{ - return new \Shopware\Components\Theme\LessDefinition( - [], - [__DIR__ . '/Views/frontend/_public/src/less/all.less'] - ); -} -``` -Adds the plugin `all.less` file to the Shopware LESS compiling step. This allows the plugin to implement custom frontend styling via LESS. +## Plugin ListProductService (Service Decoration) -## Plugin ListProductService +```xml + + + + + +``` ```php getId(); }, $listProducts); - //select all SEO category ids, indexed by product id + //select all seo category ids, indexed by product id $ids = $this->getCategoryIds($ids, $context); //now select all category data for the selected ids @@ -363,6 +376,7 @@ class SeoCategoryService /** * @param $ids * @param $context + * @return array */ private function getCategoryIds($ids, ShopContextInterface $context) { diff --git a/source/developers-guide/general-resources.html b/source/developers-guide/general-resources.html index e4369ffaf8..020b300617 100644 --- a/source/developers-guide/general-resources.html +++ b/source/developers-guide/general-resources.html @@ -10,11 +10,15 @@ --- diff --git a/source/developers-guide/global-variables-in-templates/index.md b/source/developers-guide/global-variables-in-templates/index.md index da76eb8172..132a95a651 100644 --- a/source/developers-guide/global-variables-in-templates/index.md +++ b/source/developers-guide/global-variables-in-templates/index.md @@ -40,7 +40,7 @@ Our example will just add the user login status to the template. */ public function onPostDispatch(\Enlight_Controller_ActionEventArgs $args) { - $args->getSubject()->View()->assign('sUserloggedIn', Shopware()->Modules()->Admin()->sCheckUser()); + $args->getSubject()->View()->assign('sUserLoggedIn', Shopware()->Modules()->Admin()->sCheckUser()); } ``` diff --git a/source/developers-guide/header.svg b/source/developers-guide/header.svg new file mode 100644 index 0000000000..beec991d0a --- /dev/null +++ b/source/developers-guide/header.svg @@ -0,0 +1,35 @@ + + +WurmDev + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/developers-guide/http-cache/index.md b/source/developers-guide/http-cache/index.md index 7a238abc57..b839f6e1ed 100644 --- a/source/developers-guide/http-cache/index.md +++ b/source/developers-guide/http-cache/index.md @@ -4,8 +4,6 @@ title: Shopware HTTP cache github_link: developers-guide/http-cache/index.md shopware_version: 5.1.2 indexed: true -redirect: - - /blog/2015/12/04/working-with-the-http-cache/index.html group: Developer Guides subgroup: General Resources menu_title: HTTP Cache @@ -35,14 +33,14 @@ It mainly has the following configuration options: This is highly recommended, as this will allow you to have full cached pages AND almost instant cache updates, if e.g. the price of an item changes. * Alternate proxy URL: When not using Shopware built in cache, enter the IP of your cache here. This -will allow you to invalidate / clear your remote cache from within Shopware. +will allow you to invalidate / clear your remote cache from within Shopware. **If you use Nginx without Varnish, you have to add the URL to Shopware here with a custom URL path, because Nginx does not allow BAN requests on "/". Example: http://example.com/ban** * Admin view: Do not cache frontend pages if you are signed in as an admin. * [Cache controller times](/blog/2015/02/11/understanding-the-shopware-http-cache/#whitelisted-controllers): Which controller should be cached for how long? The `Controller` column contains the controller's module + the controller's name. The `time` column the caching time in seconds (TTL). Only controllers / pages in this will be cached at all. * [Controller tags not to be cached](/blog/2015/02/11/understanding-the-shopware-http-cache/#nocache-tags): Do not cache these controllers if the user session has a certain state. The rule of thumb is: if a user is logged in, the -`price` tag is active, if some items are in the cart or the `checkout` tag is active. So, as you can see, Shopware will not +`price` tag is active, if some items are in the cart, the `checkout` tag is active. So, as you can see, Shopware will not cache listing pages of logged in users - they might have different prices. See the link before for more details. @@ -241,9 +239,8 @@ Also, be aware that this approach only applies to Shopware's built-in reverse pr have to make your [Varnish configuration](/sysadmins-guide/varnish-setup/) aware of this cookie. -## Trusted Proxies -Another topic that is quite relevant for you, if you are working with bigger environments, is the [`trustedproxies` -configuration](http://symfony.com/doc/current/components/http_foundation/trusting_proxies.html). +## Cluster environments +Another topic that is quite relevant for you, if you are working with bigger environments, is the [`trustedproxies`](http://symfony.com/doc/current/components/http_foundation/trusting_proxies.html) and `purge_allowed_ips` configuration. It does not affect the cache directly, but as soon as you have e.g. a Varnish and a load balancer in play, you will need to deal with the fact that your IP address is replaced with e.g. the proxy's IP address. This might have effects on @@ -253,6 +250,8 @@ triggered the request. Of course, Shopware cannot simply rely on this header, as The `trustedproxies` configuration defines which clients (proxies) are allowed to set the `HTTP_X_FORWARDED_FOR` header. Headers from other IPs are ignored. +If you are running the build-in HTTP shopware cache on your shopware slave servers, you need to whitelist your servers in order to allow the cache to be invalided by this servers. This is set via `purge_allowed_ips` configuration. You also need to [configure an alternative proxy URL](/developers-guide/http-cache/#configuring-the-cache) in backend settings (Settings > Cache / Performance > Settings > HTTP-Cache). Your slave servers need to be accessible via a distinct URL, e.g. http://fe01.domian.tld, http://fe02.domian.tld. + In order to configure this config, simply change your `config.php` like this: ``` @@ -263,14 +262,68 @@ return [ // your default db configuration ], 'trustedproxies' => [ - '192.168.0.10', - '192.168.0.11', + '192.168.0.10', // IP address of load balancer + '192.168.0.11', // IP address of varnish cache + ], + 'httpcache' => [ + 'purge_allowed_ips' => [ + '192.168.0.101', // IP address of shopware slave 1 @ http://fe01.domian.tld + '192.168.0.102', // IP address of shopware slave 2 @ http://fe02.domian.tld + ], ], ] ``` In this case the proxy with the IP `192.168.0.10` is allowed to set the `HTTP_X_FORWARDED_FOR` header. +## Ignore some HTTP parameters + +There are certain use cases where a GET parameter has a new value for every request, generating a new entry in the +HTTP cache every time. An example would be the Google Adwords ClickId parameter `gclid` which contains a new id for +every click that was generated by Google Adwords. This leads to a bad performance for the visitor since existing caches +aren't being used. + +Shopware v5.3.5 added the possibility to blacklist parameters like this using the configuration. It provides a list of +known parameters that fall in this category but disables them by a block comment. This was done to not change the +behaviour of the HTTP cache between patch releases while providing the possibility for shops to easily use this feature. + +Starting with Shopware v5.4.0 this list of parameters will be active by default: + +``` +'httpcache' => [ + 'ignored_url_parameters' => [ +/* + 'pk_campaign', // Piwik + 'piwik_campaign', + 'pk_kwd', + 'piwik_kwd', + 'pk_keyword', + 'pixelId', // Yahoo + 'kwid', + 'kw', + 'adid', + 'chl', + 'dv', + 'nk', + 'pa', + 'camid', + 'adgid', + 'utm_term', // Google + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_content', + 'gclid', + 'cx', + 'ie', + 'cof', + 'siteurl', + '_ga', +*/ + ], +], +``` + ## More into detail This document covers general basics of working with Shopware's HTTP cache. There is an addition [blog post](/blog/2015/02/11/understanding-the-shopware-http-cache/) about the fundamental basics of Shopware's HTTP caching. diff --git a/source/developers-guide/implementing-your-own-captcha/index.md b/source/developers-guide/implementing-your-own-captcha/index.md index 6fc43f0002..2f8318505f 100644 --- a/source/developers-guide/implementing-your-own-captcha/index.md +++ b/source/developers-guide/implementing-your-own-captcha/index.md @@ -2,12 +2,16 @@ layout: default title: Implementing your own captcha github_link: developers-guide/implementing-your-own-captcha/index.md +shopware_version: 5.3.0 tags: - captcha - recaptcha - example indexed: true -shopware_version: 5.3.0 +group: Developer Guides +subgroup: General Resources +menu_title: Captcha +menu_order: 120 ---
    @@ -90,7 +94,7 @@ class SwagReCaptcha extends Plugin ```xml + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> @@ -211,20 +215,21 @@ The interface you are implementing defines three methods: - Has to be unique - `validate(Enlight_Controller_Request_Request $request)` - This method will ingest the form post request after it is send. - It has to return true or false, indicating wether the captcha was solved correctly or not. + It has to return true or false, indicating whether the captcha was solved correctly or not. - `getTemplateData()` - Has to return an array which will be assigned to your smarty template. In the case of ReCaptcha we need to make a call to Google from php, so `Guzzle` is injected into the constructor, as well as `Shopware_Components_Config` since it needs the ReCaptcha sitekey and secret. -## Creating a backend menu +## Creating the plugin configuration -With the new plugin system, creating a backend config is fairly simple: Inside the `Resources` folder create a new file `config.xml` with the following content. +With the new plugin system, creating a plugin configuration is fairly simple: Inside the `Resources` folder create a new file `config.xml` with the following content. ```xml - + sitekey @@ -275,10 +280,10 @@ To achieve this, create a new file `services.xml` in the `Resources` folder with - + ``` -Notice the `` tag, it causes shopware to recognize the service as captcha implementation. \ No newline at end of file +Notice the `` tag, it causes shopware to recognize the service as captcha implementation. diff --git a/source/developers-guide/index.html b/source/developers-guide/index.html index 254c793278..108e01b156 100755 --- a/source/developers-guide/index.html +++ b/source/developers-guide/index.html @@ -1,88 +1,108 @@ --- -layout: default +layout: guides title: Developer Guides github_link: developers-guide/index.html menu_title: Developer Guides menu_order: 20 menu_style: bullet --- - - -

    Developing Plugins

    - - -

    General resources

    - - -

    Tutorials

    - - -

    Backend and ExtJS resources

    - - -

    Rest API

    - \ No newline at end of file + +
    + +
    + + {% if page.title is defined %} +

    + {{ page.title }} +

    + {% endif %} + +
    + + {% if page.shopware_version %} + + as of version + {{ page.shopware_version }} + + {% endif %} + +
    + + {# START AND UDEMY #} +
    +
    +
    +
    + "Developing a plugin for Shopware is quite easy. Use the Startup Guide in the first step!"
    mitelg, web developer @shopware
    + +
    + +
    +
    + +
    +
    +

    Video Developer Training

    + +
    +
    + +
    +
    + + {# DEVELOPER MENU #} + + + {# GITTER CHAT #} +
    + +
    +

    Shopware developer chat

    +

    Slack is a free chat where you are able to chat immediately - read more.

    +
    + +
    + +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + Michael Telgmann @Telgi
    Slack channels are public and save history indefinitely. Right? +
    +
    +
    + +
    +
    +
    + dnoegel @dnoegel
    Be careful. There be dragons. On the other hand, you don't need that icky bouncer anymore. ;) +
    +
    +
    + + + +
    + +
    + +
    {# END START GUIDE #} diff --git a/source/developers-guide/legacy-plugin-system/index.md b/source/developers-guide/legacy-plugin-system/index.md new file mode 100644 index 0000000000..92bd3db48f --- /dev/null +++ b/source/developers-guide/legacy-plugin-system/index.md @@ -0,0 +1,807 @@ +--- +layout: default +title: The legacy Plugin System +github_link: developers-guide/plugin-system/index.md +indexed: true +shopware_version: 4.3.6 +group: Developer Guides +subgroup: Developing plugins +menu_title: The legacy Plugin System +menu_order: 120 +--- + +
    + +## Directory Structure + +The legacy Plugins are located in the `/.../engine/Shopware/Plugins/( Community | Default | Local )/` directory. There is a separation in `Frontend`, `Core` or `Backend`. + +``` +engine +└──Shopware + └──Plugins + └──Community + └──Default + └──Local + └── Backend + └── Core + └── Frontend + └──SwagSloganOfTheDay + └──Bootstrap.php +``` + +## Plugin Name + +The plugin name should always be prefixed with your developer prefix so it's unique in the Shopware universe. +To submit plugins to the [shopware store](http://store.shopware.com/) you have to obtain your developer prefix in the [Shopware Account](https://account.shopware.com). + +In the following examples the developer prefix "Swag" will be used (short for shopware AG). + +## Minimal Plugin Example + +The most minimal Plugin is just a directory and one bootstrap file. +The directory must be named after the plugin name. The bootstrap file is called `Bootstrap.php`: + +### Plugin Bootstrap file + +The Bootstrap `Bootstrap.php` has no namespace and extend the class `Shopware_Components_Plugin_Bootstrap`: + +```php +subscribeEvent('Enlight_Controller_Front_StartDispatch', 'registerSubscriber'); + return true; + } + + public function registerSubscriber() + { + $this->get('events')->addSubscriber( + new SubscriberClass( + $this->Path(), + $this->get('dbal_connection') + ) + ); + } +} +``` + +### A subscriber class +A subscriber class implements the `Shopware\Plugins\Subscribers\SubscriberInterface` + +```php +pluginDir = $pluginDir; + $this->connection = $connection; + } + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Listing' => 'extendListingTemplate' + ]; + } + + public function extendListingTemplate(\Enlight_Event_EventArgs $args) + { + $args->getSubject()->View()->addTemplateDir( + $this->pluginDir . '/Views/' + ); + } +} +``` + +### Access to the DI container + +Inside the plugin bootstrap the DI container can be accessed via `$this->get()` + +```php + public function install(\Enlight_Controller_EventArgs $args) + { + $conn = $this->get('dbal_connection'); + $conn->.... // do some query + } +``` + +## Plugin Install / Update + +During plugin installation / deinstallation / update / activate / deactivate a method on the plugin bootstrap is called that can optionally be overwritten. You can do a lot of things with the provided context, e.g.: + +- stop process by throwing an exception and notify user with a message +- notify user on success with a message +- flush specified caches +- within update(), additionally: get currently installed version number of your plugin +- within secureUninstall() keep user generated data, if he wishes so + +Checkout the examples: + +```php +get('loader')->registerNamespace('ShopwarePlugins\PluginName', $this->Path()); + } + + public function install() + { + } + + public function uninstall() + { + } + + public function secureUninstall() + { + } + + public function update() + { + // Check if Shopware version matches + if (!$this->assertMinimumVersion('5.2.0')) { + throw new Exception('This plugin requires Shopware 5.2.0 or a later version'); + } + + } + + public function enable() + { + return ['success' => true, 'invalidateCache' => ['proxy', 'frontend', 'backend', 'theme']]; + } + + public function disable() + { + return ['success' => true, 'invalidateCache' => ['proxy', 'frontend', 'backend', 'theme']]; + } + +} +``` + +### Decorate a service +The following example shows you how to decorate a service which implements an interface and gets defined in the Shopware dependency injection container. + +```php +service = $service; + } + + public function getList(array $numbers, ProductContextInterface $context) + { + $products = $this->service->getList($numbers, $context); + //... + return $products; + } + + public function get($number, ProductContextInterface $context) + { + return array_shift($this->getList([$number], $context)); + } +} +``` + +The original `\Shopware\Bundle\StoreFrontBundle\Service\Core\ListProductService` defined with the service id `shopware_storefront.list_product_service`. The following service definition decorates this service using the service above: + +To use the after init resource event **Enlight_Bootstrap_AfterInitResource_...** is quite important in this case. + +```php +$this->subscribeEvent('Enlight_Bootstrap_AfterInitResource_shopware_list_product_service', 'decorateService'); + + ... + +public function decorateService() +{ + $originalService = $this->container->get('shopware_storefront.list_product_service'); + + $this->container->set( + 'shopware_storefront.list_product_service', + new ListProductServiceDecorator($originalService) + ); +} +``` + +## Register plugin controller with template in a subscriber +```php +container = $container; + $this->path = $path; + } + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Dispatcher_ControllerPath_Frontend_MyController' => 'registerController', + ]; + } + + public function registerController(\Enlight_Event_EventArgs $args) + { + $this->container->get('template')->addTemplateDir( + $this->path . '/Views' + ); + + return $this->path . '/Controllers/Frontend/MyController.php'; + } +} +``` +Controller: +```php +subscribeEvent('Theme_Compiler_Collect_Plugin_Less', 'addLessFiles'); + $this->subscribeEvent('Theme_Compiler_Collect_Plugin_Javascript', 'addJsFiles'); +} + +/** + * @return ArrayCollection + */ +public function addLessFiles() +{ + return new Doctrine\Common\Collections\ArrayCollection([ + new LessDefinition( + [], + [__DIR__ . '/Views/frontend/_public/src/less/all.less'], + __DIR__ + ), + ]); +} + +/** + * @return ArrayCollection + */ +public function addJsFiles() +{ + return new ArrayCollection([ + __DIR__ . '/Views/frontend/_public/src/js/jquery.swag_live_shopping.js', + ]); +} +``` + +## Add console commands +For register a console command create a command file which extends the class "ShopwareCommand". +Then register the event `Shopware_Console_Add_Command` and return a new ArrayCollection with your created commands. + +```php +$this->subscribeEvent( + 'Shopware_Console_Add_Command', + 'onAddConsoleCommand' +); + +/** + * Adds the console commands + * + * @return ArrayCollection + */ +public function onAddConsoleCommand() +{ + return new ArrayCollection( + [ + new Command1(), + new Command2(), + ] + ); +} +``` + +```php +createEmotionComponent([ + 'name' => 'Vimeo Video', + 'xtype' => 'emotion-components-vimeo', + 'template' => 'emotion_vimeo', + 'cls' => 'emotion-vimeo-element', + 'description' => 'A simple vimeo video element for the shopping worlds.' +]); +``` +In the install() method of our plugin we register a new element and save it in the variable $vimeoElement for later use. The createEmotionComponent() method expects a configuration array. + +### Adding configuration fields to the element +After registering the new element we can add different form fields to the element which can be filled by the user to configure the element. For each type of field there is a helper function which can be called on the newly registered component. We will add some configuration fields to our example element for the different embed options the Vimeo platform offers. + +```php +$vimeoElement->createTextField([ + 'name' => 'vimeo_video_id', + 'fieldLabel' => 'Video ID', + 'supportText' => 'Enter the ID of the video you want to embed.', + 'allowBlank' => false +]); + +$vimeoElement->createHiddenField([ + 'name' => 'vimeo_video_thumbnail' +]); + +$vimeoElement->createTextField([ + 'name' => 'vimeo_interface_color', + 'fieldLabel' => 'Interface Color', + 'supportText' => 'Enter the #hex color code for the video player interface.', + 'defaultValue' => '#0096FF' +]); + +$vimeoElement->createCheckboxField([ + 'name' => 'vimeo_autoplay', + 'fieldLabel' => 'Autoplay', + 'defaultValue' => false +]); + +... + +``` +### Creating a frontend template for the element +After registering the element and creating all the configuration fields we already see a full functional shopping world element in the backend module which can be placed on the design canvas. All we have to do now is to provide a frontend template to define the layout in the store. In the `Views` directory of our plugin we create the necessary directory structure to the file. Template files for shopping world elements can automatically be added by creating the hierarchy structure in the special directory called `emotion_components`. The full path to the template file would be `Views/emotion_components/widgets/emotion/components/{name}.tpl`. + +The name of the file has to match the definition in your createEmotionComponent() method. You can access your configuration fields inside the template file as properties of the $Data smarty variable. Let's create the embed code for displaying the Vimeo video. + +```smarty +{block name="widgets_emotion_components_vimeo_element"} + + {$videoURL = "https://player.vimeo.com/video/{$Data.vimeo_video_id}?color={$Data.vimeo_interface_color|substr:1}"} + + {if !$Data.vimeo_show_title} + {$videoURL = "{$videoURL}&title=0"} + {/if} + + {if !$Data.vimeo_show_portrait} + {$videoURL = "{$videoURL}&portrait=0"} + {/if} + + {if !$Data.vimeo_show_author} + {$videoURL = "{$videoURL}&byline=0"} + {/if} + + {if $Data.vimeo_loop} + {$videoURL = "{$videoURL}&loop=1"} + {/if} + + {if $Data.vimeo_autoplay} + {$videoURL = "{$videoURL}&autoplay=1"} + {/if} + + +{/block} +``` +#### Process the element data before output +When you have to process the saved element data before it is passed to the frontend, you have the possibility to register to the Shopware_Controllers_Widgets_Emotion_AddElement controller event. Here you get the original data to manipulate the output. + +```php +public function install() +{ + // ... + + $this->subscribeEvent( + 'Shopware_Controllers_Widgets_Emotion_AddElement', + 'onEmotionAddElement' + ); +} + +public function onEmotionAddElement(Enlight_Event_EventArgs $args) +{ + $element = $args->get('element'); + + if ($element['component']['xType'] !== 'emotion-components-vimeo') { + return; + } + + $data = $args->getReturn(); + + // Do some stuff with the element data + + $args->setReturn($data); +} +``` +Because the event is called for every element we have to do a check before processing the data. You can get the element info from the event arguments with $args->get('element'). To test for a specific element we can validate the defined xType. When the element is the right one we can get the data of the configuration form with $args->getReturn(). After processing the data we have to set the new output for the frontend with $args->setReturn($data). + +#### Advanced: Adding a custom emotion component in ExtJS +If you want to go a little further by creating custom configuration fields for your element you have the possibility to create your own ExtJS component for the element. Here you have full access to the configuration form in ExtJS. You can manipulate existing fields or add new fields which are more complex than the standard form elements. + +The file for the component is also located in the emotion_components directory, so it will be detected automatically. The complete path to the file is Views/emotion_components/backend/{name}.js. + +For the Vimeo example we use the custom ExtJS component to make a call to the Vimeo api for receiving information about the preview image of the video and save it in the hidden input we already created via the helper functions. + +```js +//{block name="emotion_components/backend/vimeo_video"} +Ext.define('Shopware.apps.Emotion.view.components.VimeoVideo', { + + extend: 'Shopware.apps.Emotion.view.components.Base', + + alias: 'widget.emotion-components-vimeo', + + initComponent: function () { + var me = this; + + me.callParent(arguments); + + me.videoThumbnailField = me.getForm().findField('vimeo_video_thumbnail'); + me.videoIdField = me.getForm().findField('vimeo_video_id'); + + me.videoIdField.on('change', Ext.bind(me.onIdChange, me)); + }, + + onIdChange: function (field, value) { + var me = this; + + me.setVimeoPreviewImage(value); + }, + + setVimeoPreviewImage: function (vimeoId) { + var me = this; + + if (!vimeoId) { + return false; + } + + var url = Ext.String.format('https://vimeo.com/api/v2/video/[0].json', vimeoId), + xhr = new XMLHttpRequest(), + response; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + response = Ext.JSON.decode(xhr.responseText); + + if (response[0]) { + me.videoThumbnailField.setValue(response[0]['thumbnail_large']); + } + } + }; + + xhr.open('GET', url, true); + xhr.send(); + } +}); +//{/block} + +``` +#### Advanced: Adding a custom designer component in ExtJS +Since Shopware 5.2 you are able to create a custom ExtJS component for the designer elements. Here you have the possibility to add an icon and a preview template for the element, which gets shown in the grid of the designer. + +For extending the designer components we have to do a classic template extension of the backend files. So we create a new file in the necessary template hierarchy Views/backend/emotion/{pluginName}/view/detail/elements. + +Otherwise than the custom emotion component we have to register the template manually by extending the template inheritance system with our new file. We can subscribe to the PostDispatch event of the emotion module in the install() method of our plugin to do so. + +```php +public function install() +{ + // ... + + $this->subscribeEvent( + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Emotion', + 'onPostDispatchBackendEmotion' + ); +} + +public function onPostDispatchBackendEmotion(Enlight_Controller_ActionEventArgs $args) +{ + $controller = $args->getSubject(); + $view = $controller->View(); + + $view->addTemplateDir($this->Path() . 'Views/'); + $view->extendsTemplate('backend/emotion/vimeo_element/view/detail/elements/vimeo_video.js'); +} +``` +Now that we added our template file we can add our custom component by extending the Smarty {block} of the base class. + +```js +// +//{block name="backend/emotion/view/detail/elements/base"} +//{$smarty.block.parent} +Ext.define('Shopware.apps.Emotion.view.detail.elements.VimeoVideo', { + + extend: 'Shopware.apps.Emotion.view.detail.elements.Base', + + alias: 'widget.detail-element-emotion-components-vimeo', + + componentCls: 'emotion--vimeo-video', + + icon: 'data:image/png;base64,...', + + createPreview: function () { + var me = this, + preview = '', + image = me.getConfigValue('vimeo_video_thumbnail'), + style; + + if (Ext.isDefined(image)) { + style = Ext.String.format('background-image: url([0]);', image); + + preview = Ext.String.format('
    ', style); + } + + return preview; + } +}); +//{/block} +``` + +## Add a new payment method +Use the "createPayment" method to add payment methods to the database inside plugin installations. + +```php +public function install(InstallContext $context) +{ + $this->createPayment( + [ + 'name' => 'payment name', + 'description' => 'Pay by the new Payment', + 'action' => 'payment_paymentName', + 'active' => 0, + 'position' => 0, + 'additionalDescription' => 'Lorem ipsum ... ', + ] + ); +} +``` + +### Plugin Configuration / Forms + +Backend plugin configuration can be extended by the usage of `$this->Form()->setElement()` in the install method of the plugin Bootstrap.php. + +```php +$form = $this->Form(); + +// Now we can use this instance of Shopware\Models\Config\Form in $form to add elements to it via `setElement(). + +$form->setElement( + + 'color', + 'yourSetting', + [ + 'label' => 'Your label', + 'description' => Lorem ipsum ...', + 'value' => '#ffffff', + 'scope' => Element::SCOPE_SHOP, + ] +); +``` + +A textfield would be defined as followed: +```php +public function createConfiguration() +{ + $form = $this->Form(); + + $form->setElement( + 'text', + 'simpleTextField', + [ + 'label' => 'Text', + 'value' => 'Preselection', + 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP, + 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut', + 'required' => true + ] + ); + + $translations = [ + 'en_GB' => [ + 'simpleTextField' => [ + 'label' => 'Translated text', + 'description' => 'Translated description' + ] + ] + ]; + + $this->addFormTranslations($translations); +} +``` + +#### Options parameter +The options parameter of the setElement function allows to set several configurations on the form element. +* **Label** +The label parameter allows to create a simple descriptional label for the form element. +* **Value** +The value parameter stands for the default value of the field if this hasn´t been edited yet. It will directly be shown in the configuration element. +* **Scope** +With help of the scope parameter it is possible to generate subshop specific configurations. Leaving this option out results in a configuration option that applies for all subshops. +* **Description** +The description parameter allows to provide a more detailed description of the configuration element. +* **Required** +The required parameter specifies whether the configuration item is mandatory or not. + +#### Possible elements: +* color +* date +* datetime +* html +* interval +* mediaselection, +* number, +* select, +* text, +* textarea +* time + +#### Read the configuration +To read out the configuration of your plugin use this code snippet in your plugin bootstrap: + +```php +$setting = $this->Config()->get('yourSetting') +``` + +### Backend Menu Items + +Example in the install method of the plugin Bootstrap.php: + +```php +$this->createMenuItem( + [ + 'label' => 'Your plugin laben', + 'controller' => 'your backend controller', + 'class' => 'your-icon', + 'action' => 'Index', + 'active' => 1, + 'parent' => $this->Menu()->findOneBy(['controller' => 'Marketing']), + ] +); +``` + +For available parent controllers take a look into the table `s_core_menu` (column `controller`). For example you can use one of the following: +- Article +- Content +- Customer +- ConfigurationMenu +- Marketing + +To know which class for which icon take a look at the Backend icon set overview. + +### Plugin Cronjob +Create a cronJob by using the "createCronJob" method of the plugin Bootstrap.php. + +```php +$this->createCronJob( + 'ImportExport - AutoImport', + 'yourCronJobEvent', + '86400', + true +); +``` + +Register a new listener to listen to the "CronAutoImport" event + +```php +$this->subscribeEvent( + 'Shopware_CronJob_yourCronJobEvent', + 'onCronJobCall' +); +``` + +Implement the method in the plugin Bootstrap.php. + +```php + /** + * Event listener for thecron job + */ + public function onCronJobCall() + { + // do some fancy things + } +``` diff --git a/source/developers-guide/lightweight-backend-modules-api/index.md b/source/developers-guide/lightweight-backend-modules-api/index.md index 6915085f4b..dc86d5c842 100644 --- a/source/developers-guide/lightweight-backend-modules-api/index.md +++ b/source/developers-guide/lightweight-backend-modules-api/index.md @@ -128,7 +128,7 @@ Sends a message to a subwindow **Example**: ```js postMessageApi.sendMessageToSubWindow({ - name: 'customSubWindow', + component: 'customSubWindow', params: { msg: 'Your message', foo: [ 'bar', 'batz' ] diff --git a/source/developers-guide/lightweight-backend-modules/index.md b/source/developers-guide/lightweight-backend-modules/index.md index 283aabf09e..857f1e2bb1 100644 --- a/source/developers-guide/lightweight-backend-modules/index.md +++ b/source/developers-guide/lightweight-backend-modules/index.md @@ -67,7 +67,7 @@ The Response object contains the result of the called method and has the followi ```js { "jsonrpc": "2.0", - "result": [ "some": "data" ], + "result": { "some": "data" }, "error": null, "component": "main", "instance": "550e8400-e29b-11d4-a716-446655440000", @@ -78,55 +78,32 @@ The Response object contains the result of the called method and has the followi A response will always be sent from the Server to the Client which sent the Request object. The `result` member can be `null` if an error occurred on the Server and the value of this member should have been determined by the method invoked on the Server. Please keep in mind that the entire inter-process communication is asynchronous, therefore the processing of the Response object has to be handled in a callback method. ## How to create a simple backend module? -Creating a new backend module using the lightweight backend module is super easy. Basically you just have to create a new menu entry in the Shopware administration and register a backend controller to get it working. Let's start with the menu entry. In our plugin install method we call the method `$this->createMenuItem()` to add the new entry: - -```php -public function install() -{ - try { - $this->registerBackendController(); - $this->createMenu(); - return array( - 'success' => true, - 'invalidateCache' => array('backend') - ); - - } catch (Exception $e) { - return array('success' => false, 'message' => $e->getMessage()); - } -} -``` - -Now let's implement the actual menu entry: - -```php -$this->createMenuItem(array( - 'label' => 'PlainHTML Modul', - 'onclick' => 'createSimpleModule("ExampleModulePlainHtml", { "title": "Plain HTML Module" })', - 'class' => 'sprite-star', - 'active' => 1, - 'parent' => $this->Menu()->findOneBy(['label' => 'Einstellungen']) -)); +Creating a new backend module using the lightweight backend module is super easy. +Basically you just have to create a new menu entry in the Shopware administration and register a backend controller to get it working. Let's start with the menu entry. + +```xml + + + + + SwagLightweightModule + + + ExampleModulePlainHtml + index + sprite-application-block + Marketing + Shopware.ModuleManager.createSimplifiedModule("ExampleModulePlainHtml", { "title": "Lightweight Backend Module" }) + + + ``` -Please note that we call the `createSimpleModule()` JavaScript method in the `onclick` property of the menu entry, which means that, when the user clicks on the menu entry, the `onclick` method will be called and the backend module will be create. The first argument of the `createSimpleModule()` is the name of the backend controller and the second argument is an object to customize the appearance of the window. You can set the title, width and / or height of the window. - -### Registering a custom backend controller - -The next step is to register the new backend controller in the `Bootstrap.php` file of your plugin. In the example -below, we do that in the `registerBackendController()` method by calling `registerController()`: - -```php -function registerBackendController() -{ - $this->registerController( - 'Backend', - 'ExampleModulePlainHtml' - ); -} -``` +Please note that we call the `createSimplifiedModule()` JavaScript method in the `onclick` property of the menu entry, which means that, when the user clicks on the menu entry, the `onclick` method will be called and the backend module will be create. The first argument of the `createSimplifiedModule()` is the name of the backend controller and the second argument is an object to customize the appearance of the window. You can set the title, width and / or height of the window. -Okay, our custom backend controller is registered in the application, now we can implement the controller. Please note we already registered our template directory in the `onGetBackendController()` event handler method. +Now we can implement the controller. Please note we already registered our template directory in the `onGetBackendController()` event handler method. The minimum requirement for the controller is that it extends from `Enlight_Controller_Action` and has the `indexAction` method implemented. The actual function body of the method can be left empty. @@ -143,10 +120,11 @@ class Shopware_Controllers_Backend_ExampleModulePlainHtml extends Enlight_Contro
    We strongly recommend using Bootstrap with our backend inspired theme to get the look and feel of the shopware backend. -Check it out: Bootstrap Shopware Backend Theme +Check it out: Bootstrap Shopware Backend Theme
    -Now let's start templating our backend module. We recommend using [Bootstrap](http://getbootstrap.com/) as the frontend framework. First let's take a look on the directory structure in the `Views/backend` directory of our plugin: +Now let's start templating our backend module. We recommend using [Bootstrap](http://getbootstrap.com/) as the frontend framework. +First let's take a look on the directory structure in the `Views/backend` directory of our plugin: ```bash |-- _base @@ -236,7 +214,7 @@ Here's a entire list of all available events: * component/set-body-style ## Demo plugin -A demo plugin which highlights the new functionality can be found on Github in our ["shopwareLabs" repository](https://github.com/shopwareLabs/SwagLightweightModule). +A demo plugin which highlights the new functionality can be found [on GitHub](https://github.com/shopware5/SwagLightweightModule). ## API documentation diff --git a/source/developers-guide/media-optimizer/index.md b/source/developers-guide/media-optimizer/index.md index 7ca09076c6..8e0393e28a 100644 --- a/source/developers-guide/media-optimizer/index.md +++ b/source/developers-guide/media-optimizer/index.md @@ -57,16 +57,16 @@ By default, the optimizer will search files within the `media` directory. Additi A full scan of files can take a long time to complete. For that, there is a `--modified` (`-m`) option to filter a range by providing a [PHP compatible time string](https://secure.php.net/manual/en/datetime.formats.php). Here are some examples: -#### Files of the last 24 hours +#### Files of the last 2 days ```bash -$ bin/console sw:media:optimize --modified="after 24 hours ago" +$ bin/console sw:media:optimize --modified="2 days ago" ``` #### Files of the last 2 weeks ```bash -$ bin/console sw:media:optimize --modified="after 2 weeks ago" +$ bin/console sw:media:optimize --modified="2 weeks ago" ``` ### Skip initial scan @@ -153,4 +153,4 @@ $ bin/console sw:media:optimize --info | jpegtran | Yes | image/jpeg, image/jpg | | pngcrush | Yes | image/png | +-------------------+----------+-----------------------+ -``` \ No newline at end of file +``` diff --git a/source/developers-guide/models/index.md b/source/developers-guide/models/index.md index bff03fd033..b9eccc8c4f 100644 --- a/source/developers-guide/models/index.md +++ b/source/developers-guide/models/index.md @@ -12,18 +12,19 @@ group: Developer Guides subgroup: Developing plugins --- -The [Doctrine framework](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/) has been integrated since Shopware 4 and offers the possibility of centrally defining the database structure in PHP. In addition, Doctrine offers the ability to centrally define all queries in a single system, called `Repositories`, to be used later at various points in the system. +The [Doctrine framework](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/) has been integrated since Shopware 4 and offers the possibility of centrally defining the database structure in PHP. +In addition, Doctrine offers the ability to centrally define all queries in a single system, called `Repositories`, to be used later at various points in the system. -In order to create models for your plugin, you should create a directory named `Models` and call `$this->registerCustomModels()` in your bootstrap's `install()` method to automatically register them. +In order to create models for your plugin, you should create a directory named `Models`.
    ## Namespaces -Shopware models make use of the PHP namespaces. This makes it possible to create your own article model in a plugin, without interfering with the default Shopware article model. With classes defined in other namespaces, it is not always necessary to use the full namespace. We can *include* them with an `use` statement: +Shopware models make use of the PHP namespaces. This makes it possible to create your own product model in a plugin, without interfering with the default Shopware product model. With classes defined in other namespaces, it is not always necessary to use the full namespace. We can *include* them with an `use` statement: ```php -namespace Shopware\CustomModels\MyPluginName; +namespace MyPluginName\Models; use Symfony\Component\Validator\Constraints as Assert, Doctrine\Common\Collections\ArrayCollection, @@ -65,8 +66,8 @@ With associations, the links between the different models can be defined in Doct If you have difficulty deciding when to use `@ORM\OneToMany` or when to use `@ORM\ManyToOne`, there is a little trick that can help you. In the `@ORM\OneToMany` and `@ORM\ManyToOne` associations, you can simply enter the model names and replace the word "_To_" with "_Has_". **Examples** -* `@ORM\One` **Article** Has Many **Details** -* `@ORM\Many` **Article** Has One **Supplier** +* `@ORM\Many` **Article** Has Many **Details** +* `@ORM\One` **Article** Has One **Supplier** _The name of the model is always on the left side of the model, with which the association is defined._ @@ -82,29 +83,65 @@ To find more in depth documentation about Doctrine events, please refer to the o When a model object is first added to the database, the `prePersist` and `postPersist` events on the model are triggered. These are passed on to the Shopware event system so that they can be handled. -**Event subscriber** -```php -$this->subscribeEvent( - 'Shopware\Models\Article\Article::prePersist', - 'prePersistArticle' -); - -$this->subscribeEvent( - 'Shopware\Models\Article\Article::postPersist', - 'postPersistArticle' -); -``` - -**Event listener** ```php -public function prePersistArticle(Enlight_Event_EventArgs $arguments) { - $modelManager = $arguments->get('entityManager'); - $model = $arguments->get('entity'); -} - -public function postPersistArticle(Enlight_Event_EventArgs $arguments) { - $modelManager = $arguments->get('entityManager'); - $model = $arguments->get('entity'); +getEntityManager(); + + $model = $arguments->getEntity(); + + if(!$model instanceof Article) { + return; + } + + // modify product data + } + + /** + * @param LifecycleEventArgs $arguments + */ + public function postPersist(LifecycleEventArgs $arguments) + { + /** @var ModelManager $modelManager */ + $modelManager = $arguments->getEntityManager(); + + $model = $arguments->getEntity(); + + // modify models or do some other fancy stuff + } } ``` @@ -113,26 +150,44 @@ Once a model has been added to the database, the `preUpdate` and `postUpdate` ev **Event subscriber** ```php -$this->subscribeEvent( - 'Shopware\Models\Article\Article::preUpdate', - 'preUpdateArticle' -); - -$this->subscribeEvent( - 'Shopware\Models\Article\Article::postUpdate', - 'postUpdateArticle' -); +public function getSubscribedEvents() +{ + return [ + Events::preUpdate, + Events::postUpdate, + ]; +} ``` **Event listener** ```php -public function preUpdateArticle(Enlight_Event_EventArgs $arguments) { - $modelManager = $arguments->get('entityManager'); - $model = $arguments->get('entity'); +/** + * @param LifecycleEventArgs $arguments + */ +public function preUpdate(LifecycleEventArgs $arguments) +{ + /** @var ModelManager $modelManager */ + $modelManager = $arguments->getEntityManager(); + + $model = $arguments->getEntity(); + + if(!$model instanceof Article) { + return; + } + + // modify product data } -public function postUpdateArticle(Enlight_Event_EventArgs $arguments) { - $modelManager = $arguments->get('entityManager'); - $model = $arguments->get('entity'); +/** + * @param LifecycleEventArgs $arguments + */ +public function postUpdate(LifecycleEventArgs $arguments) +{ + /** @var ModelManager $modelManager */ + $modelManager = $arguments->getEntityManager(); + + $model = $arguments->getEntity(); + + // modify models or do some other fancy stuff } ``` @@ -141,32 +196,62 @@ Once a model is removed from the database, the `preRemove` and `postRemove` even **Event subscriber** ```php -$this->subscribeEvent( - 'Shopware\Models\Article\Article::preRemove', - 'preRemoveArticle' -); - -$this->subscribeEvent( - 'Shopware\Models\Article\Article::postRemove', - 'postRemoveArticle' -); + /** + * {@inheritdoc} + */ +public function getSubscribedEvents() +{ + return [ + Events::preRemove, + Events::postRemove, + ]; +} + ``` **Event listener** ```php -public function preRemoveArticle(Enlight_Event_EventArgs $arguments) { - $modelManager = $arguments->get('entityManager'); - $model = $arguments->get('entity'); -} + /** + * @param LifecycleEventArgs $arguments + */ + public function preRemove(LifecycleEventArgs $arguments) + { + /** @var ModelManager $modelManager */ + $modelManager = $arguments->getEntityManager(); + + $model = $arguments->getEntity(); + + if(!$model instanceof Article) { + return; + } + + // modify product data + } + + /** + * @param LifecycleEventArgs $arguments + */ + public function postRemove(LifecycleEventArgs $arguments) + { + /** @var ModelManager $modelManager */ + $modelManager = $arguments->getEntityManager(); + + $model = $arguments->getEntity(); + + // modify models or do some other fancy stuff + } +``` + +Each event subscriber class is registered in the `services.xml` with the `doctrine.event_subscriber` tag. -public function postRemoveArticle(Enlight_Event_EventArgs $arguments) { - $modelManager = $arguments->get('entityManager'); - $model = $arguments->get('entity'); -} +```xml + + + ``` ## Example Plugin You can download an example plugin here, which shows you the basic structure and registration of your own models in your plugin. -[Download SwagModelPlugin.zip](/exampleplugins/SwagModelPlugin.zip) +[Download SwagModelPlugin.zip](/exampleplugins/SwagModel.zip) diff --git a/source/developers-guide/password-encoder/index.md b/source/developers-guide/password-encoder/index.md index 67807ed4c0..f664845eab 100644 --- a/source/developers-guide/password-encoder/index.md +++ b/source/developers-guide/password-encoder/index.md @@ -55,10 +55,12 @@ Let's say a user wants to log in with his email / password combination: ``` function isValidLogin($email, $plaintext) { - $result = Shopware()->Db()->fetchRow( + + $result = $this->container->get('dbal_connection')->fetch( 'SELECT hash, encoder FROM s_user WHERE email = ?', - array($email) + [$email] ); + if (!$result) { return false; } @@ -70,53 +72,66 @@ function isValidLogin($email, $plaintext) { First of all we will read the hashed password `hash` as well as the used hash algorithm `encoder` from database. Then the method `isPasswordValid` of the password manager is used, to check if `$result['hash']` and `$plaintext` match for the given encoder name. - ### Implementing own password encoders The following example shows how to create a custom password encoder. Imagine you are importing user data from a shop system *FancyShop* where the passwords hashes are stored by reversing and md5 hashing the password strings (which is not a very good idea). The following example will show how to create a new password encoder "Md5Reversed", which will allow the customers to log in with their password and automatically store a new, more secure bcrypt hash. ### Register the encoder -In order to register the new encoder with Shopware, a plugin needs to subscribe to the `Shopware_Components_Password_Manager_AddEncoder` event and add an instance of the new encoder to the password encoder collection: In the `onAddEncoder` callback method the plugin's namespace is registered by calling the `registerMyComponents` method. Furthermore, the encoder collection is extracted using `$hashes = $args->getReturn();`. Finally, a new element is added and the collection of password encoders is returned. +In order to register the new encoder with Shopware, a plugin needs to subscribe to the `Shopware_Components_Password_Manager_AddEncoder` event and add an instance of the new encoder to the password encoder collection: Furthermore, the encoder collection is extracted using `$hashes = $args->getReturn();`. Finally, a new element is added and the collection of password encoders is returned. + +```xml + + + + + + + + + + ``` -class Shopware_Plugins_Frontend_SwagMd5Reversed_Bootstrap extends Shopware_Components_Plugin_Bootstrap -{ - public function getVersion() { … } - public function getLabel() { … } +```php +subscribeEvent( - 'Shopware_Components_Password_Manager_AddEncoder', - 'onAddEncoder' - ); +namespace SwagMd5Reversed\Subscriber; - return true; +use Enlight\Event\SubscriberInterface; +use Enlight_Event_EventArgs; +use SwagMd5Reversed\Components\Md5ReversedEncoder; + +class AddEncoderSubscriber implements SubscriberInterface +{ + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + 'Shopware_Components_Password_Manager_AddEncoder' => 'onAddEncoder' + ]; } + /** + * Add the encoder to the internal encoder collection + * + * @param Enlight_Event_EventArgs $args + * @return array + */ public function onAddEncoder(Enlight_Event_EventArgs $args) { - $this->registerMyComponents(); - - $hashes = $args->getReturn(); - - $hashes[] = new \Shopware\SwagMd5Reversed\Md5ReversedEncoder(); - - return $hashes; + $passwordHashHandler = $args->getReturn(); + $passwordHashHandler[] = new Md5ReversedEncoder(); - } - - public function registerMyComponents() - { - $this->Application()->Loader()->registerNamespace( - 'Shopware\SwagMd5Reversed', - $this->Path() - ); + return $passwordHashHandler; } } ``` @@ -135,10 +150,10 @@ A valid password encoder must implement the `PasswordEncoderInterface`. It defin ### Example implementation: -``` +```php @@ -454,7 +454,7 @@ public function returnAction() } if (!$success) { - //do errror handling like redirecting to error page + //do error handling like redirecting to error page } // continue with saving order ... @@ -520,4 +520,4 @@ In shopware 5.3 and later this should be done via the here \ No newline at end of file +An example plugin can be found here diff --git a/source/developers-guide/phpstorm/index.md b/source/developers-guide/phpstorm/index.md index f01413ba65..38d3e776c2 100644 --- a/source/developers-guide/phpstorm/index.md +++ b/source/developers-guide/phpstorm/index.md @@ -21,11 +21,11 @@ You can find installation instructions on the official [PhpStorm Website](https: ### Requirements for developing -First of all you need a working Shopware installation. Just visit our [Github Repository](https://github.com/shopware/shopware) +First of all you need a working Shopware installation. Just visit our [Github Repository](https://github.com/shopware5/shopware) and follow the `README.md` installation instructions. You do not want to worry about a local webserver or database server? Then take a look at our [Vagrant and PHPStorm](/developers-guide/vagrant-phpstorm/) guide to set up a virtual machine, ready to develop with Shopware. If you want to contribute to the Shopware repository also check out our -[Contributing](/contributing/) guides. +[contribution guideline](/community/contribution-guideline/). ### Benefits of using PhpStorm @@ -96,4 +96,3 @@ From the context menu `Mark directory as` -> `Excluded` - `tests/Unit/` - `tests/Functional/` - diff --git a/source/developers-guide/plugin-configuration/img/boolean.png b/source/developers-guide/plugin-configuration/img/boolean.png new file mode 100644 index 0000000000..56b71321a1 Binary files /dev/null and b/source/developers-guide/plugin-configuration/img/boolean.png differ diff --git a/source/developers-guide/plugin-configuration/img/button.png b/source/developers-guide/plugin-configuration/img/button.png new file mode 100644 index 0000000000..268168cba7 Binary files /dev/null and b/source/developers-guide/plugin-configuration/img/button.png differ diff --git a/source/developers-guide/plugin-configuration/index.md b/source/developers-guide/plugin-configuration/index.md index 1e91c510ba..2f41a8ee6f 100644 --- a/source/developers-guide/plugin-configuration/index.md +++ b/source/developers-guide/plugin-configuration/index.md @@ -8,9 +8,8 @@ subgroup: Developing plugins menu_title: Plugin configuration menu_order: 70 --- -This document will give you a brief introduction about how to set configuration options for legacy plugins, which parameters are available and -how to use them. By using the [new plugin system](/developers-guide/plugin-system) (since 5.2) the following code examples -are __deprecated__. With the new plugin system all configurations can be done with help of `config.xml` file resulting in the examples shown here. +This document will give you a brief introduction about how to set configuration options for plugins, which tags and attributes are available and how to use them. +All configurations is done with help of the `config.xml` file resulting in the examples shown here.
    @@ -19,595 +18,381 @@ After getting a short introduction by reading the Plugin Quick Introduction, we the configuration options for plugins. Shopware delivers a big amount of helper functions for generating standard configuration fields in backend. +
    +Be careful with sensitive data! + +Please be aware to not save any files that contain sensitive data to your plugin directory (e.g. config or log files) as these might be accessible by public. +
    + ## Creating forms -To create a simple plugin configuration we can use the function `Form()` +A plugin configuration can be created with a Resources/config.xml file. -``` -$form = $this->Form(); -``` +```xml + + + -Now we can use this instance of `Shopware\Models\Config\Form` in `$form` to add elements to it via `setElement(). -``` -$form->setElement(type : String, name : String, [options : array | null= null]); + + ``` + +Now add elements in the elements tag. + A textfield would be defined as followed: -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField'); -} +```xml + + + + + simpleTextField + + + preselection + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. + + + ``` Which would lead to a simple textfield. Simple textfield -## Options Parameter -The options parameter of the `setElement` function allows to set several configurations on the form element. - ### Label -The label parameter allows to create a simple descriptional label for the form element. -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array('label' => 'Text') - ); -} +The label tag allows to create a simple descriptional label for the form element. +```xml + + ``` Textfield with label -### Value -The value parameter stands for the default value of the field if this hasn´t been edited yet. It will directly be shown in the configuration element. -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl' - ) - ); -} +### Description +The description tag allows to provide a more detailed description of the configuration element. + +```xml +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. +Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. ``` -Textfield with default value +Textfield with description -### Scope -With help of the scope parameter it is possible to generate subshop specific configurations. Leaving this option out results in a configuration option -that applies for all subshops. -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl', - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP - ) - ); -} +### Value +The value tag stands for the default value of the field if this hasn´t been edited yet. It will directly be shown in the configuration element. +```xml +preselection ``` -Configuration with scope - -You can read more on subshop specific configuration at the end of this article reading the [Subshop specific configuration](#subshop-specific-plugin-configur) part. +Textfield with default value -### Description -The description parameter allows to provide a more detailed description of the configuration element. +### Options +The options tag allows you to set configs of the ExtJs representation of a specific configuration element. See [ExtJs Docs](https://docs.sencha.com/extjs/4.1.1/#!/api/Ext.form.field.Number-cfg-minValue) +```xml + + testNumber + + + 0 + + ``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl', - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP, - 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut' - ) - ); -} -``` - -Textfield with description ### Required -The required parameter specifies whether the configuration item is mandatory or not. +The required attribute specifies whether the configuration item is mandatory or not. ``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl', - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP, - 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut', - 'required' => true - ) - ); -} + ``` Textfield marked as required +### Scope +With help of the scope attribute it is possible to generate subshop specific configurations. Leaving this option out results in a configuration option +that applies for all subshops. +```xml + +``` + +Configuration with scope + +You can read more on subshop specific configuration at the end of this article reading the [Subshop specific configuration](#subshop-specific-plugin-configuration) part. + ## Element Types Below all supported configuration elements including their design and source code are described again. -### Colorpicker +### Boolean +```xml + + ... + ``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('color', 'myColorfield', - array( - 'label' => 'Color', - 'value' => NULL - ) - ); -} + +Booleanfield + +### Colorpicker +```xml + + ... + ``` Colorpicker ### Datefield -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('date', 'myDatefield', - array( - 'label' => 'Date', - 'value' => NULL - ) - ); -} +```xml + + ... + ``` Datefield ### Datetime field -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('datetime', 'myDatetimefield', - array( - 'label' => 'Date-Time', - 'value' => NULL - ) - ); -} +```xml + + ... + ``` Datetime field ### HTML editor -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('html', 'myHtmlfield', - array( - 'label' => 'HTML', - 'value' => NULL - ) - ); -} +```xml + + ... + ``` HTML editor ### Time interval -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('interval', 'myIntervalfield', - array( - 'label' => 'Interval', - 'value' => NULL - ) - ); -} +```xml + + ... + ``` Time interval ### Mediaselection -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('mediaselection','myMediaselectionfield', - array( - 'label' => 'Media', - 'value' => NULL - ) - ); -} +```xml + + ... + ``` Mediaselection ### Numberfield -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('number', 'myNumberfield', - array( - 'label' => 'Number', - 'minValue' => 0 - ) - ); -} +```xml + + ... + ``` Numberfield ### Selectionfield / combobox -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('select', 'mySelectionfield', - array('label' => 'Select', - 'store' => array( - array(1, 'Testvariable 1'), - array(2, 'Testvariable 2'), - array(3, 'Testvariable 3') - ) - ) - ); -} +```xml + + + + + + + + ``` Combobox ### Selectionfield / remote combobox +```xml + + ... + Shopware.apps.Base.store.CustomerGroup + ``` -public function createConfiguration() -{ - $form->setElement('combo', 'myCombo', - array( - 'label'=>'Backend-User','value'=>'Please select', - 'store' => 'base.CustomerGroup', - 'scope' => \Shopware\Models\Config\Element::SCOPE_SHOP - ) - ); -} -``` - Remotecombobox -### Textfield +### Multi selectionfield / multi combobox / multi remote combobox + +To make your selectfield / combobox multiple selectable, you need to create the options node: + +```xml + + ... + Shopware.apps.Base.store.CustomerGroup + + true + + +``` + +### Remote combobox with own store +Define and create your own ExtJs data store for the backend Plugin configuration + +```xml + + ... + + + + id + name + true + + +``` +Note that the `//new ` is an important hack to load the store + +```php + public function yourAction() + { + $data = [ + ['id' => 1, 'name' => 'foo'], + ['id' => 2, 'name' => 'bar'], + ]; + + $this->view->assign([ + 'data' => $data, + 'total' => count($data), + ]); + } ``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => NULL - ) - ); -} +The controller action should assign a associated array. + +### Textfield +```xml + + ... + ``` Textfield ### Textarea -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('textarea', 'myTextareafield', - array( - 'label' => 'Text-Area', - 'value' => NULL - ) - ); -} +```xml + + ... + ``` Textarea ### Timefield -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('time', 'myTimefield', - array( - 'label' => 'Time', - 'value' => NULL - ) - ); -} +```xml + + ... + ``` Timefield -## Configure basic settings -The plugin configurations of the various plugins are automatically -reachable via the basic settings under the "Additional settings" entry: -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl', - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP, - 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut', - 'required' => true - ) - ); -} -``` - -Plugin configuration in base settigns - -If you think they are better off under another entry, you can move your configuration entry to another submenu in the settings: -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl', - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP, - 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.', - 'required' => true - ) - ); - - $repository = Shopware()->Models()->getRepository('Shopware\Models\Config\Form'); - $form->setParent( - $repository->findOneBy(array('name' => 'Interface')) - ); -} -``` - -Plugin configuration in base settigns other position - -## Translation of configurations -The Shopware backend is fully translatable. To translate the custom plugin configurations you can use following code: -``` -public function createConfiguration() -{ - $form = $this->Form(); - - $form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl', - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP, - 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.', - 'required' => true - ) - ); - - //contains all translations - $translations = array( - 'en_GB' => array( - 'myTextField' => 'Text input', - ), - 'es_ES' => array( - 'myTextField' => 'introducción de texto', - ), - 'fr_FR' => array( - 'myTextField' => 'La saisie de texte', - ), - //... - ); - - $this->addFormTranslations($translations); -} -``` -It is also possible to create translated options for the selection element like this: -``` - /** - * Creates the plugin configuration - */ - private function createPluginConfig() - { - $store = [ - ['normal', ['de_DE' => 'Nicht anders darstellen', 'en_GB' => "Don't change displaying"]], - ['pseudo', ['de_DE' => 'Alten Preis als neuen Pseudopreis setzen', 'en_GB' => 'Set old price as new pseudo price']], - ['price', ['de_DE' => 'Nur neuen Preis hervorheben', 'en_GB' => 'Only highlight new price']] - ]; - - $form = $this->Form(); - $form->setElement( - 'select', - 'promotionPriceDisplaying', - [ - 'label' => 'Preisdarstellung', - 'description' => 'Durch bestimmte Promotions kann sich der Preise eines Aritkels ändern. Hier können Sie definieren, wie der Preis auf der Detailseite dargestellt werden soll.', - 'store' => $store, - 'value' => 'normal', - 'scope' => Element::SCOPE_SHOP, - ] - ); - - $translation = [ - 'en_GB' => [ - 'promotionPriceDisplaying' => [ - 'label' => 'Price displaying', - 'description' => 'The price of a product could be changed by particular promotions. Define here, how the price should be displayed on the detail page.' - ] - ] - ]; - - $this->addFormTranslations($translation); - } +### Button +```xml + + ... + ``` +Buttonfield -Translate your configuration - -Let´s have a look what we did here: -``` -$form = $this->Form(); -$form->setElement('text', 'simpleTextField', - array( - 'label' => 'Text', - 'value' => 'Vorauswahl', - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP, - 'description' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr', - 'required' => true - ) -); -``` -We created a new configuration textfield. Now we define our translations. We create an array with all the translations as key values inside: -``` -//contains all translations -$translations = array( - 'en_GB' => array( - 'myTextField' => 'Text input', - ), - 'es_ES' => array( - 'myTextField' => 'introducción de texto', - ), - 'fr_FR' => array( - 'myTextField' => 'La saisie de texte', - ), - //... -); +## Configure basic settings +The plugin configurations of the various plugins are automatically +reachable via the basic settings under the "Additional settings" entry: +```xml + + + + + simpleTextField + + + preselection + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. + + + ``` -The first-level keys define the language of the translation. The second-level keys define the translatable element. The value of the second-level key is -the translation itself. -After defining the translations it is now possible to add them to the related element using the helper function `addFormTranslations($translations)`. - -In the __Basic Settings__ our field in English backend now looks like this: - -Configuration in English backend +Plugin configuration in base settigns ## Subshop specific plugin configuration In this chapter we want to show how to make your configurations subshop specific. To demonstrate this we write a little plugin which replaces the Shopware logo with random text. -### Bootstrap -``` - + + + + show + + + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut. + + + +``` + +With the help of the scope attribute we can assign configurations per subshop. Without the scope attribute the configuration is used in all subshops. We´ve mentioned this earlier this article. + +```php +public function onPostDispatch(Enlight_Event_EventArgs $arguments) { - public function getLabel() - { - return 'Plugin Sichtbarkeit'; - } - - public function getInfo() - { - return array( - 'label' => $this->getLabel(), - 'version' => $this->getVersion(), - 'link' => 'http://www.shopware.de' - ); - } - - public function getVersion() - { - return '1.0.0'; + $shop = false; + if ($this->container->initialized('shop')) { + $shop = $this->container->get('shop'); } - - public function install() - { - $this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatch', - 'onPostDispatch' - ); - - $form = $this->Form(); - $parent = $this->Forms()->findOneBy(array('name' => 'Frontend')); - $form->setParent($parent); - $form->setElement( - 'checkbox', - 'show', - array( - 'label' => 'Plugin Anzeigen', - 'value' => true, - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP - ) - ); - - return true; - } - - public function onPostDispatch(Enlight_Event_EventArgs $arguments) - { - $config = $this->Config(); - if (empty($config->show)) { - return; - } - - $controller = $arguments->getSubject(); - $view = $controller->View(); - - $view->assign('swagSubshopVisibility', 'Test text.'); - - $view->addTemplateDir($this->Path() . 'Views/'); + + if (!$shop) { + $shop = $this->container->get('models')->getRepository(\Shopware\Models\Shop\Shop::class)->getActiveDefault(); } -} -``` -First we register to the `PostDispatch` event by using the `install()` method. The `postDispatch` event -gets triggered after the __PostDispatch__ and will trigger the actual process. Next we add the plugin -configuration via the `Form()` method like described earlier in this article. Later we will change the -visibility of these configuration options related to subshops. -``` -$form = $this->Form(); -``` -Now we have an instance of `Shopware\Models\Config\Form` in the variable `$form`. We can then assign elements with -help of the `setElement()` method. -``` -$form->setElement('checkbox', 'show', array( - 'label' => 'Plugin Anzeigen', - 'value' => true, - 'scope' => Shopware\Models\Config\Element::SCOPE_SHOP -)); -``` -With the help of the __scope__ parameter we can assign configurations per subshop. Without the scope parameter the configuration -is used in all subshops. We´ve mentioned this earlier this article. -``` -public function onPostDispatch(Enlight_Event_EventArgs $arguments) -{ - $config = $this->Config(); - if (empty($config->show)) { + + $config = $this->container->get('shopware.plugin.cached_config_reader')->getByPluginName('PluginName', $shop); + if (!(bool) $config['show']) { return; } @@ -616,14 +401,14 @@ public function onPostDispatch(Enlight_Event_EventArgs $arguments) $view->assign('swagSubshopVisibility', 'Test text.'); - $view->addTemplateDir($this->Path() . 'Views/'); + $view->addTemplateDir($this->pluginBasePath . '/Resources/views/'); } -``` -Within the `postDispatch()` method we create an instance of the configuration. Afterwards we test for the just created __show__ attribute. If -this attribute is empty the method ends and the plugin will not be executed or shown any further. If __show__ is set the template is loaded and -assigned via __smarty__. ``` + +Within the onPostDispatch() method in a subscriber read the configuration with help of the service. Afterwards we test for the just created `show` attribute. If this attribute is empty the method ends and the plugin will not be executed or shown any further. If `show` is set the template is loaded and assigned via smarty. + +```smarty {extends file="parent:frontend/index/logo-container.tpl"} {block name="frontend_index_logo"}
    @@ -632,7 +417,6 @@ assigned via __smarty__. {/block} ``` -Our new template extends the parent __logo-container.tpl__ and overwrites the __frontend_index_logo__ block and sets the text we used in the `bootstrap.php`. Now we can open the plugin configuration via -Plugin Manager and configure it differently for every subshop. +Our new template extends the parent `logo-container.tpl` and overwrites the `frontend_index_logo` block and sets the text. Now we can open the plugin configuration via Plugin Manager and configure it differently for every subshop. What's next? Continue reading about the new [Shopware 5.2 Plugin System](/developers-guide/plugin-system). diff --git a/source/developers-guide/plugin-extension-by-plugin/SwagExtendCustomProducts.zip b/source/developers-guide/plugin-extension-by-plugin/SwagExtendCustomProducts.zip deleted file mode 100644 index 6447fd9c86..0000000000 Binary files a/source/developers-guide/plugin-extension-by-plugin/SwagExtendCustomProducts.zip and /dev/null differ diff --git a/source/developers-guide/plugin-extension-by-plugin/img/fileOverview.jpg b/source/developers-guide/plugin-extension-by-plugin/img/fileOverview.jpg new file mode 100644 index 0000000000..3265bc9f10 Binary files /dev/null and b/source/developers-guide/plugin-extension-by-plugin/img/fileOverview.jpg differ diff --git a/source/developers-guide/plugin-extension-by-plugin/img/fileOverview.png b/source/developers-guide/plugin-extension-by-plugin/img/fileOverview.png deleted file mode 100644 index a8c447ce1f..0000000000 Binary files a/source/developers-guide/plugin-extension-by-plugin/img/fileOverview.png and /dev/null differ diff --git a/source/developers-guide/plugin-extension-by-plugin/index.md b/source/developers-guide/plugin-extension-by-plugin/index.md index 93a8d9753d..115e44f0d6 100644 --- a/source/developers-guide/plugin-extension-by-plugin/index.md +++ b/source/developers-guide/plugin-extension-by-plugin/index.md @@ -21,99 +21,52 @@ This document will describe how existing plugins can be extended. Why change a plugin with a plugin? To keep the changes compatible if the plugins gets an update. -The objective is to implement an own option type in SwagCustomProducts, which can be used in a template. This new option makes it possible for a customer to upload files with special mime types. - -To achieve the objective we decorate a service to extend the function, extend ExtJs by Smarty blocks and implement new ExtJs and Smarty templates. +The target is to implement an own option type in SwagCustomProducts, which can be used in a template. This new option makes it possible for a customer to upload files with special mime types. +To achieve this, we decorate a service, extend ExtJs with Smarty blocks and implement new ExtJs files and Smarty templates. On base of the new 5.2 plugin system, we extend the plugin SwagCustomProducts with the following functions. - - Decoration of the FileTypeWhiteList.php to add other file extensions to the whitelist. - Adding a custom option type. ## The file and directory structure -![Directory and file overview](img/fileOverview.png) +![Directory and file overview](img/fileOverview.jpg) ## Create the plugin basics -At first create the base structure of the plugin, including plugin.xml, config.xml, services.xml and the SwagExtendCustomProducts.php. -These files are the base of each plugin. +Create the base structure of the plugin, including plugin.xml, services.xml and the SwagExtendCustomProducts.php. -For more information about the 5.2 Plugin system click here +For more information about the Shopware 5.2 plugin system click here ### SwagExtendCustomProducts/plugin.xml -contains the base information about the plugin, the label in English and German, the plugin version, the required Shopware version and the changelog. -This file is equal to the old plugin.json. +contains the base information about the plugin, the label in English and German, the plugin version, the required Shopware version and the changelog. It also makes sense to set the plugin `SwagCustomProducts` as requirement, as we want to extend it. ```xml + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> + + 1.0.0 (c) by shopware AG proprietary http://store.shopware.com shopware AG + Erstveröffentlichung; First release; + + + + ``` -### SwagExtendCustomProducts/Resources/config.xml -describes possible global plugin settings. - -In this file you can easily implement a bunch of plugin settings which the customer can use to control the plugin. -Each element node represents a single plugin setting. -You can also translate the label and the description in your language. - -Possible types for elements are: - -- text -- password -- textarea -- boolean -- color -- date -- datetime -- time -- interval -- html -- mediaselection -- number -- select -- combo - -```xml - - - - - - - extend - - - Wenn Sie Custom Products erweitern wollen müssen Sie diese Option aktivieren. - - If you want to extend Custom Products, you must enable this option. - - - - senselessly - - - Das ist ein sinnloser Text. - This is a senseless text. - - - - -``` +The `minVersion="4.0.0"` attribute makes sure, that we use the correct version of CustomProduct. ### SwagExtendCustomProducts/Resources/services.xml @@ -124,22 +77,6 @@ Make sure that Subscriber-Services implement the tag. ``` -Also you can define each type of service here. - -```xml - - - - -``` - -With the tag -```xml - -``` -you can specify other services as parameter to inject them into the service. - - ```xml @@ -148,219 +85,46 @@ you can specify other services as parameter to inject them into the service. xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - + + %swag_extend_custom_products.plugin_dir% + - - - + + %swag_extend_custom_products.plugin_dir% + - - + + - - - + + - - ``` -### SwagExtendCustomProducts/SwagExtendCustomProducts.php -is like the Bootstrap.php in the old plugin-system. -Overwrite functions to execute your code here. - -For more information about the functions you can use and overwrite take a look into the wiki The 5.2 Plugin system or in the class _\Shopware\Components\Plugin_ which you can find in the folder _ /engine/Shopware/Components_. - -In our example we overwrite the build and the install methods. - - The build method, to add a new parameter into the dependency container. -wherever you have the container available you can call the parameter by: - -```php -$this->container->getParameter('swag_extend_custom_products.plugin_dir'); -``` - - The install method, to initialize our installation procedure. - -```php -setParameter('swag_extend_custom_products.plugin_dir', $this->getPath()); - - parent::build($container); - } - - /** - * Overwrites the install method to execute specific installation code. - * - * @param InstallContext $context - */ - public function install(InstallContext $context) - { - $installer = new Installer($this->container); - $installer->install(); - } -} -``` - -### SwagExtendCustomProducts/Setup/Installer.php -To keep track of the code, swap out the installation code in this extra file. -In this example it checks if the plugin "Custom Products (v2)" is installed, but you can add other code here if necessary for your installation. - -```php -container = $container; - } - - /** - * @return bool - */ - public function install() - { - // Checks if the plugin Custom Products (v2) is installed. - $verifier = new Verifier($this->container->get('dbal_connection')); - if(!$verifier->isDependingPluginInstalled()) { - throw new RuntimeException('The Plugin "Custom Products (v2)" is required.'); - } - - return true; - } -} -``` - -### SwagExtendCustomProducts/Components/Verifier.php -Simple class to check if a plugin is installed. - -```php -connection = $connection; - } - - /** - * @return bool - */ - public function isDependingPluginInstalled() - { - $queryBuilder = $this->connection->createQueryBuilder(); - - return (bool)$queryBuilder->select('id') - ->from('s_core_plugins') - ->where('name LIKE "SwagCustomProducts"') - ->andWhere('active = 1') - ->execute() - ->fetchColumn(); - } -} - -``` - ## Decorate the WhiteListService -We decide to decorate the service by the event method because the plugin we want to expand is still based on the old plugin system. Thus, the service wasn't injected into the container yet. - - -To add new items into the white list we must decorate the service. -That means, call the original service and create our own service. This contains the original service, implements the same interface and extends the functions. -At the end set the service to the container instead of the original. - - - To decorate the WhiteListService of the Custom Product (v2) plugin we need the event name that invokes the Service. _custom_products.file_upload.file_type_whitelist_ - - In combination with the prefix _Enlight_Bootstrap_AfterInitResource_ we create the full event name: **_Enlight_Bootstrap_AfterInitResource_custom_products.file_upload.file_type_whitelist_** - - For more information about Shopware events read the Event guide - -The Subscriber **_SwagExtendCustomProducts/Subscriber/FileTypeDecorator.php_** - -```php -container = $container; - } - - public static function getSubscribedEvents() - { - return [ - 'Enlight_Bootstrap_AfterInitResource_custom_products.file_upload.file_type_whitelist' => 'decorateFileTypeWhiteList' - ]; - } - - public function decorateFileTypeWhiteList() - { - /** @var FileTypeWhitelist $fileTypeWhiteList */ - $fileTypeWhiteList = $this->container->get('custom_products.file_upload.file_type_whitelist'); - - $this->container->set( - 'custom_products.file_upload.file_type_whitelist', - new FileTypeWhiteListDecorator($fileTypeWhiteList) - ); - } -} +To add new items into the white list we must decorate the service by using the `services.xml`. + + ```xml + + + ``` -If Custom Products (v2) calls the event, set the decorator into the container to replace the original service. + +The container replaces the original service with our own. If the service is called the 'FileTypeWhiteListDecorator' is returned. **_SwagExtendCustomProducts/Decorators/FileTypeWhiteListDecorator.php_** adds an array of new mime types to the white list. @@ -370,51 +134,55 @@ Now the customer in the frontend can upload the files with the new mime types. namespace SwagExtendCustomProducts\Decorators; -use ShopwarePlugins\SwagCustomProducts\Components\FileUpload\FileTypeWhitelist; -use ShopwarePlugins\SwagCustomProducts\Components\FileUpload\FileTypeWhitelistInterface; -use ShopwarePlugins\SwagCustomProducts\Components\Types\Types\FileUploadType; -use ShopwarePlugins\SwagCustomProducts\Components\Types\Types\ImageUploadType; +use SwagCustomProducts\Components\FileUpload\FileTypeWhitelist; +use SwagCustomProducts\Components\FileUpload\FileTypeWhitelistInterface; +use SwagCustomProducts\Components\Types\Types\FileUploadType; class FileTypeWhiteListDecorator implements FileTypeWhitelistInterface { - /** @var FileTypeWhitelist */ + /** + * @var FileTypeWhitelistInterface + */ private $fileTypeWhitelist; /** - * inject the original FileTypeWhiteListDecorator + * Inject the original FileTypeWhiteListDecorator * - * @param FileTypeWhitelist $fileTypeWhitelist + * @param FileTypeWhitelistInterface $fileTypeWhitelist */ - public function __construct(FileTypeWhitelist $fileTypeWhitelist) + public function __construct(FileTypeWhitelistInterface $fileTypeWhitelist) { $this->fileTypeWhitelist = $fileTypeWhitelist; } - + /** - * @param string $type - * @return string|void + * {@inheritdoc} */ public function getMimeTypeWhitelist($type) { - switch ($type) { - case FileUploadType::TYPE: - return $this->getMimeTypeWhitelistForFiles(); - case ImageUploadType::TYPE: - return FileTypeWhitelist::$mimeTypeWhitelist['image']; - default: - return; + if ($type === FileUploadType::TYPE) { + return $this->getMimeTypeWhitelistForFiles(); } + + return $this->fileTypeWhitelist->getMimeTypeWhitelist($type); } /** - * @param string $type - * @return string|void + * {@inheritdoc} */ public function getExtensionWhitelist($type) { return $this->fileTypeWhitelist->getExtensionWhitelist($type); } + /** + * {@inheritdoc} + */ + public function getMediaOverrideType($extension) + { + return $this->fileTypeWhitelist->getMediaOverrideType($extension); + } + /** * Add new mimeTypes to whiteList * @@ -446,8 +214,6 @@ class FileTypeWhiteListDecorator implements FileTypeWhitelistInterface ## Create the new option type For adding a new option type to Custom Products (v2) subscribe to _SwagCustomProduct_Collect_Types_ event, which is fired in _SwagCustomProducts/Components/Types/TypeFactory.php_ -More information about extending the backend. - ```php $this->eventManager->collect('SwagCustomProduct_Collect_Types', $collection); ``` @@ -463,29 +229,32 @@ namespace SwagExtendCustomProducts\Subscriber; use Doctrine\Common\Collections\ArrayCollection; use Enlight\Event\SubscriberInterface; -use Enlight_Event_EventArgs; use SwagExtendCustomProducts\Components\Types\CustomType; class TypeFactory implements SubscriberInterface { + /** + * {@inheritdoc} + */ public static function getSubscribedEvents() { return [ - 'SwagCustomProduct_Collect_Types' => 'onCollectTypes' + 'SwagCustomProduct_Collect_Types' => 'onCollectTypes', ]; } /** * Returns our new type(s) as ArrayCollection - * @param Enlight_Event_EventArgs $arguments * * @return ArrayCollection */ - public function onCollectTypes(Enlight_Event_EventArgs $arguments) + public function onCollectTypes() { - return new ArrayCollection([ - CustomType::TYPE => new CustomType() - ]); + return new ArrayCollection( + [ + CustomType::TYPE => new CustomType(), + ] + ); } } ``` @@ -501,7 +270,7 @@ Define the type of the "customType" with a string and control whether the class namespace SwagExtendCustomProducts\Components\Types; -use ShopwarePlugins\SwagCustomProducts\Components\Types\TypeInterface; +use SwagCustomProducts\Components\Types\TypeInterface; class CustomType implements TypeInterface { @@ -509,7 +278,7 @@ class CustomType implements TypeInterface const COULD_CONTAIN_VALUES = false; /** - * @return string + * {@inheritdoc} */ public function getType() { @@ -517,7 +286,7 @@ class CustomType implements TypeInterface } /** - * @return boolean + * {@inheritdoc} */ public function couldContainValues() { @@ -528,6 +297,8 @@ class CustomType implements TypeInterface After that, add the translation to the _SwagCustomProducts/Views/backend/swag_custom_products/view/components/type_translator.js_. Use the block "backend/swag_custom_products/components/typeTranslator/snippets" to add new Snippets. +More information about extending the backend. + ```js snippets: { types: { @@ -569,57 +340,50 @@ To extend ExtJs with our files we need two cases: namespace SwagExtendCustomProducts\Subscriber; use Enlight\Event\SubscriberInterface; -use Enlight_Event_EventArgs; -use Enlight_View_Default; -use Shopware_Proxies_ShopwareControllersBackendSwagCustomProductsProxy as Subject; -use Symfony\Component\DependencyInjection\ContainerInterface; class Backend implements SubscriberInterface { - /** @var ContainerInterface */ - private $container; - - /** @var string */ + /** + * @var string + */ private $path; - public function __construct(ContainerInterface $container) + /** + * @param string $pluginPath + */ + public function __construct($pluginPath) { - $this->container = $container; - $this->path = $this->container->getParameter('swag_extend_custom_products.plugin_dir'); + $this->path = $pluginPath; } + /** + * {@inheritdoc} + */ public static function getSubscribedEvents() { return [ - 'Enlight_Controller_Action_PostDispatch_Backend_SwagCustomProducts' => 'extendBackendModule' + 'Enlight_Controller_Action_PostDispatch_Backend_SwagCustomProducts' => 'extendBackendModule', ]; } - public function extendBackendModule(Enlight_Event_EventArgs $arguments) + /** + * @param \Enlight_Event_EventArgs $arguments + */ + public function extendBackendModule(\Enlight_Event_EventArgs $arguments) { - /** @var Subject $subject */ + /** @var \Shopware_Controllers_Backend_SwagCustomProducts $subject */ $subject = $arguments->get('subject'); - /** @var Enlight_View_Default $view */ $view = $subject->View(); - // add the template dir to the view. - $view->addTemplateDir( - $this->path . '/Resources/Views/' - ); - - $files = []; + $view->addTemplateDir($this->path . '/Resources/Views/'); if ($arguments->get('request')->getActionName() === 'index') { - $files[] = 'backend/swag_custom_products/view/option/types/custom_type.js'; + $view->extendsTemplate('backend/swag_custom_products/view/option/types/custom_type.js'); } if ($arguments->get('request')->getActionName() === 'load') { - $files[] = 'backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js'; - } - - foreach ($files as $file) { - $view->extendsTemplate($file); + $view->extendsTemplate('backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js'); } } } @@ -630,10 +394,7 @@ Add the option type in ExtJs. Create **_SwagExtendCustomProducts/Resources/Views The AbstractTypeContainer is an abstract ExtJs class which defines functions and "template functions" you can use or overwrite. ```js -// - //{block name="backend/swag_custom_products/view/option/types/customType"} - // Take the original Custom Product Type and only use "Custom Type" as suffix. // Custom Products is building this path in "Shopware.apps.SwagCustomProducts.view.option.Detail" Ext.define('Shopware.apps.SwagCustomProducts.view.option.types.CustomType', { @@ -648,10 +409,12 @@ At last create a template for the frontend to display the new option type. ```html {block name="frontend_detail_swag_custom_products_options_customtype"} + id="custom-products-option-{$key}" + data-field="true" + {if $option['required']} + data-validate="true" + data-validate-message="{s name='detail/validate/textfield'}{/s}" + {/if}/> {/block} ``` @@ -665,43 +428,43 @@ Also create a subscriber to add the option template to the view. namespace SwagExtendCustomProducts\Subscriber; use Enlight\Event\SubscriberInterface; -use Enlight_Event_EventArgs; -use Enlight_View_Default; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Shopware_Proxies_ShopwareControllersBackendSwagCustomProductsProxy as Subject; class Frontend implements SubscriberInterface { - /** @var ContainerInterface */ - private $container; - - /** @var string */ + /** + * @var string + */ private $path; - public function __construct(ContainerInterface $container) + /** + * @param string $pluginPath + */ + public function __construct($pluginPath) { - $this->container = $container; - $this->path = $this->container->getParameter('swag_extend_custom_products.plugin_dir'); + $this->path = $pluginPath; } + /** + * {@inheritdoc} + */ public static function getSubscribedEvents() { return [ - 'Enlight_Controller_Action_PreDispatch_Frontend_Detail' => 'extendFrontend' + 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail' => 'extendFrontendDetail', ]; } - public function extendFrontend(Enlight_Event_EventArgs $arguments) + /** + * @param \Enlight_Event_EventArgs $arguments + */ + public function extendFrontendDetail(\Enlight_Event_EventArgs $arguments) { - /** @var Subject $subject */ + /** @var \Shopware_Controllers_Frontend_Detail $subject */ $subject = $arguments->get('subject'); - /** @var Enlight_View_Default $view */ $view = $subject->View(); - $view->addTemplateDir( - $this->path . '/Resources/Views/' - ); + $view->addTemplateDir($this->path . '/Resources/Views/'); } } ``` @@ -710,38 +473,4 @@ Now we can use the plugin that extends the plugin **Custom Products (v2)** - We can upload files with a new mime type - We can use our own custom type to configure a product -The full Plugin is available to download it here: Example plugin here. - -## General - -### Call the PluginBootstrap of other plugins - -**New method** if the plugin is based on the 5.2 plugin system -```php -$customProducts = $this->container()->get('kernel')->getPlugins()['SwagCustomProducts']; -``` - -**Old method** -```php -/** @var Shopware_Plugins_Frontend_SwagCustomProducts_Bootstrap $customProducts */ -$customProducts = Shopware()->Plugins()->Frontend()->SwagCustomProducts(); -``` - -### Decorate services by using the new plugin system. -To decorade a service you only add a new service to the .../.../Resources/services.xml and inject the old service as parameter - -```xml - - - - - - - - - - - -``` +The full Plugin is available for download here: Example plugin here. diff --git a/source/developers-guide/plugin-guidelines/index.md b/source/developers-guide/plugin-guidelines/index.md new file mode 100644 index 0000000000..182a6c214c --- /dev/null +++ b/source/developers-guide/plugin-guidelines/index.md @@ -0,0 +1,528 @@ +--- +layout: default +title: Plugin guidelines +github_link: developers-guide/plugin-guidelines/index.md +indexed: true +menu_title: Plugin guidelines +menu_order: 50 +group: Developer Guides +subgroup: Developing plugins +--- + +
    + +# Plugin guidelines + +There are some issues we come across frequently, when reviewing plugins for the +community store. This document is intended to help plugin developers implement +plugins according to +[our quality guidelines](https://docs.shopware.com/en/plugin-standard-for-community-store). +We hope the examples provided here will be helpful, please feel free to submit +corrections and suggestions to our +[devdocs repository](https://github.com/shopware5/devdocs/) +on Github. + +## Internationalisation + +### Snippets + +If your plugin contains user-facing text content, we suggest that you should make +use of the snippet system provided by Shopware. When using the snippet system, +the text content can be easily distributed, using `*.ini` files. Also, every +text contained in a snippet is editable by shop administrators and translatable +as well. You can use this article in +[our documentation](https://developers.shopware.com/designers-guide/snippets/) +for guidance on how and where to use snippets. + +### Plugin metadata + +For a plugin to be ready for the community store, the text content of the +`plugin.xml` and similar files needs to be translated into the languages +provided by the plugin. This is an example of a correctly translated element in +a `config.xml`-file: + +```xml + + + + + + examplesetting + + + 1 + + + + +``` + +Note that there is a label for each language provided by the plugin, in this +case english and german. + +## Logging 101 + +When you need to inform the user or administrators about a noteworthy event +regarding your plugin (like an error, or if you fall back to a default setting, +...) please use the plugin-logger provided by Shopware. This class is present in +the Symfony DIC with the ID `pluginlogger`. Depending on the class you're +writing, there are several methods to access the `pluginlogger`. + +Most of the time, you should inject the pluginlogger into the classes that need +it: + +`SwagExample/Resources/services.xml`: +```xml + + + + + + + + + + +``` + +`SwagExample/Subscriber/SomeSubscriber.php`: +```php +logger = $logger; + } + + /** + * This method writes a message to the plugin error log. + */ + public function someMethod(): void + { + $this->logger->addError('Insert helpful error message here'); + } +} +``` + +In case you're writing a small plugin which only consists of a plugin base +class, you may also get the logger directly from the DIC: + +```php +$logger = $this->container->get('pluginlogger'); +``` + +### Plugin-specific logger + +If your plugins minimum Shopware version is greater than or equal to v5.6.0, you +can and should use the provided plugin-specific logger. This is a service in the +DIC as well. Its ID is a combination of the plugin's service prefix (by default +this is the plugin's name in `snake_case`) and `.logger`. So for a plugin called +`SwagExample`, the service-ID of the plugin-specific logger would be +`swag_example.logger`. You can read more about the plugin-specific logger in the +corresponding +[upgrade document](https://github.com/shopware5/shopware/blob/5.6/UPGRADE-5.6.md#plugin-specific-logger). + +## Plugin installation routine + +When implementing a plugin's `install`-method, many developers choose to add a +call to clear caches. We advise not to do this, and instead clear the necessary +caches when the `activate`-method is called. Also please consider which caches +actually need to be cleared for your plugin to work, since regenerating caches +may cause a high load on the server. + +## Plugin uninstallation routine + +When implementing a plugin's `uninstall`-method, please be careful and don't +delete any plugin data when the plugin is reinstalled. You can find out if the +user wants to keep the plugins data by examining the `$context`: + +```php +/** + * {@inheritdoc} + */ +public function uninstall(UninstallContext $context): void +{ + $this->secureUninstall(); + + if (!$context->keepUserData()) { + $this->removeAllTables(); + $this->someOtherDestructiveMethod(); + } + + // Clear only cache when switching from active state to uninstall + if ($context->getPlugin()->getActive()) { + $context->scheduleClearCache(UninstallContext::CACHE_LIST_ALL); + } +} +``` + +When the plugin is reinstalled, the `keepUserData` method returns `true` as +well, so the uninstall method is safe when implemented in the way shown above. + +## Adding and Removing attributes + +For guidance on how to add and remove attributes, please read our +[documentation](https://developers.shopware.com/developers-guide/attribute-system/#delete-an-existing-attribute). + +## Clearing the cache when configuration changes + +Sometimes when a value is updated via a plugin's configuration form, the cache +needs to be cleared. The following is a simple example subscriber, which takes +care of this: + +```php +pluginName = $pluginName; + $this->cacheManager = $cacheManager; + } + + public static function getSubscribedEvents(): array + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_Config' => 'onPostDispatchConfig' + ]; + } + + public function onPostDispatchConfig(\Enlight_Event_EventArgs $args): void + { + /** @var Shopware_Controllers_Backend_Config $subject */ + $subject = $args->get('subject'); + $request = $subject->Request(); + + // If this is a POST-Request, and affects our plugin, we may clear the config cache + if($request->isPost() && $request->getParam('name') === $this->pluginName) { + $this->cacheManager->clearByTag(CacheManager::CACHE_TAG_CONFIG); + } + } +} +``` + +## Adding E-Mail-Templates + +Plugins have the ability to create additional E-Mail-Templates using the +Mail-model. The handling of templates is different in this case, since the +template contents are saved in the model and written to the database accordingly. + +```php +installMailTemplate(); + } + + public function uninstall(UninstallContext $context): void + { + $this->uninstallMailTemplate(); + } + + /** + * installMailTemplate takes care of creating the new E-Mail-Template + */ + private function installMailTemplate(): void + { + $entityManager = $this->container->get('models'); + $mail = new Mail(); + + // After creating an empty instance, some technical info is set + $mail->setName(self::MAIL_TEMPLATE_NAME); + $mail->setMailtype(Mail::MAILTYPE_USER); + + // Now the templates basic information can be set + $mail->setSubject($this->getSubject()); + $mail->setContent($this->getContent()); + $mail->setContentHtml($this->getContentHtml()); + + /** + * Finally the new template can be persisted. + * + * transactional is a helper method which wraps the given function + * in a transaction and executes a rollback if something goes wrong. + * Any exception that occurs will be thrown again and, since we're in + * the install method, shown in the backend as a growl message. + */ + $entityManager->transactional(static function ($em) use ($mail) { + /** @var ModelManager $em */ + $em->persist($mail); + }); + } + + /** + * uninstallMailTemplate takes care of removing the plugin's E-Mail-Template + */ + private function uninstallMailTemplate(): void + { + $entityManager = $this->container->get('models'); + $repo = $entityManager->getRepository(Mail::class); + + // Find the mail-type we created + $mail = $repo->findOneBy(['name' => self::MAIL_TEMPLATE_NAME]); + + $entityManager->transactional(static function ($em) use ($mail) { + /** @var ModelManager $em */ + $em->remove($mail); + }); + } + + private function getSubject(): string + { + return 'Default Subject'; + } + + private function getContent(): string + { + /** + * Notice the string:{...} in the include's file-attribute. + * This causes the referenced config value to be loaded into + * a string and passed on as the template's content. This works + * because the file-attribute can accept any template resource + * which includes paths to files and several other types as well. + * For more information about template resources, have a look here: + * https://www.smarty.net/docs/en/resources.string.tpl + */ + return <<<'EOD' +{include file="string:{config name=emailheaderplain}"} + +{* Content *} + +{include file="string:{config name=emailfooterplain}"} +EOD; + } + + private function getContentHtml(): string + { + return <<<'EOD' +{include file="string:{config name=emailheaderhtml}"} + +{* Content *} + +{include file="string:{config name=emailfooterhtml}"} +EOD; + } +} +``` + +## Adding URLs to the sitemap.xml + +Since Shopware v5.5, new URLs may be added to the `sitemap.xml` by registering a +service with a specific tag. The general principle is described in our +[Upgrade Guide](https://developers.shopware.com/developers-guide/shopware-5-upgrade-guide-for-developers/#sitemap). + +The registration of such a service in a plugins `services.xml` could look like +this: + +```xml + + + +``` + +## Validating user input + +Inadequate or even missing validation of user input is a security risk. If your +plugin doesn't validate user input, it is not eligible for the community store. +Since input validation is not a Shopware-specific issue and implementing it can +be dependent on the business case, we can only provide broad orientation here. +If you'd like to read more about input validation and how to implement it, have +a look at the +[cheatsheets released by the OWASP](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Input_Validation_Cheat_Sheet.md). + +## Testing communication with external APIs + +Some plugins depend on external services to provide certain functionality (our +PayPal integration does for example). When communicating with external APIs, +connectivity might be an issue, or the external service might be unavailable. +In order for the users of your plugin to be able to test the functionality / +availibility of the external service, you might want to add a button to the +plugin's backend module which executes a simple request to to assure a +successful connection. The following sections briefly describe how such a +button could be implemented. + +Since browsers block AJAX-requests to domains other than the origin of the +corresponding script to protect the user, any HTTP-requests to test the +external service need to be proxied through the Shopware server. Apart from +this, the basic components that need to be implemented are the following: + +1. Shopware Backend controller which accepts the AJAX-Request an dispatches a + call to the external service +2. Shopware Backend module (button) which sends an AJAX-Request to the Backend + controller + +`SwagExample/Resources/services.xml`: +```xml + + + + + + + + + + + + + + +``` + +`SwagExample/Controller/Backend/SwagExampleTest.php`: +```php +client = $client; + $this->logger = $logger; + + parent::__construct(); + } + + public function testAction() + { + try { + $response = $this->client->get(self::EXTERNAL_API_BASE_URL); + + if ((int) $response->getStatusCode() === Response::HTTP_OK) { + $this->View()->assign('response', 'Success!'); + } else { + $this->View()->assign('response', 'Oh no! Something went wrong :('); + } + } catch (RequestException $exception) { + $this->logger->addError($exception->getMessage()); + + $this->response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); + $this->View()->assign('response', $exception->getMessage()); + } + } +} +``` + +When the `testAction` of the controller shown above is called, it dispatches a +request to an external service. The response can be examined (HTTP status code, +...) to determine, if the request was successful. + +The following code example shows how the test button could be built in directly +into a plugin's `config.xml`. + +`SwagExample/Resources/config.xml`: +```xml + + + + + + buttonTest + + + + + + + + + + + +``` + +When the button is clicked, the backend module dispatches a Request to the +`SwagExampleTest`-controller's `testAction` and shows the output using the +built-in `createGrowlMessage` method. diff --git a/source/developers-guide/plugin-lastregistrations-widget/index.md b/source/developers-guide/plugin-lastregistrations-widget/index.md index 52fcec60a0..95803cdea1 100644 --- a/source/developers-guide/plugin-lastregistrations-widget/index.md +++ b/source/developers-guide/plugin-lastregistrations-widget/index.md @@ -27,7 +27,7 @@ As always in the new plugin system we set our metadata in the `plugin.xml` ``` + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware5/shopware/5.3/engine/Shopware/Components/Plugin/schema/plugin.xsd"> @@ -39,7 +39,7 @@ As always in the new plugin system we set our metadata in the `plugin.xml` ``` -Since we use the new system for our plugin we set the `compability minVersion` to `5.2.0`. +Since we use the new system for our plugin we set the `compatibility minVersion` to `5.2.0`. ## Plugin bootstrap file ## ### SwagLastRegistrationsWidget.php @@ -97,7 +97,7 @@ is called. In the app.js we add our own stores, models etc. } ``` The __install()__ method creates a new widget Entity and adds it to our plugin. It is important to set the same - __name__ as in our view alias(`Resources/views/backend/index/swag_last_registrations/view/main.js`) + __name__ as in our view alias(`Resources/views/backend/index/swag_last_registrations/view/main.js`) as well as adding a snippet for this __name__ under `Resources/snippets/backend/widget/labels.ini`. `uninstall()` ``` @@ -157,7 +157,7 @@ class Shopware_Controllers_Backend_SwagLastRegistrationsWidget extends Shopware_ } } ``` -The controller has only an __getLastRegistrationsAction__ function which creates an sql query to fetch the last registered users and adds them to the view. +The controller has only an __listAction__ function which creates an sql query to fetch the last registered users and adds them to the view. ## ExtJS Part ## ### Resources/views/backend/index/swag_last_registrations/app.js @@ -305,6 +305,7 @@ The rest should be self explanatory. For more help take a look at the __ExtJS do ### Resources/views/backend/index/swag_last_registrations/store/account.js ``` +// Ext.define('Shopware.apps.Index.swagLastRegistrationsWidget.store.Account', { /** * Extends the default Ext Store @@ -333,6 +334,7 @@ Ext.define('Shopware.apps.Index.swagLastRegistrationsWidget.store.Account', { ### Resources/views/backend/index/swag_last_registrations/model/account.js ``` +// Ext.define('Shopware.apps.Index.swagLastRegistrationsWidget.model.Account', { extend: 'Ext.data.Model', @@ -348,4 +350,3 @@ Ext.define('Shopware.apps.Index.swagLastRegistrationsWidget.model.Account', { ## Download plugin ## The whole plugin can be downloaded here. - diff --git a/source/developers-guide/plugin-license/index.md b/source/developers-guide/plugin-license/index.md index 8dbd96438f..a049d04816 100755 --- a/source/developers-guide/plugin-license/index.md +++ b/source/developers-guide/plugin-license/index.md @@ -15,6 +15,14 @@ menu_order: 90 Should you wish to, you can have your plugin run a license check when used in a Shopware shop. This will ensure that the target shop has permission to use your plugin. This document covers the steps necessary to implement this check. +
    + +The license check works only for plugins for Shopware versions older than 5.5.0. +If you want your plugin to be compatible with Shopware 5.5, no license check should be implemented. +Read [here](/developers-guide/shopware-5-upgrade-guide-for-developers/#system-requirements-changes) more about that. + +
    + ## Requesting the license check To request a license check, you must first login into your [Shopware account](http://account.shopware.com) and request a license validation in the detail page of your plugin. Once you have done so, you will be given a code snippet similar to this: @@ -36,7 +44,7 @@ public function checkLicense($throwException = true) } ``` -This method validates if the current shop has a valid license for your plugin. It should be placed inside your plugin's Bootstrap class, and used whenever you want to ensure that the current shop has a valid license for your plugin (for example, during plugin installation). +This method validates if the current shop has a valid license for your plugin. It should be placed inside your plugin base class, services, subscribers, or whenever you want to ensure that the current shop has a valid license (for example, during plugin installation). ## Executing the license check @@ -46,92 +54,51 @@ The checkLicense has an optional `throwException` parameter which, when set to f ## Example -``` +```php $this->getVersion(), - 'label' => $this->getLabel() - ); - } - - public function update($oldVersion) - { - // If no license is available, an exception is thrown - $this->checkLicense(); - - return true; - } - - - public function install() +class YourPlugin extends Plugin +{ + public function install(InstallContext $installContext) { - // If no license is available, an exception is thrown $this->checkLicense(); - $this->subscribeEvent('Enlight_Controller_Dispatcher_ControllerPath_Backend_MyPlugin', 'onGetControllerPath'); - $this->subscribeEvent('Enlight_Controller_Action_PostDispatch_Frontend_Listing', 'onPostDispatchFrontendListing'); - // ... - - return true; } - public function onGetControllerPath(\Enlight_Event_EventArgs $args) + public function update(UpdateContext $updateContext) { - // an exception occurs, if no license is available - // this is ok for e.g. backend controllers $this->checkLicense(); - return __DIR__ . '/Controllers/Backend/MyPlugin.php'; - } - - public function onPostDispatchFrontendListing(\Enlight_Event_EventArgs $args) - { - // If no license is available, the method gracefully exits - if (!$this->checkLicense(false)) { - return; - } - - /** @var $action \Enlight_Controller_Action */ - $action = $args->getSubject(); - $view = $action->View(); // ... } - public function checkLicense($throwException = true) + private function checkLicense($throwException = true) { // license check code available on your Shopware account } } ``` -In this example, you can see that the `checkLicense` method is called during installation and update. In the `onGetControllerPath` method, it is being called without an argument, meaning it will throw an exception in case the validation fails. This would display a visible, informative message to the shop owner, warning him about this failure. - -In the `onPostDispatchFrontendListing` method, the validation will not throw an exception, but instead make the method prematurely return, causing the plugin to not perform its expected action, but also not informing the frontend user of this validation issue. +In this example, you can see that the `checkLicense` method is called during installation and update. +If you use the checkLicense method in the frontend, set the `$ throwException` parameter to false to prevent the validation from throwing an exception, but instead make the method prematurely return, causing the plugin to not perform its expected action, but also not informing the frontend user of this validation issue. ## Custom validation While you are free to customize the `checkLicense` method, we highly recommend that you don't. Should you require any additional checks besides the license check provided by us, you should create a wrapper method to implement your own logic -``` +```php -**_sql**: (Not in release packages) Contains various deltas and migrations to set up Shopware or migrate the database of an old Shopware version to the database of a new versions. +**_sql**: (Not in release packages) Contains various deltas and migrations to set up Shopware or migrate the database of an old Shopware version to the database of a new version. **bin**: Contains the `console` command which can be executed to run the Shopware command line tools -**build**: (Not in release packages) Contains our ant-based build engine with various scripts to e.g. install dependencies, reset the database etc. When installing Shopware from GitHub you want to run `ant build-unit` first. If you use the install package, you can just use our web-based installer +**build**: (Not in release packages) Contains our ant-based build engine with various scripts to e.g. install dependencies, +reset the database etc. When installing Shopware from GitHub you want to run `ant build-unit` first. +If you use the "install" package, you can just use our web-based installer + +**custom/plugins** This directory contains all plugins based on the plugin system introduced with Shopware 5.2. -**cache**: Contains various caches and generated files like smarty compile cache, proxy caches, HTML cache etc. **engine/Library**: Some libraries / dependencies which are not available in composer. -**engine/Shopware**: The actual Shopware application with subdirectories for our bundles, services, models and plugins. +**engine/Shopware**: The actual Shopware application with subdirectories for our bundles, services and models. **files**: Files for ESD products or generated documents are stored here -**logs**: Our logging directory. Whenever an exception occurs in the Shopware stack, you'll find the stack trace and some context in these log files. - **media**: Files uploaded via the Shopware MediaManager are stored in here (images, thumbnails and other uploads) **recovery**: Our web-based update and install tool can be found here. -**snippets**: Contains Shopware snippets for frontend and backend in a simple INI format. Snippets will automatically be deployed to database during installation. (Not in release packages) - -**templates**: The Shopware 4 template base (emotion) +**snippets**: Contains Shopware snippets for frontend and backend in a simple INI format. Snippets will automatically be deployed to the database during installation. (Not in release packages) **tests**: PHPUnit and Mink tests **themes**: The Shopware 5 template base (bare, responsive) +**var/cache**: Contains various caches and generated files like smarty compile cache, proxy caches, HTML cache etc. + +**var/logs**: Our logging directory. Whenever an exception occurs in the Shopware stack, you'll find the stack trace and some context in these log files. + **vendor**: Dependencies from composer. Shipped with the release package, git users will need to run `composer install` or `ant build-unit` **web**: The concatenated and minified javascript and CSS templates @@ -53,13 +57,37 @@ On the left you can see Shopware's default directory structure, as you will find
    ## MVC -Shopware makes use of the [MVC design pattern](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). For this reason, the representational aspect of the application (view), the controlling and user input (controller) as well as the data layer and business logic (model) are decoupled throughout the application. +Shopware makes use of the [MVC design pattern](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). +For this reason, the representational aspect of the application (view), the controlling and user input (controller) as well as the data layer and business logic (model) are decoupled throughout the application. ### View -Shopware uses [Smarty](http://en.wikipedia.org/wiki/Smarty) as template engine. When using the new responsive template base, you will find all templates in `themes`. The old Shopware 4 template can still be found in `templates`. +Shopware uses [Smarty](http://en.wikipedia.org/wiki/Smarty) as template engine. You will find all templates in `themes`. ### Model -[Doctrine](http://en.wikipedia.org/wiki/Doctrine_(PHP)) is used as ORM and database abstraction layer. You will find the various Doctrine models in `engine/Shopware/Models`. The models are grouped by domain, so you will find article related models in the `Article` directory, customer related models in the `Customer` directory and so on. The business logic of Shopware can be found in `Core`, `Components` or `Bundle` - depending how tight the service in question is coupled to Shopware itself. +[Doctrine](http://en.wikipedia.org/wiki/Doctrine_(PHP)) is used as ORM and database abstraction layer. +You will find the various Doctrine models in `engine/Shopware/Models`. +The models are grouped by domain, so you will find product related models in the `Article` directory, customer related models in the `Customer` directory and so on. +The business logic of Shopware can be found in `Core`, `Components` or `Bundle` - depending how tight the service in question is coupled to Shopware itself. + +### Controller auto-registration + +The auto-registration is available in Shopware 5.2.7 or above. +To make use of it, create a file like `SwagControllerExample/Controllers/(Backend|Frontend|Widgets|Api)/MyController.php` +and follow our controller naming conventions. After that, you'll be able to call `MyController`. +The registration of the template would be done, i.e. in the `preDispatch()`-Method of your controller. + +```php +class Shopware_Controllers_Frontend_MyController extends \Enlight_Controller_Action +{ + public function preDispatch() + { + $pluginPath = $this->container->getParameter('swag_controller_example.plugin_dir'); + + $this->get('template')->addTemplateDir($pluginPath . '/Resources/views/'); + $this->get('snippets')->addConfigDir($pluginPath . '/Resources/snippets/'); + } +} +``` ### Controller Controllers take care of user input of any kind, process the input in the model and respond with an answer which is usually generated by the template. @@ -71,23 +99,31 @@ Shopware's controllers can be found in `engine/Shopware/Controllers` and are sep * backend (for the Shopware administrative panel) * api (for our REST API) -In Shopware, any request will hit a controller, depending on the type of the request. Therefore, three decisions are made: +In Shopware, any request will hit a controller, depending on the type of the request. +Therefore, three decisions are made: * which module is requested (default: frontend) * which controller is requested (default: Index) * which controller action should be called (default: index) -If module, controller or action are not explicitly defined, Shopware will fall back to the defaults mentioned above. For this reason, a call the the shop's root directory will be dispatched to the frontend module, index controller and index action. This is equivalent to the call to `http://my-shop.com/frontend/index/index`. In practice, however, these "technical" URLs are usually hidden by the SEO engine. +If module, controller or action are not explicitly defined, Shopware will fall back to the defaults mentioned above. +For this reason, a call to the shop's root directory will be dispatched to the frontend module, index controller and index action. +This is equivalent to the call to `http://my-shop.com/frontend/index/index`. +In practice, however, these "technical" URLs are usually hidden by the SEO engine. -Inside a controller you have easy access to the `Request` and `Response` object, as well as to the DI container, so you can call other components and services. +Inside a controller you have easy access to the `Request` and `Response` object, as well as to the DI container, +so you can call other components and services. ## Technologies -Shopware as an open source shopping system uses many well known libraries. We use Symfony components like the dependency injection container, the console tools and some other and are HTTP compliant with the Symfony HTTP kernel. +Shopware as an open source shopping system uses many well known libraries. +We use Symfony components like the dependency injection container, the console tools and some other and are HTTP compliant with the Symfony HTTP kernel. Other well known libraries are also used and included in Shopware, like Guzzle HTTP client, Doctrine, Smarty, Monolog and Phpunit, so that most developers should feel quite comfortable regarding the used technologies. -The actual HTTP stack of Shopware is currently powered by Zend Framework which Shopware uses with a thin layer called `Enlight` on top. As we plan to move towards Symfony step by step, Enlight might come in handy as a transitional framework. +The actual HTTP stack of Shopware is currently powered by Zend Framework which Shopware uses with a thin layer called `Enlight` on top. +As we plan to move towards Symfony step by step, Enlight might come in handy as a transitional framework. -## Hooking into the system -Shopware uses plugins to extend the base systems. Changes in the core are never required and never recommended. +## Hooking into the system with events +Shopware uses plugins to extend the base systems. +Changes in the core are never required and never recommended. Extensions of the core system can basically be separated into logical extensions, data extensions and template extensions. Some global events are very powerful when it comes to extending Shopware's behaviour: @@ -95,257 +131,286 @@ Some global events are very powerful when it comes to extending Shopware's behav * Enlight_Controller_Action_PreDispatch_* * Enlight_Controller_Action_PostDispatchSecure_* -These events will be emitted whenever a controller action is about to be called ("PreDispatch") or **after** the controller action was called ("PostDispatch"). This way it is very easy to get notified any time a certain controller is executed to perform some extensions: - +These events will be emitted whenever a controller action is about to be called ("PreDispatch") or **after** the controller action was called ("PostDispatch"). +This way it is very easy to get notified any time a certain controller is executed to perform some extensions: + +```xml + + + + + + + ``` -$this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail', - 'myDetailCallback' -) + +```php + 'myDetailCallback' + ]; + } + + public function myDetailCallback(\Enlight_Controller_ActionEventArgs $args) + { + // Do some action + } +} ``` -This snippet will call the callback function `myDetailCallback` any time the detail controller (and therefore: the detail page) was requested. In your callback you could now load template extensions or modify view assignments. +This snippet will call the callback function `myDetailCallback` any time the detail controller (and therefore: the detail page) was requested. +In your callback you could now load template extensions or modify view assignments. If you want to read more about the events in Shopware you can take a look at the [events article](https://developers.shopware.com/developers-guide/event-guide/#further-events). ## Extending the database -Your plugin is free to create its own table in the Shopware database in order to store additional data. But there is even a more convenient way: The Shopware attribute system. -Shopware attributes are basically tables in a OneToOne relation to important Shopware entities. So, for every entry in `s_user` (the customer table), there is also an entry in `s_user_attributes`. In Shopware, it is very easy to add a new column to this table and to automatically add it to the customer doctrine attribute model. Whenever Shopware reads a customer, article or another entity from database, it will also read the attributes, so that you can make use of attributes in many places and modules. +Your plugin is free to create its own table in the Shopware database in order to store additional data. +But there is even a more convenient way: The Shopware attribute system. +Shopware attributes are basically tables in a OneToOne relation to important Shopware entities. +So, for every entry in `s_user` (the customer table), there is also an entry in `s_user_attributes`. +In Shopware, it is very easy to add a new column to this table and to automatically add it to the customer doctrine attribute model. +Whenever Shopware reads a customer, product or another entity from database, it will also read the attributes, so that you can make use of attributes in many places and modules. +If you want to read more about extending the database in Shopware you can take a look at the [attribute system article](https://developers.shopware.com/developers-guide/attribute-system/#schema-operations-and-configuration) ## Extending the template -In many cases you might want to modify the template. Shopware makes use of the Smarty block system for that. Blocks are basically named areas inside the template that you can prepend, append or even replace. Shopware's default frontend theme has more than 1.500 blocks - so more than 1.500 extension points for you as a plugin developer. +In many cases you might want to modify the template. +Shopware makes use of the Smarty block system for that. +Blocks are basically named areas inside the template that you can prepend, append or even replace. +Shopware's default frontend theme has more than 1.500 blocks - so more than 1.500 extension points for you as a plugin developer. A smarty block will usually look like this: +```smarty +{block name="Shopware_frontend_checkout_cart"} +
    + Some HTML content +
    +{/block} ``` - {block name="Shopware_frontend_checkout_cart"} -
    - Some HTML content -
    - {/block} -``` - - # Writing our first little plugin The following example will show how to write a very simple plugin, which extends the frontend and adds a little "slogan" to the page. -## The plugin Bootstrap -The main entry point of every plugin is the `Bootstrap.php` file in your plugin directory. This is placed in `engine\Shopware\Plugins\Local\Frontend\SwagSloganOfTheDay`. +## Plugin Name -We recommend developing plugins in the `Local` directory. There is no functional difference between this and the `Community` directory, so you could also place it there. The `Frontend` part of the path could also be `Backend` or `Core` - again there is no functional difference, its just for sorting and orientation. As we want to write a plugin which mainly extends the frontend, we will put it to the `Frontend` directory. `SwagSloganOfTheDay` finally is your plugin - the name needs to have at least a developer prefix (`Swag`) and a name (`SloganOfTheDay`). +The plugin name must be fashioned according to the [PHP variable name rules](http://php.net/manual/language.variables.basics.php) and should always be prefixed with your developer prefix, so it's unique in the Shopware universe. +To submit plugins to the [shopware store](https://store.shopware.com/en) you have to obtain your developer prefix in the [Shopware Account](https://account.shopware.com). - In the `Bootstrap.php` file, we create a simple class: +In the following examples the developer prefix "Swag" will be used (short for shopware AG). -``` - subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend', - 'onFrontendPostDispatch' - ); - - $this->createConfig(); + $slogans = [ + 'An apple a day keeps the doctor away', + 'Let’s get ready to rumble', + 'A rolling stone gathers no moss', + ]; - return true; + return array_rand(array_flip($slogans)); } + } ``` -The `getVersion` method just needs to return your plugin version as a string. If you later upload your plugin to the store, Shopware will be able to offer customers updates of your plugin depending on the version. +## The plugin base file +The main entry point of every plugin is the plugin base file in your plugin directory `SwagSloganOfTheDay.php`. +This is placed in either `custom/plugins/SwagSloganOfTheDay` or `custom/project/SwagSloganOfTheDay` if it is a project-specific plugin by intention. -
    -Use a valid version + In the `SwagSloganOfTheDay.php` file, we create a simple class: -The returned version has to be compatible with the [php version_compare](http://php.net/version_compare) function, otherwise, Shopware cannot detect any possible updates. -
    - -`getLabel` should return the name of your plugin in a human readable way - `SwagSloganOfTheDay` might be a bit technical, so we choose `Slogan of the day`. - -These are the only "metadata" methods your actually should implement, everything else is optional. +```php + subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Frontend', - 'onFrontendPostDispatch' - ); - - $this->createConfig(); - return true; } ``` -3 things happen here: - * we subscribe to an event - * we call the createConfig method - * we return a success flag - -In order to subscribe to an event, we use the `$this->subscribeEvent` helper method from the bootstrap base class. The first parameter is the event to subscribe to, in this case `Enlight_Controller_Action_PostDispatchSecure_Frontend`. As mentioned above, this event will be triggered every time **after** a controller in the frontend was executed. The second parameter is the callback function that should be called - `onFrontendPostDispatch` - -The `createConfig` method will create the configuration for our plugin. We will have a look at it later. +Both the PHP namespace and the class name must match the [plugin name](#plugin-name) equally and no additional path parts must be added. -Finally `return true;` will tell Shopware that everything went fine and no problems occurred. If you return `false` or just nothing, Shopware will not finish the plugin installation. +It's important to extend the `Shopware\Components\Plugin` class. -### The update method -It is necessary to explain that __files__, removed in update versions, __will not be automatically removed__ from file system while updating. This can cause problems with __templates__ and rendering smarty. So if a template file is removed -in a newer plugin version, it is necessary to remove the deleted template during the update process manually. +Now we add the first event subscriber to the SwagSloganOfTheDay plugin. +For this we add a folder Subscriber in which we create a new PHP class `RouteSubscriber.php`, and add this subscriber to the services.xml. -A working example could be like what we did in our __SwagBundle__ plugin. +## Subscriber classes ```php -/** - * Update function of the bundle plugin - * - * @param string $oldVersion - * @return bool|void - * @throws Exception - */ - public function update($oldVersion) - { - ... +unlinkOldFiles($oldVersion); - } - - ... - } +use Enlight\Event\SubscriberInterface; +use Shopware\Components\Plugin\ConfigReader; +use SwagSloganOfTheDay\Components\SloganPrinter; - /** - * Unlinks old files which are not available for this version anymore. - * @param string $oldVersion - */ - private function unlinkOldFiles($oldVersion) - { - $filesToDelete = []; +class RouteSubscriber implements SubscriberInterface +{ + private $pluginDirectory; + private $sloganPrinter; + private $config; + + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Frontend' => 'onPostDispatch' + ]; + } - //Initial version for the 5.2 Plugin version. - if (version_compare($oldVersion, '3.2.0', '<')) { - $filesToDelete[] = __DIR__ . '/Subscriber/ControllerPath.php'; - $filesToDelete[] = __DIR__ . '/Views/responsive'; - $filesToDelete[] = __DIR__ . '/Views/emotion'; - } + public function __construct($pluginName, $pluginDirectory, SloganPrinter $sloganPrinter, ConfigReader $configReader) + { + $this->pluginDirectory = $pluginDirectory; + $this->sloganPrinter = $sloganPrinter; - //Iterate through all provided file paths and unlink every file. - foreach ($filesToDelete as $filePath) { - if (!file_exists($filePath)) { - continue; - } + $this->config = $configReader->getByPluginName($pluginName); + } - //Delete file from filesystem. - unlink($filePath); - } - } -``` + public function onPostDispatch(\Enlight_Controller_ActionEventArgs $args) + { + /** @var \Enlight_Controller_Action $controller */ + $controller = $args->get('subject'); + $view = $controller->View(); -### Event callback + $view->addTemplateDir($this->pluginDirectory . '/Resources/views'); -Now the event callback function should be implemented: + $view->assign('swagSloganFontSize', $this->config['swagSloganFontSize']); + $view->assign('swagSloganItalic', $this->config['swagSloganItalic']); + $view->assign('swagSloganContent', $this->config['swagSloganContent']); + if (!$this->config['swagSloganContent']) { + $view->assign('swagSloganContent', $this->sloganPrinter->getSlogan()); + } + } +} ``` -public function onFrontendPostDispatch(Enlight_Event_EventArgs $args) -{ - /** @var \Enlight_Controller_Action $controller */ - $controller = $args->get('subject'); - $view = $controller->View(); - $view->addTemplateDir( - __DIR__ . '/Views' - ); +As defined in `getSubscribedEvents` the callback is called `onPostDispatch`. +It takes an `Enlight_Controller_ActionEventArgs` object as parameter, which holds context about the event - in this case the context of the controller that has been executed. - $view->assign('slogan', $this->getSlogan()); - $view->assign('sloganSize', $this->Config()->get('font-size')); - $view->assign('italic', $this->Config()->get('italic')); +First we can extract the reference of the executed frontend controller using `$controller = $args->get('subject');`. +Using this controller we can get a reference of the view instance: `$view = $controller->View()`. -} +Now we want to inject our own template directory to Shopware using the `addTemplateDir` method. +It takes a path of a template directory as parameter. +Whenever rendering a template, Shopware will now automatically check your `View` directory for extensions and load them dynamically. -public function getSlogan() - { - return array_rand( - array_flip( - array( - 'An apple a day keeps the doctor away', - 'Let’s get ready to rumble', - 'A rolling stone gathers no moss', - ) - ) - ); - } -``` +
    +Be careful with sensitive data! -As defined in `$this->subscribeEvent` the callback is called `onFrontendPostDispatch`. It takes an `Enlight_Event_EventArgs` object as parameter, which holds context about the event - in this case the context of the controller that has been executed. +Please make sure that you not assign sensitive data to the template on global events like [PostDispatch](/developers-guide/event-list/#enlight_controller_front_postdispatch). +The view variables are shared across multiple renders. +The JsonRenderer for example will send all variables to the client. +
    -First of all we can extract the reference of the executed frontend controller using `$controller = $args->get('subject');`. Using this controller we can get a reference of the view instance: `$view = $controller->View()`. +Finally, we want to assign some configuration to the template - in this case a slogan, +a flag that indicates if the slogan should be italic or not and the font size of the slogan. +For assignments `$view->assign('name', 'value')` is used - this way we will be able to access it in the Smarty template using `{$name}`. -Now we want to inject our own template directory to Shopware using the `addTemplateDir` method. It takes a path of a template directory as parameter. Whenever rendering a template, Shopware will now automatically check your `View` directory for extensions and load them dynamically. +The slogan itself is randomly selected in the `getSlogan` method. +This could easily be moved to a plugin configuration text field, so that the shop owner can enter his slogans line by line. +But as seen above, we already picked the best ones. -Finally we want to assign some configuration to the template - in this case a slogan, a flag that indicates if the slogan should be italic or not and the font size of the slogan. For assignments `$view->assign('name', 'value')` is used - this way we will be able to access it in the Smarty template using `{$name}`. +Subscriber classes implements the SubscriberInterface -The slogan itself is randomly selected in the `getSlogan` method. This could easily be moved to a plugin configuration text field, so that the shop owner can enter his slogans line by line. But as seen above, we already picked the best ones. +A Subscriber class is registered as a service in the services.xml and are identified for Shopware via the Subscriber tag. +```xml + +``` -### The configuration +```xml + + %swag_slogan_of_the_day.plugin_name% + %swag_slogan_of_the_day.plugin_dir% + + + -The calls to `$this->Config()->get('name')` will return our plugin configuration value for 'name'. In order to create the configuration, we will need to implement the `createConfig` method we called in the `install` method: + + ``` -private function createConfig() -{ - $this->Form()->setElement( - 'select', - 'font-size', - array( - 'label' => 'Font size', - 'store' => array( - array(12, '12px'), - array(18, '18px'), - array(25, '25px') - ), - 'value' => 12 - ) - ); - $this->Form()->setElement('boolean', 'italic', array( - 'value' => true, - 'label' => 'Italic' - )); -} +### The configuration + +In order to create the plugin configuration, we will need to implement the `Resources/config.xml`. + +```xml + + + + + swagSloganContent + + + Dieser Slogan wird in in der Storefront angezeigt + This slogan will be shown on the storefront + + + swagSloganFontSize + + + 12 + + + + + + + + swagSloganItalic + + + false + + + ``` -In this case two config elements are added - a `select` element with the name "font-size" which will draw a combobox in the plugin configuration. The content of the combobox is defined in the "store" array element. Additionally we are able to have a label for the configuration using the "label" key and a default value using the "value" key. +In this case two config elements are added - a `select` element with the name "swagSloganFontSize" which will draw a combobox in the plugin configuration. +The content of the combobox is defined in the "store" element. +Additionally, we ar able to define a label for the configuration using the "label" property and a default value using the "value" property. -As a second config element we add a `boolean` element. It is called "italic", enabled by default and has the label "Italic". +As a second config element we add a `boolean` element. +It is called "swagSloganItalic", enabled by default and has the label "Italic". This will result in a configuration form like this: @@ -353,15 +418,55 @@ This will result in a configuration form like this: If you want to read more about plugin configuration start reading [Plugin configuration](/developers-guide/plugin-configuration/) article. +
    +Be careful with sensitive data! + +Please be aware to not save any files that contain sensitive data to your plugin directory (e.g. config or log files) as these might be accessible by public. +
    + + +### Frontend resources auto-registration + +Additions to CSS, LESS and JavaScript resources had to be registered via `Theme_Compiler_Collect_Plugin_*` events. +Since Shopware 5.2.13 the registration will be done automatically. +Just place the resources into the following directories: + +* `custom/plugins/SwagResource/Resources/frontend/css/**.css` +* `custom/plugins/SwagResource/Resources/frontend/js/**.js` +* `custom/plugins/SwagResource/Resources/frontend/less/all.less` + +The `css` and `js` directories may contain arbitrary subdirectories. +`@import`s in `all.less` will be resolved. + +``` +SwagResource +├── Resources +│ └── frontend +│ ├── css +│ │ ├── * +│ │ │ └── *.css +│ │ └── *.css +│ ├── js +│ │ ├── * +│ │ │ └── *.js +│ │ └── *.js +│ └── less +│ └── all.less +└── SwagResource.php +``` + ### Template extension -So far we have set up the bootstrap - but where does the actual template extension come from? +So far we have set up the plugin - but where does the actual template extension come from? -We already registered the `View` directory and can now create the template in the `Views/frontend/index/index.tpl` template. This template is actually an extension of Shopware's default `frontend/index/index.tpl` which can be found in `themes/Frontend/Bare/frontend/index/index.tpl`. This template defines the whole default structure of the Shopware responsive template - and is a perfect place for global extensions. As we created a file with the same name, the template manager of Shopware will automatically load this template file, when the default index.tpl is loaded. +We already registered the `Resources/views` directory in the RouteSubscriber and can now create the template in the `Resources/views/frontend/index/index.tpl` file. +This template is actually an extension of Shopware's default `frontend/index/index.tpl` which can be found in `themes/Frontend/Bare/frontend/index/index.tpl`. +This template defines the whole default structure of the Shopware responsive template - and is a perfect place for global extensions. +As we created a file with the same name, the template manager of Shopware will automatically load this template file, when the default index.tpl is loaded. Now our plugin's `index.tpl` might look like this: -``` +```smarty {extends file="parent:frontend/index/index.tpl"} {block name="frontend_index_navigation_categories_top_include"} @@ -373,34 +478,36 @@ Now our plugin's `index.tpl` might look like this: } .slogan { {if $italic}font-style:italic;{/if} - font-size:{$sloganSize}px; + font-size:{$swagSloganFontSize}px; }
    - {$slogan} + {$swagSloganContent}
    - + {$smarty.block.parent} {/block} ``` -The directive `{extends file="parent:frontend/index/index.tpl"}` will tell Shopware to not completely replace the default index.tpl file - but to extend from it. Now we can overwrite single or multiple blocks in the default index.tpl using the `block` directive: +The directive `{extends file="parent:frontend/index/index.tpl"}` will tell Shopware to not completely replace the default index.tpl file - but to extend from it. +Now we can overwrite single or multiple blocks in the default index.tpl using the `block` directive: -``` +```smarty {block name="frontend_index_navigation_categories_top_include"} ... - + {$smarty.block.parent} {/block} ``` -This tells Smarty to prepend the content of our block to the default content of the block `frontend_index_navigation_categories_top_include`. That block is the block, which defines the top category navigation in the default template - a nice place to show a profound slogan to the customer! +This tells Smarty to prepend the content of our block to the default content of the block `frontend_index_navigation_categories_top_include`. +That block is the block, which defines the top category navigation in the default template - a nice place to show a profound slogan to the customer! Within the block we can use default Smarty / HTML. In the example above, we define some style sheets first: -``` +```html ``` -As you can see, we use Smarty syntax here to change the style dynamically corresponding to the configuration we assigned to the template in the `onFrontendPostDispatch` callback method. So the `italic` style will only be set if the `italic` option is set. The same way the font size is set depending on the config. +As you can see, we use Smarty syntax here to change the style dynamically corresponding to the configuration we assigned to the template in the `onFrontendPostDispatch` callback method. +So the `italic` style will only be set if the `italic` option is set. The same way the font size is set depending on the config. Now the only thing left to do is show the slogan: -``` +```html
    {$slogan}
    ``` -### Installing and result -You can now install the plugin with Shopware's plugin manager. After installation, the plugin configuration can be opened by clicking the "pencil" symbol. +### Install and activate + +Now the plugin can be installed using the Shopware [CLI Commands](/developers-guide/shopware-5-cli-commands/) or the Plugin Manager in the backend. + +```bash +$ php ./bin/console sw:plugin:refresh +Successfully refreshed +``` + +```bash +$ php ./bin/console sw:plugin:install --activate SwagSloganOfTheDay +Plugin SwagSloganOfTheDay has been installed successfully. +Plugin SwagSloganOfTheDay has been activated successfully. +``` + +After installation, the plugin configuration can be opened by clicking the "pencil" symbol. After clearing the cache, your frontend might look like this: ![Slogan of the day in the frontend](img/result.png) You can find a installable ZIP package of this plugin here. + +### The plugin base file methods +In the next case we create a simple plugin that extends the s_articles_attributes table using the attribute crud service. + +```php +container->get('shopware_attribute.crud_service'); + $attributeCrudService->update( + 's_articles_attributes', + 'quick_start', + // possible types you can find in: /.../engine/Shopware/Bundle/AttributeBundle/Service/TypeMapping.php + TypeMapping::TYPE_STRING + ); + + // this attribute is implemented in a later version of the plugin + // so we have to implement the update method. See below. + $attributeCrudService->update( + 's_articles_attributes', + 'quick_start_guid', + TypeMapping::TYPE_STRING + ); + } + + public function uninstall(UninstallContext $uninstallContext) + { + // If the user wants to keep his data we will not delete it while uninstalling the plugin + if ($uninstallContext->keepUserData()) { + return; + } + + $attributeCrudService = $this->container->get('shopware_attribute.crud_service'); + + $attributeCrudService->delete('s_articles_attributes', 'quick_start_guid'); + $attributeCrudService->delete('s_articles_attributes', 'quick_start'); + + // clear cache + $uninstallContext->scheduleClearCache(UninstallContext::CACHE_LIST_ALL); + } + + public function update(UpdateContext $updateContext) + { + $currentVersion = $updateContext->getCurrentVersion(); + $updateVersion = $updateContext->getUpdateVersion(); + + if (version_compare($currentVersion, '1.0.2', '<=')) { + $attributeCrudService = $this->container->get('shopware_attribute.crud_service'); + $attributeCrudService->update( + 's_articles_attributes', + 'quick_start_guid', + 'string' + ); + } + + if (version_compare($currentVersion, '1.0.5', '<=')) { + // do update for version + } + } + + public function activate(ActivateContext $activateContext) + { + // on plugin activation clear the cache + $activateContext->scheduleClearCache(ActivateContext::CACHE_LIST_ALL); + } + + public function deactivate(DeactivateContext $deactivateContext) + { + // on plugin deactivation clear the cache + $deactivateContext->scheduleClearCache(DeactivateContext::CACHE_LIST_ALL); + } +} +``` + +Each method, such as install, update etc. has its own context, which is passed into the method. +The context provides various information and methods for the plugin. +Like:`->scheduleClearCache()`, `->getCurrentVersion()`, `->getUpdateVersion()`, or `->keepUserData()`. + +### The clear cache method +```php +$context->scheduleClearCache(InstallContext::CACHE_LIST_ALL); +``` +Possible settings for the parameter `caches` contains each context as constant. +`InstallContext::CACHE_LIST_DEFAULT` +`InstallContext::CACHE_LIST_ALL` +`InstallContext::CACHE_LIST_FRONTEND` +`InstallContext::CACHE_TAG_CONFIG` +`InstallContext::CACHE_TAG_HTTP` +`InstallContext::CACHE_TAG_PROXY` +`InstallContext::CACHE_TAG_ROUTER` +`InstallContext::CACHE_TAG_TEMPLATE` +`InstallContext::CACHE_TAG_THEME` + +## Container Configuration + +The [Symfony DependencyInjection Component](https://symfony.com/doc/4.4/components/dependency_injection.html) + +The container configuration is the main extension point for Shopware plugins. +In this configuration new services can be defined, core services can be decorated or replaced or functionality can be added to the system. + +``` +SwagSloganOfTheDay +├── Resources +│ └── services.xml +├──SloganPrinter.php +└──SwagSloganOfTheDay.php +``` + +```php + 'onRouteStartup' + ]; + } + + public function onRouteStartup(\Enlight_Controller_EventArgs $args) + { + $sloganPrinter = $this->container->get('swag_slogan_of_the_day.slogan_printer'); + $sloganPrinter->print(); + } +} +``` + +`SwagSloganOfTheDay/Resources/services.xml` + +```xml + + + + + + + + + + +``` + +### Decorate a service + +The following example shows you how to decorate a service which implements an interface and gets defined in the Shopware dependency injection container. + +```php +service = $service; + } + + public function getList(array $numbers, ProductContextInterface $context) + { + $products = $this->service->getList($numbers, $context); + //... + return $products; + } + + public function get($number, ProductContextInterface $context) + { + return array_shift($this->getList([$number], $context)); + } +} +``` +The original `\Shopware\Bundle\StoreFrontBundle\Service\Core\ListProductService` defined with the service id `shopware_storefront.list_product_service`. +The following service definition decorates this service using the service above: + +```xml + + + + + + + + + + + + +``` + +For more information see [How to Decorate Services](https://symfony.com/doc/4.4/service_container/service_decoration.html) + +### Extended Container Configuration + +By overwriting the `build()`-method the `ContainerBuilder` can be extended: + +```php +setParameter('swag_slogan_of_the_day.fancy_variable', $fancyVariable); + $container->addCompilerPass(new SloganCompilerPass()); + + parent::build($container); + } +} +``` +### Commands as Services + +As of Shopware 5.2.2 you can also register commands as a service and tag it with `console.command` in the `Resources/services.xml`: + +```xml + + + + + + + + + + +``` + +You can read more in the Symfony Documentation: [How to Define Commands as Services](https://symfony.com/doc/4.4/console/commands_as_services.html). + + +## Add backend emotion components + +Since shopware 5.2.10 the `Shopware\Components\Emotion\ComponentInstaller` service can be used to generate backend emotion components inside plugin installations: + +```php +public function install(InstallContext $context) +{ + $installer = $this->container->get('shopware.emotion_component_installer'); + + $vimeoElement = $installer->createOrUpdate( + $this->getName(), + 'Vimeo Video', + [ + 'name' => 'Vimeo Video', + 'xtype' => 'emotion-components-vimeo', + 'template' => 'emotion_vimeo', + 'cls' => 'emotion-vimeo-element', + 'description' => 'A simple vimeo video element for the shopping worlds.' + ] + ); + + $vimeoElement->createTextField( + [ + 'name' => 'vimeo_video_id', + 'fieldLabel' => 'Video ID', + 'supportText' => 'Enter the ID of the video you want to embed.', + 'allowBlank' => false + ] + ); +} +``` + +Registering the `Shopware\Components\Emotion\EmotionComponentViewSubscriber` as event subscriber allows to add the required template directory automatically: +Make sure that the service id is unique. Prefix it e.g. with the plugin name. + +```xml + + + + + + + + + %swag_emotion.plugin_dir% + + + + +``` + +By convention, the following template structure is required: +``` +SwagEmotion +├── Resources +│ ├── views +│ │ └── emotion_components +│ │ ├── backend +│ │ │ └── vimeo_video.js +│ │ └── widgets +│ │ └── emotion +│ │ └── components +│ │ └── emotion_vimeo.tpl +│ └── services.xml +└──SwagEmotion.php +``` +## Add a new payment method + +Since Shopware 5.2.13 the `Shopware\Components\Plugin\PaymentInstaller` service can be used to add payment methods to the database inside plugin installations. + +```php +public function install(InstallContext $context) +{ + /** @var \Shopware\Components\Plugin\PaymentInstaller $installer */ + $installer = $this->container->get('shopware.plugin_payment_installer'); + + $options = [ + 'name' => 'example_payment_invoice', + 'description' => 'Example payment method invoice', + 'action' => 'PaymentExample', + 'active' => 0, + 'position' => 0, + 'additionalDescription' => + '' + . '
    ' + . ' Pay save and secured by invoice with our example payment provider.' + . '
    ' + ]; + $payment = $installer->createOrUpdate($this->getName(), $options); +} +``` +## Plugin Resources + +Plugin metadata and configurations will be configured by using xml files which will be placed like in the example below. +IDEs like PhpStorm support auto-completion by default for these files if schema file location is valid. + +``` +SwagSloganOfTheDay +├──plugin.xml +├── Resources +│ ├── config.xml +│ ├── cronjob.xml +│ └── menu.xml +└──SwagSloganOfTheDay.php +``` + +You can find the schemas of the xml files in `engine/Shopware/Components/Plugin/schema`. + - **config.xml:** Defines the plugin configuration form which you can access by the `Basic Settings` or in the detail window of a plugin. + - **cronjob.xml:** Defines cronjobs installed with the plugin. + - **menu.xml:** Defines new menu items in the backend menu structure of Shopware. + - **plugin.xml:** Defines the metadata of your plugin, i.e. label, version, compatibility or the changelog. + +
    +At the moment it is necessary that the order of the xml elements is equal to the schema file, otherwise you will receive an exception.
    +You can use the CLI to install the plugin with extended error messages: php ./bin/console sw:plugin:install SwagSloganOfTheDay -v +
    + +## Plugin Metadata + +To provide meta information about your plugin you need a `plugin.xml` file. +If you want to sell your plugin via the [Community Store](https://store.shopware.com/en) the entries marked with `*` are required. + +Example `plugin.xml`: + +```xml + + + + + + 1.0.1 + (c) by shopware AG + MIT + https://store.shopware.com/en + shopware AG + + + + Farbe geändert; Schriftgröße geändert; + changed color; changed font-size; + + + + Erstveröffentlichung + First release + + +``` + +### Label * + +The label will be displayed in the plugin manager module in the Shopware backend as plugin name. +Describe in short what your plugin is about. Use the `lang` attribute to translate the label. +If you want to sell your plugin in the store, you should provide at least a german and an english translation. +If you want to distribute only in the german or in the international store only german/english is required. + +### Version * + +
    +Use a valid version + +The returned version has to be compatible with the [php version_compare](http://php.net/version_compare) function, otherwise, Shopware cannot detect any possible updates. +
    + +| valid | invalid | +|-------|---------| +| `1.0.0` | `v1.0.0` +| | `v1.0` | +| | `1.0` | +| | `1` | + +### Copyright * + +Set a copyright for your plugin here. + +### License * + +Set the license model you want to use for your plugin. Examples: `MIT`, `proprietary` + +### Link + +Set a link to your homepage, so that shop owners could reach your homepage from the plugin manager module. + +### Author * + +Set your name or the name of your company here. + +### Compatibility * + +With which Shopware versions is your plugin compatible? +You can set a minimum and maximum Shopware version with attributes. +Example: `` +For the Shopware version apply the same rules as for the plugin [version](#version-) +Note: As you use the new plugin system the minimum Shopware version is always at least `5.2.0` + +### Changelog * + +Maintain your changelog within the `plugin.xml`. +Create a new entry for each released version. +Each changelog entry has a version attribute, which must match the [version](#version-) of the plugin. +The changes have a `lang` attribute, so you could provide translations for your changelog. +Multiple changes can be separated with semicolons. +If you want to sell your plugin in the store, you should provide at least a german and an english translation. +If you want to distribute only in the german or in the international store only german/english is required. + +## Require other plugins +If you need other plugins to be installed, then you are able to define them in your plugin.xml file like this +```xml + + + ... + + ... + + + + + +``` + +## Plugin Configuration / Forms + +Backend plugin configuration can be extended by `Resources/config.xml` file. + +```xml + + + + + slogan + + + XML is fun! + This slogan will be shown on the storefront + + + +``` + +To read out the configuration of your plugin use this code snippet in your base plugin class: + +```php +$config = $this->container->get('shopware.plugin.cached_config_reader')->getByPluginName($this->getName()); +``` +Use it like this in other places: + +```php +$config = $this->container->get('shopware.plugin.cached_config_reader')->getByPluginName('SwagSloganOfTheDay'); +``` + +The config reader service will return an array with the config element names as keys. +If you are using scoped configuration elements, don't forget to pass the shop service `$this->container->get('shop')` as a second parameter to `getByPluginName()` method. +Without the shop context, the method will fall back to the main shop context. + +### add store values + +It is possible to define custom config stores directly inside your `config.xml`. + +A custom config store is defined like this: +```xml + + + + + selectArray + + + + + + + + + + selectExtjsStore + + Shopware.apps.Base.store.Category + + + +``` +There are two unique constraints: +* Inside a store, a value tag's value must only occur once +* Inside an option tag, a label tag's lang attribute value must only occur once + +Additionally, the order is fixed. The value tag must be defined before the label tag(s). + +There must be at least one option tag and inside each option tag there must be at least one value and one label tag. + +### add additional options + +With the `options` element you are able to customize your input field for your plugin configuration. + +Example: +```xml + + + + + readOnlyText + + + Dieser Text ist nur zum Lesen + + true + + + + +``` + +### add buttons +Since Shopware 5.2.17 it is possible to place buttons on your configuration form. + +Example: +```xml + + + + + buttonTest + + + + + + + + + + +``` + +The given `label` is the display name of the button. +You can define an option `handler` as callback for click events. + +Visit [Plugin Configuration](/developers-guide/plugin-configuration/) for further information. + +## Backend Menu Items + +Example `Resources/menu.xml`: + +```xml + + + + + SloganOfTheDay + + + SwagSloganOfTheDay + index + sprite-metronome + Marketing + + + SloganOfTheDayChild + + + SwagSloganOfTheDay + detail + sprite-application-block + + + + + +``` + +For available parent controllers take a look into the table `s_core_menu` (column `controller`). +For example, you can use one of the following: +- Article +- Content +- Customer +- ConfigurationMenu +- Marketing + +As you can see in the example above, you are also able to add child menu entries for your new menu item. +Just add them under the `` element + +The menu item won't be displayed if controller and action are missing. + +To know which class for which icon take a look at the Backend icon set overview. + +## Plugin Cronjob + +Example `Resources/cronjob.xml`: + +```xml + + + + + Send birthday email + Shopware_CronJob_SendBirthdayMail + true + 86400 + true + + +``` + +This file will be analyzed and applied on every plugin installation and update. +When this file is applied new cronjobs will be added, existing cronjobs updated and missing ones will be deleted. +Whether a cronjob exists will be determined by the given action. + +The cronjob manager will publish an event according to the name given with the `action` tag, whenever your cronjob is due to execute. +Just subscribe to this event and register a handler. Example (in your plugin or subscriber class): + +```php +public static function getSubscribedEvents() +{ + return [ + 'Shopware_CronJob_SendBirthdayMail' => 'onSendBirthdayMailCronjob' + ]; +} + +public function onSendBirthdayMailCronjob(\Shopware_Components_Cron_CronJob $job) +{ + // do some fancy things +} +``` + +## Access to other plugins + +Other plugins can be accessed via the `getPlugins()` method of the kernel. + +```php +$swagExample = $this->container->get('kernel')->getPlugins()['SwagExample']; +$path = $swagExample->getPath(); +``` + +## Adding acl privilege dependencies + +When creating a new ACL resource for your custom backend application you can define possible dependencies that the privileges of your new application has to existing/other resources and privileges. +These relations help the shop owner while selecting your resource privilege to select all other required privileges. +To achieve this, create a new plugin migration for table `s_core_acl_privilege_requirements` +and insert your resource privilege id into the column `privilege_id` +and your resource id into column `required_privilege_id` your acl resource needs. + +Example migration + +```php +addSql($sql); + } + + public function down(bool $keepUserData): void + { + } +} +``` diff --git a/source/developers-guide/plugin-system/index.md b/source/developers-guide/plugin-system/index.md deleted file mode 100644 index 3dc2277e9d..0000000000 --- a/source/developers-guide/plugin-system/index.md +++ /dev/null @@ -1,804 +0,0 @@ ---- -layout: default -title: The 5.2 Plugin System -github_link: developers-guide/plugin-system/index.md -indexed: true -shopware_version: 5.2 -group: Developer Guides -subgroup: Developing plugins -menu_title: The 5.2 Plugin System -menu_order: 120 ---- - -
    -This document is work in progress and not finished yet. -Please feel free to open a pull request on github to extend parts of this document. -
    - -
    - -## Parallel mode -The new plugin system runs fully in parallel to the "legacy" plugin system. - -## Directory Structure - -The 5.2 Plugins are located in the `custom/plugins/` directory. There is no separation in `frontend`, `core` or `backend` like in the "legacy" plugin system. - -## Plugin Name - -The plugin name should always be prefixed with your developer prefix so it's unique in the Shopware universe. -To submit plugins to the [shopware store](http://store.shopware.com/) you have to obtain your developer prefix in the [Shopware Account](https://account.shopware.com). - -In the following examples the developer prefix "Swag" will be used (short for shopware AG). - -## Minimal Plugin Example - -The most minimal Plugin is just a directory and one bootstrap file. -The directory must be named after the plugin name. The bootstrap file is called `SwagSloganOfTheDay.php`: - -### Directory structure - -``` -SwagSloganOfTheDay -└──SwagSloganOfTheDay.php -``` - -### Plugin Bootstrap file - -The Bootstrap `SwagSloganOfTheDay.php` must be namespaced with your plugin name and extend `\Shopware\Components\Plugin`: - -```php - 'onRouteStartup' - ]; - } - - public function onRouteStartup(\Enlight_Controller_EventArgs $args) - { - die('A rolling stone gathers no moss'); - } -} -``` - -### Access to the DI-Container - -Inside the plugin bootstrap the DI-Container can be accessed with the `$this->container` property: - -```php - public function onRouteStartup(\Enlight_Controller_EventArgs $args) - { - $conn = $this->container->get('dbal_connection'); - $conn->.... // do some query - } -``` - -## Autoloading - -The plugin namespace is registered as a [PSR-4](http://www.php-fig.org/psr/psr-4/) Autoloading prefix. -For example the class `\SwagSloganOfTheDay\Log\Writer` will be loaded from file `SwagSloganOfTheDay/Log/Writer.php`. - -## Plugin Install / Update - -During plugin installation / deinstallation / update / activate / deactivate a method on the plugin bootstrap is called that can optionally be overwritten. - -```php - 'onRouteStartup' - ]; - } - - public function onRouteStartup(\Enlight_Controller_EventArgs $args) - { - $sloganPrinter = $this->container->get('swag_slogan_of_the_day.slogan_printer'); - $sloganPrinter->print(); - } -} -``` - -`SwagSloganOfTheDay/Resources/services.xml` - -```xml - - - - - - - - - - -``` - -### Decorate a service -The following example shows you how to decorate a service which implements an interface and gets defined in the Shopware dependency injection container. -```php -service = $service; - } - - public function getList(array $numbers, ProductContextInterface $context) - { - $products = $this->service->getList($numbers, $context); - //... - return $products; - } - - public function get($number, ProductContextInterface $context) - { - return array_shift($this->getList([$number], $context)); - } -} -``` -The original `\Shopware\Bundle\StoreFrontBundle\Service\Core\ListProductService` defined with the service id `shopware_storefront.list_product_service`. The following service definition decorates this service using the service above: - -```xml - - - - - - - - - - - - -``` - -For more information see http://symfony.com/doc/current/service_container/service_decoration.html - -### Extended Container Configuration - -By overwriting the `build()`-method the `ContainerBuilder` can be extended: - -```php -setParameter('swag_slogan_of_the_day.plugin_dir', $this->getPath()); - $container->addCompilerPass(new SloganCompilerPass()); - - parent::build($container); - } -} -``` - -### Event subscriber -The new plugin system has the ability to add event subscriber by adding subscribers in the `services.xml`. - -``` -SwagSloganOfTheDay -├── Resources -│ └── services.xml -├──SloganPrinter.php -├──RouteSubscriber.php -└──SwagSloganOfTheDay.php -``` - -The `onRouteStartup` subscriber above now will be encapsulated in a subscriber class. - -`SwagSloganOfTheDay/RouteSubscriber.php` - -```php - 'onRouteStartup' - ]; - } - - public function __construct(SloganPrinter $sloganPrinter) - { - $this->sloganPrinter = $sloganPrinter; - } - - public function onRouteStartup(\Enlight_Controller_EventArgs $args) - { - $this->sloganPrinter->print(); - } -} -``` - -After adding the `RouteSubscriber.php`, the subscriber can be added to the `services.xml` as a tagged service ([Symfony - Working with Tagged Services](http://symfony.com/doc/current/components/dependency_injection/tags.html)). -This allows Shopware to load all event subscriber automatically so you don't need to register the subscriber manually. - -`SwagSloganOfTheDay/Resources/services.xml` -```xml - - - - - - - - - - - - - - - -``` - -## Register plugin controller with template -```php - 'registerController', - ]; - } - - public function registerController(\Enlight_Event_EventArgs $args) - { - $this->container->get('template')->addTemplateDir( - $this->getPath() . '/Resources/views/' - ); - - return $this->getPath() . '/Controllers/Frontend/MyController.php'; - } -} -``` - -### Controller auto-registration - -The auto-registration is available in Shopware 5.2.7 or above. -To make use of it, create a file like `SwagControllerExample/Controllers/(Backend|Frontend|Widgets|Api)/MyController.php` -and follow our controller naming conventions. After that, you'll be able to call `MyController`. -The registration of the template would be done, i.e. in the `preDispatch()`-Method of your controller. - -```php -class Shopware_Controllers_Frontend_MyController extends \Enlight_Controller_Action -{ - public function preDispatch() - { - $pluginPath = $this->container->getParameter('swag_controller_example.plugin_dir'); - - $this->get('template')->addTemplateDir($pluginPath . '/Resources/views/'); - $this->get('snippets')->addConfigDir($pluginPath . '/Resources/snippets/'); - } -} -``` - -## Add console commands - -There are two ways to add Shopware [CLI Commands](/developers-guide/shopware-5-cli-commands/). - -### Implement registerCommands - -You can implement the method `registerCommands()` and add commands to the Console application: - -```php -add(new FirstCommand()); - $application->add(new SecondCommand()); - } -} -``` - -### Commands as Services - -As of Shopware 5.2.2 you can also register commands as a service and tag it with `console.command` in the `Resources/services.xml`: - -```xml - - - - - - - - - - -``` - -You can read more in the Symfony Documentation: [How to Define Commands as Services](https://symfony.com/doc/2.8/cookbook/console/commands_as_services.html). - - -## Add backend emotion components -Since shopware 5.2.10 the `Shopware\Components\Emotion\ComponentInstaller` service can be used to generate backend emotion components inside plugin installations: -```php -public function install(InstallContext $context) -{ - $installer = $this->container->get('shopware.emotion_component_installer'); - - $vimeoElement = $installer->createOrUpdate( - $this->getName(), - 'Vimeo Video', - [ - 'name' => 'Vimeo Video', - 'xtype' => 'emotion-components-vimeo', - 'template' => 'emotion_vimeo', - 'cls' => 'emotion-vimeo-element', - 'description' => 'A simple vimeo video element for the shopping worlds.' - ] - ); - - $vimeoElement->createTextField( - [ - 'name' => 'vimeo_video_id', - 'fieldLabel' => 'Video ID', - 'supportText' => 'Enter the ID of the video you want to embed.', - 'allowBlank' => false - ] - ); -} -``` - -Registering the `Shopware\Components\Emotion\EmotionComponentViewSubscriber` as event subscriber allows to add the required template directory automatically: -```xml - - - - - - - %swag_emotion.plugin_dir% - - - - -``` - -By convention, the following template structure is required: -``` -SwagEmotion -├── Resources -│ ├── views -│ │ └── emotion_components -│ │ ├── backend -│ │ │ └── vimeo_video.js -│ │ └── widgets -│ │ └── emotion -│ │ └── components -│ │ └── emotion_vimeo.tpl -│ └── services.xml -└──SwagEmotion.php -``` -## Add a new payment method -Since Shopware 5.2.13 the `Shopware\Components\Plugin\PaymentInstaller` service can be used to add payment methods to the database inside plugin installations. - -```php -public function install(InstallContext $context) -{ - /** @var \Shopware\Components\Plugin\PaymentInstaller $installer */ - $installer = $this->container->get('shopware.plugin_payment_installer'); - - $options = [ - 'name' => 'example_payment_invoice', - 'description' => 'Example payment method invoice', - 'action' => 'PaymentExample', - 'active' => 0, - 'position' => 0, - 'additionalDescription' => - '' - . '
    ' - . ' Pay save and secured by invoice with our example payment provider.' - . '
    ' - ]; - $payment = $installer->createOrUpdate($this->getName(), $options); -} -``` -## Plugin Resources - -Plugin meta data and configurations will be configured by using xml files which will be placed like in the example below. -IDEs like PhpStorm support auto completion by default for these files if schema file location is valid. - -``` -SwagSloganOfTheDay -├──plugin.xml -├── Resources -│ ├── config.xml -│ └── menu.xml -└──SwagSloganOfTheDay.php -``` - -You can find the schema of the xml files in `engine/Shopware/Components/Plugin/schema`. - - **config.xml:** Defines the plugin configuration form which you can access by the `Basic Settings` or in the detail window of a plugin. - - **menu.xml:** Defines new menu items in the backend menu structure of Shopware. - - **plugin.xml:** Defines the meta data of your plugin, i.e. label, version, compatibility or the changelog. - -
    -At the moment it is necessary that the order of the xml elements is equal to the schema file, otherwise you will receive an exception.
    -You can use the CLI to install the plugin with extended error messages: php ./bin/console sw:plugin:install SwagSloganOfTheDay -v -
    - -### Plugin Metadata - -Entires can be separated with semicolons when documenting multiple changes in the changelog. - -Example `plugin.xml`: - -```xml - - - - - - 1.0.0 - http://example.org - shopware AG - - - - Farbe geändert; Schriftgröße geändert; - changed color; changed font-size; - - -``` - -### Plugin Configuration / Forms - - -Backend plugin configuration can be extended by `Resources/config.xml` file. This replaces the usage of `$this->Form()` on old `Shopware_Components_Plugin_Bootstrap`. - - -```xml - - - - - slogan - - - XML is fun! - This slogan will be shown on the storefront - - - -``` - -To read out the configuration of your plugin use this code snippet in your base plugin class: - -```php -$config = $this->container->get('shopware.plugin.config_reader')->getByPluginName($this->getName()); -``` -Use it like this in other places: -```php -$config = Shopware()->Container()->get('shopware.plugin.config_reader')->getByPluginName('SwagSloganOfTheDay'); -``` -The config reader service will return an array with the config element names as keys. - -#### add store values -As of Shopware 5.2.11 it is possible to define custom config stores directly inside your `config.xml`. - -A custom config store is defined like this: -```xml - - - - - selectArray - - - - - - - - - - selectExtjsStore - - Shopware.apps.Base.store.Category - - - -``` -There are two unique constraints: -* Inside a store, a value tag's value must only occur once -* Inside an option tag, a label tag's lang attribute value must only occur once - -Additionally, the order is fixed. The value tag must be defined before the label tag(s). - -There must be at least one option tag and inside each option tag there must be at least one value and one label tag. - -#### add buttons -Since Shopware 5.2.17 it is possible to place buttons on your configuration form. - -Example: -```xml - - - - - buttonTest - - - - - - - - - - -``` -The given `label` is the display name of the button. -You can define an option `handler` as callback for click events. - -### Backend Menu Items - -Example `Resources/menu.xml`: - -```xml - - - - - SloganOfTheDay - - - SwagSloganOfTheDay - index - sprite-metronome - Marketing - - - SloganOfTheDayChild - - - SwagSloganOfTheDay - detail - sprite-application-block - - - - - -``` - -For available parent controllers take a look into the table `s_core_menu` (column `controller`). For example you can use one of the following: -- Article -- Content -- Customer -- ConfigurationMenu -- Marketing - -As you can see in the example above, you are also able to add child menu entries for your new menu item. Just add them under the `` element - -The menu item won't be displayed if controller and action are missing. - -To know which class for which icon take a look at the Backend icon set overview. - -### Plugin Cronjob - -Example `Resources/cronjob.xml`: - -```xml - - - - - Send birthday email - Shopware_CronJob_SendBirthdayMail - true - 86400 - true - - - -``` - -## Access to other plugins - -Other plugins can be accessed via the `getPlugins()` method of the kernel. - -```php -$swagExample = Shopware()->Container()->get('kernel')->getPlugins()['SwagExample']; -$path = $swagExample->getPath(); -``` - - - -## Update from legacy plugin system - -Shopware recognizes whether the plugin is based on the legacy or 5.2 plugin system and moves it to the correct directory. Shopware does not support moving of extracted plugins based on the 5.2 plugin system, if they are placed in the legacy directory structure. -Further the zip archive structure changed. - -**Legacy zip structure:** -``` -SwagSloganOfTheDay.zip -└──Frontend - └──SwagSloganOfTheDay - ├──Bootstrap.php - └──... -``` - -**New 5.2 zip structure:** -``` -SwagSloganOfTheDay.zip -└──SwagSloganOfTheDay - ├──SwagSloganOfTheDay.php - └──... -``` - -## Example Plugins - -- github.com/shyim/shopware-profiler -- github.com/bcremer/SwagModelTest -- github.com/shopwareLabs/SwagBackendOrder diff --git a/source/developers-guide/plugin-testing/index.md b/source/developers-guide/plugin-testing/index.md index 586d8a19f3..485d2211b6 100644 --- a/source/developers-guide/plugin-testing/index.md +++ b/source/developers-guide/plugin-testing/index.md @@ -13,68 +13,72 @@ menu_order: 80 ## Unit tests If you want to start writing a new plugin keep in mind that unit testing helps you to deliver higher quality plugins. -With help of the __cli tools__ you can easily start with plugin skeleton which has all relevant snippets to start directly with -testing. One of the first things that is important for testing is a `phpunit.xml[.dist]`. It could look like this: +With help of the __[cli tools](https://github.com/shopwareLabs/sw-cli-tools)__ you can easily start with plugin skeleton which has all relevant snippets to start directly with testing. +One of the first things that is important for testing is a `phpunit.xml[.dist]`. It could look like this: ```xml - - - tests - + + + ./Tests + + + + + + ./Tests + + + ``` ### Basics -The starting point for testing your own plugin ist the __phpunit.xml[.dist]___ and the `Bootstrap.php` file in the `\tests` folder -of your plugin. +The starting point for testing your own plugin is the __phpunit.xml[.dist]___ in your plugin root directory and the `Bootstrap.php` file in the `./Tests/Functional/` directory of your plugin. With the `Bootstrap.php` you can setup your environment properly to prepare it for testing. You can __initialize the kernel__, register __event subscribers__ and __namespaces__ or you could initialize the __shop context__ to make currencies available. To prepare your plugin for testing it is necessary to require the Shopware helper bootstrap. - ```php - - The path to this file depends on wheter you are using the legacy plugin system or the new 5.2 Plugin system. -
    - The helper bootstrap starts the testing kernel and makes - several functions like `Shopware()` available for you. You can then use the __service container__. - - ### Writing tests - - You can then place your test which could look like this: - ```php - [ - 'some_config' => 'foo' - ] - ); - - public function testMyService() - { - $service = new MyService(); - $result = $service->add(1, 1); - - $this->assertEquals(2, $result); - } - } - ``` -You can run this test from your plugin folder by typing `phpunit` if you have installed phpunit globally. Otherwise you could use the onboard -phpunit version shopware comes with. - -With help of the `$ensureLoadedPlugins` static you can assure that your plugin is installed and activated and you can even configure it. It is not required -to assure that and the less your test needs the __Shopware stack__, the better the test is written. -We have written a tiny example plugin just to show how you could start testing your work. -You can find a installable ZIP package of it here. +```php + +The path to this file depends on whether you are using the legacy plugin system or the new 5.2 Plugin system. +
    +The helper bootstrap starts the testing kernel and makes several functions like `Shopware()` available for you. You can then use the __service container__. +### Writing tests +You can then place your test which could look like this: +```php + [ + 'some_config' => 'foo' + ] + ]; + public function testMyService() + { + $service = new MyService(); + $result = $service->add(1, 1); + + $this->assertEquals(2, $result); + } +} +``` +You can run this test from your plugin root directory by typing `phpunit` if you have installed phpunit globally. +Otherwise you could use the onboard phpunit version shopware comes with. + +With help of the `$ensureLoadedPlugins` static you can assure that your plugin is installed and activated and you can even configure it. +It is not required to assure that and the less your test needs the __Shopware stack__, the better the test is written. + +We have written a tiny example plugin just to show how you could start testing your work. +You can find a installable ZIP package of it here. diff --git a/source/developers-guide/product-export/index.md b/source/developers-guide/product-export/index.md new file mode 100644 index 0000000000..2b05e1c764 --- /dev/null +++ b/source/developers-guide/product-export/index.md @@ -0,0 +1,55 @@ +--- +layout: default +title: Extending product exports +github_link: developers-guide/product-export/index.md +shopware_version: 5.3.0 +tags: + - product + - export + - developers + - beginner +indexed: true +group: Developer Guides +subgroup: General Resources +menu_title: Product exports +menu_order: 600 +--- + +
    + +## Introduction +This article will show examples for extending product exports in shopware + +## Modify the article variables +The `Shopware_Modules_Export_ExportResult_Filter_Fixed` event can be used to modify the exported product data. A use case could be adding new variables to every product or edit one of the existing variables. You can also create your own filter to remove products which meet certain criteria. Here is a basic example how to use that event in a plugin base file: +```php +public static function getSubscribedEvents() +{ + return [ + 'Shopware_Modules_Export_ExportResult_Filter_Fixed' => 'onFilterExportResult', + ]; +} + +public function onFilterExportResult(\Enlight_Event_EventArgs $args) +{ + $products = $args->getReturn(); + /** This is the id of the feed being exported */ + $feedId = $args->get('feedId'); + /** @var \sExport $sExport */ + $sExport = $args->get('subject'); + + /** + * Here is the instance of the sExport class which can be used to add new variables to smarty for example + */ + $sExport->sSmarty->assign('newVariable', ['custom' => 'This is a custom variable available in the export template']); + + /** + * in $products are all the products as array which can be modified here + */ + foreach($products as &$product) { + $product['randomNumber'] = random_int(1, 100); + } + + return $products; +} +``` diff --git a/source/developers-guide/rest-api/api-resource-address/index.md b/source/developers-guide/rest-api/api-resource-address/index.md index 0577cdf735..f2d4f21e01 100755 --- a/source/developers-guide/rest-api/api-resource-address/index.md +++ b/source/developers-guide/rest-api/api-resource-address/index.md @@ -1,22 +1,29 @@ --- layout: default -title: REST API - Address Resource +title: REST API - Address resource github_link: developers-guide/rest-api/api-resource-address/index.md shopware_version: 5.2.0 -indexed: false +menu_title: Address resource +menu_order: 20 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's address resource. With this resource, it is possible to retrieve, update and delete any customer address of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's address resource. +With this resource, it is possible to retrieve, update and delete any customer address of your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|------------------------|---------------------|----------------------|-----------------------|---------------------| -| /api/addresses | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|----------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/addresses | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -35,25 +42,25 @@ Single address details can be retrieved via the address ID: | Shopware\Models\Customer\Address | s_user_addresses | -| Field | Type | Original Object | -|--------------------------|-----------------------|----------------------------------------------------------| -| id | integer (primary key) | | -| customer | integer (primary key) | **[Customer](../models/#customer)** | -| company | string | | -| department | string | | -| salutation | string | | -| firstname | string | | -| lastname | string | | -| street | string | | -| zipcode | string | | -| city | string | | -| phone | string | | -| vatId | string | | -| additionalAddressLine1 | string | | -| additionalAddressLine2 | string | | -| country | int (foreign key) | **[Country](../models/#country)** | -| state | int (foreign key) | | -| attribute | array | | +| Field | Type | Original Object | +|------------------------|-----------------------|-------------------------------------| +| id | integer (primary key) | | +| customer | integer (primary key) | **[Customer](../models/#customer)** | +| company | string | | +| department | string | | +| salutation | string | | +| firstname | string | | +| lastname | string | | +| street | string | | +| zipcode | string | | +| city | string | | +| phone | string | | +| vatId | string | | +| additionalAddressLine1 | string | | +| additionalAddressLine2 | string | | +| country | int (foreign key) | **[Country](../models/#country)** | +| state | int (foreign key) | | +| attribute | array | | ## GET (List) @@ -66,47 +73,57 @@ To get a list of all addresses, simply query: ### Return Value -| Model | Table | -|------------------------------------|------------------| -| Shopware\Models\Customer\Address | s_user_addresses | +| Model | Table | +|----------------------------------|------------------| +| Shopware\Models\Customer\Address | s_user_addresses | -This API call returns an array of elements, one for each address. Each of these elements has the same structure like a single element above. +This API call returns an array of elements, one for each address. +Each of these elements has the same structure as a single element from above, but without the detailed customer data. Appended to the above mentioned list, you will also find the following data: -| Field | Type | Comment | -|---------------------|-----------------------|---------------------------------------------------| -| total | integer | The total number of address resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of address resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) and PUT (update) `POST` and `PUT` operations support the following data structure: -| Model | Table | -|------------------------------------|------------------| -| Shopware\Models\Customer\Address | s_user_addresses | - -| Field | Type | Comment | -|--------------------------|-----------------------|------------------------------------------------------| -| id | integer (primary key) | If null, a new entity will be created | -| company | string | | -| department | string | | -| salutation (required) | string | | -| firstname (required) | string | | -| lastname (required) | string | | -| street (required) | string | | -| zipcode (required) | string | | -| city (required) | string | | -| phone | string | | -| vatId | string | | -| additionalAddressLine1 | string | | -| additionalAddressLine2 | string | | -| country (required) | int (foreign key) | **[Country](../models/#country)** | -| state | int (foreign key) | | -| attribute | array | | - +| Model | Table | +|----------------------------------|------------------| +| Shopware\Models\Customer\Address | s_user_addresses | + +| Field | Type | Comment | +|------------------------|-----------------------|---------------------------------------| +| id | integer (primary key) | If null, a new entity will be created | +| customer (required) | integer (foreign key) | | +| company | string | | +| department | string | | +| salutation (required) | string | | +| firstname (required) | string | | +| lastname (required) | string | | +| street (required) | string | | +| zipcode (required) | string | | +| city (required) | string | | +| phone | string | | +| vatId | string | | +| additionalAddressLine1 | string | | +| additionalAddressLine2 | string | | +| country (required) | int (foreign key) | **[Country](../models/#country)** | +| state | int (foreign key) | | +| attribute | array | | + + +## PUT (update) + +
    +Note: Changing a customer id on addresses is not supported, +you can leave the customer id out of the request, +or just set the same customer id which owns the address. +
    ## DELETE To delete an address, simply call the specified resource with the `DELETE` operation as the following example shows: diff --git a/source/developers-guide/rest-api/api-resource-article/index.md b/source/developers-guide/rest-api/api-resource-article/index.md index c0a5d5bead..0cd633a5b5 100755 --- a/source/developers-guide/rest-api/api-resource-article/index.md +++ b/source/developers-guide/rest-api/api-resource-article/index.md @@ -1,29 +1,38 @@ --- layout: default -title: REST API - Article Resource +title: REST API - Product resource (article) github_link: developers-guide/rest-api/api-resource-article/index.md -indexed: false +menu_title: Product resource +menu_order: 30 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's article resource. With this resource, it's possible to retrieve, update and delete any article of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's article resource. +The name "article" is a legacy misnomer, it is used to describe products. +With this resource, it's possible to retrieve, update and delete any product of your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|-----------------------|------------------------|------------------------|-----------------------|-----------------------| -| /api/articles | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|---------------|------------------------|------------------------|------------------------|------------------------|------------------------|------------------------|------------------------| +| /api/articles | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | If you want to access this resource, simply query the following URL: * **http://my-shop-url/api/articles** ## Structure -In this part, we will have a look at the data provided by this resource and its structure. You will be guided through all seven different operations separately. +In this part, we will have a look at the data provided by this resource and its structure. +You will be guided through all seven different operations separately. ### GET @@ -36,24 +45,27 @@ This API call requires one of the following parameters to be defined: | Article Id | id | s_articles.id | /api/articles/2 | | Detail Number | number | s_articles.ordernumber | /api/articles/SW10003?useNumberAsId=true | -* **useNumberAsId=true** - This tells the API to query the article's data by its detail number, instead of its actual identifier. Otherwise, the syntax is just **/api/articles/id**. It's not possible to provide both parameters at the same time. +* **useNumberAsId=true** - This tells the API to query the product's data by its detail number, instead of its actual identifier. +Otherwise, the syntax is just **/api/articles/id**. +It's not possible to provide both parameters at the same time. #### Optional Parameters Optional parameters can be provided: -* language `id` or `locale` (from `s_core_locales`). If used, the returned info will be provided in the specified language (if available) -* `considerTaxInput`: By default, all returned prices are net values. If the boolean `considerTaxInput` is set to true, gross values will be returned instead. +* language `id` or `shop` (from `s_core_shops`). If used, the returned info will be provided in the specified language (if available) +* `considerTaxInput`: By default, all returned prices are net values. +If the boolean `considerTaxInput` is set to true, gross values will be returned instead. -| Identifier | Parameter | DB column | Example call | -|------------------|-----------|------------------------|------------------------------------------| -| language | id | s_core_locales | /api/articles/2?language=de_DE | -| considerTaxInput | boolean | | /api/articles/2?considerTaxInput=true | +| Identifier | Parameter | DB column | Example call | +|------------------|-----------|--------------|---------------------------------------| +| language | id | s_core_shops | /api/articles/2?language=3 | +| considerTaxInput | boolean | | /api/articles/2?considerTaxInput=true | You can use one or more of these parameters together. Here is an example of a parametrized URL: -* **http://my-shop-url/api/articles/2?considerTaxInput=true&language=de_DE** -* **http://my-shop-url/api/articles/SW10003?useNumberAsId=true&considerTaxInput=true&language=de_DE** +* **http://my-shop-url/api/articles/2?considerTaxInput=true&language=3** +* **http://my-shop-url/api/articles/SW10003?useNumberAsId=true&considerTaxInput=true&language=3** #### Return value @@ -61,6 +73,69 @@ This call will return an array of the model **Shopware\Models\Article\Article** The following table shows the fields, types and original objects of this array. +| Field | Type | Original object | +|-------------------|-----------------------|----------------------------------------------------| +| id | integer (primary key) | | +| mainDetailId | integer (foreign key) | **[Detail](../models/#article-detail)** | +| supplierId | integer (foreign key) | **[Supplier](../models/#supplier)** | +| taxId | integer (foreign key) | **[Tax](../models/#tax)** | +| priceGroupId | integer (foreign key) | **[PriceGroup](../models/#price-group)** | +| filterGroupId | integer (foreign key) | **[ConfiguratorSet](../models/#property-group)** | +| configuratorSetId | integer (foreign key) | **[ConfiguratorSet](../models/#configurator-set)** | +| name | string | | +| description | string | | +| descriptionLong | string | | +| added | date/time | | +| active | boolean | | +| pseudoSales | integer | | +| highlight | boolean | | +| keywords | string | | +| metaTitle | string | | +| changed | date/time | | +| priceGroupActive | boolean | | +| lastStock | boolean | | +| crossBundleLook | boolean | | +| notification | boolean | | +| template | string | | +| mode | integer | | +| availableFrom | date/time | | +| availableTo | date/time | | +| mainDetail | object | **[Detail](../models/#article-detail)** | +| tax | object | **[Tax](../models/#tax)** | +| propertyValue | object | **[PropertyValue](../models/#property-value)** | +| supplier | object | **[Supplier](../models/#supplier)** | +| propertyGroup | object | **[PropertyGroup](../models/#property-group)** | +| customerGroups | object array | **[CustomerGroup](../models/#customer-group)** | +| images | object array | **[Image](../models/#image)** | +| configuratorSet | object | **[ConfiguratorSet](../models/#configurator-set)** | +| links | object array | **[Link](../models/#link)** | +| downloads | object array | **[Download](../models/#download)** | +| categories | object array | **[Category](../models/#category)** | +| similar | object array | **[Similar](../models/#similar-(get))** | +| related | object array | **[Related](../models/#related-(get))** | +| details | object array | **[Detail](../models/#article-detail)** | +| translations | object array | **[Translation](../models/#translation)** | + +### GET (List) + +#### Optional Parameters + +Optional parameters can be provided: +* language `id` or `locale` (from `s_core_locales`). +If used, the returned info will be provided in the specified language (if available) + +| Identifier | Parameter | DB column | Example call | +|------------|-----------|----------------|------------------------------| +| language | id | s_core_locales | /api/articles/language=de_DE | + +You can use one or more of these parameters together. + +Here is an example of a parametrized URL: + +* **http://my-shop-url/api/articles/?language=de_DE** + +#### Return Value + | Field | Type | Original object | |---------------------|-----------------------|----------------------------------------------------| | id | integer (primary key) | | @@ -88,113 +163,49 @@ The following table shows the fields, types and original objects of this array. | mode | integer | | | availableFrom | date/time | | | availableTo | date/time | | -| mainDetail | object | **[Detail](../models/#article-detail)** | -| tax | object | **[Tax](../models/#tax)** | -| propertyValue | object | **[PropertyValue](../models/#property-value)** | -| supplier | object | **[Supplier](../models/#supplier)** | -| propertyGroup | object | **[PropertyGroup](../models/#property-group)** | -| customerGroups | object array | **[CustomerGroup](../models/#customer-group)** | -| images | object array | **[Image](../models/#image)** | -| configuratorSet | object | **[ConfiguratorSet](../models/#configurator-set)** | -| links | object array | **[Link](../models/#link)** | -| downloads | object array | **[Download](../models/#download)** | -| categories | object array | **[Category](../models/#category)** | -| similar | object array | **[Similar](../models/#similar)** | -| related | object array | **[Related](../models/#related)** | -| details | object array | **[Detail](../models/#article-detail)** | -| translations | object array | **[Translation](../models/#translation)** | - -### GET (List) - -#### Optional Parameters - -Optional parameters can be provided: -* language `id` or `locale` (from `s_core_locales`). If used, the returned info will be provided in the specified language (if available) -* `considerTaxInput`: By default, all returned prices are net values. If the boolean `considerTaxInput` is set to true, gross values will be returned instead. - -| Identifier | Parameter | DB column | Example call | -|------------------|-----------|------------------------|------------------------------------------| -| language | id | s_core_locales | /api/articles/language=de_DE | -| considerTaxInput | boolean | | /api/articles/considerTaxInput=true | - -You can use one or more of these parameters together. - -Here is an example of a parametrized URL: - -* **http://my-shop-url/api/articles/considerTaxInput=true&language=de_DE** - -#### Return Value - -| Field | Type | Original object | -|---------------------|-----------------------|--------------------------------------------------| -| id | integer (primary key) | | -| mainDetailId | integer (foreign key) | **[Detail](../models/#article-detail)** | -| supplierId | integer (foreign key) | **[Supplier](../models/#supplier)** | -| taxId | integer (foreign key) | **[Tax](../models/#tax)** | -| priceGroupId | integer (foreign key) | **[PriceGroup](../models/#price-group)** | -| filterGroupId | integer (foreign key) | **[ConfiguratorSet](../models/#property-group)** | -| configuratorSetId | integer (foreign key) | **[ConfiguratorSet](../models/#configurator-set)**| -| name | string | | -| description | string | | -| descriptionLong | string | | -| added | date/time | | -| active | boolean | | -| pseudoSales | integer | | -| highlight | boolean | | -| keywords | string | | -| metaTitle | string | | -| changed | date/time | | -| priceGroupActive | boolean | | -| lastStock | boolean | | -| crossBundleLook | boolean | | -| notification | boolean | | -| template | string | | -| mode | integer | | -| availableFrom | date/time | | -| availableTo | date/time | | ### POST (create) -| Field | Type | Notice | Original Object / Database table | -|-----------------------|-----------------------|-------------------------------------------------------------------------------|----------------------------------------------------| -| name (required) | string | | | -| taxId (required) | integer (foreign key) | Required if no tax object provided | `s_core_tax.id` | -| tax (required) | object | | **[Tax](../models/#tax)** | -| mainDetail (required) | object | | **[Detail](../models/#article-detail)** | -| supplierId (required) | integer (foreign key) | Required if no supplier object provided | `s_articles_supplier.id` | -| supplier (required) | object | Will be created if it does not exist | **[Supplier](../models/#supplier)** | -| priceGroupId | integer (foreign key) | | `s_core_pricegroups.id` | -| filterGroupId | integer (foreign key) | | `s_filter.id` | -| description | string | | | -| descriptionLong | string | | | -| added | date/time | | | -| active | boolean | | | -| pseudoSales | integer | | | -| highlight | boolean | | | -| keywords | string | | | -| metaTitle | string | | | -| changed | date/time | | | -| priceGroupActive | boolean | | | -| lastStock | boolean | | | -| crossBundleLook | boolean | | | -| notification | boolean | | | -| template | string | | | -| mode | integer | | | -| availableFrom | date/time | | | -| availableTo | date/time | | | -| propertyValues | object array | If provided it requires filterGroupId to be set | **[PropertyValue](../models/#property-value)** | -| customerGroups | object array | | **[CustomerGroup](../models/#customer-group)** | -| images | object array | | **[Image](../models/#image)** | -| configuratorSet | object | | **[ConfiguratorSet](../models/#configurator-set)** | -| downloads | object array | | **[Download](../models/#download)** | -| categories | object array | | **[Category](../models/#category)** | -| similar | object array | | **[Similar](../models/#similar)** | -| related | object array | | **[Related](../models/#related)** | -| variants | object array | | **[Detail](../models/#article-detail)** | +| Field | Type | Notice | Original Object / Database table | +|-----------------------|-----------------------|-------------------------------------------------|----------------------------------------------------| +| name (required) | string | | | +| taxId (required) | integer (foreign key) | Required if no tax object provided | `s_core_tax.id` | +| tax (required) | object | | **[Tax](../models/#tax)** | +| mainDetail (required) | object | | **[Detail](../models/#article-detail)** | +| supplierId (required) | integer (foreign key) | Required if no supplier object provided | `s_articles_supplier.id` | +| supplier (required) | object | Will be created if it does not exist | **[Supplier](../models/#supplier)** | +| priceGroupId | integer (foreign key) | | `s_core_pricegroups.id` | +| filterGroupId | integer (foreign key) | | `s_filter.id` | +| description | string | | | +| descriptionLong | string | | | +| added | date/time | | | +| active | boolean | | | +| pseudoSales | integer | | | +| highlight | boolean | | | +| keywords | string | | | +| metaTitle | string | | | +| changed | date/time | | | +| priceGroupActive | boolean | | | +| lastStock | boolean | | | +| crossBundleLook | boolean | | | +| notification | boolean | | | +| template | string | | | +| mode | integer | | | +| availableFrom | date/time | | | +| availableTo | date/time | | | +| propertyValues | object array | If provided it requires filterGroupId to be set | **[PropertyValue](../models/#property-value)** | +| customerGroups | object array | | **[CustomerGroup](../models/#customer-group)** | +| images | object array | | **[Image](../models/#image)** | +| configuratorSet | object | | **[ConfiguratorSet](../models/#configurator-set)** | +| downloads | object array | | **[Download](../models/#download)** | +| categories | object array | | **[Category](../models/#category)** | +| similar | object array | | **[Similar](../models/#similar-(put,-post))** | +| related | object array | | **[Related](../models/#related-(put,-post))** | +| variants | object array | | **[Detail](../models/#article-detail)** | ### PUT (update) -Articles can be identified using the following: +Products can be identified using the following: | Identifier | Parameter | DB column | Example call | |---------------|-----------|------------------------|------------------------------------------| @@ -205,25 +216,39 @@ The data structure used is similar to the one used for creation (**POST** reques ## DELETE -The article(s) to delete can be defined using the following syntax: +The product(s) to delete can be defined using the following syntax: -| Identifier | Parameter | DB column | Example call | -|---------------|-----------|------------------------|------------------------------------------| -| Article Id | id | s_articles.id | /api/articles/2 | +| Identifier | Parameter | DB column | Example call | +|------------|-----------|---------------|-----------------| +| Article Id | id | s_articles.id | /api/articles/2 | ## DELETE (Stack) -In order to delete more than one article at once, it's possible to provide an array of ids to the REST API. -Simply pass the array of article ids to the following URL (example) +In order to delete more than one product at once, it's possible to provide an array of objects with ids or product numbers to the REST API. +Simply pass the array of objects to the following URL (example) * **[DELETE] http://my-shop-url/api/articles/** -without providing an id as seen in the single `DELETE` request. As data provide the array of ids you wish to delete. +without providing an id as seen in the single `DELETE` request. + +### Example +* Deletes product with id 1 and product with number SW00002 + +```javascript +[ + {"id": 1}, + {"mainDetail": + { + "number": "SW00002" + } + } +] +``` ## PUT (Stack) -Updating many articles at once requires an array of article data being provided to the following URL using the `PUT` request (example): +Updating many products at once requires an array of product data being provided to the following URL using the `PUT` request (example): * **[PUT] http://my-shop-url/api/articles/** -Simply provide the same data as described in the create statement. +Simply provide the same data as described in the "create" statement. diff --git a/source/developers-guide/rest-api/api-resource-cache/index.md b/source/developers-guide/rest-api/api-resource-cache/index.md index 8ff9d6a792..61cfb76907 100755 --- a/source/developers-guide/rest-api/api-resource-cache/index.md +++ b/source/developers-guide/rest-api/api-resource-cache/index.md @@ -1,22 +1,29 @@ --- layout: default -title: REST API - Cache Resource +title: REST API - Cache resource github_link: developers-guide/rest-api/api-resource-cache/index.md -indexed: false +menu_title: Cache resource +menu_order: 50 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's cache resource. With this resource, it is possible to get information about your current cache status, as well as clear its content. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's cache resource. +With this resource, it is possible to get information about your current cache status, as well as clear its content. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|----------------------|-----------------------|-----------------------|------------------------|------------------------|-----------------------|-----------------------| -| /api/caches | ![Yes](../img/yes.png)| ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|-------------|------------------------|------------------------|----------------------|----------------------|----------------------|------------------------|------------------------| +| /api/caches | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | If you want to access this resource, simply query the following URL: @@ -32,15 +39,15 @@ Single cache details can be retrieved by using its id: ### Return Value -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| dir | string | The path to this cache directory | -| size | string | Including size unit | -| files | integer | Amount of files within the cache directory | -| freeSpace | string | Free space, including the size unit | -| name | string | The name of the cache | -| backend | string | | -| id | string | The identifier of this cache | +| Field | Type | Comment | +|-----------|---------|--------------------------------------------| +| dir | string | The path to this cache directory | +| size | string | Including size unit | +| files | integer | Amount of files within the cache directory | +| freeSpace | string | Free space, including the size unit | +| name | string | The name of the cache | +| backend | string | | +| id | string | The identifier of this cache | ## GET (List) @@ -52,24 +59,25 @@ To get a list of all caches, simply query: ### Return Value -This API call returns an array of elements, one for each cache type. Each of these elements has the following structure: +This API call returns an array of elements, one for each cache type. +Each of these elements has the following structure: -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------------------| -| dir | string | The path to this cache directory | -| size | string | Spaced used by this cache's content, including size unit | -| files | integer | Number of files within the cache directory | -| freeSpace | string | Free space, including the size unit | -| name | string | The name of the cache | -| backend | string | | -| id | string | The identifier of this cache | +| Field | Type | Comment | +|-----------|---------|----------------------------------------------------------| +| dir | string | The path to this cache directory | +| size | string | Spaced used by this cache's content, including size unit | +| files | integer | Number of files within the cache directory | +| freeSpace | string | Free space, including the size unit | +| name | string | The name of the cache | +| backend | string | | +| id | string | The identifier of this cache | -Appended to the above mentioned list, you will also find the following data: +Appended to the above-mentioned list, you will also find the following data: -| Field | Type | Comment | -|---------------------|-----------------------|----------------------------------------------| -| total | integer | The number of cache resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The number of cache resources | +| success | boolean | Indicates if the call was successful or not. | ## DELETE To delete a cache's content, simply call the specified resource with the `DELETE` operation, as the following example shows: @@ -83,4 +91,19 @@ To delete all caches, simply call * **(DELETE) http://my-shop-url/api/caches** -without providing a cache id. +without providing a cache id. You can also supply a list of caches to be deleted. + +{% include 'api_badge.twig' with {'route': '/api/caches', 'method': 'DELETE', 'body': true} %} +```json +[ + { + "id": "opcache" + }, + { + "id": "config" + }, + { + "id": "http" + } +] +``` diff --git a/source/developers-guide/rest-api/api-resource-categories/index.md b/source/developers-guide/rest-api/api-resource-categories/index.md index afaaa482dd..0a2dce4cd2 100755 --- a/source/developers-guide/rest-api/api-resource-categories/index.md +++ b/source/developers-guide/rest-api/api-resource-categories/index.md @@ -1,22 +1,29 @@ --- layout: default -title: REST API - Categories Resource +title: REST API - Category resource github_link: developers-guide/rest-api/api-resource-categories/index.md -indexed: false +menu_title: Category resource +menu_order: 70 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's categories resource. With this resource, it is possible to retrieve, update and delete any category data of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's categories resource. +With this resource, it is possible to retrieve, update and delete any category data of your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|------------------------|---------------------|----------------------|-----------------------|---------------------| -| /api/categories | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|-----------------|------------------------|------------------------|-------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/categories | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -35,36 +42,37 @@ Single category details can be retrieved via the category ID: | Shopware\Models\Category\Category | s_categories | -| Field | Type | Original Object | -|---------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| parentId | integer (foreign key) | **[Category](../models/#category)** | -| streamId | integer | | -| name | string | | -| position | integer | | -| metaTitle | string | | -| metaKeywords | string | | -| metaDescription | string | | -| cmsHeadline | string | | -| cmsText | string | | -| active | boolean | | -| template | string | | -| productBoxLayout | string | | -| blog | boolean | | -| path | string | | -| showFilterGroups | boolean | | -| external | boolean | | -| hideFilter | boolean | | -| hideTop | boolean | | -| changed | DateTime | | -| added | DateTime | | -| mediaId | integer (foreign key) | **[Media](../models/#media)** | -| attribute | array | | -| emotions | array | **[Media](../models/#media)** | -| media | Media | | -| customerGroups | array | | -| childrenCount | integer | | -| articleCount | integer | | +| Field | Type | Original Object | +|------------------|-----------------------|-------------------------------------------| +| id | integer (primary key) | | +| parentId | integer (foreign key) | **[Category](../models/#category)** | +| streamId | integer | | +| name | string | | +| position | integer | | +| metaTitle | string | | +| metaKeywords | string | | +| metaDescription | string | | +| cmsHeadline | string | | +| cmsText | string | | +| active | boolean | | +| template | string | | +| productBoxLayout | string | | +| blog | boolean | | +| path | string | | +| showFilterGroups | boolean | | +| external | string | | +| hideFilter | boolean | | +| hideTop | boolean | | +| changed | DateTime | | +| added | DateTime | | +| mediaId | integer (foreign key) | **[Media](../models/#media)** | +| attribute | array | | +| emotions | array | **[Media](../models/#media)** | +| media | Media | | +| customerGroups | array | | +| childrenCount | integer | | +| articleCount | integer | | +| translations | object array | **[Translation](../models/#translation)** | ## GET (List) @@ -77,67 +85,100 @@ To get a list of all categories, simply query: ### Return Value -| Model | Table | -|------------------------------------|------------------| -| Shopware\Models\Category\Category | s_categories | +| Model | Table | +|-----------------------------------|--------------| +| Shopware\Models\Category\Category | s_categories | -This API call returns an array of elements, one for each category. Each of these elements has the following structure: +This API call returns an array of elements, one for each category. +Each of these elements has the following structure: -| Field | Type | Original Object | -|---------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| active | boolean | | -| name | string | | -| position | integer | | -| parentId | integer (foreign key) | **[Category](../models/#category)** | -| childrenCount | integer | | -| articleCount | integer | | +| Field | Type | Original Object | +|---------------|-----------------------|-------------------------------------| +| id | integer (primary key) | | +| active | boolean | | +| name | string | | +| position | integer | | +| parentId | integer (foreign key) | **[Category](../models/#category)** | +| childrenCount | integer | | +| articleCount | integer | | Appended to the above mentioned list, you will also find the following data: -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of category resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of category resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) and PUT (update) `POST` and `PUT` operations support the following data structure: -| Model | Table | -|------------------------------------|------------------| -| Shopware\Models\Category\Category | s_categories | - -| Field | Type | Comment | Original Object / Database Column | -|---------------------|-----------------------|------------------------------------------------------|-------------------------------------------------------------------------------| -| name (required) | string | | | -| id | integer (primary key) | If null, a new entity will be created | `s_category.id` | -| parent | object | Required if no parentId is provided | **[Category](../models/#category)** | -| parentId | integer | | `s_category.id` | -| position | integer | | | -| metaKeywords | string | | | -| metaDescription | string | | | -| cmsHeadline | string | | | -| cmsText | string | | | -| template | string | | | -| path | string | | | -| active | boolean | | | -| blog | boolean | | | -| showFilterGroup | boolean | | | -| external | string | | | -| hideFilter | boolean | | | -| hideTop | boolean | | | -| noViewSelect | boolean | | | -| changed | date/time | | | -| added | date/time | | | -| attribute | array | Array with optional indexes from 1-6 and its values | | -| media | array | Array with either `mediaId` or `link` property | | - +| Model | Table | +|-----------------------------------|--------------| +| Shopware\Models\Category\Category | s_categories | + +| Field | Type | Comment | Original Object / Database Column | +|------------------|-----------------------|---------------------------------------------------------------------------------|-------------------------------------------| +| name (required) | string | | | +| id | integer (primary key) | If null, a new entity will be created | `s_category.id` | +| parentId | integer | The field `parent` can be used with the same value as well | `s_category.id` | +| position | integer | | | +| metaTitle | string | | | +| metaKeywords | string | | | +| metaDescription | string | | | +| cmsHeadline | string | | | +| cmsText | string | | | +| template | string | | | +| path | string | | | +| active | boolean | | | +| blog | boolean | | | +| showFilterGroup | boolean | Only for SW < 5.2 | | +| external | string | | | +| externalTarget | string | `_blank` or `_parent` | | +| hideFilter | boolean | | | +| facetIds | string | | | +| hideSortings | boolean | | | +| hideTop | boolean | | | +| noViewSelect | boolean | Only for SW < 5.2 | | +| productBoxLayout | string | `extend`, `basic`, `minimal`, `image` or `list` | | +| changed | date/time | | | +| attribute | array | Array with optional indexes from 1-6 and its values | | +| media | object | Array with either `mediaId` or `link` property | | +| translations | array | Array with either `shopId` `link` property and fields that should be translated | **[Translation](../models/#translation)** | + +### Example (POST) +* Creates a new sub category with parent category id 3 and multiple properties +```javascript +{ + "name": "My Category", + "parent": 3, + "position": 1, + "metaTitle": "My Category Meta Title", + "metaKeywords": "my,category,meta,keywords", + "metaDescription": "My Category Meta Description", + "cmsHeadline": "The Category", + "cmsText": "Discover the advantages of an api created category", + "active": true, + "blog": false, + "external": "", + "externalTarget": "", + "hideFilter": false, + "facetIds": "|2|3|", + "hideSortings": false, + "sortingIds": "|1|2|", + "hideTop": true, + "productBoxLayout": "minimal", + "changed": "2018-01-01 18:00:00", + "media": { + "link": "https://my-image-url/path/to/image.jpg" + } +} +``` ## DELETE -To delete a cache, simply call the specified resource with the `DELETE` operation as the following example shows: +To delete a category, simply call the specified resource with the `DELETE` operation as the following example shows: * **(DELETE) http://my-shop-url/api/categories/id** diff --git a/source/developers-guide/rest-api/api-resource-countries/index.md b/source/developers-guide/rest-api/api-resource-countries/index.md index cc43f8ee35..6ca83abc41 100755 --- a/source/developers-guide/rest-api/api-resource-countries/index.md +++ b/source/developers-guide/rest-api/api-resource-countries/index.md @@ -1,22 +1,29 @@ --- layout: default -title: REST API - Countries Resource +title: REST API - Country resource github_link: developers-guide/rest-api/api-resource-countries/index.md shopware_version: 5.2 -indexed: false +menu_title: Country resource +menu_order: 90 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's countries resource. With this resource, it is possible to retrieve, update and delete any country of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's countries resource. +With this resource, it is possible to retrieve, update and delete any country of your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|-----------------------|---------------------|----------------------|-----------------------|---------------------| -| /api/countries | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|----------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/countries | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply append your shop URL with @@ -28,34 +35,34 @@ If you want to access this resource, simply append your shop URL with It is required to parametrize this API call. The following parameters are available: -| Identifier | Parameter | DB column | Example call | -|-----------------|-----------|------------------------------|-------------------------------------------| -| Country Id | id | s_core_countries.id | /api/countries/2 +| Identifier | Parameter | DB column | Example call | +|------------|-----------|---------------------|------------------| +| Country Id | id | s_core_countries.id | /api/countries/2 | ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Country\Country | s_core_countries | - -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| iso | string | | -| isoName | string | | -| position | integer | | -| description | string | | -| shippingFree | boolean | | -| taxFree | boolean | | -| taxFreeUstId | boolean | | -| taxFreeUstIdChecked | boolean | | -| active | boolean | | -| iso3 | string | | -| displayStateInRegistration | boolean | | -| forceStateInRegistration | boolean | | -| areaId | integer (foreign key) | **[Area](../models/#area)** | -| states | object array | **[State](../models/#state)** | +| Model | Table | +|---------------------------------|------------------| +| Shopware\Models\Country\Country | s_core_countries | + +| Field | Type | Original Object | +|----------------------------|-----------------------|-------------------------------| +| id | integer (primary key) | | +| name | string | | +| iso | string | | +| isoName | string | | +| position | integer | | +| description | string | | +| shippingFree | boolean | | +| taxFree | boolean | | +| taxFreeUstId | boolean | | +| taxFreeUstIdChecked | boolean | | +| active | boolean | | +| iso3 | string | | +| displayStateInRegistration | boolean | | +| forceStateInRegistration | boolean | | +| areaId | integer (foreign key) | **[Area](../models/#area)** | +| states | object array | **[State](../models/#state)** | ## GET (List) @@ -66,37 +73,38 @@ To get a list of all countries, simply query: * **http://my-shop-url/api/countries/** ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Country\Country | s_core_countries | - -This API call returns an array of elements, one for each country. Each of these elements has the following structure: - -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| iso | string | | -| isoName | string | | -| position | integer | | -| description | string | | -| shippingFree | boolean | | -| taxFree | boolean | | -| taxFreeUstId | boolean | | -| taxFreeUstIdChecked | boolean | | -| active | boolean | | -| iso3 | string | | -| displayStateInRegistration | boolean | | -| forceStateInRegistration | boolean | | -| areaId | integer (foreign key) | **[Area](../models/#area)** | -| states | object array | **[State](../models/#state)** | | +| Model | Table | +|---------------------------------|------------------| +| Shopware\Models\Country\Country | s_core_countries | + +This API call returns an array of elements, one for each country. +Each of these elements has the following structure: + +| Field | Type | Original Object | +|----------------------------|-----------------------|-------------------------------| +| id | integer (primary key) | | +| name | string | | +| iso | string | | +| isoName | string | | +| position | integer | | +| description | string | | +| shippingFree | boolean | | +| taxFree | boolean | | +| taxFreeUstId | boolean | | +| taxFreeUstIdChecked | boolean | | +| active | boolean | | +| iso3 | string | | +| displayStateInRegistration | boolean | | +| forceStateInRegistration | boolean | | +| areaId | integer (foreign key) | **[Area](../models/#area)** | +| states | object array | **[State](../models/#state)** | | Appended to the above mentioned list, you will also find the following data: -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of country resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of country resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) and PUT (update) diff --git a/source/developers-guide/rest-api/api-resource-customer-group/index.md b/source/developers-guide/rest-api/api-resource-customer-group/index.md index a15b8c6914..0ac1bc8ff8 100755 --- a/source/developers-guide/rest-api/api-resource-customer-group/index.md +++ b/source/developers-guide/rest-api/api-resource-customer-group/index.md @@ -1,22 +1,29 @@ --- layout: default -title: REST API - Customer Groups Resource +title: REST API - Customer groups resource github_link: developers-guide/rest-api/api-resource-index/index.md -indexed: false +menu_title: Customer group resource +menu_order: 120 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's customer groups resource. With this resource, it is possible to retrieve, update and delete any customer group data of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's customer groups resource. +With this resource, it is possible to retrieve, update and delete any customer group data of your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|-----------------------|---------------------|----------------------|-----------------------|---------------------| -| /api/customerGroups | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|---------------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/customerGroups | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -31,22 +38,22 @@ Single customer group details can be retrieved via the customer group id: ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Customer\Group | s_core_customergroups | - -| Field | Type | Original Object | -|-----------------------|-----------------------|-----------------------------------------------------------------------------| -| id | integer (primary key) | | -| key | string | | -| name | string | | -| tax | boolean | | -| taxInput | boolean | | -| mode | boolean | | -| discount | integer | | -| minimumOrder | integer | | -| minimumOrderSurcharge | integer | | -| discounts | array | **[Surcharge](../models/#group-surcharge)** | +| Model | Table | +|--------------------------------|-----------------------| +| Shopware\Models\Customer\Group | s_core_customergroups | + +| Field | Type | Original Object | +|-----------------------|-----------------------|---------------------------------------------| +| id | integer (primary key) | | +| key | string | | +| name | string | | +| tax | boolean | | +| taxInput | boolean | | +| mode | boolean | | +| discount | integer | | +| minimumOrder | integer | | +| minimumOrderSurcharge | integer | | +| discounts | array | **[Surcharge](../models/#group-surcharge)** | ## GET (List) @@ -58,51 +65,51 @@ To get a list of all customer groups, simply query: * **http://my-shop-url/api/customerGroups/** ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Customer\Group | s_core_customergroups | +| Model | Table | +|--------------------------------|-----------------------| +| Shopware\Models\Customer\Group | s_core_customergroups | This API call returns an array of elements, one for each customer group. Each of these elements has the following structure: -| Field | Type | Original Object | -|-----------------------|-----------------------|-----------------------------------------------------------------------------| -| id | integer (primary key) | | -| key | string | | -| name | string | | -| tax | boolean | | -| taxInput | boolean | | -| mode | boolean | | -| discount | integer | | -| minimumOrder | integer | | -| minimumOrderSurcharge | integer | | -| discounts | array | **[Surcharge](../models/#group-surcharge)** | - -Appended to the above mentioned list, you will also find the following data: - -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of customer group resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Original Object | +|-----------------------|-----------------------|---------------------------------------------| +| id | integer (primary key) | | +| key | string | | +| name | string | | +| tax | boolean | | +| taxInput | boolean | | +| mode | boolean | | +| discount | integer | | +| minimumOrder | integer | | +| minimumOrderSurcharge | integer | | +| discounts | array | **[Surcharge](../models/#group-surcharge)** | + +Appended to the above-mentioned list, you will also find the following data: + +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of customer group resources | +| success | boolean | Indicates if the call was successful or not. | ## POST and PUT `POST` and `PUT` operations support the following data structure: -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Customer\Group | s_core_customergroups | - -| Field | Type | Original Object | -|-----------------------|-----------------------|-----------------------------------------------------------------------------| -| id | integer (primary key) | | -| key | string | | -| name | string | | -| tax | boolean | | -| taxInput | boolean | | -| mode | boolean | | -| discount | integer | | -| minimumOrder | integer | | -| minimumOrderSurcharge | integer | | -| discounts | array | **[Surcharge](../models/#group-surcharge)** | +| Model | Table | +|--------------------------------|-----------------------| +| Shopware\Models\Customer\Group | s_core_customergroups | + +| Field | Type | Original Object | +|-----------------------|-----------------------|---------------------------------------------| +| id | integer (primary key) | | +| key | string | | +| name | string | | +| tax | boolean | | +| taxInput | boolean | | +| mode | boolean | | +| discount | integer | | +| minimumOrder | integer | | +| minimumOrderSurcharge | integer | | +| discounts | array | **[Surcharge](../models/#group-surcharge)** | ## DELETE To delete a customer group, simply call the specified resource with the `DELETE` operation, as the following example shows: diff --git a/source/developers-guide/rest-api/api-resource-customer/index.md b/source/developers-guide/rest-api/api-resource-customer/index.md index 40ee302ba5..665e20a160 100755 --- a/source/developers-guide/rest-api/api-resource-customer/index.md +++ b/source/developers-guide/rest-api/api-resource-customer/index.md @@ -1,21 +1,28 @@ --- layout: default -title: REST API - Customer Resource +title: REST API - Customer resource github_link: developers-guide/rest-api/api-resource-customer/index.md -indexed: false +menu_title: Customer resource +menu_order: 100 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's customer resource. With this resource, it is possible to retrieve, update and delete any customer of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's customer resource. +With this resource, it is possible to retrieve, update and delete any customer of your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|-----------------------|---------------------|----------------------|-----------------------|---------------------| -| /api/customers | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|----------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/customers | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply append your shop URL with @@ -25,51 +32,57 @@ If you want to access this resource, simply append your shop URL with #### Required Parameters -It is required to parametrize this API call. The following parameters are available: +It is required to parametrize this API call. +The following parameters are available: -| Identifier | Parameter | DB column | Example call | -|-----------------|-----------|------------------------------|-------------------------------------------| -| Customer Id | id | s_user.id | /api/customers/2 | -| Customer number | number | s_user_billingaddress.number | /api/customers/20003?useNumberAsId=true | +| Identifier | Parameter | DB column | Example call | +|-----------------|-----------|------------------------------|-----------------------------------------| +| Customer Id | id | s_user.id | /api/customers/2 | +| Customer number | number | s_user_billingaddress.number | /api/customers/20003?useNumberAsId=true | -* **useNumberAsId=true** - This tells the API to query the customer's data by its number, instead of its actual identifier. Otherwise, the syntax is just **/api/customers/id**. It's not possible to provide both parameters at the same time. +* **useNumberAsId=true** - This tells the API to query the customer's data by its number, instead of its actual identifier. +Otherwise, the syntax is just **/api/customers/id**. +It's not possible to provide both parameters at the same time. ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Customer\Customer | s_user | - -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| paymentId | integer (foreign key) | **[Payment](../models/#payment-data)** | -| groupKey | string (foreign key) | **[CustomerGroup](../models/#customer-group)** | -| shopId | string (foreign key) | **[Shop](../models/#shop)** | -| priceGroupId | integer (foreign key) | **[PriceGroup](../models/#price-group)** | -| encoderName | string | | -| hashPassword | string | | -| active | boolean | | -| email | string | | -| firstLogin | date/time | | -| lastLogin | date/time | | -| accountMode | integer | | -| confirmationKey | string | | -| sessionId | string | | -| newsletter | boolean | | -| validation | string | | -| affiliate | boolean | | -| paymentPreset | integer | | -| languageId | integer (foreign key) | | -| referer | string | | -| internalComment | string | | -| failedLogins | integer | | -| lockedUntil | date/time | | -| attribute | object | **[CustomerAttribute](../models/#customer-attribute)** | -| billing | object | **[Billing](../models/#billing)** | -| paymentData | array | **[PaymentData](../models/#payment-data)** | -| shipping | object | **[Shipping](../models/#shipping)** | -| debit | object | **[Debit](../models/#debit)** | +| Model | Table | +|-----------------------------------|--------| +| Shopware\Models\Customer\Customer | s_user | + +| Field | Type | Original Object | +|-------------------------------|-----------------------|--------------------------------------------------------| +| id | integer (primary key) | | +| paymentId | integer (foreign key) | **[Payment](../models/#payment-data)** | +| groupKey | string (foreign key) | **[CustomerGroup](../models/#customer-group)** | +| shopId | string (foreign key) | **[Shop](../models/#shop)** | +| priceGroupId | integer (foreign key) | **[PriceGroup](../models/#price-group)** | +| encoderName | string | | +| hashPassword | string | | +| active | boolean | | +| email | string | | +| doubleOptInConfirmDate | date/time | | +| doubleOptInEmailSentDate | date/time | | +| doubleOptInRegister | boolean | | +| firstLogin (date of creation) | date/time | | +| lastLogin | date/time | | +| accountMode | integer | | +| confirmationKey | string | | +| sessionId | string | | +| newsletter | boolean | | +| validation | string | | +| affiliate | boolean | | +| paymentPreset | integer | | +| languageId | integer (foreign key) | | +| referer | string | | +| internalComment | string | | +| failedLogins | integer | | +| lockedUntil | date/time | | +| attribute | object | **[CustomerAttribute](../models/#customer-attribute)** | +| billing | object | **[Billing](../models/#address)** | +| paymentData | array | **[PaymentData](../models/#payment-data)** | +| shipping | object | **[Shipping](../models/#address)** | +| debit | object | **[Debit](../models/#debit)** | ## GET (List) @@ -80,48 +93,55 @@ To get a list of all customers, simply query: * **http://my-shop-url/api/customers/** ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Customer\Customer | s_user | +| Model | Table | +|-----------------------------------|--------| +| Shopware\Models\Customer\Customer | s_user | This API call returns an array of elements, one for each customer. Each of these elements has the following structure: -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| paymentId | integer (foreign key) | **[Payment](../models/#payment-instance)** | -| groupKey | string (foreign key) | **[CustomerGroup](../models/#customer-group)** | -| shopId | string (foreign key) | **[Shop](../models/#shop)** | -| priceGroupId | integer (foreign key) | **[PriceGroup](../models/#price-group)** | -| encoderName | string | | -| hashPassword | string | | -| active | boolean | | -| email | string | | -| firstLogin | date/time | | -| lastLogin | date/time | | -| accountMode | integer | | -| confirmationKey | string | | -| sessionId | string | | -| newsletter | boolean | | -| validation | string | | -| affiliate | boolean | | -| paymentPreset | integer | | -| languageId | integer (foreign key) | | -| referer | string | | -| internalComment | string | | -| failedLogins | integer | | -| lockedUntil | date/time | | - -Appended to the above mentioned list, you will also find the following data: - -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of category resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Original Object | +|-------------------------------|-----------------------|------------------------------------------------| +| id | integer (primary key) | | +| paymentId | integer (foreign key) | **[Payment](../models/#payment-instance)** | +| groupKey | string (foreign key) | **[CustomerGroup](../models/#customer-group)** | +| shopId | string (foreign key) | **[Shop](../models/#shop)** | +| priceGroupId | integer (foreign key) | **[PriceGroup](../models/#price-group)** | +| encoderName | string | | +| hashPassword | string | | +| active | boolean | | +| email | string | | +| doubleOptInConfirmDate | date/time | | +| doubleOptInEmailSentDate | date/time | | +| doubleOptInRegister | boolean | | +| firstLogin (date of creation) | date/time | | +| lastLogin | date/time | | +| accountMode | integer | | +| confirmationKey | string | | +| sessionId | string | | +| newsletter | boolean | | +| validation | string | | +| affiliate | boolean | | +| paymentPreset | integer | | +| languageId | integer (foreign key) | | +| referer | string | | +| internalComment | string | | +| failedLogins | integer | | +| lockedUntil | date/time | | + +Appended to the above-mentioned list, you will also find the following data: + +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of category resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) and PUT (update) -To `POST` or `PUT` content, use the same data as provided in the GET operation. +To `POST` or `PUT` content, use the same data as provided in the GET operation. Only exceptions are for `POST` as following: + +| Field | Type | Comment | +|---------------|---------|---------------------------------------------------------------------| +| sendOptinMail | boolean | Used to decide if an necessary opt-in confirmation mail will be sent | ## DELETE diff --git a/source/developers-guide/rest-api/api-resource-generate-article-images/index.md b/source/developers-guide/rest-api/api-resource-generate-article-images/index.md index 21f46e03a9..c557a33af0 100755 --- a/source/developers-guide/rest-api/api-resource-generate-article-images/index.md +++ b/source/developers-guide/rest-api/api-resource-generate-article-images/index.md @@ -1,21 +1,26 @@ --- layout: default -title: REST API - Generate Article Images Resource +title: REST API - Generate product images resource github_link: developers-guide/rest-api/api-resource-generate-article-images/index.md -indexed: false +menu_title: Generate product images resource +menu_order: 130 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -This resource allows you to regenerate article thumbnails. +This resource allows you to regenerate product thumbnails. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|---------------------|---------------------|-----------------------|---------------------|---------------------|---------------------|---------------------| -| /api/generateArticleImages | ![No](../img/no.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|----------------------------|----------------------|----------------------|------------------------|----------------------|----------------------|----------------------|----------------------| +| /api/generateArticleImages | ![No](../img/no.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -25,9 +30,9 @@ If you want to access this resource, simply query the following URL: This operation allows you to regenerate the thumbnails of a specific article. -| Identifier | Parameter | Column | Example call | -|----------------|-------------|-----------------------------------|------------------------------------------------------| -| Article Id | id | s_articles.id | /api/generateArticleImages/2 | -| Article Number | number | s_articles_details.ordernumber | /api/generateArticleImages/20003?useNumberAsId=true | +| Identifier | Parameter | Column | Example call | +|----------------|--------------|--------------------------------|-----------------------------------------------------| +| Article Id | id | s_articles.id | /api/generateArticleImages/2 | +| Article Number | number | s_articles_details.ordernumber | /api/generateArticleImages/20003?useNumberAsId=true | You can identify the product by either the ID or the detail number. diff --git a/source/developers-guide/rest-api/api-resource-manufacturers/index.md b/source/developers-guide/rest-api/api-resource-manufacturers/index.md index 7831758610..ed8c13f0ba 100755 --- a/source/developers-guide/rest-api/api-resource-manufacturers/index.md +++ b/source/developers-guide/rest-api/api-resource-manufacturers/index.md @@ -1,23 +1,30 @@ --- layout: default -title: REST API - Manufacturers Resource +title: REST API - Manufacturer resource github_link: developers-guide/rest-api/api-resource-manufacturers/index.md shopware_version: 5.1.3 -indexed: false +menu_title: Manufacturer resource +menu_order: 140 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's manufacturers resource. With this resource, it is possible to retrieve, update and delete any manufacturer data of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's manufacturers resource. +With this resource, it is possible to retrieve, update and delete any manufacturer data of your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|------------------------|---------------------|----------------------|-----------------------|---------------------| -| /api/manufacturers | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|--------------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/manufacturers | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -31,22 +38,22 @@ Single manufacturer details can be retrieved via the manufacturer ID: * **http://my-shop-url/api/manufacturers/id** ### Return Value -| Model | Table | -|------------------------------------|------------------| -| Shopware\Models\Article\Supplier | s_articles_supplier | - - -| Field | Type | Original Object | -|---------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| image | string (foreign key, path) | **[Media](../models/#media)** | -| link | string | | -| description | string | | -| metaTitle | string | | -| metaKeywords | string | | -| metaDescription | string | | -| attribute | array | | +| Model | Table | +|----------------------------------|---------------------| +| Shopware\Models\Article\Supplier | s_articles_supplier | + + +| Field | Type | Original Object | +|-----------------|----------------------------|-------------------------------| +| id | integer (primary key) | | +| name | string | | +| image | string (foreign key, path) | **[Media](../models/#media)** | +| link | string | | +| description | string | | +| metaTitle | string | | +| metaKeywords | string | | +| metaDescription | string | | +| attribute | array | | ## GET (List) @@ -59,52 +66,79 @@ To get a list of all manufacturers, simply query: ### Return Value -| Model | Table | -|------------------------------------|------------------| -| Shopware\Models\Article\Supplier | s_articles_supplier | +| Model | Table | +|----------------------------------|---------------------| +| Shopware\Models\Article\Supplier | s_articles_supplier | -This API call returns an array of elements, one for each manufacturer. Each of these elements has the following structure: +This API call returns an array of elements, one for each manufacturer. +Each of these elements has the following structure: -| Field | Type | Original Object | -|---------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| image | string | | -| link | string | | -| description | string | | -| metaTitle | string | | -| metaKeywords | string | | -| metaDescription | string | | +| Field | Type | Original Object | +|-----------------|-----------------------|-----------------| +| id | integer (primary key) | | +| name | string | | +| image | string | | +| link | string | | +| description | string | | +| metaTitle | string | | +| metaKeywords | string | | +| metaDescription | string | | -Appended to the above mentioned list, you will also find the following data: +Appended to the above-mentioned list, you will also find the following data: -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of manufacturer resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of manufacturer resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) and PUT (update) `POST` and `PUT` operations support the following data structure: -| Model | Table | -|------------------------------------|------------------| -| Shopware\Models\Article\Supplier | s_articles_supplier | - -| Field | Type | Comment | Original Object / Database Column | -|---------------------|-----------------------|------------------------------------------------------|-------------------------------------------------------------------------------| -| name (required) | string | | | -| id | integer (primary key) | If null, a new entity will be created | `s_articles_supplier.id` | -| image | array | Array with either `mediaId` or `link` property | | -| link | string | | | -| description | string | | | -| metaTitle | string | | | -| metaKeywords | string | | | -| metaDescription | string | | | -| changed | date/time | | | -| attribute | array | Array with optional indexes from 1-6 and its values | | +| Model | Table | +|----------------------------------|---------------------| +| Shopware\Models\Article\Supplier | s_articles_supplier | + +| Field | Type | Comment | Original Object / Database Column | +|-----------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------| +| name (required) | string | | | +| id | integer (primary key) | If null, a new entity will be created | `s_articles_supplier.id` | +| image | array | Array with either `mediaId` or `link` property; can be null or an empty object to delete an image assignment from an existing manufacturer | | +| link | string | | | +| description | string | | | +| metaTitle | string | | | +| metaKeywords | string | | | +| metaDescription | string | | | +| changed | date/time | | | +| attribute | array | Array with optional indexes from 1-6 and its values | | + +### Examples +#### POST +* Adds a new manufacturer with an image +* **http://my-shop-url/api/manufacturers** + +```javascript +{ + "name": "My manufacturer with image", + "image": + { + "link": "http://my-image-url/path/to/image.png" + } +} +``` + +#### PUT +* Changes the manufacturers name and deletes the image assignment +* **http://my-shop-url/api/manufacturers/1** + +```javascript +{ + "name": "My manufacturer without image", + "image": null +} +``` ## DELETE diff --git a/source/developers-guide/rest-api/api-resource-media/index.md b/source/developers-guide/rest-api/api-resource-media/index.md index 348c33a8d8..44dc292e57 100755 --- a/source/developers-guide/rest-api/api-resource-media/index.md +++ b/source/developers-guide/rest-api/api-resource-media/index.md @@ -1,113 +1,145 @@ --- layout: default -title: REST API - Media Resource +title: REST API - Media resource github_link: developers-guide/rest-api/api-resource-media/index.md -indexed: false +menu_title: Media resource +menu_order: 150 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's media resource. With this resource, it is possible to retrieve, create and delete any media of your shop. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's media resource. +With this resource, it is possible to retrieve, create and delete any media of your shop. +We will also have a look at the associated data structures. ## General information -This resources handles everything related to the media that is stored in your shop. This includes article images, blog images and downloadable files. +This resource handles everything related to the media that is stored in your shop. +This includes article images, blog images and downloadable files. This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|----------------------|---------------------|----------------------|-----------------------|---------------------| -| /api/media | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/media | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ## GET To get information about a specific media, you can simply call the API as shown in this example: -* **http://my-shop-url/api/media/id** +``` +GET http://my-shop-url/api/media/{id} +``` ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Media\Media | s_media | - -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| albumId | integer (foreign key) | | -| name | string | | -| description | string | | -| path | string | | -| type | string | | -| extension | string | | -| userId | integer (foreign key) | | -| created | date | | -| fileSize | integer | | +| Model | Table | +|-----------------------------|---------| +| Shopware\Models\Media\Media | s_media | + +| Field | Type | Original Object | +|-------------|-----------------------|-----------------| +| id | integer (primary key) | | +| albumId | integer (foreign key) | | +| name | string | | +| description | string | | +| path | string | | +| type | string | | +| extension | string | | +| userId | integer (foreign key) | | +| created | date | | +| fileSize | integer | | ## GET (List) -To get list list of media, simply call +To get a list of media, simply call the url without providing any id: -* **http://my-shop-url/api/media/** - -without providing any id. +``` +GET http://my-shop-url/api/media +``` ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Media\Media | s_media | -and -This API call returns an array of elements, one for each media. Each of these elements has the following structure: - - -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| albumId | integer (foreign key) | | -| name | string | | -| description | string | | -| path | string | | -| type | string | | -| extension | string | | -| userId | integer (foreign key) | | -| created | date | | -| fileSize | integer | | - -Appended to the above mentioned list, you will also find the following data: - -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of media resources | -| success | boolean | Indicates if the call was successful or not. | +| Model | Table | +|-----------------------------|---------| +| Shopware\Models\Media\Media | s_media | + +This API call returns an array of elements, one for each media. +Each of these elements has the following structure: + + +| Field | Type | Original Object | +|-------------|-----------------------|-----------------| +| id | integer (primary key) | | +| albumId | integer (foreign key) | | +| name | string | | +| description | string | | +| path | string | | +| type | string | | +| extension | string | | +| userId | integer (foreign key) | | +| created | date | | +| fileSize | integer | | + +Appended to the above-mentioned list, you will also find the following data: + +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of media resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) + If you wish to add new data to the shop's media collection, simply create an array and send it via `POST` request to the API. -The following keys can be provided in the array: - -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Media\Media | s_media | - -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| album (required) | integer (foreign key) | | -| name | string | Auto generated if not provided | -| file (required) | string | Path to the file that should be uploaded | -| description (required)| string | | -| path | string | Auto generated if not provided | -| type | string | Auto generated if not provided | -| extension | string | Auto generated if not provided | -| userId | integer (foreign key) | | -| created | date | Auto generated if not provided | -| fileSize | integer | Auto generated if not provided | - -**The most of these values are generated automatically (such as `fileSize` and `created`). It is not recommended to set them manually** + +``` +POST http://my-shop-url/api/media +``` + +| Model | Table | +|-----------------------------|---------| +| Shopware\Models\Media\Media | s_media | + +| Field | Type | Original Object | +|------------------------|-----------------------|------------------------------------------| +| album (required) | integer (foreign key) | | +| name | string | Auto generated if not provided | +| file (required) | string | Path to the file that should be uploaded | +| description (required) | string | | +| path | string | Auto generated if not provided | +| type | string | Auto generated if not provided | +| extension | string | Auto generated if not provided | +| userId | integer (foreign key) | | +| created | date | Auto generated if not provided | +| fileSize | integer | Auto generated if not provided | + +**The most of these values are generated automatically (such as `fileSize` and `created`). +It is not recommended setting them manually** + +## PUT (update) + +As of Shopware 5.3, you can replace a media file by providing a dataURI or link to fetch a resource from. + +``` +PUT http://my-shop-url/api/media/{id} +``` + +Replace the `id` with the specific media id. + +| Field | Type | Description | +|-----------------|--------|----------------------| +| file (required) | string | Path / URL / dataURI | ## DELETE In order to delete a specific media, simply call the following URL using the `DELETE` operation: -* **(DELETE) http://my-shop-url/api/media/id** +``` +DELETE http://my-shop-url/api/media/{id} +``` Replace the `id` with the specific media id. diff --git a/source/developers-guide/rest-api/api-resource-orders/index.md b/source/developers-guide/rest-api/api-resource-orders/index.md index f58832b061..b0fdf5d651 100755 --- a/source/developers-guide/rest-api/api-resource-orders/index.md +++ b/source/developers-guide/rest-api/api-resource-orders/index.md @@ -1,22 +1,29 @@ --- layout: default -title: REST API - Orders Resource +title: REST API - Order resource github_link: developers-guide/rest-api/api-resource-orders/index.md -indexed: false +menu_title: Order resource +menu_order: 170 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation, you can learn more about the API's orders resource. With this resource, it is possible to retrieve and update any order in your shop. We will also have a look at the associated data structures. +In this part of the documentation, you can learn more about the API's orders resource. +With this resource, it is possible to retrieve and update any order in your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|------------------------|------------------------|------------------------|----------------------|------------------------|----------------------|----------------------| -| /api/orders | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|-------------|------------------------|------------------------|------------------------|----------------------|------------------------|----------------------|----------------------| +| /api/orders | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -28,102 +35,155 @@ If you want to access this resource, simply query the following URL: This API call requires one of the following parameters to be defined: -| Identifier | Parameter | DB column | Example call | -|-----------------|-----------|------------------------------|----------------------------------------| -| Order Id | id | s_order.id | /api/orders/2 | -| Order number | number | s_order.number | /api/orders/20003?useNumberAsId=true | +| Identifier | Parameter | DB column | Example call | +|--------------|-----------|----------------|--------------------------------------| +| Order Id | id | s_order.id | /api/orders/2 | +| Order number | number | s_order.number | /api/orders/20003?useNumberAsId=true | -* **useNumberAsId=true** - This tells the API to query the order's data by its number, instead of its actual identifier. Otherwise, the syntax is just **/api/orders/id**. It's not possible to provide both parameter at the same time. +* **useNumberAsId=true** - This tells the API to query the order's data by its number, instead of its actual identifier. +Otherwise, the syntax is just **/api/orders/id**. It's not possible to provide both parameter at the same time. ### Return Value -| Model | Table | -|------------------------------------|-----------------------| -| Shopware\Models\Customer\Customer | s_user | - -| Field | Type | Original Object | -|-----------------------|-----------------------|-------------------------------------------------------------------------------| -| id | integer (primary key) | | -| number | string | | -| customerId | integer (foreign key) | **[Customer](../api-resource-customer)** | -| paymentId | integer (foreign key) | **[PaymentData](../models/#payment-data)** | -| dispatchId | integer (foreign key) | **[Dispatch](../models/#dispatch)** | -| partnerId | integer (foreign key) | | -| shopId | integer (foreign key) | **[Shop](../models/#shop)** | -| invoiceAmount | double | | -| invoiceAmountNet | double | | -| invoiceShipping | double | | -| invoiceShippingNet | double | | -| orderTime | date/time | | -| transactionId | string | | -| comment | string | | -| customerComment | string | | -| internalComment | string | | -| net | boolean | | -| taxFree | boolean | | -| temporaryId | string | | -| referer | string | | -| clearedDate | date/time | | -| trackingCode | string | | -| languageIso | integer (foreign key) | **[Locale](../models/#locale)** | -| currency | string | | -| currencyFactor | double | | -| remoteAddress | string | | -| deviceType | string | | -| details | array | **[Detail](../models/#order-detail)** | -| documents | array | **[Document](../models/#document)** | -| payment | object | **[Payment](../models/#payment-instance)** | -| paymentStatus | object | **[PaymentStatus](../models/#payment-status)** | -| orderStatus | object | **[OrderStatus](../models/#order-status)** | -| customer | object | **[Customer](../models/#customer)** | -| paymentInstances | array | **[PaymentInstance](../models/#payment-instance)** | -| billing | object | **[Billing](../models/#billing)** | -| shipping | object | **[Shipping](../models/#shipping)** | -| shop | object | **[Shop](../models/#shop)** | -| dispatch | object | **[Dispatch](../models/#dispatch)** | -| attribute | object | **[Attribute](../models/#order-attribute)** | -| languageSubShop | object | **[Shop](../models/#shop)** | -| paymentStatusId | integer (foreign key) | **[Status](../models/#payment-status)** | -| orderStatusId | integer (foreign key) | **[OrderStatus](../models/#order-status)** | +| Model | Table | +|-----------------------------|---------| +| Shopware\Models\Order\Order | s_order | + +| Field | Type | Original Object | +|-----------------------|-----------------------|----------------------------------------------------| +| id | integer (primary key) | | +| number | string | | +| customerId | integer (foreign key) | **[Customer](../api-resource-customer)** | +| paymentId | integer (foreign key) | **[PaymentData](../models/#payment-data)** | +| dispatchId | integer (foreign key) | **[Dispatch](../models/#dispatch)** | +| partnerId | integer (foreign key) | | +| shopId | integer (foreign key) | **[Shop](../models/#shop)** | +| invoiceAmount | double | | +| invoiceAmountNet | double | | +| invoiceShipping | double | | +| invoiceShippingNet | double | | +| invoiceShippingTaxRate| double | | +| orderTime | date/time | | +| transactionId | string | | +| comment | string | | +| customerComment | string | | +| internalComment | string | | +| net | boolean | | +| taxFree | boolean | | +| temporaryId | string | | +| referer | string | | +| clearedDate | date/time | | +| trackingCode | string | | +| languageIso | integer (foreign key) | **[Locale](../models/#locale)** | +| currency | string | | +| currencyFactor | double | | +| remoteAddress | string | | +| deviceType | string | | +| details | array | **[Detail](../models/#order-detail)** | +| documents | array | **[Document](../models/#document)** | +| payment | object | **[Payment](../models/#payment)** | +| paymentStatus | object | **[PaymentStatus](../models/#payment-status)** | +| orderStatus | object | **[OrderStatus](../models/#order-status)** | +| customer | object | **[Customer](../models/#customer)** | +| paymentInstances | array | **[PaymentInstance](../models/#payment-instance)** | +| billing | object | **[Billing](../models/#billing)** | +| shipping | object | **[Shipping](../models/#shipping)** | +| shop | object | **[Shop](../models/#shop)** | +| dispatch | object | **[Dispatch](../models/#dispatch)** | +| attribute | object | **[Attribute](../models/#order-attribute)** | +| languageSubShop | object | **[Shop](../models/#shop)** | +| paymentStatusId | integer (foreign key) | **[Status](../models/#payment-status)** | +| orderStatusId | integer (foreign key) | **[OrderStatus](../models/#order-status)** | + +## GET (List) + +#### Required Parameters + +For this operation, no parameters are required. To get a list of all orders, simply query: +* **http://my-shop-url/api/orders** + +### Return Value + +| Model | Table | +|-----------------------------|---------| +| Shopware\Models\Order\Order | s_order | + +| Field | Type | Original Object | +|-----------------------|-----------------------|--------------------------------------------| +| id | integer (primary key) | | +| number | string | | +| customerId | integer (foreign key) | **[Customer](../api-resource-customer)** | +| paymentId | integer (foreign key) | **[PaymentData](../models/#payment-data)** | +| dispatchId | integer (foreign key) | **[Dispatch](../models/#dispatch)** | +| partnerId | integer (foreign key) | | +| shopId | integer (foreign key) | **[Shop](../models/#shop)** | +| invoiceAmount | double | | +| invoiceAmountNet | double | | +| invoiceShipping | double | | +| invoiceShippingNet | double | | +| orderTime | date/time | | +| transactionId | string | | +| comment | string | | +| customerComment | string | | +| internalComment | string | | +| net | boolean | | +| taxFree | boolean | | +| temporaryId | string | | +| referer | string | | +| clearedDate | date/time | | +| trackingCode | string | | +| languageIso | integer (foreign key) | **[Locale](../models/#locale)** | +| currency | string | | +| currencyFactor | double | | +| remoteAddress | string | | +| deviceType | string | | +| attribute | object | | +| customer | object | **[Customer](../models/#costumer)** | +| paymentStatusId | integer (foreign key) | **[Status](../models/#payment-status)** | +| orderStatusId | integer (foreign key) | **[OrderStatus](../models/#order-status)** | ## PUT (update) Orders can be identified using the following: -| Identifier | Parameter | DB column | Example call | -|---------------|-----------|------------------------|------------------------------------------| -| Article Id | id | s_articles.id | /api/articles/2 | -| Detail Number | number | s_articles.ordernumber | /api/articles/SW10003?useNumberAsId=true | +| Identifier | Parameter | DB column | Example call | +|--------------|-----------|----------------|--------------------------------------| +| Order Id | id | s_order.id | /api/orders/2 | +| Order Number | number | s_order.number | /api/orders/20003?useNumberAsId=true | The data structure used is similar to the one returned in the `GET` request. ## POST (create) -
    Please be aware: When an order is created using the API, no calculations for tax, shipping cost, etc. are done. Also no checks regarding validity of the provided values will be executed.
    +
    Please be aware: +When an order is created using the API, no calculations for tax, shipping cost, etc. are done. +Also no checks regarding validity of the provided values will be executed.
    + +Creation of orders via REST-API is available since Shopware 5.2.21. Orders can be created using the following fields: -| Field | Type | Original Object / Database table | -|-------------------------------|-----------------------|----------------------------------------------------| -| customerId (required) | integer (foreign key) | **[Customer](../api-resource-customer)** -| paymentId (required) | integer (foreign key) | **[PaymentData](../models/#payment-data)** -| dispatchId (required) | integer (foreign key) | **[Dispatch](../models/#dispatch)** -| paymentStatusId (required) | integer (foreign key) | **[Status](../models/#payment-status)** -| orderStatusId (required) | integer (foreign key) | **[OrderStatus](../models/#order-status)** -| shopId (required) | integer (foreign key) | **[Shop](../models/#shop)** -| invoiceAmount (required) | double | -| invoiceAmountNet (required) | double | -| invoiceShipping (required) | double | -| invoiceShippingNet (required) | double | -| partnerId | string | -| orderTime | date/time | -| net (required) | integer | -| taxFree (required) | integer | -| languageIso | integer | -| currency | string | -| currencyFactor(required) | double | -| remoteAddress (required) | string | -| details (required) | array | **[Detail](../models/#order-detail)** -| documents | array | -| billing (required) | object | **[Billing](../models/#billing)** -| shipping (required) | object | **[Shipping](../models/#shipping)** +| Field | Type | Original Object / Database table | +|-------------------------------|-----------------------|--------------------------------------------| +| customerId (required) | integer (foreign key) | **[Customer](../api-resource-customer)** | +| paymentId (required) | integer (foreign key) | **[PaymentData](../models/#payment-data)** | +| dispatchId (required) | integer (foreign key) | **[Dispatch](../models/#dispatch)** | +| paymentStatusId (required) | integer (foreign key) | **[Status](../models/#payment-status)** | +| orderStatusId (required) | integer (foreign key) | **[OrderStatus](../models/#order-status)** | +| shopId (required) | integer (foreign key) | **[Shop](../models/#shop)** | +| invoiceAmount (required) | double | | +| invoiceAmountNet (required) | double | | +| invoiceShipping (required) | double | | +| invoiceShippingNet (required) | double | | +| partnerId | string | | +| orderTime | date/time | | +| net (required) | integer | | +| taxFree (required) | integer | | +| languageIso | integer | | +| currency | string | | +| currencyFactor(required) | double | | +| remoteAddress (required) | string | | +| details (required) | array | **[Detail](../models/#order-detail)** | +| documents | array | | +| billing (required) | object | **[Billing](../models/#billing)** | +| shipping (required) | object | **[Shipping](../models/#shipping)** | diff --git a/source/developers-guide/rest-api/api-resource-payment-methods/index.md b/source/developers-guide/rest-api/api-resource-payment-methods/index.md new file mode 100755 index 0000000000..63fc28b67a --- /dev/null +++ b/source/developers-guide/rest-api/api-resource-payment-methods/index.md @@ -0,0 +1,117 @@ +--- +layout: default +title: REST API - Payment method resource +github_link: developers-guide/rest-api/api-resource-payment-methods/index.md +shopware_version: 5.5.3 +menu_title: Payment method resource +menu_order: 190 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API +--- + +## Introduction + +In this part of the documentation, you can learn more about the API's payment methods resource. +With this resource, it's possible to retrieve and update any payment method in your shop. +We will also have a look at the associated data structures. + +## General Information + +This resource supports the following operations: + +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|---------------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/paymentMethods | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | + +If you want to access this resource, simply query the following URL: + +* **http://my-shop-url/api/paymentMethods** + +## GET + +You can retrieve a payment method by using its id + +* **http://my-shop-url/api/paymentMethods/id** + +### Return Value + +| Field | Type | Original Object | +|-----------------------|-----------------------|-----------------------------------| +| id | integer (primary key) | | +| name | string | | +| description | string | | +| template | string | | +| class | string | | +| hide | boolean | | +| additionalDescription | string | | +| debitPercent | float | | +| surcharge | float | | +| surchargeString | string | | +| position | integer | | +| active | boolean | | +| esdActive | boolean | | +| mobileInactive | boolean | | +| embedIFrame | string | | +| hideProspect | integer | | +| action | string | | +| pluginId | integer | | +| source | string | | +| countries | array | **[Country](../models/#country)** | +| shops | array | | +| attribute | array | | + + +## GET (List) + +To get more than one payment method at once, simply remove the id parameter from the request URL. + +* **http://my-shop-url/api/paymentMethods/** + +### Return value + +*Since this returns a list, the following fields will be added to the array:* + +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of cache resources | +| success | boolean | Indicates if the call was successful or not. | + +## POST (create) and PUT (update) + +You can post or put data by querying the following URL: + +* **(POST or PUT) http://my-shop-url/api/paymentMethods/id** + +| Field | Type | Original Object | +|-----------------------|-----------------------|-----------------------------------| +| id | integer (primary key) | | +| name | string | | +| description | string | | +| template | string | | +| class | string | | +| hide | boolean | | +| additionalDescription | string | | +| debitPercent | float | | +| surcharge | float | | +| surchargeString | string | | +| position | integer | | +| active | boolean | | +| esdActive | boolean | | +| mobileInactive | boolean | | +| embedIFrame | string | | +| hideProspect | integer | | +| action | string | | +| pluginId | integer | | +| source | string | | +| countries | array | **[Country](../models/#country)** | +| shops | array | | +| attribute | array | | + +## DELETE +To delete a payment method, simply query this URL with a `DELETE` request: + +* **http://my-shop-url/api/paymentMethods/id** + +Replace the `id` with the specific payment method id. diff --git a/source/developers-guide/rest-api/api-resource-property-group/index.md b/source/developers-guide/rest-api/api-resource-property-group/index.md index 26a31ffaa5..c04a0a0232 100755 --- a/source/developers-guide/rest-api/api-resource-property-group/index.md +++ b/source/developers-guide/rest-api/api-resource-property-group/index.md @@ -1,21 +1,28 @@ --- layout: default -title: REST API - Property Groups Resource +title: REST API - Property group Resource github_link: developers-guide/rest-api/api-resource-property-group/index.md -indexed: false +menu_title: Property group resource +menu_order: 210 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation, you can learn more about the API's property groups resource. With this resource, it's possible to retrieve and update any property group in your shop. We will also have a look at the associated data structures. +In this part of the documentation, you can learn more about the API's property groups resource. +With this resource, it's possible to retrieve and update any property group in your shop. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|--------------------------|-----------------------|-----------------------|---------------------|------------------------|-----------------------|---------------------| -| /api/propertyGroups | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|---------------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/propertyGroups | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -29,15 +36,17 @@ You can retrieve a property group by using its id ### Return Value -| Field | Type | Original Object | -|---------------------|-----------------------|---------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| position | integer | | -| comparable | boolean | | -| sortMode | integer | | -| options | array | **[Option](../models/#property-group-option)** | -| attribute | array | **[Attribute](../models/#property-group-attribute)** | +| Field | Type | Original Object | +|------------|-----------------------|------------------------------------------------------| +| id | integer (primary key) | | +| name | string | | +| position | integer | | +| comparable | boolean | | +| sortMode | integer | | +| options | array | **[Option](../models/#property-group-option)** | +| attribute | array | **[Attribute](../models/#property-group-attribute)** | + +Note the camel case notation for 'sortMode'. ## GET (List) @@ -49,10 +58,10 @@ To get more than one property group at once, simply remove the id parameter from *Since this returns a list, the following fields will be added to the array:* -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of cache resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of cache resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) and PUT (update) @@ -60,13 +69,15 @@ You can post or put data by querying the following URL: * **(POST or PUT) http://my-shop-url/api/propertyGroups/id** -| Field | Type | Original Object | -|---------------------|-----------------------|---------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| position | integer | | -| comparable | boolean | | -| sortMode | integer | | +| Field | Type | Original Object | +|------------|-----------------------|-----------------| +| id | integer (primary key) | | +| name | string | | +| position | integer | | +| comparable | boolean | | +| sortmode | integer | | + +Note the small case notation for 'sortmode' to go with POST or PUT as opposed for GET. ## DELETE To delete a property group, simply query this URL with a `DELETE` request: diff --git a/source/developers-guide/rest-api/api-resource-shops/index.md b/source/developers-guide/rest-api/api-resource-shops/index.md index 76b8583720..f704abb2ca 100755 --- a/source/developers-guide/rest-api/api-resource-shops/index.md +++ b/source/developers-guide/rest-api/api-resource-shops/index.md @@ -1,21 +1,28 @@ --- layout: default -title: REST API - Shops Resource +title: REST API - Shop resource github_link: developers-guide/rest-api/api-resource-shops/index.md -indexed: false +menu_title: Shop resource +menu_order: 220 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's shops resource. With this resource, it is possible to retrieve, delete and update any shop in your system. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's shops resource. +With this resource, it is possible to retrieve, delete and update any shop in your system. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|-----------------------|-----------------------|---------------------|-----------------------|-----------------------|---------------------| -| /api/shops | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|------------|------------------------|------------------------|------------------------|----------------------|------------------------|------------------------|----------------------| +| /api/shops | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -30,26 +37,26 @@ You can retrieve data of a shop by providing the specific id ### Return Value -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| mainId | integer (foreign key) | **[Shop](../models/#shop)** | -| categoryId | integer (foreign key) | **[Category](../models/#category)** | -| name | string | | -| title | string | | -| position | integer | | -| host | string | | -| basePath | string | | -| baseUrl | string | | -| hosts | string | | -| secure | boolean | | -| alwaysSecure | boolean | | -| secureHost | string | | -| secureBasePath | string | | -| default | boolean | | -| active | boolean | | -| customerScope | boolean | | -| currency | object | **[Currency](../models/#currency)** | +| Field | Type | Original object | +|----------------|-----------------------|-------------------------------------| +| id | integer (primary key) | | +| mainId | integer (foreign key) | **[Shop](../models/#shop)** | +| categoryId | integer (foreign key) | **[Category](../models/#category)** | +| name | string | | +| title | string | | +| position | integer | | +| host | string | | +| basePath | string | | +| baseUrl | string | | +| hosts | string | | +| secure | boolean | | +| alwaysSecure | boolean | | +| secureHost | string | | +| secureBasePath | string | | +| default | boolean | | +| active | boolean | | +| customerScope | boolean | | +| currency | object | **[Currency](../models/#currency)** | ## GET (List) @@ -59,45 +66,45 @@ To get more than one shop at once, simply remove the id parameter from the reque ### Return value -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| mainId | integer (foreign key) | **[Shop](../models/#shop)** | -| categoryId | integer (foreign key) | **[Category](../models/#category)** | -| name | string | | -| title | string | | -| position | integer | | -| host | string | | -| basePath | string | | -| baseUrl | string | | -| hosts | string | | -| secure | boolean | | -| alwaysSecure | boolean | | -| secureHost | string | | -| secureBasePath | string | | -| default | boolean | | -| active | boolean | | -| customerScope | boolean | | +| Field | Type | Original object | +|----------------|-----------------------|-------------------------------------| +| id | integer (primary key) | | +| mainId | integer (foreign key) | **[Shop](../models/#shop)** | +| categoryId | integer (foreign key) | **[Category](../models/#category)** | +| name | string | | +| title | string | | +| position | integer | | +| host | string | | +| basePath | string | | +| baseUrl | string | | +| hosts | string | | +| secure | boolean | | +| alwaysSecure | boolean | | +| secureHost | string | | +| secureBasePath | string | | +| default | boolean | | +| active | boolean | | +| customerScope | boolean | | *Since this returns a list, the following fields will be added to the array:* -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of shop resources | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of shop resources | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) and PUT (update) You can post or put data by sending the following data to this URL: * **(POST or PUT) http://my-shop-url/api/shops/id** -| Field | Type | Original Object | -|---------------------|-----------------------|---------------------------------------------------------| -| name | string | | -| categoryId | integer | | -| localeId | integer | | -| currencyId | integer | | -| customerGroupId | integer | | +| Field | Type | Original Object | +|-----------------|---------|-----------------| +| name | string | | +| categoryId | integer | | +| localeId | integer | | +| currencyId | integer | | +| customerGroupId | integer | | ## DELETE To delete a shop, simply call this URL with the DELETE request: diff --git a/source/developers-guide/rest-api/api-resource-translation/index.md b/source/developers-guide/rest-api/api-resource-translation/index.md index 120d7c8951..c0285625e9 100755 --- a/source/developers-guide/rest-api/api-resource-translation/index.md +++ b/source/developers-guide/rest-api/api-resource-translation/index.md @@ -2,20 +2,27 @@ layout: default title: REST API - Translations Resource github_link: developers-guide/rest-api/api-resource-translation/index.md -indexed: false +menu_title: Translation resource +menu_order: 230 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation you can learn more about the API's translation resource. With this resource, it is possible to retrieve, delete and update any translation in your shops. We will also have a look at the associated data structures. +In this part of the documentation you can learn more about the API's translation resource. +With this resource, it is possible to retrieve, delete and update any translation in your shops. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|--------------------|-----------------------|-----------------------|------------------------|------------------------|-----------------------|-----------------------| -| /api/translations | ![No](../img/no.png)| ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|-------------------|----------------------|------------------------|------------------------|------------------------|------------------------|------------------------|------------------------| +| /api/translations | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | If you want to access this resource, simply query the following URL: @@ -29,20 +36,20 @@ You can retrieve data of translations by providing the specific id ### Return Value -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| type | string | | -| data | array | | -| key | integer | | -| shopId | integer (foreign key) | **[Shop](../models/#shop)** | -| shop | object | **[Shop](../models/#shop)** | +| Field | Type | Original object | +|--------|-----------------------|-----------------------------| +| type | string | | +| data | array | | +| key | integer | | +| shopId | integer (foreign key) | **[Shop](../models/#shop)** | +| shop | object | **[Shop](../models/#shop)** | *Since this returns a list, the following fields will be added to the array:* -| Field | Type | Comment | -|---------------------|-----------------------|-------------------------------------------------| -| total | integer | The total number of translations | -| success | boolean | Indicates if the call was successful or not. | +| Field | Type | Comment | +|---------|---------|----------------------------------------------| +| total | integer | The total number of translations | +| success | boolean | Indicates if the call was successful or not. | ## POST (create) @@ -50,39 +57,39 @@ To post a translation, you need to identify it by the following parameters ### Required Parameters -| Identifier | Parameter | Database Column | Example Call | -|-----------------------|-------------------|---------------------------|-------------------------------------------------------| -| Translation Id | id | `s_core_translations.id` | /api/translations/2 | -| Element number | - | - | /api/translations/20003?useNumberAsId=true | +| Identifier | Parameter | Database Column | Example Call | +|----------------|-----------|---------------------------|--------------------------------------------| +| Translation Id | id | `s_core_translations.id` | /api/translations/2 | +| Element number | - | - | /api/translations/20003?useNumberAsId=true | ### Data You can use this data to add a new translation to the shop -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| locale | string | | -| language | string | | -| territory | string | | -| locale | object | | -| type | string | | -| data | array | | -| key | integer | | -| shopId | integer (foreign key) | **[Shop](../models/#shop)** | +| Field | Type | Original object | +|-----------|-----------------------|-----------------------------| +| id | integer (primary key) | | +| locale | string | | +| language | string | | +| territory | string | | +| locale | object | | +| type | string | | +| data | array | | +| key | integer | | +| shopId | integer (foreign key) | **[Shop](../models/#shop)** | You can post or put data by sending the following data to this URL: * **(POST or PUT) http://my-shop-url/api/translations/id** -| Field | Type | Original Object | -|---------------------|-----------------------|---------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| position | integer | | -| comparable | boolean | | -| sortMode | integer | | +| Field | Type | Original Object | +|------------|-----------------------|-----------------| +| id | integer (primary key) | | +| name | string | | +| position | integer | | +| comparable | boolean | | +| sortMode | integer | | ## DELETE To delete a shop, simply call this URL with the `DELETE` request: @@ -107,3 +114,4 @@ Updating many articles at once requires an array of translation data being provi * **[PUT] http://my-shop-url/translations/** Simply provide the same data as described in the `GET` request. +Unlike other batch endpoints, a "key" field is required instead of an "id" field. diff --git a/source/developers-guide/rest-api/api-resource-user/index.md b/source/developers-guide/rest-api/api-resource-user/index.md new file mode 100644 index 0000000000..7aba31523c --- /dev/null +++ b/source/developers-guide/rest-api/api-resource-user/index.md @@ -0,0 +1,218 @@ +--- +layout: default +title: REST API - User resource +github_link: developers-guide/rest-api/api-resource-user/index.md +menu_title: User resource +menu_order: 250 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API +--- + +## Introduction + +This chapter of the documentation is about the API's user resource. +With this resource, it's possible to retrieve, update, create and +delete backend user of your shop. + +
    +Notice
    +The /users endpoint was introduced with Shopware 5.3.5 +and is not available for older versions. +
    + + +For each scenario, we provide an example of the data +which is required, as well as an exemplary response. +Please read the page covering the **[REST API Basics](/developers-guide/rest-api/)** if you haven't yet. + +## Get a list of users +If you want to get multiple user at once, you can call the /users endpoint. + +*Available arguments:* + +| Argument | Type | Required | Description | +|----------|--------------|----------|------------------------------------------------------------------------| +| limit | int | | Max. number of returned data sets | +| start | int | | Offset (ideal for batch processing, when working with large data sets) | +| sort | string array | | ORDER BY clause | +| filter | string array | | Filter properties by expressions | + + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/users', 'method': 'GET', 'body': true} %} +```json +{ + "limit": 10, + "start": 1, + "sort": [ + { + "property": "username", + "direction": "DESC" + } + ] +} +``` + +*Example output:* + +```json +{ + "total": 2, + "data": [ + { + "id": 1, + "roleId": 1, + "localeId": 1, + "username": "demo", + "password": "$2y$10$d7s.jETNFL1lZL3OzY7PneVWGk16aRZR9iuGyGHnw3X5EzssJ304W", + "encoder": "bcrypt", + "apiKey": "DaxN5BdfmcyglZMEwopy8Z46yADINhqViztSfcvI", + "sessionId": "f0ga0998i8b86aaahfjhjva760", + "lastLogin": "2017-11-01T11:10:24+0100", + "name": "Demo user", + "email": "demo@example.com", + "active": 1, + "failedLogins": 0, + "lockedUntil": "2010-01-01T00:00:00+0100", + "extendedEditor": false, + "disabledCache": false + }, + { + "id": 2, + "roleId": 6, + "localeId": 1, + "username": "test", + "password": "$2y$10$SuT6CVqrHsnZbG29kqsVq.DYXhx.JbF4X13bLlkxOb9dl/a4OIQym", + "encoder": "bcrypt", + "apiKey": "ohwrzHP70iwUkBdzPEx6iUfSc3sLrHZ7678dy3Ie", + "sessionId": "", + "lastLogin": "2017-11-01T10:48:41+0100", + "name": "asdasd", + "email": "asdsad.asd@asd.de", + "active": 1, + "failedLogins": 0, + "lockedUntil": "2017-11-01T10:48:41+0100", + "extendedEditor": false, + "disabledCache": false + } + ], + "success": true +} +``` + +Attention: The properties apiKey, sessionId and password are missing, +if the API user neither has the "update" nor the "create" privilege. + +## Get one user +If you want to get detailed information about a specific user, +you can call /users/{userId} + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/users/2', 'method': 'GET'} %} + +*Example output:* + +```json +{ + "data": { + "id": 2, + "roleId": 6, + "localeId": 1, + "username": "test", + "password": "$2y$10$SuT6CVqrHsnZbG29kqsVq.DYXhx.JbF4X13bLlkxOb9dl/a4OIQym", + "encoder": "bcrypt", + "apiKey": "ohwrzHP70iwUkBdzPEx6iUfSc3sLrHZ7678dy3Ie", + "sessionId": "", + "lastLogin": "2017-11-01T10:48:41+0100", + "name": "asdasd", + "email": "asdsad.asd@asd.de", + "active": 1, + "failedLogins": 0, + "lockedUntil": "2017-11-01T10:48:41+0100", + "extendedEditor": false, + "disabledCache": false, + "attribute": null + }, + "success": true +} +``` + +### Update a user +If you want to update a user, you can send a PUT request to /users/{userId} + + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/users/2', 'method': 'PUT', 'body': true} %} +```json +{ + "username": "test2" +} +``` + +*Example output:* + +```json +{ + "success": true, + "data": { + "id": 2, + "location": "http://localhost/shopware/api/users/2" + } +} +``` + +## Create a new user +If you want to create a user, you can send a POST request to /users/ + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/users', 'method': 'POST', 'body': true} %} +```json +{ + "roleId": 1, + "localeId": 1, + "username": "example", + "name": "test", + "email": "test@example.org", + "active": 1, + "extendedEditor": false, + "disabledCache": false +} +``` + +Note: If you do not pass a password, the API will generate a +secure password and send it in the response. + +*Example output:* + +```json +{ + "success": true, + "data": { + "id": 3, + "location": "http://localhost/shopware/api/users/3", + "password": "Ar4dETspCp$jk$7" + } +} +``` + +## Delete a user +If you want to update a user, you can send a DELETE request to /users/{userId} + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/users/2', 'method': 'DELETE'} %} + +*Example output:* +```json +{ + "success": true +} +``` + +Attention: Due to a safety precaution, the API user who made the API call, can not delete itself. diff --git a/source/developers-guide/rest-api/api-resource-variants/index.md b/source/developers-guide/rest-api/api-resource-variants/index.md index b1835ccdfd..b7af5d8623 100644 --- a/source/developers-guide/rest-api/api-resource-variants/index.md +++ b/source/developers-guide/rest-api/api-resource-variants/index.md @@ -1,21 +1,28 @@ --- layout: default -title: REST API - Variants Resource +title: REST API - Variants resource github_link: developers-guide/rest-api/api-resource-variants/index.md -indexed: false +menu_title: Variants resource +menu_order: 45 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation, you can learn more about the API's variants resource. With this resource, it's possible to retrieve, delete and update any variant in your shops. We will also have a look at the associated data structures. +In this part of the documentation, you can learn more about the API's variants resource. +With this resource, it's possible to retrieve, delete and update any variant in your shops. +We will also have a look at the associated data structures. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|--------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------| -| /api/variants | ![No](../img/no.png)| ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|---------------|----------------------|------------------------|------------------------|------------------------|------------------------|------------------------|------------------------| +| /api/variants | ![No](../img/no.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | ![Yes](../img/yes.png) | If you want to access this resource, simply query the following URL @@ -29,47 +36,48 @@ You can retrieve the variants data by providing the specific id ### Required Parameters -| Identifier | Parameter | Database Column | Example call | -|-------------------|-------------------|-------------------------------------|-------------------------------------------------------------------------| -| Detail id | id | `s_articles_details.id` | /api/variants/2 | -| Detail number | number | `s_articlies_details.ordernumber` | /api/variants/SW10003?useNumberAsId=true | +| Identifier | Parameter | Database Column | Example call | +|---------------|-----------|----------------------------------|------------------------------------------| +| Detail id | id | `s_articles_details.id` | /api/variants/2 | +| Detail number | number | `s_articles_details.ordernumber` | /api/variants/SW10003?useNumberAsId=true | Option parameters can be provided: -* `considerTaxInput`: By default, all returned prices are net values. If the boolean `considerTaxInput` is set to true, gross values will be returned instead. +* `considerTaxInput`: By default, all returned prices are net values. +If the boolean `considerTaxInput` is set to true, gross values will be returned instead. ### Return Value -| Model | Table | -|-----------------------------------|---------------------------| -| Shopware\Models\Article\Detail | `s_articles_details` | - -| Field | Type | Original object | -|---------------------|-----------------------|---------------------------------------------------------| -| id | integer (primary key) | | -| articleId | integer (foreign key) | **[Article](../api-resource-article)** | -| unitId | integer (foreign key) | | -| number | string | | -| supplierNumber | string | | -| kind | integer | | -| additionalText | string | | -| active | boolean | | -| inStock | integer | | -| stockMin | integer | | -| weight | string | | -| len | string | | -| height | string | | -| ean | string | | -| position | integer | | -| minPurchase | integer | | -| purchaseSteps | integer | | -| maxPurchase | integer | | -| purchaseUnit | string | | -| shippingFree | boolean | | -| releaseDate | date/time | | -| shippingTime | string | | -| prices | array | **[Price](../models/#price)** | -| attribute | object | **[Attribute](../models/#article-attribute)** | -| configuratorOptions | array | **[ConfiguratorOptions](../models/#configurator-option)**| +| Model | Table | +|--------------------------------|----------------------| +| Shopware\Models\Article\Detail | `s_articles_details` | + +| Field | Type | Original object | +|---------------------|-----------------------|-----------------------------------------------------------| +| id | integer (primary key) | | +| articleId | integer (foreign key) | **[Article](../api-resource-article)** | +| unitId | integer (foreign key) | | +| number | string | | +| supplierNumber | string | | +| kind | integer | | +| additionalText | string | | +| active | boolean | | +| inStock | integer | | +| stockMin | integer | | +| weight | string | | +| len | string | | +| height | string | | +| ean | string | | +| position | integer | | +| minPurchase | integer | | +| purchaseSteps | integer | | +| maxPurchase | integer | | +| purchaseUnit | string | | +| shippingFree | boolean | | +| releaseDate | date/time | | +| shippingTime | string | | +| prices | array | **[Price](../models/#price)** | +| attribute | object | **[Attribute](../models/#article-attribute)** | +| configuratorOptions | array | **[ConfiguratorOptions](../models/#configurator-option)** | ## POST (create) To post a variant, you need to provide the data as shown below: @@ -78,37 +86,37 @@ To post a variant, you need to provide the data as shown below: ### Data You can use this data to add a new variant to the shop -| Model | Table | -|-----------------------------------|---------------------------| -| Shopware\Models\Article\Detail | `s_articles_details` | - -| Field | Type | Original object | -|---------------------|-----------------------|---------------------------------------------------------| -| id | integer (primary key) | | -| articleId | integer (foreign key) | **[Article](../api-resource-article)** | -| unitId | integer (foreign key) | | -| number | string | | -| supplierNumber | string | | -| kind | integer | | -| additionalText | string | | -| active | boolean | | -| inStock | integer | | -| stockMin | integer | | -| weight | string | | -| len | string | | -| height | string | | -| ean | string | | -| position | integer | | -| minPurchase | integer | | -| purchaseSteps | integer | | -| maxPurchase | integer | | -| purchaseUnit | string | | -| shippingFree | boolean | | -| releaseDate | date/time | | -| shippingTime | string | | -| prices | array | **[Price](../models/#price)** | -| attribute | object | **[Attribute](../models/#article-attribute)** | -| configuratorOptions | array | **[ConfiguratorOptions](../models/#configurator-option)**| +| Model | Table | +|--------------------------------|----------------------| +| Shopware\Models\Article\Detail | `s_articles_details` | + +| Field | Type | Original object | +|---------------------|-----------------------|-----------------------------------------------------------| +| id | integer (primary key) | | +| articleId | integer (foreign key) | **[Article](../api-resource-article)** | +| unitId | integer (foreign key) | | +| number | string | | +| supplierNumber | string | | +| kind | integer | | +| additionalText | string | | +| active | boolean | | +| inStock | integer | | +| stockMin | integer | | +| weight | string | | +| len | string | | +| height | string | | +| ean | string | | +| position | integer | | +| minPurchase | integer | | +| purchaseSteps | integer | | +| maxPurchase | integer | | +| purchaseUnit | string | | +| shippingFree | boolean | | +| releaseDate | date/time | | +| shippingTime | string | | +| prices | array | **[Price](../models/#price)** | +| attribute | object | **[Attribute](../models/#article-attribute)** | +| configuratorOptions | array | **[ConfiguratorOptions](../models/#configurator-option)** | You can post or put data by sending the following data to this URL: @@ -118,53 +126,53 @@ You can post or put data by sending the following data to this URL: To put data to a variant, simply provide one of the following parameters to identify it: -| Identifier | Parameter | Database Column | Example Call | -|-------------------|---------------|-----------------------------------|---------------------------------------------------| -| Detail Id | id | `s_articles_details.id` | /api/variants/2 | -| Detail number | number | `s_articles_details.ordernumber` | /api/variants/SW10003?useNumberAsId=true | +| Identifier | Parameter | Database Column | Example Call | +|---------------|-----------|----------------------------------|------------------------------------------| +| Detail Id | id | `s_articles_details.id` | /api/variants/2 | +| Detail number | number | `s_articles_details.ordernumber` | /api/variants/SW10003?useNumberAsId=true | **The data is the same as shown in the POST operation.** You can use this data to update a variant. -| Model | Table | -|-----------------------------------|---------------------------| -| Shopware\Models\Article\Detail | `s_articles_details` | - -| Field | Type | Original object | -|---------------------|-----------------------|---------------------------------------------------------| -| id | integer (primary key) | | -| articleId | integer (foreign key) | **[Article](../api-resource-article)** | -| unitId | integer (foreign key) | | -| number | string | | -| supplierNumber | string | | -| kind | integer | | -| additionalText | string | | -| active | boolean | | -| inStock | integer | | -| stockMin | integer | | -| weight | string | | -| len | string | | -| height | string | | -| ean | string | | -| position | integer | | -| minPurchase | integer | | -| purchaseSteps | integer | | -| maxPurchase | integer | | -| purchaseUnit | string | | -| shippingFree | boolean | | -| releaseDate | date/time | | -| shippingTime | string | | -| prices | array | **[Price](../models/#price)** | -| attribute | object | **[Attribute](../models/#article-attribute)** | -| configuratorOptions | array | **[ConfiguratorOptions](../models/#configurator-option)**| +| Model | Table | +|--------------------------------|----------------------| +| Shopware\Models\Article\Detail | `s_articles_details` | + +| Field | Type | Original object | +|---------------------|-----------------------|-----------------------------------------------------------| +| id | integer (primary key) | | +| articleId | integer (foreign key) | **[Article](../api-resource-article)** | +| unitId | integer (foreign key) | | +| number | string | | +| supplierNumber | string | | +| kind | integer | | +| additionalText | string | | +| active | boolean | | +| inStock | integer | | +| stockMin | integer | | +| weight | string | | +| len | string | | +| height | string | | +| ean | string | | +| position | integer | | +| minPurchase | integer | | +| purchaseSteps | integer | | +| maxPurchase | integer | | +| purchaseUnit | string | | +| shippingFree | boolean | | +| releaseDate | date/time | | +| shippingTime | string | | +| prices | array | **[Price](../models/#price)** | +| attribute | object | **[Attribute](../models/#article-attribute)** | +| configuratorOptions | array | **[ConfiguratorOptions](../models/#configurator-option)** | ## DELETE To delete a variant, simply provide one of the following parameters to identify it: -| Identifier | Parameter | Database Column | Example Call | -|-------------------|---------------|-----------------------------------|---------------------------------------------------| -| Detail Id | id | `s_articles_details.id` | /api/variants/2 | -| Detail number | number | `s_articles_details.ordernumber` | /api/variants/SW10003?useNumberAsId=true | +| Identifier | Parameter | Database Column | Example Call | +|---------------|-----------|----------------------------------|------------------------------------------| +| Detail Id | id | `s_articles_details.id` | /api/variants/2 | +| Detail number | number | `s_articles_details.ordernumber` | /api/variants/SW10003?useNumberAsId=true | ## DELETE (Stack) @@ -181,4 +189,4 @@ Updating many articles at once requires an array of variant data being provided * **[PUT] http://my-shop-url/variants/** -Simply provide the same data as described in the create statement. +Simply provide the same data as described in the "create! statement. diff --git a/source/developers-guide/rest-api/api-resource-version/index.md b/source/developers-guide/rest-api/api-resource-version/index.md index d217e4c3e3..642959029c 100755 --- a/source/developers-guide/rest-api/api-resource-version/index.md +++ b/source/developers-guide/rest-api/api-resource-version/index.md @@ -1,21 +1,27 @@ --- layout: default -title: REST API - Version Resource +title: REST API - Version resource github_link: developers-guide/rest-api/api-resource-version/index.md -indexed: false +menu_title: Version resource +menu_order: 270 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API --- ## Introduction -In this part of the documentation, you can learn more about the API's version resource. With this resource, it is possible to retrieve the version of your Shopware installation. +In this part of the documentation, you can learn more about the API's version resource. +With this resource, it is possible to retrieve the version of your Shopware installation. ## General Information This resource supports the following operations: -| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-----------------------------|-----------------------|---------------------|---------------------|---------------------|---------------------|-----------------|-----------------| -| /api/version | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | +| Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|--------------|------------------------|----------------------|----------------------|----------------------|----------------------|----------------------|----------------------| +| /api/version | ![Yes](../img/yes.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | ![No](../img/no.png) | If you want to access this resource, simply query the following URL: @@ -23,10 +29,11 @@ If you want to access this resource, simply query the following URL: ### GET -This resource supports only the `GET` operation, which retrieves a simple array, containing details about your current Shopware installation's version: +This resource supports only the `GET` operation, which retrieves a simple array, +containing details about your current Shopware installation's version: -| Field | Type | Description | -|-------------------|---------------------------|--------------------------------------------------| -| version | string | The actual shopware version (e.g 5.0) | -| revision | string | The release date by standard (e.g 201504010102) | -| success | boolean | A value indicating if the request was successful | +| Field | Type | Description | +|----------|---------|--------------------------------------------------| +| version | string | The actual shopware version (e.g 5.0) | +| revision | string | The release date by standard (e.g 201504010102) | +| success | boolean | A value indicating if the request was successful | diff --git a/source/developers-guide/rest-api/examples/article/index.md b/source/developers-guide/rest-api/examples/article/index.md index 68945154aa..f123d3085c 100644 --- a/source/developers-guide/rest-api/examples/article/index.md +++ b/source/developers-guide/rest-api/examples/article/index.md @@ -1,9 +1,9 @@ --- layout: default -title: REST API - Examples using the article resource +title: REST API - Examples using the product resource github_link: developers-guide/rest-api/examples/article/index.md -menu_title: The article resource -menu_order: 20 +menu_title: Product examples +menu_order: 40 indexed: true menu_style: bullet group: Developer Guides @@ -14,31 +14,22 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the article resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[article API resource](/developers-guide/rest-api/api-resource-article/)** if you haven't yet, to get more information about the article resource and the data it provides. +In this article, you will find examples of the `article` resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. +Please read the page covering the **[article API resource](/developers-guide/rest-api/api-resource-article/)** if you haven't yet, +to get more information about the article resource and the data it provides. -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. One of its advantages is that, instead of providing query arguments directly in the URL, you can do so by means of method argument. The client application will, internally, handle the full URL generation. You can also place variables using this technique. An example call would look like this: +## Example 1 - GET +These example show you how to obtain information about a single product, by using either its ID or product number. +The API calls look, respectively, like this: -``` -$client->post('articles', array( - 'name' => 'NewId', - 'taxId' => 1, - 'mainDetail' => array( - 'number' => 'SW123456' - ) -)); -``` +{% include 'api_badge.twig' with {'route': '/api/articles/3', 'method': 'GET'} %} -## Example 1 - GET -These example show you how to obtain information about a single article, by using either its ID or article number. The API calls look, respectively, like this: -``` -$client->get('articles/3'); -$client->get('articles/SW10003?useNumberAsId=true'); -``` +{% include 'api_badge.twig' with {'route': '/api/articles/SW10003?useNumberAsId=true', 'method': 'GET'} %} ### Result: -``` +```json { "data":{ "id":3, @@ -104,6 +95,7 @@ $client->get('articles/SW10003?useNumberAsId=true'); "pseudoPrice":0, "basePrice":0, "percent":0, + "regulationPrice":0, "customerGroup":{ "id":1, "key":"EK", @@ -285,7 +277,7 @@ $client->get('articles/SW10003?useNumberAsId=true'); "translations":{ "2":{ "name":"Munsterland Aperitif 16%", - "descriptionLong":"

    Io copia moeror immo pro audio modestia. Permaneo animosus etsi furax, aversor, faenum Pecus, mus me dux ferociter interpellatio certo. infrequentia Illis Quamquam Invidus, indutus [...] " + "descriptionLong":"

    Io copia moeror immo pro audio modestia. Permaneo animosus etsi furax, aversor, faenum Pecus, mus me dux ferociter interpellatio certo. infrequentia Illis Quamquam Invidus, indutus [...] ", "shopId":2 } } @@ -295,16 +287,17 @@ $client->get('articles/SW10003?useNumberAsId=true'); ``` ## Example 2 - GET (List) -This example shows you how to obtain information about a single article. -With the optional `limit` parameter, it's possible to specify how many articles you wish the API call to return. +This example shows you how to obtain information about a product list. +With the optional `limit` parameter, it's possible to specify how many products you wish the API call to return. -``` -$client->get('articles'); -$client->get('articles?limit=2); -``` +{% include 'api_badge.twig' with {'route': '/api/articles?limit=2', 'method': 'GET'} %} + +A maximum of 1000 entries is returned. By using the `start` parameter you can get the following entries. + +{% include 'api_badge.twig' with {'route': '/api/articles?start=1001', 'method': 'GET'} %} ### Result -``` +```json { "data":[ { @@ -367,429 +360,600 @@ $client->get('articles?limit=2); } ``` -## Example 3 - Update article data -To update an article it's always required to provide the id of the article to update. -In this example, we will update the name of the article with the id 3 -``` -$client->put('articles/3', array( - 'name' => 'NewName' -)); +## Example 3 - Update product data +To update a product it's always required to provide the id of the product to update. +In this example, we will update the name of the product with the id 3 + +{% include 'api_badge.twig' with {'route': '/api/articles/3', 'method': 'PUT', 'body': true} %} +```json +{ + "name": "NewName" +} ``` + ### Result -``` +```json { "success":true, "data":{ "id":3, - "location":"http:\/\/localhost\/master\/api\/articles\/3" + "location":"http:\/\/localhost\/shop\/api\/articles\/3" } } ``` -## Example 4 - Delete an article -To delete an article, it's always required to provide the id of the article to delete. If the number is provided, an error will be returned with the response code 500. +## Example 4 - Delete a product +To delete a product, it's always required to provide the id of the product to delete. +If the number is provided, an error will be returned with the response code 500. **Attention, this action can not be undone** -``` -$client->delete('articles/3'); -``` +{% include 'api_badge.twig' with {'route': '/api/articles/3', 'method': 'DELETE'} %} ### Result -``` +```json { - success: true + "success": true } ``` -## Example 5 - Article configuration / variation - -### Step 1 - Create a new article -``` -$minimalTestArticle = array( - 'name' => 'Sport Shoes', - 'active' => true, - 'tax' => 19, - 'supplier' => 'Sport Shoes Inc.', - 'categories' => array( - array('id' => 15), - ), - 'mainDetail' => array( - 'number' => 'turn', - 'active' => true, - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 999, - ), - ) - ), -); - -$client->post('articles', $minimalTestArticle); +## Example 5 - Product configuration / variation +### Step 1 - Create a new product +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Sport Shoes", + "active": true, + "tax": 19, + "supplier": "Sport Shoes Inc.", + "categories": [ + { + "id": 15 + } + ], + "mainDetail": { + "number": "turn", + "active": true, + "prices": [ + { + "customerGroupKey": "EK", + "price": 999 + } + ] + } +} ``` -### Step 2 - Update the created article -``` -$updateArticle = array( - 'configuratorSet' => array( - 'groups' => array( - array( - 'name' => 'Size', - 'options' => array( - array('name' => 'S'), - array('name' => 'M'), - array('name' => 'L'), - array('name' => 'XL'), - array('name' => 'XXL'), - ) - ), - array( - 'name' => 'Color', - 'options' => array( - array('name' => 'White'), - array('name' => 'Yellow'), - array('name' => 'Blue'), - array('name' => 'Black'), - array('name' => 'Red'), - ) - ), - ) - ), - 'taxId' => 1, - 'variants' => array( - array( - 'isMain' => true, - 'number' => 'turn', - 'inStock' => 15, - 'additionaltext' => 'L / Black', - 'configuratorOptions' => array( - array('group' => 'Size', 'option' => 'L'), - array('group' => 'Color', 'option' => 'Black'), - ), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 1999, - ), - ) - ), - array( - 'isMain' => false, - 'number' => 'turn.1', - 'inStock' => 15, - 'additionnaltext' => 'S / Black', - 'configuratorOptions' => array( - array('group' => 'Size', 'option' => 'S'), - array('group' => 'Color', 'option' => 'Black'), - ), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 999, - ), - ) - ), - array( - 'isMain' => false, - 'number' => 'turn.2', - 'inStock' => 15, - 'additionnaltext' => 'S / Red', - 'configuratorOptions' => array( - array('group' => 'Size', 'option' => 'S'), - array('group' => 'Color', 'option' => 'Red'), - ), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 999, - ), - ) - ), - array( - 'isMain' => false, - 'number' => 'turn.3', - 'inStock' => 15, - 'additionnaltext' => 'XL / Red', - 'configuratorOptions' => array( - array('group' => 'Size', 'option' => 'XL'), - array('group' => 'Color', 'option' => 'Red'), - ), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 999, - ), - ) - ) - ) -); - -$client->put('articles/193', $updateArticle); +### Step 2 - Update the created product +{% include 'api_badge.twig' with {'route': '/api/articles/193', 'method': 'PUT', 'body': true} %} +```json +{ + "configuratorSet": { + "groups": [ + { + "name": "Size", + "options": [ + { + "name": "S" + }, + { + "name": "M" + }, + { + "name": "L" + }, + { + "name": "XL" + }, + { + "name": "XXL" + } + ] + }, + { + "name": "Color", + "options": [ + { + "name": "White" + }, + { + "name": "Yellow" + }, + { + "name": "Blue" + }, + { + "name": "Black" + }, + { + "name": "Red" + } + ] + } + ] + }, + "taxId": 1, + "variants": [ + { + "isMain": true, + "number": "turn", + "inStock": 15, + "additionaltext": "L \/ Black", + "configuratorOptions": [ + { + "group": "Size", + "option": "L" + }, + { + "group": "Color", + "option": "Black" + } + ], + "prices": [ + { + "customerGroupKey": "EK", + "price": 1999 + } + ] + }, + { + "isMain": false, + "number": "turn.1", + "inStock": 15, + "additionaltext": "S \/ Black", + "configuratorOptions": [ + { + "group": "Size", + "option": "S" + }, + { + "group": "Color", + "option": "Black" + } + ], + "prices": [ + { + "customerGroupKey": "EK", + "price": 999 + } + ] + }, + { + "isMain": false, + "number": "turn.2", + "inStock": 15, + "additionaltext": "S \/ Red", + "configuratorOptions": [ + { + "group": "Size", + "option": "S" + }, + { + "group": "Color", + "option": "Red" + } + ], + "prices": [ + { + "customerGroupKey": "EK", + "price": 999 + } + ] + }, + { + "isMain": false, + "number": "turn.3", + "inStock": 15, + "additionaltext": "XL \/ Red", + "configuratorOptions": [ + { + "group": "Size", + "option": "XL" + }, + { + "group": "Color", + "option": "Red" + } + ], + "prices": [ + { + "customerGroupKey": "EK", + "price": 999 + } + ] + } + ] +} ``` ### Result -``` +```json { - success: true + "success": true } ``` -## Example 6 - Article Properties +## Example 6 - Product Properties -It's also possible to add article properties using the article resource. +It's also possible to add product properties using the `article` resource. In order to perform this action, an array like this is required: -``` -$filterTest = array( - 'name' => 'My awesome liquor', - 'description' => 'hmmmmm', - - 'active' => true, - 'taxId' => 1, - - 'mainDetail' => array( - 'number' => 'brand1', - 'inStock' => 15, - 'active' => true, - - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'from' => 1, - 'price' => 50 - ) - ) - ), - - 'filterGroupId' => 1, - 'propertyValues' => array( - array( - 'option' => array('name' => "Alcohol content"), - 'value' => '10%' - ), - array( - 'option' => array('name' => "Color"), - 'value' => 'rot' - ) - ) -); - -$client->post('articles', $filterTest); - +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'POST', 'body': true} %} +```json +{ + "name": "My awesome liquor", + "description": "hmmmmm", + "active": true, + "taxId": 1, + "mainDetail": { + "number": "brand1", + "inStock": 15, + "active": true, + "prices": [ + { + "customerGroupKey": "EK", + "from": 1, + "price": 50 + } + ] + }, + "filterGroupId": 1, + "propertyValues": [ + { + "option": { + "name": "Alcohol content" + }, + "value": "10%" + }, + { + "option": { + "name": "Color" + }, + "value": "rot" + } + ] +} ``` -Options (`option`) and values (`value`) can be identified by name (see example above) or by identifier (`id`). Since the values within option and options are unique in groups, it's possible to automatically recognize if a new one has to be added to the shop, or if an existing entry has to be updated. PropertyGroups (`filterGroupID`) have to be added through another resource **[PropertyGroups](../../api-resource-property-group)**. +Options (`option`) and values (`value`) can be identified by name (see example above) or by identifier (`id`). +Since the values within option and options are unique in groups, +it's possible to automatically recognize if a new one has to be added to the shop, or if an existing entry has to be updated. +PropertyGroups (`filterGroupID`) have to be added through another resource **[PropertyGroups](../../api-resource-property-group)**. +{% include 'api_badge.twig' with {'route': '/api/propertyGroups', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Liquor", + "position": 1, + "comparable": 1, + "sortmode": 2 +} ``` -$properties = array( - "name" => "Liquor", - 'position' => 1, - 'comparable' => 1, - 'sortmode' => 2 -); +The returned identifier may be set as `filterGroupId`, just like the example `$filterTest` shows. + +## Example 7 - Link new or existing images to a property -$client->post('propertyGroups', $properties); +Its possible to assign images to a specific product property. +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'POST', 'body': true} %} +```json +{ + "name": "My awesome liquor", + "description": "hmmmmm", + "active": true, + "taxId": 1, + "mainDetail": { + "number": "brand1", + "inStock": 15, + "active": true, + "prices": [ + { + "customerGroupKey": "EK", + "from": 1, + "price": 50 + } + ] + }, + "images": [ + { + "link": "http:\/\/example.org\/test.jpg", + "main": 1, + "position": 1, + "options": { + "name": "Alcohol content" + } + }, + { + "mediaId": 57, + "main": 0, + "position": 2, + "options": { + "name": "Color" + } + } + ], + "filterGroupId": 1, + "propertyValues": [ + { + "option": { + "name": "Alcohol content" + }, + "value": "10%" + }, + { + "option": { + "name": "Color" + }, + "value": "rot" + } + ] +} ``` -The returned identifier may be set as `filterGroupId`, just like the example `$filterTest` shows. -## Example 7 - Creating and referencing units +## Example 8 - Creating and referencing units It's possible to specify units using the `unit` key. The snippet below shows how: -``` -$articleWithUnit = array( - 'name' => 'Sport shoes', - 'tax' => 19, - 'supplier' => 'Sport shoes Inc.', - - 'mainDetail' => array( - 'number' => 'turn33', - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 999, - ), - ), - 'unit' => array( - 'unit' => 'xyz', - 'name' => 'New Unit' - ) - ), -); - -$client->post('articles', articleWithUnit); +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Sport shoes", + "tax": 19, + "supplier": "Sport shoes Inc.", + "active": true, + "mainDetail": { + "number": "turn33", + "active": true, + "prices": [ + { + "customerGroupKey": "EK", + "price": 999 + } + ], + "unit": { + "unit": "xyz", + "name": "New Unit" + } + } +} ``` -The API itself checks if the unit already exist. If it does, the old unit will be overwritten with the new values, otherwise it simply will be added as new unit to the shop. +The API itself checks if the unit already exist. +If it does, the old unit will be overwritten with the new values, otherwise it simply will be added as new unit to the shop. ## Further examples -``` -$testArticle = array( - 'name' => 'NewTestArticle', - 'active' => true, - 'tax' => 19, // alternatively 'taxId' => 1, - 'supplier' => 'Test Supplier', // alternatively 'supplierId' => 2, - - 'categories' => array( - array('id' => 15), - array('id' => 16), - ), - - 'images' => array( - array('link' => 'http://lorempixel.com/640/480/food/'), - array('link' => 'http://lorempixel.com/640/480/food/'), - ), - - 'mainDetail' => array( - 'number' => 'swTEST' . uniqid(), - 'inStock' => 16, - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 99.34, - ), - ) - ), -); -$client->post('articles', $testArticle); -``` +### Linking images -``` -$updateInStock = array( - 'mainDetail' => array( - 'inStock' => 66 - ) -); -$client->put('articles/3', $updateInStock); +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'POST', 'body': true} %} +```json +{ + "name": "NewTestArticle", + "active": true, + "tax": 19, + "supplier": "Test Supplier", + "categories": [ + { + "id": 15 + }, + { + "id": 16 + } + ], + "images": [ + { + "link": "http:\/\/lorempixel.com\/640\/480\/food\/" + }, + { + "link": "http:\/\/lorempixel.com\/640\/480\/food\/" + } + ], + "mainDetail": { + "number": "swTEST5d9b1a3c6521f", + "active": true, + "inStock": 16, + "prices": [ + { + "customerGroupKey": "EK", + "price": 99.34 + } + ] + } +} ``` +### Updating the number of products in stock + +{% include 'api_badge.twig' with {'route': '/api/articles/3', 'method': 'PUT', 'body': true} %} +```json +{ + "mainDetail": { + "inStock": 66 + } +} ``` -$updateVariantInStock = array( - 'variants' => array( - array( - // update per primary key - 'id' => 726, - 'inStock' => 99, - ), - array( - // update per ordernumber key - 'number' => 'SW10204.5', - 'inStock' => 999, - ), - ) -); -$client->put('articles/205', $updateVariantInStock); + +### Updating the number of variants in stock + +{% include 'api_badge.twig' with {'route': '/api/articles/205', 'method': 'PUT', 'body': true} %} +```json +{ + "variants": [ + { + "id": 726, + "inStock": 99 + }, + { + "number": "SW10204.5", + "inStock": 999 + } + ] +} ``` +### Creating a configuratorSet and variants + +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'POST', 'body': true} %} +```json +{ + "name": "ConfiguratorTest", + "description": "A test article", + "descriptionLong": "

    I'm a **test article<\/b><\/p>", + "active": true, + "taxId": 1, + "supplierId": 2, + "categories": [ + { + "id": 15 + } + ], + "mainDetail": { + "number": "swTEST5d9b1a9cbcc9d", + "active": true, + "prices": [ + { + "customerGroupKey": "EK", + "price": 999 + } + ] + }, + "configuratorSet": { + "groups": [ + { + "name": "Size", + "options": [ + { + "name": "S" + }, + { + "name": "M" + }, + { + "name": "L" + }, + { + "name": "XL" + }, + { + "name": "XXL" + } + ] + }, + { + "name": "Color", + "options": [ + { + "name": "White" + }, + { + "name": "Yellow" + }, + { + "name": "Blue" + }, + { + "name": "Black" + } + ] + } + ] + }, + "variants": [ + { + "isMain": true, + "number": "swTEST5d9b1a9cbcc9f", + "inStock": 15, + "additionaltext": "S \/ Schwarz", + "configuratorOptions": [ + { + "group": "Size", + "option": "S" + }, + { + "group": "Color", + "option": "Black" + } + ], + "prices": [ + { + "customerGroupKey": "EK", + "price": 999 + } + ] + }, + { + "number": "swTEST5d9b1a9cbcca0", + "inStock": 10, + "additionaltext": "S \/ Wei\u00df", + "configuratorOptions": [ + { + "group": "Size", + "option": "S" + }, + { + "group": "Color", + "option": "White" + } + ], + "prices": [ + { + "customerGroupKey": "EK", + "price": 888 + } + ], + "attribute": { + "attr1": "S\/White Attr1", + "attr2": "SomeText" + } + }, + { + "number": "swTEST5d9b1a9cbcca1", + "inStock": 5, + "additionaltext": "XL \/ Blue", + "configuratorOptions": [ + { + "group": "Size", + "option": "XL" + }, + { + "group": "Color", + "option": "Blue" + } + ], + "prices": [ + { + "customerGroupKey": "EK", + "price": 555 + } + ] + } + ] +} ``` -$configuratorArticle = array( - 'name' => 'ConfiguratorTest', - 'description' => 'A test article', - 'descriptionLong' => '

    I\'m a test article

    ', - 'active' => true, - 'taxId' => 1, - 'supplierId' => 2, - - 'categories' => array( - array('id' => 15), - ), - - 'mainDetail' => array( - 'number' => 'swTEST' . uniqid(), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 999, - ), - ) - ), - - 'configuratorSet' => array( - 'groups' => array( - array( - 'name' => 'Size', - 'options' => array( - array('name' => 'S'), - array('name' => 'M'), - array('name' => 'L'), - array('name' => 'XL'), - array('name' => 'XXL'), - ) - ), - array( - 'name' => 'Color', - 'options' => array( - array('name' => 'White'), - array('name' => 'Yellow'), - array('name' => 'Blue'), - array('name' => 'Black'), - ) - ), - ) - ), - 'variants' => array( - array( - 'isMain' => true, - 'number' => 'swTEST' . uniqid(), - 'inStock' => 15, - 'additionaltext' => 'S / Schwarz', - 'configuratorOptions' => array( - array('group' => 'Size', 'option' => 'S'), - array('group' => 'Color', 'option' => 'Black'), - ), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 999, - ), - ) - ), - array( - 'number' => 'swTEST' . uniqid(), - 'inStock' => 10, - 'additionaltext' => 'S / Weiß', - 'configuratorOptions' => array( - array('group' => 'Size', 'option' => 'S'), - array('group' => 'Color', 'option' => 'White'), - ), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 888, - ), - ), - 'attribute' => array( - 'attr1' => 'S/White Attr1', - 'attr2' => 'SomeText', - ), - ), - array( - 'number' => 'swTEST' . uniqid(), - 'inStock' => 5, - 'additionaltext' => 'XL / Blue', - 'configuratorOptions' => array( - array('group' => 'Size', 'option' => 'XL'), - array('group' => 'Color', 'option' => 'Blue'), - ), - 'prices' => array( - array( - 'customerGroupKey' => 'EK', - 'price' => 555, - ), - ) - ) - ), -); - -$client->post('articles', $configuratorArticle); +### Updating the SEO category of a product + +{% include 'api_badge.twig' with {'route': '/api/articles/SW10239?useNumberAsId=true', 'method': 'PUT', 'body': true} %} +```json +{ + "seoCategories": [ + { + "shopId": 1, + "categoryId": 15 + } + ], + "categories": [ + { + "id": 15 + } + ] +} ``` + +If you just want to add another seo category, you have to add the value: `__options_seoCategories' => false` diff --git a/source/developers-guide/rest-api/examples/batch/index.md b/source/developers-guide/rest-api/examples/batch/index.md index 2a77259c13..b1f0fdacc3 100644 --- a/source/developers-guide/rest-api/examples/batch/index.md +++ b/source/developers-guide/rest-api/examples/batch/index.md @@ -2,8 +2,8 @@ layout: default title: REST API - Examples using the batch mode github_link: developers-guide/rest-api/examples/batch/index.md -menu_title: The batch mode -menu_order: 100 +menu_title: Batch mode +menu_order: 290 indexed: true menu_style: bullet group: Developer Guides @@ -12,18 +12,16 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the provided resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[category API resource](/developers-guide/rest-api/api-resource-categories/)** if you haven't yet, to get more information about the category resource and the data it provides. - -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. +In this article, you will find examples of the provided resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. ## Batch mode -The batch mode allows to create and / or update multiple elements in one request. +The batch mode allows creating and / or updating multiple elements in one request. Notice the list of resources which supports the batch mode. The results of the different tasks (create / update) is stacked and returns one result. -In addition the batch mode supports the detach of elements in on request. +In addition, the batch mode supports the detaching of elements in on request. The following resources supports the batch mode. * Article @@ -32,23 +30,47 @@ The following resources supports the batch mode. To use the batch mode, send a PUT request without an id in the URL. -```php -$restClient->put( - 'articles/', - array( - array('id' => 1, 'name' => '...'), - array('id' => 1, 'name' => '...'), - array('name' => '...'), - array('name' => '...') - ) -); - -$restClient->delete( - 'articles/', - array( - array('id' => 2), - array('id' => 4), - array('id' => 6) - ) -); +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'PUT', 'body': true} %} +```json +[ + { + "name": "Lorem", + "taxId": 1, + "mainDetail": { + "number": "SW123456" + } + }, + { + "name": "Ipsum", + "taxId": 1, + "mainDetail": { + "number": "SW123457" + } + }, + { + "name": "Dolor", + "taxId": 1, + "mainDetail": { + "number": "SW123458" + } + } +] +``` + +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'DELETE', 'body': true} %} +```json +[ + { + "id": 2 + }, + { + "id": 4 + }, + { + "id": 6 + }, + { + "id": 8 + } +] ``` diff --git a/source/developers-guide/rest-api/examples/cache/index.md b/source/developers-guide/rest-api/examples/cache/index.md index 76017c1ba9..32a1ba403e 100644 --- a/source/developers-guide/rest-api/examples/cache/index.md +++ b/source/developers-guide/rest-api/examples/cache/index.md @@ -2,8 +2,8 @@ layout: default title: REST API - Overview of the cache resources github_link: developers-guide/rest-api/examples/cache/index.md -menu_title: The cache resources -menu_order: 90 +menu_title: Cache examples +menu_order: 60 indexed: true menu_style: bullet group: Developer Guides @@ -12,93 +12,33 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the provided resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[category API resource](/developers-guide/rest-api/api-resource-categories/)** if you haven't yet, to get more information about the category resource and the data it provides. +In this article, you will find examples of the provided resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. +Please read the page covering the **[cache API resource](/developers-guide/rest-api/api-resource-cache/)** if you haven't yet, +to get more information about the cache resource and the data it provides. -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. +## Cache Resources +### Delete all caches -## Cache Resources +{% include 'api_badge.twig' with {'route': '/api/caches', 'method': 'DELETE'} %} + +### Delete the HTTP-cache + +{% include 'api_badge.twig' with {'route': '/api/caches/http', 'method': 'DELETE'} %} + +### Delete the template cache + +{% include 'api_badge.twig' with {'route': '/api/caches/template', 'method': 'DELETE'} %} + +### Retrieve information about the cache + +{% include 'api_badge.twig' with {'route': '/api/caches', 'method': 'GET'} %} + +### Retrieve information about the HTTP-cache + +{% include 'api_badge.twig' with {'route': '/api/caches/http', 'method': 'GET'} %} + +### Retrieve information about the template-cache - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    Resources
    -
    -
    HTTP
    -
    -
    - $cacheResource->delete('all'); - - DELETE api/caches/ - -
    Deletes all caches
    -
    - $cacheResource->delete('http'); - - DELETE api/caches/http - -
    Deletes the HTTP cache
    -
    - $cacheResource->delete('template'); - - DELETE api/caches/template - -
    Deletes the template cache
    -
    - $cacheResource->getList(); - - GET api/caches/ - -
    Gets the cache information
    -
    - $cacheResource->getOne('http'); - - GET api/caches/http - -
    Gets the HTTP cache information
    -
    - $cacheResource->getOne('template'); - - GET api/caches/template - -
    Gets the template cache information
    -
    +{% include 'api_badge.twig' with {'route': '/api/caches/template', 'method': 'GET'} %} diff --git a/source/developers-guide/rest-api/examples/category/index.md b/source/developers-guide/rest-api/examples/category/index.md index 401b304e46..57c57bf231 100644 --- a/source/developers-guide/rest-api/examples/category/index.md +++ b/source/developers-guide/rest-api/examples/category/index.md @@ -2,8 +2,8 @@ layout: default title: REST API - Examples using the category resource github_link: developers-guide/rest-api/examples/category/index.md -menu_title: The category resource -menu_order: 60 +menu_title: Category examples +menu_order: 80 indexed: true menu_style: bullet group: Developer Guides @@ -12,41 +12,58 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the provided resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[category API resource](/developers-guide/rest-api/api-resource-categories/)** if you haven't yet, to get more information about the category resource and the data it provides. - -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. - +In this article, you will find examples of the provided resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. +Please read the page covering the **[category API resource](/developers-guide/rest-api/api-resource-categories/)** if you haven't yet, +to get more information about the category resource and the data it provides. ## Example 1 - Creating a category This example adds a sub-category to category 3 -``` -$createCategory = array( - 'parentId' => 3, - 'name' => 'Test category' -); -$client->post('categories', $createCategory); - +{% include 'api_badge.twig' with {'route': '/api/categories', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Test-category", + "parentId": 3 +} ``` ## Example 2 - Adding categories with attributes and additional fields +{% include 'api_badge.twig' with {'route': '/api/categories', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Test-category", + "parentId": 3, + "metaDescription": "metaTest", + "metaKeywords": "keywordTest", + "cmsHeadline": "headlineTest", + "cmsText": "cmsTextTest", + "active": true, + "noViewSelect": true, + "attribute": { + "1": "Attribute1", + "2": "Attribute2" + } +} ``` -$categoryData = array( - "name" => "Test category", - "metaDescription" => "metaTest", - "metaKeywords" => "keywordTest", - "cmsHeadline" => "headlineTest", - "cmsText" => "cmsTextTest", - "active" => true, - "noViewSelect" => true, - "attribute" => array( - 1 => "Attribute1", - 2 => "Attribute2", - ) -); -$client->post('categories', $categoryData ); +## Example 3 - Create a category with translation +{% include 'api_badge.twig' with {'route': '/api/categories', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Test-category", + "parentId": 3, + "attribute": { + "1": "Attr1" + }, + "translations": { + "2": { + "shopId": 2, + "description": "Test category, english translation", + "__attribute_attribute1": "Attr1 English" + } + } +} ``` diff --git a/source/developers-guide/rest-api/examples/customer-streams/index.md b/source/developers-guide/rest-api/examples/customer-streams/index.md new file mode 100644 index 0000000000..6f9f922f11 --- /dev/null +++ b/source/developers-guide/rest-api/examples/customer-streams/index.md @@ -0,0 +1,263 @@ +--- +layout: default +title: REST API - Examples using customer streams +github_link: developers-guide/rest-api/customer-streams/index.md +menu_title: Customer Streams Examples +menu_order: 115 +indexed: true +menu_style: bullet +group: Developer Guides +subgroup: REST API +--- + +## Introduction + +In this article, you will find examples which demonstrate how to create, delete, get and index **[Customer Streams](/developers-guide/customer-streams-extension)** and how to rebuild the search index. +For each scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. +Please read the page covering the **[REST API Basics](/developers-guide/rest-api/)** if you haven't yet. + +## Build Search Index +To assure high performance, even when working with large data sets, all customer must be indexed regularly. +The index is basically a cache which contains all information which are needed for the Customer Stream module. + +*Available arguments:* +| Argument | Type | Required | Description | +|------------------|---------|----------|-----------------------------------------------| +| buildSearchIndex | boolean | Yes | Search index will be (re)build if set to true | + + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/customer_streams', 'method': 'POST', 'body': true} %} +```json +{ + "buildSearchIndex": true +} +``` + +*Example output:* + +```json +{ + "success": true +} +``` + +## Create a new customer stream +There are two types of Customer Streams in Shopware.: + +* Dynamic: Is defined by a set of conditions which are chained via AND. +All customer which match all of these conditions will be added to the stream if they are +a) already analysed and +b) if the stream has been index. + +* Static: Is defined manually, which means that you assign customer ids to the stream. +Static streams will only change if you remove or add customers. + +### Dynamic streams +Define a new dynamic stream. + +*Available arguments:* + +| Argument | Type | Required | Description | +|-------------|---------|----------|----------------------------------------------------------| +| name | string | Yes | Name of the Customer Stream | +| static | boolean | Yes | Stream type (true=static stream/false=dynamic stream) | +| description | string | | Description of the Customer Stream | +| conditions | string | Yes | List of conditions (be aware of the format and escaping) | +| indexStream | boolean | | Stream will be index if set to true | + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/customer_streams', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Dynamic api stream", + "static": false, + "description": "Stream created over the api which will be indexed immediately", + "conditions": "{\"Shopware\\\\Bundle\\\\CustomerSearchBundle\\\\Condition\\\\HasOrderCountCondition\":{\"operator\":\"=\",\"minimumOrderCount\":1}}", + "indexStream": true +} +``` + +*Example output:* + +```json +{ + "success": true, + "data": { + "id": 9, + "location": "http://localhost/53/api/customer_streams/9" + } +} +``` + +### Static streams +Define a new static stream and assign customers to it. + +*Available arguments:* + +| Argument | Type | Required | Description | +|-------------|-----------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | Yes | Name of the Customer Stream | +| static | boolean | Yes | Stream type (true=static stream/false=dynamic stream) | +| description | string | | Description of the Customer Stream | +| customers | int array | | List of customer which will be assigned to the stream | +| freezeUp | string | | Date/Time until the stream is static (will be processed by DateTime). [Here](http://php.net/manual/de/datetime.formats.php) is a list of supported formats. | + + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/customer_streams', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Static api stream", + "static": true, + "description": "Static stream created over the api", + "customers": [ + 1, + 2 + ] +} +``` + +*Example output:* + +```json +{ + "success": true, + "data": { + "id": 5, + "location": "http://localhost/53/api/customer_streams/5" + } +} +``` +## (Re)Index an existing stream +*Available arguments:* + +| Argument | Type | Required | Description | +|-------------|---------|----------|-------------------------------------------------------------------------| +| id | integer | Yes | ID of the Customer Stream | +| indexStream | boolean | | Stream will be reindex if set to true (only works with dynamic streams) | + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/customer_streams/42', 'method': 'PUT', 'body': true} %} +```json +{ + "indexStream": true +} +``` + +*Example output:* + +```json +{ + "success": true, + "data": { + "id": 1, + "location": "http://localhost/53/api/customer_streams/1" + } +} +``` + +## Get information about an existing stream + +List customers which are assigned to a given stream id. +You can add additional conditions or sorting. +You can also define an offset/limit to process the stream in chunks. + +*Available arguments:* + +| Argument | Type | Required | Description | +|------------|--------------|----------|------------------------------------------------------------------------| +| id | integer | Yes | ID of the Customer Stream | +| offset | integer | | Offset (ideal for batch processing, when working with large data sets) | +| limit | integer | | Maximum number of returned data sets | +| conditions | string array | | Additional conditions | +| sortings | string array | | Sorting handler which will be applied to the result set | + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/customer_streams/6', 'method': 'GET', 'body': true} %} +```json +{ + "conditions": "{\"Shopware\\\\Bundle\\\\CustomerSearchBundle\\\\Condition\\\\HasOrderCountCondition\":{\"operator\":\"=>\",\"minimumOrderCount\":1}}", + "sortings": "{\"Shopware\\\\Bundle\\\\CustomerSearchBundle\\\\Sorting\\\\NumberSorting\":{\"direction\":\"DESC\"}}" +} +``` + +*Example output:* + +```json +{ + "data": [ + { + "id": 1, + "number": "20001", + "email": "test@example.com", + "attributes": { + "search": { + "id": "1", + "customernumber": "20001", + "email": "test@example.com" + } + } + }, + { + "id": 2, + "number": "20003", + "email": "mustermann@b2b.de", + "attributes": { + "search": { + "id": "2", + "customernumber": "20003", + "email": "mustermann@b2b.de" + } + } + } + ], + "total": 2, + "success": true +} +``` + +## Get a list of all streams + +List all streams and their attributes. + +*Example code:* + +{% include 'api_badge.twig' with {'route': '/api/customer_streams', 'method': 'GET'} %} + +*Example output:* +```json +{ + "data": [ + { + "id": 1, + "name": "Example stream", + "description": "Example description", + "conditions": "{\"Shopware\\\\Bundle\\\\CustomerSearchBundle\\\\Condition\\\\HasTotalOrderAmountCondition\":{\"operator\":\">=\",\"minimumOrderAmount\":150}}", + "static": false, + "freezeUp": null, + "attribute": null, + "customer_count": "2", + "newsletter_count": "0" + }, + { + "id": 2, + "name": "Static stream example", + "description": "Example description", + "conditions": "{\"Shopware\\\\Bundle\\\\CustomerSearchBundle\\\\Condition\\\\HasOrderCountCondition\":{\"operator\":\"=\",\"minimumOrderCount\":1}}", + "static": true, + "freezeUp": "2017-08-24T07:45:00+0200", + "attribute": null, + "customer_count": "1", + "newsletter_count": "0" + } + ], + "total": 2, + "success": true +} +``` diff --git a/source/developers-guide/rest-api/examples/customer/index.md b/source/developers-guide/rest-api/examples/customer/index.md index 61071baf64..2c65685f52 100644 --- a/source/developers-guide/rest-api/examples/customer/index.md +++ b/source/developers-guide/rest-api/examples/customer/index.md @@ -2,8 +2,8 @@ layout: default title: REST API - Examples using the customer resource github_link: developers-guide/rest-api/examples/customer/index.md -menu_title: The customer resource -menu_order: 30 +menu_title: Customer examples +menu_order: 110 indexed: true menu_style: bullet group: Developer Guides @@ -14,24 +14,22 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the customer resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[customer API resource](/developers-guide/rest-api/api-resource-customer)** if you haven't yet, to get more information about the customer resource and the data it provides. - -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. +In this article, you will find examples of the customer resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. +Please read the page covering the **[customer API resource](/developers-guide/rest-api/api-resource-customer)** if you haven't yet, +to get more information about the customer resource and the data it provides. ## Example 1 - Get all customers In this example, you can see how it's possible to get a list of all customers in a shop and how to limit the result to a fixed number. -``` -$client->get('customers'); -$client->get('customers?limit=20'); +{% include 'api_badge.twig' with {'route': '/api/customers', 'method': 'GET'} %} -``` +{% include 'api_badge.twig' with {'route': '/api/customers?limit=20', 'method': 'GET'} %} ### Result -``` +```json { "data":[ { @@ -43,6 +41,9 @@ $client->get('customers?limit=20'); "encoderName":"md5", "hashPassword":"a256a310bc1e5db755fd392c524028a8", "active":true, + "doubleOptinConfirmDate":"2011-11-23T12:00:00+0100", + "doubleOptinEmailSentDate":"2011-11-23T00:00:00+0100", + "doubleOptinRegister":true, "email":"test@example.com", "salutation": "mr", "firstname": "Max", @@ -71,6 +72,9 @@ $client->get('customers?limit=20'); "encoderName":"md5", "hashPassword":"352db51c3ff06159d380d3d9935ec814", "active":true, + "doubleOptinConfirmDate":null, + "doubleOptinEmailSentDate":null, + "doubleOptinRegister":false, "email":"mustermann@b2b.com", "salutation": "mr", "firstname": "Max", @@ -97,18 +101,17 @@ $client->get('customers?limit=20'); ``` -## Example 2 - Get a specific customer +## Example 2 - Get a specific customer account -This example shows you how to get a customer by its id +This example shows you how to get a customer account by its id -``` -$client->get('customers/1'); -``` +{% include 'api_badge.twig' with {'route': '/api/customers/1', 'method': 'GET'} %} ### Result -``` +```json { "data":{ + "number": "20001", "id":1, "paymentId":5, "groupKey":"EK", @@ -117,17 +120,22 @@ $client->get('customers/1'); "encoderName":"md5", "hashPassword":"a256a310bc1e5db755fd392c524028a8", "active":true, + "doubleOptinConfirmDate":null, + "doubleOptinEmailSentDate":null, + "doubleOptinRegister":false, "email":"test@example.com", "salutation": "mr", + "title": "", "firstname": "Max", "lastname": "Mustermann", "firstLogin":"2011-11-23T00:00:00+0100", "lastLogin":"2012-01-04T14:12:05+0100", + "birthday": null, "accountMode":0, "confirmationKey":"", "sessionId":"uiorqd755gaar8dn89ukp178c7", "newsletter":0, - "validation":"", + "validation":"0", "affiliate":0, "paymentPreset":0, "languageId":"1", @@ -135,106 +143,183 @@ $client->get('customers/1'); "internalComment":"", "failedLogins":0, "lockedUntil":null, - "attribute":null, - "billing":{ - "id":1, - "customerId":1, - "country":2, - "state":3, - "company":"Muster GmbH", - "department":"", - "salutation":"mr", - "number":"20001", - "firstname":"Max", - "lastname":"Mustermann", - "street":"Musterstr. 55", - "zipCode":"55555", - "city":"Musterhausen", - "phone":"05555 \/ 555555", - "fax":"", - "vatId":"", - "birthday":null, - "attribute":null + "attribute":{ + "id": 1, + "customerId": 1, + }, + "defaultBillingAddress": { + "id": 1, + "company": "Muster GmbH", + "department": "", + "salutation": "mr", + "firstname": "Max", + "title": null, + "lastname": "Mustermann", + "street": "Musterstr. 55", + "zipcode": "55555", + "city": "Musterhausen", + "phone": "05555 \/ 555555", + "vatId": "", + "additionalAddressLine1": null, + "additionalAddressLine2": null, + "countryId": 2, + "stateId": 3, + "attribute": { + "id": 1, + "customerAddressId": 1, + "text1": null, + "text2": null, + "text3": null, + "text4": null, + "text5": null, + "text6": null + }, + "country": { + "id": 2, + "name": "Deutschland", + "iso": "DE", + "isoName": "GERMANY", + "position": 1, + "description": "", + "taxFree": 0, + "taxFreeUstId": 0, + "taxFreeUstIdChecked": 0, + "active": true, + "iso3": "DEU", + "displayStateInRegistration": false, + "forceStateInRegistration": false, + "areaId": 1 + }, + "state": null }, "paymentData":[ - ], - "shipping":{ - "id":2, - "customerId":1, - "company":"shopware AG", - "department":"", - "salutation":"mr", - "firstname":"Max", - "lastname":"Mustermann", - "street":"Mustermannstra\u00dfe 55", - "zipCode":"48624", - "city":"Sch\u00f6ppingen", - "state":null, - "country":2, - "attribute":null - }, - "debit":{ - "id":2, - "customerId":1, - "account":"1234566", - "bankCode":"6654321", - "bankName":"Bank", - "accountHolder":"Owner" + "defaultShippingAddress": { + "id": 2, + "company": "shopware AG", + "department": "", + "salutation": "mr", + "firstname": "Max", + "title": null, + "lastname": "Mustermann", + "street": "Mustermannstra\u00dfe 55", + "zipcode": "48624", + "city": "Sch\u00f6ppingen", + "phone": null, + "vatId": "", + "additionalAddressLine1": null, + "additionalAddressLine2": null, + "countryId": 2, + "stateId": null, + "attribute": { + "id": 2, + "customerAddressId": 1, + "text1": null, + "text2": null, + "text3": null, + "text4": null, + "text5": null, + "text6": null + }, + "country": { + "id": 2, + "name": "Deutschland", + "iso": "DE", + "isoName": "GERMANY", + "position": 1, + "description": "", + "taxFree": 0, + "taxFreeUstId": 0, + "taxFreeUstIdChecked": 0, + "active": true, + "iso3": "DEU", + "displayStateInRegistration": false, + "forceStateInRegistration": false, + "areaId": 1 + }, + "state": null } }, "success":true } ``` -## Example 3 - Create customer +## Example 3 - Create a customer account -This shows you how to create a minimalistic customer: +This shows you how to create a minimalistic customer account: -``` -$client->post('customers', array( - 'email' => 'meier@mail.de', - 'firstname' => 'Max', - 'lastname' => 'Meier', - 'salutation' => 'mr', - 'billing' => array( - 'firstname' => 'Max', - 'lastname' => 'Meier', - 'salutation' => 'mr', - 'street' => 'Musterstrasse 55', - 'city' => 'Sch\u00f6ppingen', - 'zipcode' => '48624', - 'country' => 2 - ) -)); +{% include 'api_badge.twig' with {'route': '/api/customers', 'method': 'POST', 'body': true} %} +```json +{ + "email": "meier@mail.de", + "firstname": "Max", + "lastname": "Meier", + "salutation": "mr", + "billing": { + "firstname": "Max", + "lastname": "Meier", + "salutation": "mr", + "street": "Musterstrasse 55", + "city": "Sch\\u00f6ppingen", + "zipcode": "48624", + "country": 2 + } +} ``` ### Result -If the customer was created successfully this will be returned: - -``` -Array -( - [id] => 15 - [location] => http://www.ihredomain.de/api/customers/15 -) +```json +{ + "id": 15, + "location": "https://shop.example.com/api/customers/15" +} ``` -## Example 4 - Update specific customer +## Example 4 - Create customer account with Double-Opt-In confirmation -It's possible to update a customer by passing its identifier and the new fields. +This examples shows how to add a minimalistic customer using `'doubleOptinRegister' => true` to register him during +Double-Opt-In and `'sendOptinMail' => true` to send him the required E-Mail with a confirmation link to complete his registration: +{% include 'api_badge.twig' with {'route': '/api/customers', 'method': 'POST', 'body': true} %} +```json +{ + "email": "meier@mail.de", + "firstname": "Max", + "lastname": "Meier", + "salutation": "mr", + "doubleOptinRegister": true, + "sendOptinMail": true, + "billing": { + "firstname": "Max", + "lastname": "Meier", + "salutation": "mr", + "street": "Musterstrasse 55", + "city": "Sch\\u00f6ppingen", + "zipcode": "48624", + "country": 2 + } +} ``` -$client->put('customers/1', array( - 'email' => 'new@mail.de' -)); +### Result + +```json +{ + "id": 16, + "location": "https://shop.example.com/api/customers/16" +} ``` -## Example 5 - Delete a specific customer +## Example 5 - Update a specific customer account +{% include 'api_badge.twig' with {'route': '/api/customers/1', 'method': 'PUT', 'body': true} %} +```json +{ + "email": "updated@example.com" +} ``` -$client->delete('customers/1'); -``` +## Example 6 - Delete a specific customer account + +{% include 'api_badge.twig' with {'route': '/api/customers/12', 'method': 'DELETE'} %} diff --git a/source/developers-guide/rest-api/examples/filter/index.md b/source/developers-guide/rest-api/examples/filter/index.md index 034c24ad04..ad3a751058 100644 --- a/source/developers-guide/rest-api/examples/filter/index.md +++ b/source/developers-guide/rest-api/examples/filter/index.md @@ -3,7 +3,7 @@ layout: default title: REST API - Examples using filter github_link: developers-guide/rest-api/examples/media/index.md menu_title: Filter -menu_order: 110 +menu_order: 300 indexed: true menu_style: bullet group: Developer Guides @@ -12,16 +12,13 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the provided resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[category API resource](/developers-guide/rest-api/api-resource-categories/)** if you haven't yet, to get more information about the category resource and the data it provides. - -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. - +In this article, you will find examples of the provided resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. ## Filter by language In this article you can read more about the filter function. The filter functionality is for limiting the result. - + ```php $translationResource->getList(0, 10, array( array('property' => 'translation.shopId', 'value' => 2) @@ -38,7 +35,7 @@ $translationResource->getList(0, 1, array( )); ``` -Example output: +**Example output:** ```php array('total' => 246, 'data' => array( @@ -72,3 +69,25 @@ array('total' => 246, 'data' => array( ) )); ``` + +## Example 1 - Filter products by name + +{% include 'api_badge.twig' with {'route': '/api/articles?filter[name]=Lorem', 'method': 'GET'} %} + +{% include 'api_badge.twig' with {'route': '/api/articles?filter[0][property]=name&filter[0][value]=Ipsum%', 'method': 'GET'} %} + +{% include 'api_badge.twig' with {'route': '/api/articles?filter[0][property]=name&filter[0][expression]=LIKE&filter[0][value]=%Dolor%', 'method': 'GET'} %} + +### Result + +```json +{ + "data": [ + {}, + {}, + {} + ], + "success": true, + "total": 3 +} +``` diff --git a/source/developers-guide/rest-api/examples/media/index.md b/source/developers-guide/rest-api/examples/media/index.md index 80c2deea17..a734a62dba 100644 --- a/source/developers-guide/rest-api/examples/media/index.md +++ b/source/developers-guide/rest-api/examples/media/index.md @@ -2,8 +2,8 @@ layout: default title: REST API - Examples using the media resource github_link: developers-guide/rest-api/examples/media/index.md -menu_title: The media resource -menu_order: 70 +menu_title: Media examples +menu_order: 160 indexed: true menu_style: bullet group: Developer Guides @@ -14,11 +14,10 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the provided resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[media API resource](/developers-guide/rest-api/api-resource-media/)** if you haven't yet, to get more information about the media resource and the data it provides. - -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. - +In this article, you will find examples of the provided resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. +Please read the page covering the **[media API resource](/developers-guide/rest-api/api-resource-media/)** if you haven't yet, +to get more information about the media resource and the data it provides. ## Image assignment @@ -27,181 +26,265 @@ With the variant resource it is possible to create images for variants. This con ### Configuration 1 The image is assigned by the mediaId. -If the image array contains a mediaId, the resource first checks whether the media file is already assigned as an product image. +If the image array contains a mediaId, the resource first checks whether the media file is already assigned as a product image. If no image with the given media ID exists, the resource creates a new product image with this media ID. ### Configuration 2 Passing an image URL. -This function is copied from the article image array. The image URL can be a local file, base64, or some other type supported by the article and media resource. -Both configurations automatically create the child data sets in s_articles_img and the relation for the backend configuration. - -Example: (Updates an existing variant using the resource variants and assigns two types of images.) - -```php -// PUT /api/variants/1042 -array( - 'id' => 1042, - 'articleId' => 278, - 'images' => array( - array('mediaId' => 2), - array('link' => 'http://.....') - ), -); +This function is copied from the article image array. +The image URL can be a local file, base64, or some other type supported by the article and media resource. + +**Types:** +http, https, file, ftp, ftps +Example for file on server `file:///var/www/shopware/media/upload/test.jpg` + +Both configurations automatically create the child data sets in `s_articles_img` and the relation for the backend configuration. + +**Example:** (Updates an existing variant using the resource `variants` and assigns two types of images.) + +{% include 'api_badge.twig' with {'route': '/api/variants/1042', 'method': 'PUT', 'body': true} %} +```json +{ + "id": 1042, + "articleId": 278, + "images": [ + { + "mediaId": 2 + }, + { + "link": "http:\/\/example.com\/example.png" + } + ] +} ``` ## The configuration for image assignment In addition to the image mapping option, it is possible to create new assignments by using the article resource. -This way generates no child data sets in s_articles_img because a product could contain 10.000 variants. The generation for each relation would be too slow. +This way generates no child data sets in `s_articles_img` because a product could contain 10.000 variants. +The generation for each relation would be too slow. It is necessary to call a additional API request to generate images for each variant. -The following example creates a product with two variants. The first variant with "0,2 Liter" and the second variant with "0,5 Liter". -In addition the "0,2 Liter" variant becomes a variant image. - -```php -array( - 'name' => 'Testartikel', - 'description' => 'Test description', - 'active' => true, - 'mainDetail' => array( - 'number' => 'swTEST52b04c97da770', - 'inStock' => 15, - 'unitId' => 1, - 'prices' => array( - array('customerGroupKey' => 'EK','from' => 1,'to' => '-', 'price' => 400) - ) - ), - 'taxId' => 1, - 'supplierId' => 2, - 'images' => array( - array( - 'mediaId' => 236, - 'options' => array( - array( - array('name' => '0,2 Liter') - ) - ) - ) - ), - 'configuratorSet' => array( - 'name' => 'Test-Set', - 'groups' => array( - array( - 'id' => 5, - 'name' => 'Flascheninhalt', - 'options' => array( - array('id' => 11, 'name' => '0,2 Liter'), - array('id' => 35, 'name' => '0,5 Liter'), - ) - ) - ) - ), - 'variants' => array( - array( - 'number' => 'swTEST52b04c97dd280', - 'inStock' => 100, - 'unitId' => 1, - 'prices' => array( - array('customerGroupKey' => 'EK','from' => 1,'to' => '-','price' => 400) - ), - 'configuratorOptions' => array( - array('option' => '0,2 Liter', 'groupId' => 5) - ) - ), - array( - 'number' => 'swTEST52b04c97dd28f', - 'inStock' => 100, - 'unitId' => 1, - 'prices' => array( - array('customerGroupKey' => 'EK', 'from' => 1, 'to' => '-', 'price' => 400) - ), - 'configuratorOptions' => array( - array('option' => '0,5 Liter', 'groupId' => 5) - ) - ) - ) -); +The following example creates a product with two variants. The first variant with `"0,2 Liter"` and the second variant with `"0,5 Liter"`. +In addition, a variant image is assigned to the `"0,2 Liter"` variant. + +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'POST', 'body': true} %} +```json +{ + "name": "Testartikel", + "description": "Test description", + "active": true, + "mainDetail": { + "number": "swTEST52b04c97da770", + "inStock": 15, + "unitId": 1, + "prices": [ + { + "customerGroupKey": "EK", + "from": 1, + "to": "-", + "price": 400 + } + ] + }, + "taxId": 1, + "supplierId": 2, + "images": [ + { + "mediaId": 236, + "options": [ + [ + { + "name": "0,2 Liter" + } + ] + ] + } + ], + "configuratorSet": { + "name": "Test-Set", + "groups": [ + { + "id": 5, + "name": "Flascheninhalt", + "options": [ + { + "id": 11, + "name": "0,2 Liter" + }, + { + "id": 35, + "name": "0,5 Liter" + } + ] + } + ] + }, + "variants": [ + { + "number": "swTEST52b04c97dd280", + "inStock": 100, + "unitId": 1, + "prices": [ + { + "customerGroupKey": "EK", + "from": 1, + "to": "-", + "price": 400 + } + ], + "configuratorOptions": [ + { + "option": "0,2 Liter", + "groupId": 5 + } + ] + }, + { + "number": "swTEST52b04c97dd28f", + "inStock": 100, + "unitId": 1, + "prices": [ + { + "customerGroupKey": "EK", + "from": 1, + "to": "-", + "price": 400 + } + ], + "configuratorOptions": [ + { + "option": "0,5 Liter", + "groupId": 5 + } + ] + } + ] +} ``` ### The following example creates two assignments for the passed image. -* Show the image if the customer selects "0,5 Liter" and "Rot". +* Show the image if the customer selects `"0,5 Liter"` and "Rot". * Show the image if the customer selects "Blau". -```php -array( - 'name' => 'Testartikel', - 'images' => array( - array( - 'mediaId' => 236, - 'options' => array( - array( - array('name' => '0,5 Liter'), - array('name' => 'rot') - ), - array( - array('name' => 'blau') - ) - ), - ), - ), - 'configuratorSet' => array( - 'name' => 'Test-Set', - 'groups' => array( - array( - 'id' => 5, - 'name' => 'Flascheninhalt', - 'options' => array( - array('id' => 11,'name' => '0,2 Liter'), - array('id' => 35,'name' => '0,5 Liter'), - array('id' => 12,'name' => '0,7 Liter'), - array('id' => 32,'name' => '1,0 Liter'), - ), - ), - array( - 'id' => 6, - 'name' => 'Farbe', - 'options' => array( - array('id' => 13,'name' => 'weiss'), - array('id' => 14,'name' => 'schwarz'), - array('id' => 15,'name' => 'blau'), - array('id' => 28,'name' => 'rot'), - ), - ), - ), - ), - 'variants' => array(...) -); +{% include 'api_badge.twig' with {'route': '/api/articles/1234', 'method': 'PUT', 'body': true} %} +```json +{ + "name": "Testartikel", + "images": [ + { + "mediaId": 236, + "options": [ + [ + { + "name": "0,5 Liter" + }, + { + "name": "rot" + } + ], + [ + { + "name": "blau" + } + ] + ] + } + ], + "configuratorSet": { + "name": "Test-Set", + "groups": [ + { + "id": 5, + "name": "Flascheninhalt", + "options": [ + { + "id": 11, + "name": "0,2 Liter" + }, + { + "id": 35, + "name": "0,5 Liter" + }, + { + "id": 12, + "name": "0,7 Liter" + }, + { + "id": 32, + "name": "1,0 Liter" + } + ] + }, + { + "id": 6, + "name": "Farbe", + "options": [ + { + "id": 13, + "name": "weiss" + }, + { + "id": 14, + "name": "schwarz" + }, + { + "id": 15, + "name": "blau" + }, + { + "id": 28, + "name": "rot" + } + ] + } + ] + }, + "variants": [ + {} + ] +} ``` ### Assignment The first level of the option array defines how many assignment are created. -You can define AND / OR assignment for each assignment. - -```php -'options' => array( - array( - array('name' => '0,5 Liter'), - // AND - array('name' => 'rot') - ), - // OR - array( - array('name' => 'blau') - ) -) -``` - -### optimize the API performance - -To optimize the API performance it is necessary to send a second API request to create the child data sets in s_articles_img. +You can define `AND / OR` assignment for each assignment. -```php -PUT /api/generateArticleImages/1 -PUT /api/generateArticleImages/SW-200?useNumberAsId=true +```json +{ + "configuratorSet": { + // ... + "groups": [ + { + "options": [ + [ + { + "name": "0,5 Liter" + }, + // AND + { + "name": "rot" + } + ], + // OR + [ + { + "name": "blau" + } + ] + ] + } + ] + } +} ``` +### optimize the API performance +To optimize the API performance it is necessary to send a second API request to create the child data sets in `s_articles_img`. +{% include 'api_badge.twig' with {'route': '/api/generateArticleImages/1', 'method': 'PUT'} %} +{% include 'api_badge.twig' with {'route': '/api/generateArticleImages/SW-20001?useNumberAsId=true', 'method': 'PUT'} %} diff --git a/source/developers-guide/rest-api/examples/merge-mode/index.md b/source/developers-guide/rest-api/examples/merge-mode/index.md index 4c186c1317..214191930f 100644 --- a/source/developers-guide/rest-api/examples/merge-mode/index.md +++ b/source/developers-guide/rest-api/examples/merge-mode/index.md @@ -2,8 +2,8 @@ layout: default title: REST API - Examples using the merge mode github_link: developers-guide/rest-api/examples/merge-mode/index.md -menu_title: The merge mode -menu_order: 80 +menu_title: Merge mode +menu_order: 280 indexed: true menu_style: bullet group: Developer Guides @@ -14,105 +14,133 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the provided resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[category API resource](/developers-guide/rest-api/api-resource-categories/)** if you haven't yet, to get more information about the category resource and the data it provides. - -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. - +In this article, you will find examples of the provided resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. It is possible to change the default behavior of the API to process associated data. ## API merge Mode (Data merge) -Example situation: -* There exists a product with two images in the database. Artikel-ID: 1 and Bild-ID: 2, 3 +**Example situation:** +* There exists a product with two images in the database. `product-id: 1` and `image-id: [2, 3]` * The first example overwrites the images with two new images. * The second example adds two new images. ### Example request 1: (overwrite) -```php -// PUT /api/articles/1 -array( - '__options_images' => array('replace' => true), - 'images' => array( - 112 => array( - 'mediaId' => 112, - 'main' => 1, - ), - 113 => array( - 'mediaId' => 113, - 'main' => '2', - ) - ), -); + +{% include 'api_badge.twig' with {'route': '/api/articles/1', 'method': 'PUT', 'body': true} %} +```json +{ + "__options_images": { + "replace": true + }, + "images": { + "112": { + "mediaId": 112, + "main": 1 + }, + "113": { + "mediaId": 113, + "main": "2" + } + } +} ``` ### Example request 2: (Merge) -```php -// PUT /api/articles/1 -array( - '__options_images' => array('replace' => false), - 'images' => array( - 114 => array( - 'mediaId' => 114, - 'main' => 2, - ), - 115 => array( - 'mediaId' => 115, - 'main' => '2', - ) - ), -); +{% include 'api_badge.twig' with {'route': '/api/articles/1', 'method': 'PUT', 'body': true} %} +```json +{ + "__options_images": { + "replace": false + }, + "images": { + "114": { + "mediaId": 114, + "main": 2 + }, + "115": { + "mediaId": 115, + "main": "2" + } + } +} ``` -## The following collection implements the "merge mode" +## Entities implementing the "merge mode" + +The following list contains all entities implementing the merge mode. +The "replace" value given here represents the default value which is used +when no value for `replace` is given in the request body. + +A default value of `replace: true` means, that the existing entity should +be overwritten, `replace: false` means, that the existing entities should +be merged with the new ones provided in the API-request. -```php -$articleData = array( - // default: replace - '__options_categories' => array('replace' => true), - 'categories' => array( - array('id' => 13) - ), - // default: replace - '__options_related' => array('replace' => true), - 'related' => array( - array('id' => 13) - ), - // default: replace - '__options_similar' => array('replace' => true), - 'similar' => array( - array('id' => 13) - ), - // default: replace - '__options_downloads' => array('replace' => true), - 'downloads' => array( - array('id' => 13) - ), - // default: replace - '__options_customerGroups' => array('replace' => true), - 'customerGroups' => array( - array('id' => 13) - ), - // default: merge - '__options_images' => array('replace' => false), - 'images' => array( - array('id' => 13) - ), - // default: replace - '__options_variants' => array('replace' => true), - 'variants' => array( - array( - 'id' => 13 - ) - ), - 'mainDetail' => array( - // default: replace - '__options_prices' => array('replace' => true), - 'prices' => array( - - ) - ) -); +```json +{ + "__options_categories": { + "replace": true + }, + "categories": [ + { + "id": 13 + } + ], + "__options_related": { + "replace": true + }, + "related": [ + { + "id": 13 + } + ], + "__options_similar": { + "replace": true + }, + "similar": [ + { + "id": 13 + } + ], + "__options_downloads": { + "replace": true + }, + "downloads": [ + { + "id": 13 + } + ], + "__options_customerGroups": { + "replace": true + }, + "customerGroups": [ + { + "id": 13 + } + ], + "__options_images": { + "replace": false + }, + "images": [ + { + "id": 13 + } + ], + "__options_variants": { + "replace": true + }, + "variants": [ + { + "id": 13 + } + ], + "mainDetail": { + "__options_prices": { + "replace": true + }, + "prices": [] + } +} ``` diff --git a/source/developers-guide/rest-api/examples/order/index.md b/source/developers-guide/rest-api/examples/order/index.md index 134c5ccfe0..ca43dd4676 100644 --- a/source/developers-guide/rest-api/examples/order/index.md +++ b/source/developers-guide/rest-api/examples/order/index.md @@ -1,8 +1,8 @@ --- layout: default title: REST API - Examples using the order resource -menu_title: The order resource -menu_order: 40 +menu_title: Order examples +menu_order: 180 indexed: true menu_style: bullet group: Developer Guides @@ -13,21 +13,20 @@ subgroup: REST API ## Introduction -In this article you can read more about using the orders resource. +In this article you can read more about using the order resource. The following part will show you examples including provided data and data you need to provide if you want to use this resource. -Please read **[Orders](/developers-guide/rest-api/api-resource-orders/)** if you did not yet, to get more information about the orders resource and the data it provides. -Also we are using the API client of the following document **[API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. +Please read the page covering the **[order API resource](/developers-guide/rest-api/api-resource-orders/)** if you did not yet, +to get more information about the order resource and the data it provides. ## Example 1 - Load all orders This example shows you how to get all orders of the shop. You may also limit the result count by providing a limit parameter. -``` -$client->get('orders'); -$client->get('orders?limit=20'); //Will only get 20 orders -``` +{% include 'api_badge.twig' with {'route': '/api/orders', 'method': 'GET'} %} + +{% include 'api_badge.twig' with {'route': '/api/orders?limit=20', 'method': 'GET'} %} ### Result -``` +```json { "data":[ { @@ -160,14 +159,13 @@ $client->get('orders?limit=20'); //Will only get 20 orders To load a specific order, you have to provide either the identifier or the number of the order. -``` -$client->get('orders/15'); -$client->get('orders/2002?useNumberAsId=true'); -``` +{% include 'api_badge.twig' with {'route': '/api/orders/15', 'method': 'GET'} %} + +{% include 'api_badge.twig' with {'route': '/api/orders/2002?useNumberAsId=true', 'method': 'GET'} %} ### Result -``` +```json { "data":{ "id":15, @@ -462,7 +460,7 @@ $client->get('orders/2002?useNumberAsId=true'); "title":null, "position":0, "host":null, - "basePath":"\/master", + "basePath":"\/shop", "baseUrl":null, "hosts":"", "secure":false, @@ -520,7 +518,7 @@ $client->get('orders/2002?useNumberAsId=true'); "title":null, "position":0, "host":null, - "basePath":"\/master", + "basePath":"\/shop", "baseUrl":null, "hosts":"", "secure":false, @@ -548,39 +546,35 @@ $client->get('orders/2002?useNumberAsId=true'); ## Example 3 - Update an order **Currently, it's only possible to update the following fields of an order:** -``` -paymentStatusId -orderStatusId -trackingCode -comment -transactionId -clearedDate -``` +- `paymentStatusId` +- `orderStatusId` +- `trackingCode` +- `comment` +- `transactionId` +- `clearedDate` -This example shows you how to update those fields: -``` -$date = new DateTime(); -$date->modify('+10 days'); -$date = $date->format(DateTime::ISO8601); - -$client->put('orders/15', array( - 'paymentStatusId' => 10, - 'orderStatusId' => 8, - 'trackingCode' => '237948723894789234', - 'comment' => 'Neuer Kommentar', - 'transactionId' => '0', - 'clearedDate' => $date -)); +This example shows you how to update those fields, dates should be +encoded according to the **[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)** standard: + +{% include 'api_badge.twig' with {'route': '/api/orders/15', 'method': 'PUT', 'body': true} %} +```json +{ + "paymentStatusId": 10, + "orderStatusId": 8, + "trackingCode": "237948723894789234", + "comment": "Neuer Kommentar", + "transactionId": "0", + "clearedDate": "2019-10-18T17:58:17+0000" +} ``` ### Result -``` -( - [id] => 2 - [location] => http://www.yourdomain.com/api/orders/2 -) - +```json +{ + "id": 2, + "location": "https://shop.example.com/api/orders/2" +} ``` ## Example 4 - Creating an order @@ -591,152 +585,142 @@ This example shows you how to create an order. Currently all referenced entities If some field is missing from the request or some id provided does not exist, an exception is returned accordingly. -``` -$client->post('orders', [ - "customerId" => 1, - "paymentId" => 4, - "dispatchId" => 9, - "partnerId" => "", - "shopId" => 1, - "invoiceAmount" => 201.86, - "invoiceAmountNet" => 169.63, - "invoiceShipping" => 0, - "invoiceShippingNet" => 0, - "orderTime" => "2012-08-31 08:51:46", - "net" => 0, - "taxFree" => 0, - "languageIso" => "1", - "currency" => "EUR", - "currencyFactor" => 1, - "remoteAddress" => "217.86.205.141", - "details" => [[ - "articleId" => 220, - "taxId" => 1, - "taxRate" => 19, - "statusId" => 0, - "articleNumber" => "SW10001", - "price" => 35.99, - "quantity" => 1, - "articleName" => "Versandkostenfreier Artikel", - "shipped" => 0, - "shippedGroup" => 0, - "mode" => 0, - "esdArticle" => 0, - ], [ - "articleId" => 219, - "taxId" => 1, - "taxRate" => 19, - "statusId" => 0, - "articleNumber" => "SW10185", - "price" => 54.9, - "quantity" => 1, - "articleName" => "Express Versand", - "shipped" => 0, - "shippedGroup" => 0, - "mode" => 0, - "esdArticle" => 0, - ], [ - "articleId" => 197, - "taxId" => 1, - "taxRate" => 19, - "statusId" => 0, - "articleNumber" => "SW10196", - "price" => 34.99, - "quantity" => 2, - "articleName" => "ESD Download Artikel", - "shipped" => 0, - "shippedGroup" => 0, - "mode" => 0, - "esdArticle" => 1, - ]], - "documents" => [], - "billing" => [ - "id" => 2, - "customerId" => 1, - "countryId" => 2, - "stateId" => 3, - "company" => "shopware AG", - "salutation" => "mr", - "firstName" => "Max", - "lastName" => "Mustermann", - "street" => "Mustermannstra\u00dfe 92", - "zipCode" => "48624", - "city" => "Sch\u00f6ppingen", - ], - "shipping" => [ - "id" => 2, - "countryId" => 2, - "stateId" => 3, - "customerId" => 1, - "company" => "shopware AG", - "salutation" => "mr", - "firstName" => "Max", - "lastName" => "Mustermann", - "street" => "Mustermannstra\u00dfe 92", - "zipCode" => "48624", - "city" => "Sch\u00f6ppingen" +{% include 'api_badge.twig' with {'route': '/api/orders', 'method': 'POST', 'body': true} %} +```json +{ + "customerId": 1, + "paymentId": 4, + "dispatchId": 9, + "partnerId": "", + "shopId": 1, + "invoiceAmount": 201.86, + "invoiceAmountNet": 169.63, + "invoiceShipping": 0, + "invoiceShippingNet": 0, + "orderTime": "2012-08-31 08:51:46", + "net": 0, + "taxFree": 0, + "languageIso": "1", + "currency": "EUR", + "currencyFactor": 1, + "remoteAddress": "217.86.205.141", + "details": [ + { + "articleId": 220, + "taxId": 1, + "taxRate": 19, + "statusId": 0, + "articleNumber": "SW10001", + "price": 35.99, + "quantity": 1, + "articleName": "Versandkostenfreier Artikel", + "shipped": 0, + "shippedGroup": 0, + "mode": 0, + "esdArticle": 0 + }, + { + "articleId": 219, + "taxId": 1, + "taxRate": 19, + "statusId": 0, + "articleNumber": "SW10185", + "price": 54.9, + "quantity": 1, + "articleName": "Express Versand", + "shipped": 0, + "shippedGroup": 0, + "mode": 0, + "esdArticle": 0 + }, + { + "articleId": 197, + "taxId": 1, + "taxRate": 19, + "statusId": 0, + "articleNumber": "SW10196", + "price": 34.99, + "quantity": 2, + "articleName": "ESD Download Artikel", + "shipped": 0, + "shippedGroup": 0, + "mode": 0, + "esdArticle": 1 + } ], - "paymentStatusId" => 17, - "orderStatusId" => 0 -]); + "documents": [], + "billing": { + "id": 2, + "customerId": 1, + "countryId": 2, + "stateId": 3, + "company": "shopware AG", + "salutation": "mr", + "firstName": "Max", + "lastName": "Mustermann", + "street": "Mustermannstra\\u00dfe 92", + "zipCode": "48624", + "city": "Sch\\u00f6ppingen" + }, + "shipping": { + "id": 2, + "countryId": 2, + "stateId": 3, + "customerId": 1, + "company": "shopware AG", + "salutation": "mr", + "firstName": "Max", + "lastName": "Mustermann", + "street": "Mustermannstra\\u00dfe 92", + "zipCode": "48624", + "city": "Sch\\u00f6ppingen" + }, + "paymentStatusId": 17, + "orderStatusId": 0 +} ``` ### Result -``` -( - [id] => 60 - [location] => http://www.yourdomain.com/api/orders/60 -) +```json +{ + "id": 60, + "location": "https://shop.example.com/api/orders/60" +} ``` ## Further examples +### Filter by `paymentStatusId` + +{% include 'api_badge.twig' with {'route': '/api/orders?filter[cleared]=0', 'method': 'GET'} %} + +### Filter by `orderStatusId` + +{% include 'api_badge.twig' with {'route': '/api/orders?filter[status]=0', 'method': 'GET'} %} + +### Filter by `clearedDate` + +{% include 'api_badge.twig' with {'route': '/api/orders?filter[0][property]=clearedDate&filter[0][expression]=>=&filter[0][value]=2012-10-14', 'method': 'GET'} %} + +### Change status + +{% include 'api_badge.twig' with {'route': '/api/orders/1', 'method': 'PUT', 'body': true} %} +```json +{ + "paymentStatusId": 12, + "clearedDate": "2012-10-17" +} ``` -// filter by paymentStatusId -$filterByPaymentStatus = array( - array( - 'property' => 'cleared', - 'value' => 0 - ), -); - -// filter by orderStatusId -$filterByOrderStatus = array( - array( - 'property' => 'status', - 'value' => 0 - ), -); - -// filter by clearedDate -$filterByClearedDate = array( - array( - 'property' => 'clearedDate', - 'expression' => '>=', - 'value' => '2012-10-14' - ), -); - -$params = array( - 'filter' => $filterByPaymentStatus -); - -$client->get('orders', $params); - - -// Change status -$updateOrder = array( - 'paymentStatusId' => 12, - 'clearedDate' => '2012-10-17', -); - -//Using ordernumber as identifier -$params = array( - 'useNumberAsId' => true -); - -$client->put('orders/20001', $updateOrder, $params); + +### Change status using the `orderNumber` as identifier + +{% include 'api_badge.twig' with {'route': '/api/orders/20001?useNumberAsId=true', 'method': 'PUT', 'body': true} %} +```json +{ + "paymentStatusId": 12, + "clearedDate": "2012-10-17" +} ``` **[Back to overview](../#examples)** diff --git a/source/developers-guide/rest-api/examples/payment/index.md b/source/developers-guide/rest-api/examples/payment/index.md index 8f065d8157..b046a7fa10 100644 --- a/source/developers-guide/rest-api/examples/payment/index.md +++ b/source/developers-guide/rest-api/examples/payment/index.md @@ -1,9 +1,9 @@ --- layout: default -title: REST API - Payment and payment instances +title: REST API - Examples using the payment resource github_link: developers-guide/rest-api/examples/media/index.md -menu_title: Payment -menu_order: 120 +menu_title: Payment examples +menu_order: 200 indexed: true menu_style: bullet group: Developer Guides @@ -12,33 +12,33 @@ subgroup: REST API ## Introduction -In this article, you will find examples of the provided resource usage for different operations. For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. -Please read the page covering the **[category API resource](/developers-guide/rest-api/api-resource-categories/)** if you haven't yet, to get more information about the category resource and the data it provides. - -These examples assume you are using the provided **[demo API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. +In this article, you will find examples of the provided resource usage for different operations. +For each analyzed scenario, we provide an example of the data that you are expected to provide to the API, as well as an example response. +Please read the page covering the **[payment method API resource](/developers-guide/rest-api/api-resource-payment-methods/)** if you haven't yet, +to get more information about the payment method resource and the data it provides. ## Usage -The Rest API calls for customer data ('''/api/customers''' und '''/api/customers/{id}''') supports payment informations. +The Rest API calls for customer data (`/api/customers` and `/api/customers/{id}`) support payment information. This means you can list, create or update the payment data for customers. -Calls for order details ('''/api/orders/{id}''') covers the payment instances. +Calls for order details (`/api/orders/{id}`) covers the payment instances. This contains the information about the payment and orders. This field could not be changed. Creating or updating are not supported. -Example: - -```php -// PUT /api/customers/1 -array( - "paymentData" => array( - array( - "paymentMeanId" => 2, - "accountNumber" => "Account", - "bankCode" => "55555555", - "bankName" => "Bank", - "accountHolder" => "Max Mustermann", - ), - ), -); +**Example:** + +{% include 'api_badge.twig' with {'route': '/api/customers/1', 'method': 'PUT', 'body': true} %} +```json +{ + "paymentData": [ + { + "paymentMeanId": 2, + "accountNumber": "Account", + "bankCode": "55555555", + "bankName": "Bank", + "accountHolder": "Max Mustermann" + } + ] +} ``` diff --git a/source/developers-guide/rest-api/examples/translation/index.md b/source/developers-guide/rest-api/examples/translation/index.md index 3d562733ca..e9f19dc2a8 100644 --- a/source/developers-guide/rest-api/examples/translation/index.md +++ b/source/developers-guide/rest-api/examples/translation/index.md @@ -2,8 +2,8 @@ layout: default title: REST API - Examples using the translation resource github_link: developers-guide/rest-api/examples/translation/index.md -menu_title: The translation resource -menu_order: 50 +menu_title: Translation examples +menu_order: 240 indexed: true menu_style: bullet group: Developer Guides @@ -14,119 +14,141 @@ subgroup: REST API In this article you can read more about using the translation resource. The following part will show you examples including provided data and data you need to provide if you want to use this resource. -Please read **[Translation](/developers-guide/rest-api/api-resource-translation/)** if you did not yet, to get more information about the translation resource and the data it provides. -Also we are using the API client of the following document **[API client](/developers-guide/rest-api/#using-the-rest-api-in-your-own-application)**. +Please read the page covering the **[translation API resource](/developers-guide/rest-api/api-resource-translation/)** if you did not yet, +to get more information about the translation resource and the data it provides. ## Example 1 - Creating a new translation This example shows how to create a new article translation -```php -post('translations', [ - 'key' => 200, # s_articles.id - 'type' => 'article', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'name' => 'Dummy translation', - ... - ] -]); - +{% include 'api_badge.twig' with {'route': '/api/translations', 'method': 'POST', 'body': true} %} +```json +{ + "key": 200, + "type": "article", + "shopId": 2, + "data": { + "name": "Dummy translation", + "__attribute_attr1": "Dummy attribute translation" + } +} ``` ## Example 2 - Updating a translation -```php -put('translations/200', [ # s_articles.id - 'type' => 'article', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'name' => 'Dummy translation', - ... - ] -]); - +{% include 'api_badge.twig' with {'route': '/api/translations/200', 'method': 'PUT', 'body': true} %} +```json +{ + "type": "article", + "shopId": 2, + "data": { + "name": "Dummy translation", + "__attribute_attr1": "Dummy attribute translation" + } +} ``` ## Example 3 - Creating a property group translation -```php -post('translations', [ - 'key' => 6, # s_filter.id - 'type' => 'propertygroup', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'groupName' => 'Dummy translation', - ] -]); +{% include 'api_badge.twig' with {'route': '/api/translations', 'method': 'POST', 'body': true} %} +```json +{ + "key": 6, + "type": "propertygroup", + "shopId": 2, + "data": { + "groupName": "Dummy translation" + } +} ``` ## Example 4 - Updating a property group translation -```php -post('translations/6', [ # s_filter.id - 'type' => 'propertygroup', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'groupName' => 'Dummy translation Edited', - ] -]); +{% include 'api_badge.twig' with {'route': '/api/translations/6', 'method': 'POST', 'body': true} %} +```json +{ + "type": "propertygroup", + "shopId": 2, + "data": { + "groupName": "Dummy translation edited" + } +} ``` ## Example 5 - Creating a property option translation -```php -post('translations', [ - 'key' => 1, # s_filter_options.id - 'type' => 'propertyoption', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'optionName' => 'Dummy translation', - ] -]); +{% include 'api_badge.twig' with {'route': '/api/translations', 'method': 'POST', 'body': true} %} +```json +{ + "key": 1, + "type": "propertyoption", + "shopId": 2, + "data": { + "optionName": "Dummy translation" + } +} ``` ## Example 6 - Updating a property option translation -```php -post('translations/1', [ # s_filter_options.id - 'type' => 'propertyoption', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'optionName' => 'Dummy translation Edited', - ] -]); +{% include 'api_badge.twig' with {'route': '/api/translations/1', 'method': 'POST', 'body': true} %} +```json +{ + "type": "propertyoption", + "shopId": 2, + "data": { + "optionName": "Dummy translation edited" + } +} ``` ## Example 7 - Creating a property value translation -```php -post('translations', [ - 'key' => 166, # s_filter_values.id - 'type' => 'propertyvalue', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'optionValue' => 'Dummy translation', - ] -]); +{% include 'api_badge.twig' with {'route': '/api/translations', 'method': 'POST', 'body': true} %} +```json +{ + "key": 166, + "type": "propertyvalue", + "shopId": 2, + "data": { + "optionValue": "Dummy translation" + } +} ``` ## Example 8 - Updating a property value translation -```php -post('translations/166', [ # s_filter_values.id - 'type' => 'propertyvalue', - 'shopId' => 2, # s_core_shops.id - 'data' => [ - 'optionValue' => 'Dummy translation Edited', - ] -]); +{% include 'api_badge.twig' with {'route': '/api/translations/166', 'method': 'POST', 'body': true} %} +```json +{ + "type": "propertyvalue", + "shopId": 2, + "data": { + "optionValue": "Dummy translation edited" + } +} +``` + +## Example 9 - Updating multiple translations (batch mode) + +{% include 'api_badge.twig' with {'route': '/api/translations', 'method': 'PUT', 'body': true} %} +```json +[ + { + "key": 177, + "type": "propertyvalue", + "shopId": 2, + "data": { + "optionValue": "Dummy translation edited" + } + }, + { + "key": 178, + "type": "propertyvalue", + "shopId": 2, + "data": { + "optionValue": "Another dummy translation edited" + } + } +] ``` diff --git a/source/developers-guide/rest-api/extend-api-resource/index.md b/source/developers-guide/rest-api/extend-api-resource/index.md index 0edc02fe47..2faae29580 100644 --- a/source/developers-guide/rest-api/extend-api-resource/index.md +++ b/source/developers-guide/rest-api/extend-api-resource/index.md @@ -12,16 +12,18 @@ tags: group: Developer Guides subgroup: REST API menu_title: Extend a REST API resource -menu_order: 140 +menu_order: 330 ---
    ## Introduction -This article will provide a small example on how to extend the existing REST API resources. As of Shopware 5.2.17 the resources are loaded as services to the dependency injection container and can be easily decorated. +This article will provide a small example on how to extend the existing REST API resources. +As of Shopware 5.2.17 the resources are loaded as services to the dependency injection container and can be easily decorated. ## Plugin files -You only need a few files for this example plugin. For more information about necessary files and the Shopware 5.2 plugin system see the [5.2 plugin guide](/developers-guide/plugin-system). +You only need a few files for this example plugin. +For more information about necessary files and the Shopware 5.2 plugin system see the [5.2 plugin guide](/developers-guide/plugin-system). ### Services.xml `SwagExtendArticleResource/Resources/services.xml`: @@ -65,7 +67,8 @@ class Article extends \Shopware\Components\Api\Resource\Article } } ``` -Execute the parent `getList()` function to get the original data and add your custom field to every product. Feel free to replace this with your own implementation. +Execute the parent `getList()` function to get the original data and add your custom field to every product. +Feel free to replace this with your own implementation. ## Download plugin -The whole plugin can be downloaded here. \ No newline at end of file +The whole plugin can be downloaded here. diff --git a/source/developers-guide/rest-api/faq/index.md b/source/developers-guide/rest-api/faq/index.md new file mode 100644 index 0000000000..9d64ed42d4 --- /dev/null +++ b/source/developers-guide/rest-api/faq/index.md @@ -0,0 +1,69 @@ +--- +layout: default +title: REST API - Frequently Asked Questions +github_link: developers-guide/rest-api/faq/index.md +shopware_version: +indexed: true +tags: + - api + - rest + - faq + - problems +group: Developer Guides +subgroup: REST API +menu_title: REST API FAQ +menu_order: 340 +--- + +
    + +## Introduction +This article is a summary of problems that are reported frequently and the solutions to those problems. + +## Why do I get HTML errors instead of JSON? +If you receive errors with HTML instead of JSON (usually with a complete stacktrace) like this: + +``` +
    +Fatal error: Uncaught Shopware\Components\Api\Exception\NotFoundException: Article by id 23213213 not found in /engine/Shopware/Components/Api/Resource/Article.php:155 +Stack trace: +#0 /engine/Shopware/Controllers/Api/Articles.php(75): Shopware\Components\Api\Resource\Article->getOne('23213213', Array) +#1 /engine/Library/Enlight/Controller/Action.php(159): Shopware_Controllers_Api_Articles->getAction() +#2 /engine/Library/Enlight/Controller/Dispatcher/Default.php(523): Enlight_Controller_Action->dispatch('getAction') +#3 /engine/Library/Enlight/Controller/Front.php(223): Enlight_Controller_Dispatcher_Default->dispatch(Object(Enlight_Controller_Request_RequestHttp), Object(Enlight_Controller_Response_ResponseHttp)) +#4 /engine/Shopware/Kernel.php(182): Enlight_Controller_Front->dispatch() +#5 / in 2/engine/Shopware/Components/Api/Resource/Article.php on line 155
    +``` +This is a sign that the configs `throwExceptions` and `noErrorHandler` which are located in `config.php` are set to `true`. +Removing these entries or setting them to `false` should resolve this issue. + + +## How can I access the API if my shop is protected with a `.htaccess` file? +You need to add an exception for the API route. Here is an example for the `.htaccess` file: +``` +SetEnvIf Request_URI "(.*\/api((\/?$)|(\/.*$)))" ALLOW +Order Allow,Deny +Authtype Basic +AuthName "Shopware Testshop" +AuthUserFile /path/to/.htpasswd + +require valid-user +Allow from env=ALLOW +Allow from env=REDIRECT_ALLOW +Satisfy any + +# Rest of the shopware .htaccess +... + +``` + +## How do I configure API development tools like Postman to use them with the Shopware API? + +There are two ways to allow API dev tools to authenticate with the Shopware API: + +* API tools like Postman support the following URL schema: +http://username:apikey@mydemoshop.com/api/ (if you experience any issues with +Postman, try the Google Chrome version instead of the standalone version) +* Use the HTTP Basic authentication (introduced with Shopware 5.3.2) + +To transmit a data to the API, select JSON (application/json) as Content-Type. diff --git a/source/developers-guide/rest-api/index.md b/source/developers-guide/rest-api/index.md index 8a0f911b6f..9eeb87c90c 100755 --- a/source/developers-guide/rest-api/index.md +++ b/source/developers-guide/rest-api/index.md @@ -14,185 +14,117 @@ subgroup: REST API ## Introduction -The following page of the devdocs covers the REST API. By using the REST API, shop owners can grant access to almost all data stored -in their shop to 3rd party applications. It also allows direct manipulation of the shop data, regardless of the application or system used. +The following page of the documentation covers the REST API. +By using the REST API, shop owners can grant access to almost all data stored in their shop to 3rd party applications. +It also allows direct manipulation of the shop data, regardless of the application or system used. ## Basic Settings -To enable access to the REST API, the shop owner must authorize one (or more) users in the Shopware backend. Simply open the Shopware backend and open the `User Administration` window, under `Settings`. -From the list of existing users displayed on this window, select `Edit` for the desired user and mark the `enabled` checkbox in the `API Access` section. -You will get a randomly generated API access key, which needs to be included in your API requests for authentication. After clicking `Save`, the changes will take effect. -If the edited user is currently logged in, you might need to empty the backend cache, and then log out an log in for your changes to take effect. +To enable access to the REST API, the shop owner must authorize one (or more) users in the Shopware backend. + +Simply open the Shopware backend and open the `User Administration` window, under `Settings`. +From the list of existing users displayed on this window, +select `Edit` for the desired user and mark the `enabled` checkbox in the `API Access` section. + +You will get a randomly generated API access key, which needs to be included in your API requests for authentication. +After clicking `Save`, the changes will take effect. +If the edited user is currently logged in, you might need to clear the backend cache, +and then log out and log in for your changes to take effect. ## List of API Resources -The API has multiple resources, each responsible for managing a specific data type. These resources can be found in the `engine/Shopware/Controllers/Api/` directory of your Shopware installation. Each resource has a correspondent URI, and supports a different set of operations. +The API has multiple resources, each responsible for managing a specific data type. +These resources can be found in the `engine/Shopware/Controllers/Api/` directory of your Shopware installation. +Each resource has a correspondent URI and supports a different set of operations. To get more details about the data provided by each specified resource, click on its name. -| Name | Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | -|-------------------------------------------------------------------|-----------------------------|--------------------|-----------------|-----------------|------------------|------------------|-----------------|-----------------| -| **[Address](api-resource-address/)** | /api/addresses | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[Article](api-resource-article/)** | /api/articles | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | -| **[Cache](api-resource-cache/)** | /api/caches | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | -| **[Categories](api-resource-categories/)** | /api/categories | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[Countries](api-resource-countries/)** | /api/countries | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[CustomerGroups](api-resource-customer-group/)** | /api/customerGroups | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[Customer](api-resource-customer/)** | /api/customers | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[GenerateArticleImage](api-resource-generate-article-images/)** | /api/generateArticleImages | ![No](img/no.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | -| **[Media](api-resource-media/)** | /api/media | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[Manufacturers](api-resource-manufacturers/)** | /api/manufacturers | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[Orders](api-resource-orders/)** | /api/orders | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | -| **[PropertyGroups](api-resource-property-group/)** | /api/propertyGroups | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[Shops](api-resource-shops/)** | /api/shops | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | -| **[Translations](api-resource-translation/)** | /api/translations | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | -| **[Variants](api-resource-variants/)** | /api/variants | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | -| **[Version](api-resource-version/)** | /api/version | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | +| Name | Access URL | GET | GET (List) | PUT | PUT (Batch) | POST | DELETE | DELETE (Batch) | +|-------------------------------------------------------------------|-----------------------------|----------------------|---------------------|---------------------|---------------------|---------------------|---------------------|---------------------| +| **[Address](api-resource-address/)** | /api/addresses | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Product](api-resource-article/)** | /api/articles | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | +| **[Cache](api-resource-cache/)** | /api/caches | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | +| **[Categories](api-resource-categories/)** | /api/categories | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Countries](api-resource-countries/)** | /api/countries | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Customer](api-resource-customer/)** | /api/customers | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[CustomerGroups](api-resource-customer-group/)** | /api/customerGroups | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[GenerateProductImage](api-resource-generate-article-images/)** | /api/generateArticleImages | ![No](img/no.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | +| **[Manufacturers](api-resource-manufacturers/)** | /api/manufacturers | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Media](api-resource-media/)** | /api/media | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Orders](api-resource-orders/)** | /api/orders | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | +| **[PaymentMethods](api-resource-payment-methods/)** | /api/paymentMethods | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[PropertyGroups](api-resource-property-group/)** | /api/propertyGroups | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Shops](api-resource-shops/)** | /api/shops | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Translations](api-resource-translation/)** | /api/translations | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | +| **[Users](api-resource-user/)** | /api/users | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![No](img/no.png) | +| **[Variants](api-resource-variants/)** | /api/variants | ![Yes](img/yes.png) | ![No](img/no.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | ![Yes](img/yes.png) | +| **[Version](api-resource-version/)** | /api/version | ![Yes](img/yes.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | ![No](img/no.png) | -## Using the REST API in your own application +## Authentication -To connect to the REST API, you need a client application. As REST is widely used as an inter-application communication protocol, several client applications and integration libraries already exist, both free and commercially, for different platforms and languages. The following class illustrates a fully functional (yet basic) implementation of a REST client. Note that this example code is not maintained, and it's highly recommended that you don't use it in production environments. +We currently support two authentication mechanisms: -``` -apiUrl = rtrim($apiUrl, '/') . '/'; - //Initializes the cURL instance - $this->cURL = curl_init(); - curl_setopt($this->cURL, CURLOPT_RETURNTRANSFER, true); - curl_setopt($this->cURL, CURLOPT_FOLLOWLOCATION, false); - curl_setopt($this->cURL, CURLOPT_USERAGENT, 'Shopware ApiClient'); - curl_setopt($this->cURL, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); - curl_setopt($this->cURL, CURLOPT_USERPWD, $username . ':' . $apiKey); - curl_setopt( - $this->cURL, - CURLOPT_HTTPHEADER, - ['Content-Type: application/json; charset=utf-8'] - ); - } - - public function call($url, $method = self::METHOD_GET, $data = [], $params = []) - { - if (!in_array($method, $this->validMethods)) { - throw new Exception('Invalid HTTP-Methode: ' . $method); - } - $queryString = ''; - if (!empty($params)) { - $queryString = http_build_query($params); - } - $url = rtrim($url, '?') . '?'; - $url = $this->apiUrl . $url . $queryString; - $dataString = json_encode($data); - curl_setopt($this->cURL, CURLOPT_URL, $url); - curl_setopt($this->cURL, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($this->cURL, CURLOPT_POSTFIELDS, $dataString); - $result = curl_exec($this->cURL); - $httpCode = curl_getinfo($this->cURL, CURLINFO_HTTP_CODE); - - return $this->prepareResponse($result, $httpCode); - } - - public function get($url, $params = []) - { - return $this->call($url, self::METHOD_GET, [], $params); - } - - public function post($url, $data = [], $params = []) - { - return $this->call($url, self::METHOD_POST, $data, $params); - } - - public function put($url, $data = [], $params = []) - { - return $this->call($url, self::METHOD_PUT, $data, $params); - } - - public function delete($url, $params = []) - { - return $this->call($url, self::METHOD_DELETE, [], $params); - } - - protected function prepareResponse($result, $httpCode) - { - echo "

    HTTP: $httpCode

    "; - if (null === $decodedResult = json_decode($result, true)) { - $jsonErrors = [ - JSON_ERROR_NONE => 'No error occurred', - JSON_ERROR_DEPTH => 'The maximum stack depth has been reached', - JSON_ERROR_CTRL_CHAR => 'Control character issue, maybe wrong encoded', - JSON_ERROR_SYNTAX => 'Syntaxerror', - ]; - echo '

    Could not decode json

    '; - echo 'json_last_error: ' . $jsonErrors[json_last_error()]; - echo '
    Raw:
    '; - echo '
    ' . print_r($result, true) . '
    '; - - return; - } - if (!isset($decodedResult['success'])) { - echo 'Invalid Response'; +* Digest access authentication +* HTTP Basic authentication (introduced with Shopware 5.3.2) - return; - } - if (!$decodedResult['success']) { - echo '

    No Success

    '; - echo '

    ' . $decodedResult['message'] . '

    '; - if (array_key_exists('errors', $decodedResult) && is_array($decodedResult['errors'])) { - echo '

    ' . join('

    ', $decodedResult['errors']) . '

    '; - } - - return; - } - echo '

    Success

    '; - if (isset($decodedResult['data'])) { - echo '
    ' . print_r($decodedResult['data'], true) . '
    '; - } +### Digest access authentication - return $decodedResult; - } -} -``` +The Digest access authentication is based on a simple challenge-response paradigm. +The Digest scheme challenges using a nonce value. +A valid response contains a checksum (by default MD5) of the username, the password, the given nonce value, the HTTP method, and the requested URI. -### Creating the API client -To successfully use this client, we need to initialize it. When creating it, we give the client an API URL, an user name and the API key. +This ensures, that the password is never sent as plain text. -``` -$client = new ApiClient( - //URL of shopware REST server - 'http://www.ihredomain.de/api', - //Username - 'myUsername', - //User's API-Key - 'myAPIKey' -); -``` +You can find a detailed explanation of the digest access authentication +[here](https://tools.ietf.org/html/rfc2617#page-6) +and [here](https://en.wikipedia.org/wiki/Digest_access_authentication). -### Triggering a call with the API client -The newly created client now gives us the ability to call all resources. The first parameter describes the resource that should be queried. As the client already knows the resource's URL, we don't need to provide that information again. and can use only the resource's URI. -So, for example, the article with the ID `3` can be queried like so: +### HTTP Basic authentication -``` -$client->get('articles/3'); -``` +To use the HTTP Basic authentication, you just have to set an Authorization header which looks like this: -When creating or updating data, a second parameter needs to be given to these calls. This parameter must be an array containing the data which should be changed or created. +Authorization: Basic c2hvcDp3YXJl + +The Authorization header has to follow this scheme: +1. Combine username and password with a single colon (:). +2. Encode the string into an octet sequence ([more info](https://tools.ietf.org/id/draft-reschke-basicauth-enc-00.html)). +3. Encode the string with Base64. +4. Prepend the authorization method and a space to the encoded string. + +Please be aware that the Basic authorisation provides no confidentiality protection for the transmitted credentials. +Therefore, you should **always** use HTTPS when using Basic authentication. + + +The authentication methods are not exclusive, you can use both of them simultaneously! + +## Using the REST API in your own application + +To connect to the REST API, you need a client application. +As REST is widely used as an inter-application communication protocol, +several client applications and integration libraries already exist, +both free and commercially, for different platforms and languages. + +The examples shown in this documentation will work with any HTTP-Client. +There's a variety of command-line or GUI applications which can be used for testing, +and the standard library of your programming language of choice most likely includes a compatible HTTP-Client as well. + +Every example will be accompanied by a badge like this: + +{% include 'api_badge.twig' with {'route': '/api/articles/4', 'method': 'GET'} %} + +The first part shows the HTTP-request method and the second part shows the route. + +Some requests come with a body containing additional data like product information, +these will have a code section attached to them and look like this: + +{% include 'api_badge.twig' with {'route': '/api/articles/4', 'method': 'PUT', 'body': true} %} + +```json +{ + "name": "New name" +} +``` ## Communicating with the API @@ -203,7 +135,7 @@ The date must be in ISO 8601 format. More info about ISO can be found here: -* [ISO_8601](http://en.wikipedia.org/wiki/ISO_8601) +* [ISO_8601](https://en.wikipedia.org/wiki/ISO_8601) ### Date formatting in PHP ``` @@ -244,81 +176,79 @@ var date = new Date(string); ### Filter, Sort, Limit, Offset -Every API comes with a set of default parameters which can be used to modify the given result. All parameters can be combined in a single request. - -The following examples are snippets for our API client above. +Every API comes with a set of default parameters which can be used to modify the given result. +All parameters can be combined in a single request. #### Filter -Filtering a results can be done using the `filter` parameter in your request. The available properties can be extracted from the Shopware models below. +Filtering a results can be done using the `filter` parameter in your request. +The available properties can be extracted from the Shopware models below. Each filter can have the following properties: * property (Required) * value (Required) * expression (Default: `LIKE`, available: all MySQL expressions) -* operator (If set, concats the filter using `OR` instead of `AND`) +* operator (If set, concats the filter using `OR` instead of `AND`) **Example: Active articles with at least 1 pseudo sale** -``` -$params = [ - 'filter' => [ - [ - 'property' => 'pseudoSales', - 'expression' => '>=', - 'value' => 1 - ], - [ - 'property' => 'active', - 'value' => 1 - ] +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'GET', 'body': true} %} +```json +{ + "filter": [ + { + "property": "pseudoSales", + "expression": ">=", + "value": 1 + }, + { + "property": "active", + "value": 1 + } ] -]; - -$client->get('articles', $params); +} ``` **Example: Active articles or articles containing "beach" in their name** -``` -$params = [ - 'filter' => [ - [ - 'property' => 'name', - 'value' => '%beach%', - 'operator' => 1 - ], - [ - 'property' => 'active', - 'value' => 1 - ] +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'GET', 'body': true} %} +```json +{ + "filter": [ + { + "property": "name", + "value": "%beach%", + "operator": 1 + }, + { + "property": "active", + "value": 1 + } ] -]; - -$client->get('articles', $params); +} ``` **Example: Orders from customer which email address is "test@example.com"** Keep in mind, that the related entity must be joined in the query builder. -``` -$params = [ - 'filter' => [ - [ - 'property' => 'customer.email', - 'value' => 'test@example.com' - ] +{% include 'api_badge.twig' with {'route': '/api/orders', 'method': 'GET', 'body': true} %} +```json +{ + "filter": [ + { + "property": "customer.email", + "value": "test@example.com" + } ] -]; - -$client->get('orders', $params); +} ``` #### Sort -The sorting syntax nearly equals to the filter section above. It uses the `sort` parameter in the request. +The sorting syntax nearly equals to the filter section above. +It uses the `sort` parameter in the request. Each sorting can have the following properties: @@ -327,52 +257,57 @@ Each sorting can have the following properties: **Example: Sort by article name** -``` -$params = [ - 'sort' => [ - ['property' => 'name'] +{% include 'api_badge.twig' with {'route': '/api/articles', 'method': 'GET', 'body': true} %} +```json +{ + "sort": [ + { + "property": "name" + } ] -]; - -$client->get('articles', $params); +} ``` **Example: First, sort by order time and then by invoice amount in descending order** -``` -$params = [ - 'sort' => [ - ['property' => 'orderTime'], - ['property' => 'invoiceAmount', 'direction' => 'DESC'] +{% include 'api_badge.twig' with {'route': '/api/orders', 'method': 'GET', 'body': true} %} +```json +{ + "sort": [ + { + "property": "orderTime" + }, + { + "property": "invoiceAmount", + "direction": "DESC" + } ] -]; - -$client->get('orders', $params); +} ``` #### Limit / Offset -By default, Shopware uses a soft limit on the API with a value of `1000`. If you need more than `1000` results, increase it to your needs. The limiting uses the parameter `limit`, the offset `start`. +By default, Shopware uses a soft limit on the API with a value of `1000`. +If you need more than `1000` results, increase it to your needs. +The limiting uses the parameter `limit`, the offset `start`. **Example: Retrieve the first 50 results** -``` -$params = [ - 'limit' => 50 -]; - -$client->get('orders', $params); +{% include 'api_badge.twig' with {'route': '/api/orders', 'method': 'GET', 'body': true} %} +```json +{ + "limit": 50 +} ``` **Example: Retrieve 50 results, skipping the first 20** -``` -$params = [ - 'limit' => 50, - 'start' => 20 -]; - -$client->get('orders', $params); +{% include 'api_badge.twig' with {'route': '/api/orders', 'method': 'GET', 'body': true} %} +```json +{ + "limit": 50, + "start": 20 +} ``` ## Models diff --git a/source/developers-guide/rest-api/models/index.md b/source/developers-guide/rest-api/models/index.md index 2cabdb4941..6469021afb 100644 --- a/source/developers-guide/rest-api/models/index.md +++ b/source/developers-guide/rest-api/models/index.md @@ -3,7 +3,7 @@ layout: default title: REST API - Models github_link: developers-guide/rest-api/models/index.md menu_title: Models -menu_order: 120 +menu_order: 310 indexed: true menu_style: bullet group: Developer Guides @@ -19,24 +19,25 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|--------------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | | -| company | string | | | -| department | string | | | -| salutation | string | | | -| firstname | string | | | -| lastname | string | | | -| street | string | | | -| zipcode | string | | | -| city | string | | | -| phone | string | | | -| vatId | string | | | -| additionalAddressLine1 | string | | | -| additionalAddressLine2 | string | | | -| country | int (foreign key) | **[Country](../models/#country)** | | -| state | int (foreign key) | **[State](#state)** | | -| attribute | array | | | +| Field | Type | Original object | +|------------------------|-----------------------|-----------------------------------| +| id | integer (primary key) | | +| company | string | | +| department | string | | +| salutation | string | | +| title | string | | +| firstname | string | | +| lastname | string | | +| street | string | | +| zipcode | string | | +| city | string | | +| phone | string | | +| vatId | string | | +| additionalAddressLine1 | string | | +| additionalAddressLine2 | string | | +| country | int (foreign key) | **[Country](../models/#country)** | +| state | int (foreign key) | **[State](#state)** | +| attribute | array | | ## Area @@ -45,12 +46,12 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| name | string | | -| active | boolean | | -| countries | object array | **[Country](#country)** | +| Field | Type | Original object | +|-----------|-----------------------|-------------------------| +| id | integer (primary key) | | +| name | string | | +| active | boolean | | +| countries | object array | **[Country](#country)** | ## Article Attribute @@ -59,30 +60,30 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| attr1 | string | | -| attr2 | string | | -| attr3 | string | | -| attr4 | string | | -| attr5 | string | | -| attr6 | string | | -| attr7 | string | | -| attr8 | string | | -| attr9 | string | | -| attr10 | string | | -| attr11 | string | | -| attr12 | string | | -| attr13 | string | | -| attr14 | string | | -| attr15 | string | | -| attr16 | string | | -| attr17 | string | | -| attr18 | string | | -| attr19 | string | | -| attr20 | string | | -| articleId | integer (foreign key) | **[Article](../api-resource-article/)** | -| articleDetailId | integer (foreign key) | **[Detail](#article-detail)** | +| Field | Type | Original object | +|-----------------|-----------------------|-----------------------------------------| +| attr1 | string | | +| attr2 | string | | +| attr3 | string | | +| attr4 | string | | +| attr5 | string | | +| attr6 | string | | +| attr7 | string | | +| attr8 | string | | +| attr9 | string | | +| attr10 | string | | +| attr11 | string | | +| attr12 | string | | +| attr13 | string | | +| attr14 | string | | +| attr15 | string | | +| attr16 | string | | +| attr17 | string | | +| attr18 | string | | +| attr19 | string | | +| attr20 | string | | +| articleId | integer (foreign key) | **[Article](../api-resource-article/)** | +| articleDetailId | integer (foreign key) | **[Detail](#article-detail)** | ## Article Detail @@ -93,7 +94,7 @@ subgroup: REST API | Field | Type | Original object | |---------------------|-----------------------|-------------------------------------------------| -| number | string | | +| number | string | | | supplierNumber | string | | | additionalText | string | | | weight | string | | @@ -110,7 +111,7 @@ subgroup: REST API | configuratorOptions | object array | **[ConfiguratorOption](#configurator-option)** | | attribute | object | **[Attribute](#article-attribute)** | | id | integer (primary key) | | -| articleId | integer (foreign key) | **[Article](../api-resource-article/)** | +| articleId | integer (foreign key) | **[Article](../api-resource-article/)** | | unitId | integer (foreign key) | | | kind | integer | | | inStock | integer | | @@ -121,52 +122,58 @@ subgroup: REST API | releaseDate | date/time | | | active | boolean | | | shippingFree | boolean | | +| esd | object | **[Esd](#esd)** | ## Billing -* **Model:** Shopware\Models\Customer\Billing -* **Table:** s_user_billingaddress +* **Model:** Shopware\Models\Order\Billing +* **Table:** s_order_billingaddress ### Structure -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| customerId | integer (foreign key) | | -| countryId | integer (foreign key) | **[Country](#country)** | -| stateId | integer (foreign key) | | -| company | string | | -| department | string | | -| salutation | string | | -| number | string | | -| firstName | string | | -| lastName | string | | -| street | string | | -| zipCode | string | | -| city | string | | -| phone | string | | -| fax | string | | -| vatId | string | | -| birthday | date/time | | -| attribute | object | **[BillingAttribute](#billing-attribute)** | +| Field | Type | Original object | +|------------------------|-----------------------|--------------------------------------------| +| id | integer (primary key) | | +| orderId | integer (foreign key) | | +| customerId | integer (foreign key) | | +| countryId | integer (foreign key) | **[Country](#country)** | +| stateId | integer (foreign key) | | +| company | string | | +| department | string | | +| title | string | | +| salutation | string | | +| number | string | | +| firstName | string | | +| lastName | string | | +| street | string | | +| zipCode | string | | +| city | string | | +| additionalAddressLine1 | string | | +| additionalAddressLine2 | string | | +| phone | string | | +| vatId | string | | +| country | object | **[Country](#country)** | +| state | object/null | **[State](#state)** | +| birthday | date/time | | +| attribute | object | **[BillingAttribute](#billing-attribute)** | ## Billing Attribute -* **Model:** Shopware\Models\Attribute\CustomerBilling -* **Table:** s_user_billingaddress_attributes +* **Model:** Shopware\Models\Attribute\OrderBilling +* **Table:** s_order_billingaddress_attributes ### Structure -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| customerBillingId | integer (foreign key) | | -| text1 | string | | -| text2 | string | | -| text3 | string | | -| text4 | string | | -| text5 | string | | -| text6 | string | | +| Field | Type | Original object | +|----------------|-----------------------|-----------------| +| id | integer (primary key) | | +| orderBillingId | integer (foreign key) | | +| text1 | string | | +| text2 | string | | +| text3 | string | | +| text4 | string | | +| text5 | string | | +| text6 | string | | ## Category @@ -175,10 +182,10 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|-----------------------|-----------------------|-------------------------------------------------------| -| id | integer (primary key) | | -| name | string | | +| Field | Type | Original object | +|-------|-----------------------|-----------------| +| id | integer (primary key) | | +| name | string | | ## Configurator Group @@ -187,12 +194,12 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|-----------------------|-----------------------|-------------------------------------------------------| -| id | integer (primary key | | -| description | string | | -| name | string | | -| position | integer | | +| Field | Type | Original object | +|-------------|-----------------------|-----------------| +| id | integer (primary key) | | +| description | string | | +| name | string | | +| position | integer | | ## Configurator Option @@ -201,12 +208,12 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|-----------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| groupId | integer (foreign key) | **[ConfiguratorGroup](#configurator-group)** | -| name | string | | -| position | integer | | +| Field | Type | Original object | +|----------|-----------------------|----------------------------------------------| +| id | integer (primary key) | | +| groupId | integer (foreign key) | **[ConfiguratorGroup](#configurator-group)** | +| name | string | | +| position | integer | | ## Configurator Set @@ -215,13 +222,13 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|-----------------------|-----------------------|-------------------------------------------------------| -| id | integer (primary key | | -| name | string | | -| public | boolean | | -| type | integer | | -| groups | object array | **[ConfiguratorGroup](#configurator-group)** | +| Field | Type | Original object | +|--------|-----------------------|----------------------------------------------| +| id | integer (primary key) | | +| name | string | | +| public | boolean | | +| type | integer | | +| groups | object array | **[ConfiguratorGroup](#configurator-group)** | ## Country @@ -267,6 +274,47 @@ subgroup: REST API | symbolPosition | integer | | | position | integer | | +## Customer + +* **Model:** Shopware\Models\Customer\Customer +* **Table:** s_user + +### Structure + +| Field | Type | Original object | +|-------------------------------|-----------------------|-------------------------------------------------| +| id | integer (primary key) | | +| number | string | | +| groupKey | string (foreign key) | **[CustomerGroup](#customer-group)** | +| paymentId | integer (foreign key) | **[Payment](#payment)** | +| shopId | string (foreign key) | **[Shop](#shop)** | +| priceGroupId | integer (foreign key) | **[PriceGroup](#price-group)** | +| encoderName | string | | +| hashPassword | string | | +| active | boolean | | +| email | string | | +| firstLogin (date of creation) | date/time | | +| lastLogin | date/time | | +| accountMode | integer | | +| confirmationKey | string | | +| sessionId | string | | +| newsletter | boolean | | +| validation | string | | +| affiliate | boolean | | +| paymentPreset | integer | | +| languageId | integer (foreign key) | **[Shop](#shop)** | +| referer | string | | +| internalComment | string | | +| failedLogins | integer | | +| lockedUntil | date/time | | +| salutation | string | | +| title | string | | +| firstname | string | | +| lastname | string | | +| birthday | date | | +| defaultBillingAddress | integer (foreign key) | **[Billing](#address)** | +| defaultShippingAddress | integer (foreign key) | **[Shipping](#address)** | + ## Customer Attribute * **Model:** Shopware\Models\Attribute\Customer @@ -276,8 +324,8 @@ subgroup: REST API | Field | Type | Original object | |---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| customerId | integer (foreign key) | **[Customer](../api-resource-customer/)** | +| id | integer (primary key) | | +| customerId | integer (foreign key) | **[Customer](#customer)** | ## Customer Group @@ -289,7 +337,7 @@ subgroup: REST API | Field | Type | Original object | |-----------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | +| id | integer (primary key) | | | key | string | | | name | string | | | tax | boolean | | @@ -308,11 +356,11 @@ subgroup: REST API ### Structure -| Field | Type | Original Object | -|------------------|-----------------------|-----------------| -| id | integer (primary key) | | -| discount | integer | | -| value | integer | | +| Field | Type | Original Object | +|-----------------------|-----------------------|-------------------------------------------------| +| id | integer (primary key) | | +| discount | integer | | +| value | integer | | ## Debit @@ -321,14 +369,14 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key) | | -| customerId | integer (foreign key) | | -| account | string | | -| bankCode | string | | -| bankName | string | | -| accountHolder | string | | +| Field | Type | Original object | +|---------------------|-------------------------|-------------------------------------------------| +| id | integer (primary key) | | +| customerId | integer (foreign key) | | +| account | string | | +| bankCode | string | | +| bankName | string | | +| accountHolder | string | | ## Dispatch @@ -356,11 +404,29 @@ subgroup: REST API | bindTimeFrom | integer | | | bindTimeTo | integer | | | bindInStock | integer | | +| bindLastStock | integer | | | bindWeekdayFrom | integer | | -| bindPriceTo | integer | | +| bindWeekdayTo | integer | | +| bindWeightFrom | decimal | | +| bindWeightTo | decimal | | +| bindPriceFrom | decimal | | +| bindPriceTo | decimal | | | bindSql | string | | | statusLink | string | | | calculationSql | string | | +| attribute | object/null | **[DispatchAttribute](#dispatch-attribute)** | + +## Dispatch Attribute + +* **Model:** Shopware\Models\Attribute\Dispatch +* **Table:** s_premium_dispatch_attributes + +### Structure + +| Field | Type | Original object | +|---------------------|-----------------------|-------------------------------------------------| +| id | integer (primary key) | | +| dispatchId | integer (foreign key) | | ## Document @@ -375,7 +441,7 @@ subgroup: REST API | date | date/time | | | typeId | integer (foreign key) | **[DocumentType](#document-type)** | | customerId | integer (foreign key) | **[Customer](#customer)** | -| orderId | integer (foreign key) | **[Order](#order)** | +| orderId | integer (foreign key) | **[Order](../api-resource-orders/)** | | amount | double | | | documentId | integer (foreign key) | | | hash | string | | @@ -396,7 +462,7 @@ subgroup: REST API ## Document Type -* **Model:** Shopware\Models\Order\Document\Type +* **Model:** Shopware\Models\Document\Document * **Table:** s_order_documents ### Structure @@ -428,6 +494,31 @@ subgroup: REST API | file | string | | | size | int | | +## Esd + +* **Model:** Shopware\Models\Article\Esd +* **Table:** s_articles_esd + +### Structure + +| Field | Type | Original object | +|-----------------------|-----------------------|-------------------------------------------------------| +| file | string | | +| reuse | boolean | | +| hasSerials | boolean | | +| serials | object array | **[EsdSerial](#esd-serial)** | + +## ESD-Serial + +* **Model:** Shopware\Models\Article\EsdSerial +* **Table:** s_articles_esd_serials + +### Structure + +| Field | Type | Original object | +|-----------------------|-----------------------|-------------------------------------------------------| +| serialnumber | string | | + ## Image * **Model:** Shopware\Models\Article\Image @@ -437,7 +528,7 @@ subgroup: REST API | Field | Type | Original object | |-----------------------|-----------------------|-------------------------------------------------------| -| id | integer (primary key | | +| id | integer (primary key) | | | articleId | integer (foreign key) | **[Article](../api-resource-article/)** | | articleDetailId | integer (foreign key) | **[Detail](#article-detail)** | | description | string | | @@ -451,6 +542,12 @@ subgroup: REST API | parentId | integer | | | mediaId | integer | **[Media](../api-resource-media/)** | +
    + +The field `path` has to be the local path to the image, seen from the root of the Shopware installation. There is an additional, internal helper field `link`, which allows to supply a URL that is being downloaded and converted to the `path` field internally. See the [product examples](https://developers.shopware.com/developers-guide/rest-api/examples/article/#further-examples] for an example. + +
    + ## Link * **Model:** Shopware\Models\Article\Link @@ -518,7 +615,7 @@ subgroup: REST API | price | double | | | quantity | integer | | | articleName | string | | -| shipped | boolean | | +| shipped | integer | | | shippedGroup | integer | | | releaseDate | date/time | | | mode | integer | | @@ -557,11 +654,35 @@ subgroup: REST API | Field | Type | Original object | |---------------------|-----------------------|-------------------------------------------------| | id | integer (primary key) | | -| description | string | | +| name | string | | | position | integer | | | group | string | | | sendMail | boolean | | +## Payment + +* **Model:** Shopware\Models\Payment\Payment +* **Table:** s_core_paymentmeans + +### Structure + +| Field | Type | Original object | +|---------------------|-----------------------|-------------------------------------------------| +| id | integer (primary key) | | +| name | string | | +| description | string | | +| template | string | | +| hide | boolean | | +| additionalDescription | string | | +| debitPercent | float | | +| surcharge | integer | | +| surchargeString | string | | +| position | integer | | +| active | boolean | | +| esdActive | boolean | | +| mobileInactive | boolean | | +| pluginId | integer | | + ## Payment Data * **Model:** Shopware\Models\Customer\PaymentData @@ -606,6 +727,21 @@ subgroup: REST API | amount | string | | | createdAt | date/time | | +## Payment Status + +* **Model:** Shopware\Models\Order\Status +* **Table:** s_core_states + +### Structure + +| Field | Type | Original object | +|---------------------|-----------------------|-------------------------------------------------| +| id | integer (primary key) | | +| name | string | | +| position | integer | | +| group | string | | +| sendMail | boolean | | + ## Price * **Model:** Shopware\Models\Article\Price @@ -613,19 +749,20 @@ subgroup: REST API ### Structure -| Field | Type | Original object | -|---------------------|-----------------------|-------------------------------------------------------| -| customerGroupKey | string (foreign key) | **[CustomerGroup](#customer-group)** | -| customerGroup | object | **[CustomerGroup](#customer-group)** | -| articleDetailsId | integer (foreign key) | **[Detail](#article-detail)** | -| articleId | integer (foreign key) | **[Article](../api-resource-article/)** | -| id | integer (primary key) | | -| from | integer/string | | -| to | string | | -| price | double | | -| pseudoPrice | double | | -| basePrice | double | | -| percent | double | | +| Field | Type | Original object | +|-----------------------------|-----------------------|-------------------------------------------------------| +| customerGroupKey | string (foreign key) | **[CustomerGroup](#customer-group)** | +| customerGroup | object | **[CustomerGroup](#customer-group)** | +| articleDetailsId | integer (foreign key) | **[Detail](#article-detail)** | +| articleId | integer (foreign key) | **[Article](../api-resource-article/)** | +| id | integer (primary key) | | +| from | integer/string | | +| to | string | | +| price | double | | +| pseudoPrice | double | | +| basePrice | double | | +| percent | double | | +| regulationPrice (>= v5.7.8) | double | | ## Price Group @@ -647,7 +784,7 @@ subgroup: REST API | Field | Type | Original object | |-----------------------|-----------------------|-------------------------------------------------| -| id | integer (primary key | | +| id | integer (primary key) | | | name | string | | | position | integer | | | comparable | boolean | | @@ -690,7 +827,7 @@ subgroup: REST API | id | integer (primary key) | | | value | string | | -## Related +## Related (GET) * **Table:** s_articles_relationships @@ -699,23 +836,36 @@ subgroup: REST API | Field | Type | Original object | |-----------------------|-----------------------|-------------------------------------------------------| | id | integer (foreign key) | **[Article](../api-resource-article/)** | -| name | string | | +| name | string | Article name | + +## Related (PUT, POST) + +* **Table:** s_articles_relationships + +### Structure + +| Field | Type | Original object | +|-----------------------|-----------------------|-------------------------------------------------------| +| id | integer (foreign key) | **[Article](../api-resource-article/)** | +| number | string | Article number | ## Shipping -* **Model:** Shopware\Models\Customer\Shipping -* **Table:** s_user_shippingaddress +* **Model:** Shopware\Models\Order\Shipping +* **Table:** s_order_shippingaddress ### Structure | Field | Type | Original object | |---------------------|-----------------------|-------------------------------------------------| | id | integer (primary key) | | -| customerId | integer (foreign key) | **[Customer](../api-resource-customer/)** | +| orderId | integer (primary key) | | +| customerId | integer (foreign key) | **[Customer](#customer)** | | countryId | integer (foreign key) | **[Country](#country)** | | stateId | integer (foreign key) | **[State](#state)** | | company | string | | | department | string | | +| title | string | | | salutation | string | | | number | string | | | firstName | string | | @@ -723,19 +873,23 @@ subgroup: REST API | street | string | | | zipCode | string | | | city | string | | -| attribute | object | **[SippingAttribute](#shipping-attribute)** | +| additionalAddressLine1 | string | | +| additionalAddressLine2 | string | | +| country | object | **[Country](#country)** | +| state | object | **[State](#state)** | +| attribute | object | **[ShippingAttribute](#shipping-attribute)** | ## Shipping Attribute -* **Model:** Shopware\Models\Attribute\CustomerShipping -* **Table:** s_user_shippingaddress_attributes +* **Model:** Shopware\Models\Attribute\OrderBilling +* **Table:** s_order_shippingaddress_attributes ### Structure | Field | Type | Original object | |---------------------|-----------------------|-------------------------------------------------| | id | integer (primary key) | | -| customerShippingId | integer (foreign key) | | +| orderShippingId | integer (foreign key) | | | text1 | string | | | text2 | string | | | text3 | string | | @@ -763,7 +917,6 @@ subgroup: REST API | baseUrl | string | | | hosts | string | | | secure | boolean | | -| alwaysSecure | boolean | | | secureHost | string | | | secureBasePath | string | | | default | boolean | | @@ -773,7 +926,7 @@ subgroup: REST API **The locale is only available for languageSubShops.** -## Similar +## Similar (GET) * **Table:** s_articles_similar @@ -782,7 +935,18 @@ subgroup: REST API | Field | Type | Original object | |-----------------------|-----------------------|-------------------------------------------------------| | id | integer (foreign key) | **[Article](../api-resource-article/)** | -| name | string | | +| name | string | Article name | + +## Similar (PUT, POST) + +* **Table:** s_articles_similar + +### Structure + +| Field | Type | Original object | +|-----------------------|-----------------------|-------------------------------------------------------| +| id | integer (foreign key) | **[Article](../api-resource-article/)** | +| number | string | Article number | ## State @@ -794,7 +958,7 @@ subgroup: REST API | Field | Type | Original object | |-----------------------|-----------------------|-------------------------------------------------| | id | integer (primary key) | | -| countryId | integer (foreign key) | **[Country](../api-resource-countries/)** | +| countryId | integer (foreign key) | **[Country](#country)** | | position | integer | | | name | string | | | shortCode | string | | diff --git a/source/developers-guide/rest-api/plugin-api-extension/index.md b/source/developers-guide/rest-api/plugin-api-extension/index.md index 45822acb54..78e8e90890 100644 --- a/source/developers-guide/rest-api/plugin-api-extension/index.md +++ b/source/developers-guide/rest-api/plugin-api-extension/index.md @@ -11,14 +11,14 @@ tags: group: Developer Guides subgroup: REST API menu_title: Create your own endpoint -menu_order: 130 +menu_order: 320 ---
    ## Introduction -This article describes how to extend the REST API and create an API endpoint. We create an example plugin which -provides functions for managing banners. +This article describes how to extend the REST API and create an API endpoint. +We create an example plugin which provides functions for managing banners. Normally every basic API resource contains of two parts: * a controller which handles the different request types(POST, GET, PUT, DELETE) @@ -29,12 +29,12 @@ operations API endpoint file structure ## Plugin files -For our REST API example we only need a few files. For more information about necessary files and the 5.2 plugin system -see the [5.2 plugin guide](/developers-guide/plugin-system). +For our REST API example we only need a few files. +For more information about necessary files and the 5.2 plugin system see the [5.2 plugin guide](/developers-guide/plugin-system). ### SwagBannerApi.php -``` +```php + +``` +The API controller will be found via auto-registration. +Please refer to the [Controllers chapter](developers-guide/controller/#plugin-controllers) for further information about the controller naming convention. + +You even have the possibility to decorate existing resources. +For more information about that, have a look here: [Extend API resource](/developers-guide/rest-api/extend-api-resource/) ### Components/Api/Resource/Banner.php -The resource gets called by our controller. Every controller action relies on one method of our resource. +The resource gets called by our controller. +Every controller action relies on one method of our resource. * indexAction -> getList() -> returns a list of banners * getAction -> getOne() -> returns one banner identified by its id * putAction -> update() -> updates one banner identified by its id * postAction -> create() -> creates a new banner * deleteAction -> delete() -> deletes a banner -We recommend using doctrine models in the resource, because it allows you to use the `fromArray()` method in the -`create()` and `update()` method to write the data directly. `fromArray()` searches for the setter methods of the -attributes and saves the values to the variables which saves you time and code. +We recommend using doctrine models in the resource, +because it allows you to use the `fromArray()` method in the `create()` and `update()` method to write the data directly. +`fromArray()` searches for the setter methods of the attributes and saves the values to the variables which saves you time and code. -``` +```php get('banner/1'); -``` + +{% include 'api_badge.twig' with {'route': '/api/banner/1', 'method': 'GET'} %} Result: -``` +```json { "success": true, "data": { @@ -433,14 +438,12 @@ Result: ``` ### GET(List) -Get a list of banners. With the optional `limit` parameter, it is possible to specify how many banner you want the -API to return. -``` -$client->get('banner'); -``` +Get a list of banners. With the optional `limit` parameter, it is possible to specify how many banner you want the API to return. + +{% include 'api_badge.twig' with {'route': '/api/banner', 'method': 'GET'} %} Result -``` +```json { "success": true, "data": { @@ -485,17 +488,18 @@ Result ``` ### PUT -To update a banner it is always required to provide the id of the banner. In this example, we will update the -`description` of the banner with id 1. -``` -$client->put('banner/1', [ - 'description' => 'New description' -]); +To update a banner it is always required to provide the id of the banner. +In this example, we will update the `description` of the banner with id 1. + +{% include 'api_badge.twig' with {'route': '/api/banner/1', 'method': 'PUT', 'body': true} %} +```json +{ + "description": "New description" +} ``` Result: -``` -{ +```json { "success": true, "data": { @@ -508,18 +512,19 @@ Result: ### POST Create a new banner -``` -$client->post('banner', [ - 'description' => 'Shopware-Example-Banner1', - 'image' => 'media/image/41/f8/25/Blog-Koffer.jpg', - 'link' => 'www.shopware.com', - 'linkTarget' => '_blank', - 'categoryId' => '3', -]); +{% include 'api_badge.twig' with {'route': '/api/banner', 'method': 'POST', 'body': true} %} +```json +{ + "description": "Shopware-Example-Banner1", + "image": "media\/image\/41\/f8\/25\/Blog-Koffer.jpg", + "link": "www.shopware.com", + "linkTarget": "_blank", + "categoryId": "3" +} ``` Result: -``` +```json { "success": true, "data": { @@ -532,12 +537,10 @@ Result: ### DELETE Delete one banner identified by its id. In this case we delete the banner with id 1. -``` -$client->delete('banner/1'); -``` +{% include 'api_badge.twig' with {'route': '/api/banner/1', 'method': 'DELETE'} %} Result: -``` +```json { "success": true } @@ -545,5 +548,3 @@ Result: ## Download plugin ## The whole plugin can be downloaded here. - - diff --git a/source/developers-guide/rest.html b/source/developers-guide/rest.html index dab799a88b..e4db784f14 100644 --- a/source/developers-guide/rest.html +++ b/source/developers-guide/rest.html @@ -11,18 +11,38 @@ \ No newline at end of file +
  • REST API - Frequently Asked Questions
  • + diff --git a/source/developers-guide/risk-rules/index.md b/source/developers-guide/risk-rules/index.md index 482eca69e7..af045b03d0 100755 --- a/source/developers-guide/risk-rules/index.md +++ b/source/developers-guide/risk-rules/index.md @@ -14,7 +14,7 @@ Shopware 5.1.2 introduced a new event to provide custom risk rules: `Shopware_Mo
    -## Extend Risk Managment Backend Module +## Extend Risk Management Backend Module To add a new risk rule to the ExtJS store used by the backend module we use the smarty block `backend/risk_management/store/risk/data` @@ -22,34 +22,58 @@ that can be found in `themes/Backend/ExtJs/backend/risk_management/store/risks.j ```php -// in install() -$this->subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_RiskManagement', - 'onRiskManagementBackend' -); -``` - -```php -public function onRiskManagementBackend(Enlight_Controller_ActionEventArgs $args) -{ - $subject = $args->getSubject(); - $request = $subject->Request(); +View(); - $view->addTemplateDir(__DIR__.'/Views/'); +use Enlight\Event\SubscriberInterface; - if ($request->getActionName() === 'load') { - $view->extendsTemplate('backend/my_risk_rule/store/risks.js'); +class RiskManagement implements SubscriberInterface +{ + /* + * @var $pluginPath string + */ + private $pluginPath; + /** + * @param $pluginPath + */ + public function __construct($pluginPath) + { + $this->pluginPath = $pluginPath; + } + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Backend_RiskManagement' => 'onRiskManagementBackend' + ]; + } + public function onRiskManagementBackend(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Backend_Customer $controller */ + $controller = $args->get('subject'); + + $view = $controller->View(); + $request = $controller->Request(); + + $view->addTemplateDir($this->pluginPath . '/Resources/views'); + + if ($request->getActionName() == 'load') { + $view->extendsTemplate('backend/swag_custom_risk_rule/store/risks.js'); + } } } ``` -The file `SwagCustomRiskRule/Views/backend/my_risk_rule/store/risks.js` adds a new entry to the existing store: +The file `SwagCustomRiskRule/Resources/views/backend/my_risk_rule/store/risks.js` adds a new entry to the existing store: + +```javascript +// SwagCustomRiskRule/Resources/views/backend/my_risk_rule/store/risks.js -``` -// SwagCustomRiskRule/Views/backend/my_risk_rule/store/risks.js //{block name="backend/risk_management/store/risk/data"} -// {$smarty.block.parent} +//{$smarty.block.parent} { description: 'My Custom Rule', value: 'MyCustomRule' }, //{/block} ``` @@ -59,11 +83,14 @@ The file `SwagCustomRiskRule/Views/backend/my_risk_rule/store/risks.js` adds a n Given the new rule is named `MyCustomRule` the event we have subscribed to is `Shopware_Modules_Admin_Execute_Risk_Rule_sRiskMyCustomRule`. ```php -// in install() -$this->subscribeEvent( - 'Shopware_Modules_Admin_Execute_Risk_Rule_sRiskMyCustomRule', - 'onMyCustomRule' -); +// extend existing subscriber or define a new one + +public static function getSubscribedEvents() +{ + return [ + 'Shopware_Modules_Admin_Execute_Risk_Rule_sRiskMyCustomRule' => 'onMyCustomRule' + ]; +} ``` In the event callback we have access to the the name of the rule, the user, the basket and to the value: @@ -88,67 +115,8 @@ public function onMyCustomRule(Enlight_Event_EventArgs $args) } ``` +## The complete plugin -## The complete plugin bootstrap - - -```php -// SwagCustomRiskRule/Bootstrap.php -subscribeEvent( - 'Enlight_Controller_Action_PostDispatchSecure_Backend_RiskManagement', - 'onRiskManagementBackend' - ); - - $this->subscribeEvent( - 'Shopware_Modules_Admin_Execute_Risk_Rule_sRiskMyCustomRule', - 'onMyCustomRule' - ); - - return true; - } - - /** - * @param Enlight_Controller_ActionEventArgs $args - */ - public function onRiskManagementBackend(Enlight_Controller_ActionEventArgs $args) - { - $subject = $args->getSubject(); - $request = $subject->Request(); - - $view = $subject->View(); - $view->addTemplateDir(__DIR__.'/Views/'); - - if ($request->getActionName() === 'load') { - $view->extendsTemplate('backend/my_risk_rule/store/risks.js'); - } - } - - /** - * @param Enlight_Event_EventArgs $args - * @return bool - */ - public function onMyCustomRule(Enlight_Event_EventArgs $args) - { - $rule = $args->get('rule'); - $user = $args->get('user'); - $basket = $args->get('basket'); - $value = $args->get('value'); - - if ($basket['AmountNumeric'] > $value) { - return true; // it's a risky customer - } - - return false; - } -} - -``` - -You can find a installable ZIP package of this plugin here. +You can find an installable ZIP package of this plugin here. diff --git a/source/developers-guide/services/index.md b/source/developers-guide/services/index.md index 9f2ab694e9..a9e9f44b24 100644 --- a/source/developers-guide/services/index.md +++ b/source/developers-guide/services/index.md @@ -39,71 +39,27 @@ Instead of creating the `TaxCalculator` class everywhere you need it, you can ma This way the same instance of this class can be accessed everywhere in Shopware - even by other plugins. ## Registering the service -First of all, you should register the namespace of your plugin in your plugin's bootstrap: +To register a service, you can add it to your plugins `services.xml` like this: -```php -get('Loader')->registerNamespace( - 'ShopwarePlugins\SwagService', - $this->Path() - ); - } -} -``` - -Now the actual service can be registered using an event: - -```php -subscribeEvent( - 'Enlight_Bootstrap_InitResource_swag_service_plugin.tax_calculator', - 'onInitTaxCalculator' - ); - - return true; - } -} -``` +```xml + -The event name `Enlight_Bootstrap_InitResource_swag_service_plugin.tax_calculator` consists of two parts: + -* `Enlight_Bootstrap_InitResource_`: The base event name, which is emitted by the DI container when a service is looked up -* `swag_service_plugin.tax_calculator`: The unique service name. It is best practice to have the plugin name `swag_service_plugin` -and the service name `tax_calculator` in it. + + + -The event callback now just needs to return an instance of the requested service: - -```php -public function onInitTaxCalculator() -{ - return new \ShopwarePlugins\SwagService\Component\TaxCalculator(); -} + ``` -Be aware that the event will only be emitted (and thus the callback will only be called) when the service is actually requested. - ## Calling the service -The new `TaxCalculator` can now be requested using the Shopware DI container: - -```php -$this->get('swag_service_plugin.tax_calculator'); -``` - -This will work e.g. from controllers or plugin bootstraps. You can also use the global Shopware container singleton, if needed: - +The new `TaxCalculator` can now be requested from e.g. a controller using the Shopware DI container: ```php -Shopware()->Container()->get('swag_service_plugin.tax_calculator'); +$this->container->get('swag_service_plugin.tax_calculator'); ``` Keep in mind that any subsequent calls will return the same instance of the object - the container will keep a reference to the @@ -116,17 +72,30 @@ In many cases, your services might depend on other services. Usually you will in ```php logger = $logger; } - + + /** + * @param $netPrice float + * @param $tax float + * @return float + */ public function calculate($netPrice, $tax) { $this->logger->debug('Calculating price for tax: ' . $tax); @@ -135,15 +104,20 @@ class TaxCalculator } ``` -In order to inject this dependency, a simple update of the event callback is needed: +```xml + -```php -public function onInitTaxCalculator() -{ - return new \SwagServicePlugin\Components\TaxCalculator( - $this->get('pluginlogger') - ); -} + + + + + + + + + ``` ## Download diff --git a/source/developers-guide/shopware-5-cli-commands/index.md b/source/developers-guide/shopware-5-cli-commands/index.md index bd8bc3b924..deeb4600d7 100644 --- a/source/developers-guide/shopware-5-cli-commands/index.md +++ b/source/developers-guide/shopware-5-cli-commands/index.md @@ -36,53 +36,38 @@ php bin/console --help This will provide you with a description of the command's functionality, along with a detailed explanation of each of the command's arguments and options. -## Creating custom Shopware's CLI commands +## Creating custom Shopware CLI commands -As a plugin developer, you can create your own custom CLI commands. To do so, your plugin's `Bootstrap.php` must first register the command itself: +As a plugin developer, you can create your own custom CLI commands. To do so, you can register the commands as services and tag them with `console.command` in your plugins `services.xml` file: -```php -subscribeEvent( - 'Shopware_Console_Add_Command', - 'onAddConsoleCommand' - ); - - return true; - } +```xml + - public function afterInit() - { - $this->get('Loader')->registerNamespace( - 'ShopwarePlugins\\SwagExample', - __DIR__ . '/' - ); - } + - public function onAddConsoleCommand(Enlight_Event_EventArgs $args) - { - return new ArrayCollection(array( - new \ShopwarePlugins\SwagExample\Commands\ImportCommand(), - new \ShopwarePlugins\SwagExample\Commands\ListCommand(), - )); - } -} + + + + + + ``` -The above example registers 2 commands, `import` and `list`. You now need to implement these commands. +The above example registers the `import` command. You now need to implement that command. -Shopware's CLI commands are based on the Symfony 2 Console Component, which documentation you can find [here](http://symfony.com/doc/current/components/console/introduction.html). +Shopware's CLI commands are based on the Symfony 3 Console Component, the documentation of which you can find [here](https://symfony.com/doc/4.4/components/console.html). ```php setName('swagexample:import') + ->setName('swagcommandexample:import') ->setDescription('Import data from file.') ->addArgument( 'filepath', @@ -123,4 +108,55 @@ EOF } ``` -As you can see, a Shopware CLI command is very similar to a Symfony 2 command. You can use any of the features provided by the Symfony Console component, like you would in Symfony 2 itself. +As you can see, a Shopware CLI command is very similar to a Symfony 3 command. You can use any of the features provided by the Symfony Console component, like you would in Symfony 3 itself. + +## Add completion for commands + +Shopware uses [stecman/symfony-console-completion](https://github.com/stecman/symfony-console-completion) to add completion features you might already know from other CLI. +This composer package automatically adds completion for options and names to any command. +Some option names need values like the `--batch` option from `sw:plugin:update`, so the package can not recognize the allowed values automatically. +To add support for option value and argument completion you need to implement the interface `Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface`. +Both methods the interface expects (`completeOptionValues` and `completeArgumentValues`) behave similarly as they both expect all possible values for the option respectively argument at the cursor position to offer for completion returned as array. +For example for the `--batch` option you can simply write: +```php +public function completeOptionValues($optionName, CompletionContext $context) +{ + if ($optionName === 'batch') { + return ['active', 'inactive']; + } + + return []; +} +``` +In case of simple filename expansion you implement it like this: +```php +public function completeArgumentValues($argumentName, CompletionContext $context) +{ + if ($argumentName === 'file') { + return $this->completeInDirectory(); + } + + return []; +} +``` + +## Use completion in shell + +If you change your working directory to the project root you just have to execute the generated shell code like this: +```sh +source <(bin/console _completion --generate-hook) +``` +This can be easily added to your `.profile` file. +In case you have to use custom php parameters for the `bin/console` application you just add an alias and register the completion also for this alias. +Therefore you save the generated shell code and look for the rows that look similar to the following snippet and duplicate line 5: +```sh +alias console-debug="php -dzend_extension=xdebug.so ${PROJECT_ROOT}/bin/console" + +if [ "$(type -t _get_comp_words_by_ref)" == "function" ]; then + complete -F _console_12345567890abcdef_complete "console"; + complete -F _console_12345567890abcdef_complete "console-debug"; +else + >&2 echo "Completion was not registered for console:"; + >&2 echo "The 'bash-completion' package is required but doesn't appear to be installed."; +fi; +``` diff --git a/source/developers-guide/shopware-5-core-service-extensions/index.md b/source/developers-guide/shopware-5-core-service-extensions/index.md index aa7234e564..de26cef604 100644 --- a/source/developers-guide/shopware-5-core-service-extensions/index.md +++ b/source/developers-guide/shopware-5-core-service-extensions/index.md @@ -16,108 +16,125 @@ menu_order: 130 The new bundle services like __StoreFrontBundle__, __SearchBundle__ or __SearchBundleDBAL__ don't contain events or hooks, but can still be replaced or extended. -All services are defined in Shopware's dependency injection container, also known as DI container or DIC. A plugin can replace these services in the DIC: +All services are defined in Shopware's dependency injection container, also known as DI container or DIC. A plugin (`SwagExample` in this case) can replace these services in the DIC by overriding them in its `services.xml` file: -```php -public function onDispatchEventListener() -{ - $newService = new ReplacementServiceImplementation(); - Shopware()->Container()->set('shopware_storefront.list_product_service', $newService); -} +```xml + + + + + + + + + ``` -In this scenario, the __ReplacementServiceImplementation__ completely replaces the previous implementation, and is now fully responsible for providing the expected functionality. +In this scenario, the plugins __SwagExample\Bundle\StoreFrontBundle\ListProductService__ completely replaces the previous implementation, and is now fully responsible for providing the expected functionality. In most cases, however, custom services want to extend the default behaviour, rather than completely replace it. In these scenarios, it's possible to use a __decorator pattern__ to modify the original service's behaviour: -```php -public function onDispatchEventListener() -{ - $coreService = Shopware()->Container()->get('shopware_storefront.list_product_service'); - $newService = new DecoratorServiceImplementation($coreService); - Shopware()->Container()->set('shopware_storefront.list_product_service', $newService); -} +```xml + + + + + + + + + + + ``` -The __DecoratorServiceImplementation__ gets the existing implementation as a constructor argument, and can use it internally in its own logic. Another great advantage of using a decorator pattern over a full service replacement is that multiple implementations can provide their only logic on top of the previously existing service, regardless of it being the core service itself or an already decorated version/replacement of it. +The plugins __SwagExample\Bundle\StoreFrontBundle\ListProductService__ gets the existing implementation as a constructor argument, and can use it internally in its own logic. Another great advantage of using a decorator pattern over a full service replacement is that multiple implementations can provide their own logic on top of the previously existing service, regardless of it being the core service itself or an already decorated version/replacement of it. In the next paragraphs we will further explain how and why you should implement each of these approaches, and provide demo code that you can use as a base for your own implementation. ## Your extension plugin -If you want to develop a plugin that replaces or decorates a core service, you first need to have it hook into the core logic, so that your code is executed. Additionally, since services can be reused in multiple locations in the Shopware core (and even in other plugins), it's important to replace/decorate them at the right moment, or there might be unexpected behaviour +If you want to develop a plugin that replaces or decorates a core service, you need a basic plugin structure, so your code is executed: + +```php +subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service', - 'registerService', - 500 - ); - return true; - } + private $originalService; - public function afterInit() + public function __construct(ListProductServiceInterface $service) { - $this->get('Loader')->registerNamespace('ShopwarePlugins\ServiceExtension', $this->Path()); + $this->originalService = $service; } - public function registerService() + public function getList(array $numbers, ProductContextInterface $context) { - // implement your replacement/decoration logic here + $products = $this->originalService->getList($numbers, $context); + + // Modify product list + + return $products; } } + ``` -## Replacing an existing service +To actually decorate the existing service you have to override it in your `services.xml` as described in the [introduction](#introduction): -In some scenarios, it might be convenient to fully discard the core implementation in favour of a completely new logic. Suppose you want to implement a plugin that loads your products from a __Redis__ instance, rather than the __MySql__ database (for now, let's ignore how the __Redis__ instance is populated). In this scenario, your logic has nothing in common with the default service, so you can simply replace it with your custom implementation: +```xml + -```php - -use ShopwarePlugins\SwagRedis\StoreFrontBundle\RedisProductService; + +
    +``` -class Shopware_Plugins_Frontend_SwagRedis_Bootstrap - extends Shopware_Components_Plugin_Bootstrap -{ - public function install() - { - $this->subscribeEvent( - 'Enlight_Bootstrap_InitResource_shopware_storefront.list_product_service', - 'replaceListProductService', - 200 - ); - return true; - } +## Replacing an existing service - public function afterInit() - { - $this->get('Loader')->registerNamespace('ShopwarePlugins\SwagRedis', $this->Path()); - } +In some scenarios, it might be convenient to fully discard the core implementation in favour of a completely new logic. Suppose you want to implement a plugin that loads your products from a __Redis__ instance, rather than the __MySql__ database (for now, let's ignore how the __Redis__ instance is populated). In this scenario, your logic has nothing in common with the default service, so you can simply replace it with your custom implementation: - public function replaceListProductService() - { - return new RedisProductService(); - } -} +```xml + + + ``` -The Enlight_Bootstrap_InitResource allows to handle service initialisation. -As you can see, the old __shopware_storefront.list_product_service__ instance that existed in the DIC is overwritten by the __RedisProductService__ instance you return. +As you can see, the old __shopware\_storefront.list\_product\_service__ instance that existed in the DIC is overwritten by the __RedisProductService__ instance. + ```php subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service', - 'decorateService', - 200 - ); - return true; - } +```xml + - public function afterInit() - { - $this->get('Loader')->registerNamespace('ShopwarePlugins\SwagRedis', $this->Path()); - } + - public function decorateService() - { - $coreService = Shopware()->Container()->get('shopware_storefront.list_product_service'); - $redisService = new RedisProductService($coreService); - Shopware()->Container()->set('shopware_storefront.list_product_service', $redisService); - } -} + + ``` -This implementation is very similar to the one presented before. However, if you look closely, you will notice that the default __shopware_storefront.list_product_service__ implementation is not discarded, but passed to the __RedisProductService__ constructor. +This implementation is very similar to the one presented before. However, if you look closely, you will notice that the default __shopware_storefront.list_product_service__ implementation is not discarded, but passed to the __RedisProductService__ constructor as an argument via the `` tag. It is important to append the `.inner` to the id here - this way the original service is addressed instead of our own implementation of it. ```php connection = new RedisConnection(); - $this->service = $service; + $this->originalService = $service; } public function getList(array $numbers, Struct\ProductContextInterface $context) @@ -227,7 +225,7 @@ class RedisProductService implements ListProductServiceInterface $redisProducts = $this->connection->get(...); // if some data is missing, fallback to the database connection - $coreProducts = $this->service->getList(...); + $coreProducts = $this->originalService->getList(...); // and add the missing data to Redis $this->connection->put($coreProducts, ...); @@ -244,8 +242,7 @@ class RedisProductService implements ListProductServiceInterface } ``` -As you can see, this new version of the __RedisProductService__ is able to use the previous service to load data from the __MySql__ database and store it __Redis__. In subsequent requests, as the data is already available in __Redis__, __MySql__ is not queried, resulting in improved performance. Thus, __Redis__ is used as a product cache (again, for academic reasons, product updates were intentionally ignored). - +As you can see, this new version of the __RedisProductService__ is able to use the previous service to load data from the __MySql__ database and store it in __Redis__. In subsequent requests, as the data is already available in __Redis__, __MySql__ is not queried, resulting in improved performance. Thus, __Redis__ is used as a product cache (again, for academic reasons, product updates were intentionally ignored). ## Multiple cascading decorators @@ -253,42 +250,34 @@ As we've seen before, decorator services can reuse the core services and reuse o ```php subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service', - 'decorateService', - 400 - ); - return true; - } +use Shopware\Components\Plugin; - public function afterInit() - { - $this->get('Loader')->registerNamespace('ShopwarePlugins\SwagElasticSearch', $this->Path()); - } +class SwagElasticSearch extends Plugin {} +``` - public function decorateService() - { - $coreService = Shopware()->Container()->get('shopware_storefront.list_product_service'); - $elasticSearchService = new ElasticSearchProductService($coreService); - Shopware()->Container()->set('shopware_storefront.list_product_service', $elasticSearchService); - } -} +```xml + + + + + + ``` This __SwagElasticSearch__ example is very similar to the __Redis__ integration plugin we discussed before. The underlying implementation will also look familiar: ```php connection = new ElasticSearchConnection(); - $this->service = $service; + $this->originalService = $service; } public function getList(array $numbers, Struct\ProductContextInterface $context) @@ -319,7 +308,7 @@ class ElasticSearchProductService implements ListProductServiceInterface $elasticSearchProducts = $this->connection->get(...); // if some data is missing, fallback to the database connection - $coreProducts = $this->service->getList(...); + $coreProducts = $this->originalService->getList(...); // and add the missing data to Elastic Search $this->connection->put($coreProducts, ...); @@ -338,10 +327,6 @@ class ElasticSearchProductService implements ListProductServiceInterface Since both plugin use a decorator pattern, they extend the previously exiting service, that doesn't necessarily have to be the Shopware default __shopware_storefront.list_product_service__ implementation. Suppose that, for some reason, you want to use __Redis__ AND __ElasticSearch__ simultaneously on your Shopware shop. The above implementations can be used together. -The specified event priority in the Bootstrap files determines which event listener is executed first. In this case, __SwagElasticSearch__ has higher priority, so it will execute first, picking up the default Shopware __shopware_storefront.list_product_service__ implementation and decorating it with our very naive __ElasticSearch__ based cache. - -Following this, the event listener on __SwagRedis__ is called to decorate the __shopware_storefront.list_product_service__. Notice that, at this point, the __shopware_storefront.list_product_service__ implementation in the DIC no longer contains an instance of the default Shopware core class, but rather an instance of __ElasticSearchProductService__. However, this is no problem at all. The __RedisProductService__ will decorate the __ElasticSearchProductService__ instead, using it as a fallback, in case the product we are looking for is not yet loaded on __Redis__. - ## Other examples The above examples show how you can use service decoration to use a data source other than __MySql__ in Shopware. And, as we also showed before, you can even replace the __MySql__ access altogether with a completely different data source, provided that your service implements the expected interface and behaviour. @@ -350,36 +335,25 @@ However, you can use service decoration for tasks other than this. ```php subscribeEvent( - 'Enlight_Bootstrap_AfterInitResource_shopware_storefront.list_product_service', - 'decorateService', - 600 - ); - return true; - } +use Shopware\Components\Plugin; - public function afterInit() - { - $this->get('Loader')->registerNamespace( - 'ShopwarePlugins\SwagLiveShopping', - $this->Path() - ); - } +class SwagLiveShopping extends Plugin {} +``` - public function decorateService() - { - $coreService = Shopware()->Container()->get('shopware_storefront.list_product_service'); - $newService = new LiveShoppingService($coreService); - Shopware()->Container()->set('shopware_storefront.list_product_service', $newService); - } -} +```xml + + + + + + ``` The above __SwagLiveShopping__ plugin (not related in any way with Shopware's LiveShopping premium plugin) uses a pattern already familiar to us, in order to decorate the __shopware_storefront.list_product_service__ service. @@ -387,35 +361,32 @@ The above __SwagLiveShopping__ plugin (not related in any way with Shopware's Li ```php service = $service; + $this->originalService = $service; } public function getList(array $numbers, Struct\ProductContextInterface $context) { - $products = $this->service->getList($numbers, $context); + $products = $this->originalService->getList($numbers, $context); foreach ($products as $product) { - $product->addAttribute( - 'live_shopping', - new Struct\Attribute(['live_shopping_id' => 1]) - ); + $product->addAttribute('live_shopping', new Struct\Attribute(['live_shopping_id' => 1])); } return $products; @@ -423,28 +394,25 @@ class LiveShoppingService implements ListProductServiceInterface public function get($number, Struct\ProductContextInterface $context) { - $product = $this->service->get($number, $context); + $product = $this->originalService->get($number, $context); - $product->addAttribute( - 'live_shopping', - new Struct\Attribute(['live_shopping_id' => 1]) - ); + $product->addAttribute('live_shopping', new Struct\Attribute(['live_shopping_id' => 1])); return $product; } } ``` -The actual implementation uses the previously existing data source and, for each product retrieved from it, adds a custom attribute. Using this same approach, you can manipulate any resulting __Struct\ListProduct__ using whichever logic or criteria you which to implement. +The actual implementation uses the previously existing data source and, for each product retrieved from it, adds a custom attribute. Using this same approach, you can manipulate any resulting __Struct\ListProduct__ using whichever logic or criteria you wish to implement. -Again, notice that using the decorator pattern allows us to not worry about the underlying implementation that we are decorating, or any other decoration that might be applied on top of ours. The __LiveShoppingService__ can decorate or be decorated by __ElasticSearchProductService__ or __RedisProductService__, individually or simultaneously, and the end result would still be the same. +Again, notice that using the decorator pattern allows us to not worry about the underlying implementation that we are decorating, or any other decoration that might be applied on top of ours. The __LiveShoppingProductService__ can decorate or be decorated by __ElasticSearchProductService__ or __RedisProductService__, individually or simultaneously, and the end result would still be the same. -This is rule is, of course, not always applicable, as undesired interactions or conflicts can occur, depending on the customizations performed by each decorator, or the order in which they are executed. However, this example does demonstrate that it is very easy to provide non-conflicting implementations of different decorators, which can work together to implement multiple features on your Shopware shop. +This rule is, of course, not always applicable, as undesired interactions or conflicts may occur, depending on the customizations performed by each decorator, or the order in which they are executed. However, this example does demonstrate that it is very easy to provide non-conflicting implementations of different decorators, which can work together to implement multiple features on your Shopware shop. ## Conclusions This example is very academic, and not adequate for production environments, but it does illustrate a very important idea: decorating over replacing allows multiple plugins to extend the same service without causing conflicts or breaks in behaviour. It's even possible that your plugin never actually decorates the core implementation of a service, but an instance provided by another plugin you have installed in your current installation. If implemented correctly, this can be made fully transparent to you and your plugins, and multiple decoration layers can be added on the same service without undesired side effects. -This doesn't mean you need to always decorate existing services. In some scenarios, full replacement might be a better solution than decorating. Just keep in mind that other plugins might want to decorate your service as if it where the core implementation, to ensure both your and other plugins work as expected. +This doesn't mean you always need to decorate existing services. In some scenarios, full replacement might be a better solution than decorating. Just keep in mind that other plugins might want to decorate your service as if it were the core implementation, to ensure both your and other plugins work as expected. You can find a small example plugin here. diff --git a/source/developers-guide/shopware-5-media-service/index.md b/source/developers-guide/shopware-5-media-service/index.md index ab40dd9d76..1ac3c24e3b 100644 --- a/source/developers-guide/shopware-5-media-service/index.md +++ b/source/developers-guide/shopware-5-media-service/index.md @@ -133,18 +133,63 @@ Using the MediaService allows Shopware to better cope with problems like the one By default, adapters for local and FTP based file systems are included since Shopware 5.1. +### Built-in Adapters since Shopware 5.5 + +* Amazon S3 +* Google Cloud Platform + +**Example Configuration S3** + +```php +'cdn' => [ + 'backend' => 's3', + 'adapters' => [ + 's3' => [ + 'type' => 's3', + 'mediaUrl' => 'YOUR_S3_OR_CLOUDFRONT_ENDPOINT', + 'bucket' => 'YOUR_S3_BUCKET_NAME', + 'region' => 'YOUR_S3_REGION', + 'credentials' => [ + 'key' => 'YOUR_AWS_KEY', + 'secret' => 'YOUR_AWS_SECRET' + ] + ] + ] +] +``` + + +**Example Configuration GCP** + +```php +'cdn' => [ + 'backend' => 'gcp', + 'adapters' => [ + 'gcp' => [ + 'type' => 'gcp', + 'mediaUrl' => 'YOUR_GCP_PUBLIC_URL', + 'bucket' => 'YOUR_GCP_BUCKET', + 'projectId' => 'YOUR_GCP_PROJECT_ID', + 'keyFilePath' => 'PATH_TO_GCP_KEY_FILE', + ] + ] +] +``` + ### Existing Adapters -You can download and install the following provider plugins just like any other Shopware plugin. Keep in mind that no official support will be provided for these plugins. +You can download and install the following provider plugins just like any other Shopware plugin. +Keep in mind that no official support will be provided for these plugins. -* [Amazon S3](https://github.com/shopwareLabs/SwagMediaS3) -* [Microsoft Azure](https://github.com/shopwareLabs/SwagMediaAzure) -* [Google Cloud Platform](https://github.com/shopwareLabs/SwagMediaGCP) -* [SFTP](https://github.com/shopwareLabs/SwagMediaSFTP) +* [Amazon S3](https://github.com/shopware5/SwagMediaS3) - Required only on < 5.5 +* [Microsoft Azure](https://github.com/shopware5/SwagMediaAzure) +* [Google Cloud Platform](https://github.com/shopware5/SwagMediaGCP) - Required only on < 5.5 +* [SFTP](https://github.com/shopware5/SwagMediaSFTP) ### Build your own adapter -Since our MediaService is built on top of [Flysystem](http://flysystem.thephpleague.com), feel free to create your own adapter and share it with the community. You can take the plugins mentioned above as example. +Since our MediaService is built on top of [Flysystem](http://flysystem.thephpleague.com), feel free to create your own adapter and share it with the community. +You can take the plugins mentioned above as example. ### Migrating your files @@ -163,7 +208,7 @@ If you are still facing problems with media files, you should update your nginx #### Example: Migrating all media files to Amazon S3 -Assuming you already installed the [Amazon S3 adapter](https://github.com/ShopwareLabs/SwagMediaS3) plugin, you now need to configure it. To do so, you have to edit your Shopware `config.php` and add a new adapter called `s3` and a configuration to access Amazon S3 account like described in the [plugin description on GitHub](https://github.com/ShopwareLabs/SwagMediaS3). +Assuming you already installed the [Amazon S3 adapter](https://github.com/shopware5/SwagMediaS3) plugin, you now need to configure it. To do so, you have to edit your Shopware `config.php` and add a new adapter called `s3` and a configuration to access Amazon S3 account like described in the [plugin description on GitHub](https://github.com/shopware5/SwagMediaS3). You can now simply run this command to move all files from your local media file system to Amazon S3. @@ -237,4 +282,4 @@ bin/console sw:media:migrate --from=old_local --to=local After you've ran the command, your media files should be in the place they were at before the media service ways introduced. -Feel free to create your strategy and share it with the community. \ No newline at end of file +Feel free to create your strategy and share it with the community. diff --git a/source/developers-guide/shopware-5-performance-for-devs/index.md b/source/developers-guide/shopware-5-performance-for-devs/index.md index 5cf4a9cd0b..1879eb3767 100644 --- a/source/developers-guide/shopware-5-performance-for-devs/index.md +++ b/source/developers-guide/shopware-5-performance-for-devs/index.md @@ -7,8 +7,6 @@ tags: - performance - tips - cache -redirect: - - /developers-guide/shopware-5-performance/ group: Developer Guides subgroup: General Resources menu_title: Performance Guide @@ -36,6 +34,11 @@ You can learn more about Shopware's HTTP cache, its configuration options, behav Like mentioned before, the built in HTTP Cache is based on a PHP implementation, which is simple but has less than optimal performance. Should your site require it and your server support it, you can use tools like Varnish, which require additional installation and configuration steps, but can take full advantage of your server's capabilities to improve your shop's performance. We provide official [Varnish configuration](/sysadmins-guide/varnish-setup/) support for Enterprise customers. +## ESI tags +Excessive use of ESI tags can be a big performance problem. Especially if the cache hit rate is very low or they are completely uncached. ESI tags should be used with great care. +You can learn more about ESI tags and there implications by reading this [article](/blog/2016/07/11/on-action-tags/#slow-esi-tags). + + ## Theme Cache ### How the theme cache works @@ -48,7 +51,7 @@ In Shopware 5, along with the introduction of the new theme system, we created t - All CSS and JavaScript files are merged into single .css and .js files - The resulting files are minimized -This results in less server requests, less bandwidth usage and faster response times to the client. Both the core and plugin CSS and JavaScript files can be handled by both the LESS compiler and the theme cache. For info on how you can register your plugin resources to be handled by them, please refer to the [Theme startup guide](/designers-guide/theme-startup-guide). +This results in fewer server requests, less bandwidth usage and faster response times to the client. Both the core and plugin CSS and JavaScript files can be handled by both the LESS compiler and the theme cache. For info on how you can register your plugin resources to be handled by them, please refer to the [Theme startup guide](/designers-guide/theme-startup-guide). Please keep in mind that the theme cache is only used when using Shopware 5 themes. If you choose to use a Shopware 4 template in a Shopware 5 installation, your assets will not be compressed by the theme cache. Also, the CSS/JavaScript cache files are shared across all language shops of each shop. Different subshop have different cache content. diff --git a/source/developers-guide/shopware-5-plugin-update-guide/index.md b/source/developers-guide/shopware-5-plugin-update-guide/index.md index f83746a4da..517dcec49b 100644 --- a/source/developers-guide/shopware-5-plugin-update-guide/index.md +++ b/source/developers-guide/shopware-5-plugin-update-guide/index.md @@ -13,83 +13,128 @@ menu_order: 100 ## Introduction -In this guide we provide you with all essential information you need to keep your plugins Shopware 5.0 compatible. +In this guide we provide you with all essential information you need to keep your plugins Shopware 5.3 compatible. -Most changes are optional, as the old syntax will still work in most cases. +## Migrate the Bootstrap.php +To migrate the bootstrap.php we create a new plugin base file which has the technical name of the plugin. +Make sure the namespace and the class is called like the technical name of your plugin. +Extend the class from `Shopware\Components\Plugin\Plugin`. + +```php +scheduleClearCache(InstallContext::CACHE_LIST_DEFAULT); + } +} +``` ## Template extensions To ensure your templates files are extensible, neither __extendsTemplate__ nor __extendsBlock__ methods should be used for responsive template. Instead, you should use Shopware's auto loading mechanism. -The following example shows how template extension plugins need to be updated to achieve the best possible result for Shopware 5 templates. -The following source code was taken from the SwagExample1 plugin for Shopware 4, which displays a top seller slider and a banner in the article detail page: +The following example shows how template extension plugins need to be updated to achieve the best possible result for Shopware 5.3 templates. +The following source code displays a top seller slider and a banner in the product detail page: + +### 1. Add the template directory +Use the early "Enlight_Controller_Action_PreDispatch" event to register the template directory of your plugin. -#### SwagExample1/Bootstrap.php ```php subscribeEvent('Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail', 'onSecureDetailPostDispatch'); + /** + * @var Enlight_Template_Manager + */ + private $templateManager; - $form = $this->Form(); - $form->setElement( - 'mediaselection', - 'mediaselection', - ['label' => 'Media', 'value' => NULL] - ); + /** + * @var string + */ + private $pluginBaseDirectory; - return true; + /** + * @param Enlight_Template_Manager $templateManager + * @param string $pluginBaseDirectory + */ + public function __construct(Enlight_Template_Manager $templateManager, $pluginBaseDirectory) + { + $this->templateManager = $templateManager; + $this->pluginBaseDirectory = $pluginBaseDirectory; } - public function onSecureDetailPostDispatch(Enlight_Event_EventArgs $arguments) + /** + * Use the early "Enlight_Controller_Action_PreDispatch" event to register the template directory of the plugin. + * + * @inheritdoc + */ + public static function getSubscribedEvents() { - /**@var $controller Shopware_Controllers_Frontend_Listing*/ - $controller = $arguments->getSubject(); - $controller->View()->addTemplateDir($this->Path() . 'Views/'); - $controller->View()->extendsTemplate('frontend/detail/example1.tpl'); - $controller->View()->mediaSelection = $this->Config()->mediaselection; + return [ + 'Enlight_Controller_Action_PreDispatch' => 'onPreDispatch' + ]; + } + + public function onPreDispatch() + { + $this->templateManager->addTemplateDir($this->pluginBaseDirectory . '/Resources/views'); } } ``` +Register the event subscriber file by using the `services.xml`. To register a event subscriber use the **"shopware.event_subscriber"** tag. +`/.../SwagUpdatePlugin/Resources/services.xml` +```xml + -#### SwagExample1/Views/frontend/detail/example1.tpl -```html -{block name="frontend_detail_index_detail"} - {block name="frontend_detail_example"} -
    - {block name="frontend_detail_example_headline"} -

    My topseller

    - {/block} - - {block name="frontend_detail_example_img"} - Test - {/block} - - {block name="frontend_detail_example_topseller"} -
    - {action module=widgets controller=listing action=top_seller sCategory=3} -
    - {/block} -
    - {/block} -{/block} + + + + %swag_update_plugin.plugin_dir% + + + + +
    ``` +### 2. Create the template +The goal is to make this plugin compatible with the new Shopware 5 templates. For this purpose, the following should be considered: -The goal is to make this plugin compatible with both the new Shopware 5 and the old Shopware 4 templates. For this purpose, the following should be considered: + Inside the PostDispatch event, we have to load the corresponding template files. + The extendsTemplate function should not be used in the new template, otherwise the plugin template cannot be overwritten by other templates. -- Inside the __PostDispatch__ event, we have to distinguish between the different template version and load the corresponding template files. -- The __extendsTemplate__ function should not be used in the new template, otherwise the plugin template cannot be overwritten by other templates. -- In order for the plugin template to be easily extended by others, the template adjustments should be extracted into a separate file. + In order for the plugin template to be easily extended by others, the template adjustments should be extracted into a separate file. -First, the template structure is revised. The example1.tpl file is now divided into three new files: +First, the template structure is revised. The example1.tpl file is now divided into two new files: -- SwagExample1/Views/emotion/detail/example1.tpl (Entry point to extends the emotion template) -- SwagExample1/Views/responsive/frontend/detail/index.tpl (Entry point to extends the responsive template) -- SwagExample1/Views/common/frontend/swag_example1/detail_extension.tpl (Contains the source code for the extension) + *SwagExample1/Resources/views/frontend/detail/index.tpl (Entry point to extend the template) + *SwagExample1/Resources/views/frontend/swag_example1/detail_extension.tpl (Contains the source code for the extension) The new files contain the following source code: -#### SwagExample1/Views/common/frontend/swag_example1/detail_extension.tpl +`/.../SwagUpdatePlugin/Resources/views/frontend/detail/index.tpl` +```smarty +{extends file="parent:frontend/detail/index.tpl"} + +{block name="frontend_detail_index_detail"} + {include file="frontend/swag_example1/detail_extension.tpl"} +{/block} +``` + +`/.../SwagUpdatePlugin/Resources/views/frontend/swag_example1/detail_extension.tpl` ```html {block name="frontend_detail_example"}
    @@ -110,163 +155,166 @@ The new files contain the following source code: {/block} ``` -#### SwagExample1/Views/emotion/frontend/detail/example1.tpl -```html -{block name="frontend_detail_index_detail"} - {include file="frontend/swag_example1/detail_extension.tpl"} -{/block} -``` +Notice: Template extensions for the responsive template are loaded via the inheritance hierarchy based on the file system. Therefore, this template should be extends via {extends file = ".."}. -#### SwagExample1/Views/responsive/frontend/detail/index.tpl -```html -{extends file="parent:frontend/detail/index.tpl"} +The SwagExample1/Resources/views/frontend/detail/index.tpl file serve only as entry points into the original template. The source code for displaying the top seller sliders and the banner element, which was previously located directly in the extended template file, has now been made available globally in a separate template file, and is now simply included by the template. This has the following advantages: -{block name="frontend_detail_index_detail"} - {include file="frontend/swag_example1/detail_extension.tpl"} -{/block} -``` -
    -__Notice: Template extensions for the responsive template are loaded via the inheritance hierarchy based on the file system. Therefore, this template should be extends via {extends file = ".."}.__ + Avoid duplicate source code + Extensible plugin template for other developers -The SwagExample1/Views/responsive/frontend/detail/index.tpl and SwagExample1/Views/emotion/frontend/detail/example1.tpl files serve only as entry points into the original template. -The source code for displaying the top seller sliders and the banner element, which was previously located directly in the extended template file, has now been made available globally in a separate template file, and is now simply included by both templates. -This has the following advantages: +__Notice: To allow other templates to easily extend your templates, you should provide your code in a separate file. Include that file in other templates, inside Smarty blocks, thus allowing different entry point.__ -- Avoid duplicate source code -- Extensible plugin template for other developers +### 3. Add the plugin configuration +For adding plugin configuration you add a new xml file to the Resources directory. + +`/.../SwagUpdatePlugin/Resources/config.xml` +```xml + + + + + + mediaSelection + + + + + + +``` +Add a new element of the type `mediaselection`, set the name and the label. +For more information about plugin configuration have a look [here](https://developers.shopware.com/developers-guide/plugin-configuration/). + +Now we can read the configuration from the the `DBALConfigReader` and assign the value to the view. + +For this we create a new subscriber file called DetailSubscriber. In this file, we register the 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail' event and append our configurations variable to the view. +`/.../SwagUpdatePlugin/Resources/services.xml` +```xml + + + + + + + %swag_update_plugin.plugin_dir% + + + + + + %swag_update_plugin.plugin_name% + + + + + +``` +`/.../SwagUpdatePlugin/Subscriber/DetailSubscriber.php` -Now only the __Bootstrap.php__ logic has to be adapted, so that it loads the correct template directory, based on the version of the template currently used by the shop. -The new plugin __Bootstrap.php__ class now looks like this: ```php subscribeEvent('Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail', 'onSecureDetailPostDispatch'); + /** + * @var DBALConfigReader + */ + private $configReader; - $form = $this->Form(); - $form->setElement( - 'mediaselection', - 'mediaselection', - ['label' => 'Media', 'value' => NULL] - ); + /** + * @var string + */ + private $pluginName; - return true; + /** + * @param DBALConfigReader $configReader + * @param $pluginName + */ + public function __construct(DBALConfigReader $configReader, $pluginName) + { + $this->configReader = $configReader; + $this->pluginName = $pluginName; } - public function onSecureDetailPostDispatch(Enlight_Event_EventArgs $arguments) + public static function getSubscribedEvents() { - /**@var $controller Shopware_Controllers_Frontend_Listing*/ - $controller = $arguments->getSubject(); + return [ + 'Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail' => 'onPostDispatchFrontendDetail' + ]; + } - $controller->View()->addTemplateDir($this->Path() . 'Views/common/'); + public function onPostDispatchFrontendDetail(\Enlight_Event_EventArgs $args) + { + /** @var \Shopware_Controllers_Frontend_Detail $subject */ + $subject = $args->get('subject'); - if (Shopware()->Shop()->getTemplate()->getVersion() >= 3) { - $controller->View()->addTemplateDir($this->Path() . 'Views/responsive/'); - } else { - $controller->View()->addTemplateDir($this->Path() . 'Views/emotion/'); - $controller->View()->extendsTemplate('frontend/detail/example1.tpl'); - } + $config = $this->configReader->getByPluginName($this->pluginName); - $controller->View()->mediaSelection = $this->Config()->mediaselection; + $subject->View()->assign('mediaSelection', $config['mediaSelection']); } } ``` -- The "Views/common/" directory is always registered as a template directory. Templates inside this directory are now loaded for both template versions. -- A template version check was added, so that only the corresponding view directory is loaded. -- Responsive - Template files for the responsive template loaded automatically over the inheritance by using the same names. Therefore, only the template directory must registered here -- Emotion - In the emotion template we can load the template extension like in Shopware 4. After the template directory is registered, the template file is loaded via the __extendsTemplate__ call. - -__Notice: Do not use "_default/" or "_emotion/" at the beginning of the extend call. Use "parent:" instead.__ - -__Notice: To allow other templates to easily extend your templates, you should provide your code in a separate file. Include that file in other templates, inside Smarty blocks, thus allowing different entry point.__ - ## Uninstall -During the uninstall process, the user can now be prompted which data he wishes to remove. -Existing __uninstall()__ method should remove all data, and the new __secureUninstall()__ method should only remove non-user related data. +During the uninstall process, the user will now be prompted which data he wishes to remove. +Existing __uninstall()__ method should remove all data. +Use the `UninstallContext` which contains the __keepUserData()__ method. If the return value is `true` you should only remove non-user related data. -##### Bootstrap.php - How to use 'secureUninstall()' +### How to 'secureUninstall' ```php -// Set the new secureUninstall capability -public function getCapabilities() -{ - return array( - 'install' => true, - 'enable' => true, - 'update' => true, - 'secureUninstall' => true - ); -} +secureUninstall(); - $this->removeDatabase(); - return true; -} +namespace SwagUpdatePlugin; // Set the namespace like the technical name of your plugin +use Shopware\Components\Plugin; +use Shopware\Components\Plugin\Context\ActivateContext; +use Shopware\Components\Plugin\Context\UninstallContext; -// Remove only non-user related data. -public function secureUninstall() +// Call the class and file like the technical name of your plugin and extend from Shopware\Components\Plugin\Plugin +class SwagUpdatePlugin extends Plugin { - return true; + public function activate(ActivateContext $context) + { + $context->scheduleClearCache(InstallContext::CACHE_LIST_DEFAULT); + } + + public function uninstall(UninstallContext $context) + { + if ($context->keepUserData()) { + return; + } + + // Delete all data. + } } ``` -### Less -Besides traditional CSS, Shopware 5 includes Less support in new templates. Less is a CSS pre-processor, which can be used when styling your Shopware 5 templates. Less is a very powerful tool, and Shopware 5 extends its default feature set by adding some commonly used functions. - -For more information on how to use Less, have a look [here](http://lesscss.org/). +#### Integration of Less and Js files +Less and Js file registration will be done automatically. Just place the resources into the following directories: -#### Integration -Less files are loaded by creating a new event in the __install__ method of our plugin. + /.../SwagUpdatePlugin/Resources/frontend/css/**.css + /.../SwagUpdatePlugin/Resources/frontend/js/**.js + /.../SwagUpdatePlugin/Resources/frontend/less/all.less -##### Bootstrap.php - Using .less files in responsive template +The css and js directories may contain arbitrary sub directories. @imports in all.less will be resolved. -```php -/** - * Registers all necessary events and hooks. - */ -private function subscribeEvents() -{ - // Subscribe the needed event for less merge and compression - $this->subscribeEvent( - 'Theme_Compiler_Collect_Plugin_Less', - 'addLessFiles' - ); -} - -/** - * Provide the file collection for less - * - * @param Enlight_Event_EventArgs $args - * @return \Doctrine\Common\Collections\ArrayCollection - */ -public function addLessFiles(Enlight_Event_EventArgs $args) -{ - $less = new \Shopware\Components\Theme\LessDefinition( - //configuration - array(), - - //less files to compile - array( - __DIR__ . '/Views/responsive/frontend/_public/src/less/all.less' - ), - - //import directory - __DIR__ - ); +### Less +Besides traditional CSS, Shopware 5 includes Less support in new templates. Less is a CSS pre-processor, which can be used when styling your Shopware 5 templates. Less is a very powerful tool, and Shopware 5 extends its default feature set by adding some commonly used functions. - return new Doctrine\Common\Collections\ArrayCollection(array($less)); -} +For more information on how to use Less, have a look [here](http://lesscss.org/). -``` #### Structure convention Like in the example above, in most cases there is only one .less file to compile - the __all.less__. It includes additional files named by their content. Most likely the __all.less__ file includes the __modules.less__ and __variables.less__ files, which are often needed by default. These files include additional files from same named directories, e.g. modules.less includes files from the directory called __"_modules/"__. @@ -373,37 +421,6 @@ Additional documentation can be found in __Themes/Frontend/Bare/frontend/_includ - Use the CSS class name convention ("--" , e.g. "abo--detail-container > detail-container–image") - If possible, build small images and icons with CSS -### Javascript -Javascript can now be merged and compressed automatically by Shopware 5. - -#### Integration - -##### Bootstrap.php - Using Javascript merge and compression -```php -/** - * Registers all necessary events and hooks. - */ -private function subscribeEvents() -{ - // Subscribe the needed event for js merge and compression - $this->subscribeEvent( - 'Theme_Compiler_Collect_Plugin_Javascript', - 'addJsFiles' - ); -} - -/** - * Provide the file collection for js files - * - * @param Enlight_Event_EventArgs $args - * @return \Doctrine\Common\Collections\ArrayCollection - */ -public function addJsFiles(Enlight_Event_EventArgs $args) -{ - $jsFiles = array(__DIR__ . '/Views/responsive/frontend/_public/src/js/script.js'); - return new Doctrine\Common\Collections\ArrayCollection($jsFiles); -} -``` #### jQuery plugins Shopware 5 already includes several jQuery plugins you can use to implement useful features, like sliders or search fields. All of these can be found in the 'Themes/Frontend/Responsive/frontend/_public/src/js/' directory. @@ -447,239 +464,6 @@ Implement as many (useful) Smarty blocks as possible, so your templates are exte In listings, you should use the new listing logic (Conditions, ConditionHandler, Facet, FacetHandler, etc.). Keep the required additional data in mind, e.g. the new view variable "pageSizes". -## Updating an example plugin -In the following steps we'll update our "SwagExample1" Plugin to run with Shopware 5 and fully responsive. - -### Creating directory structure -In the first step of updating our plugin to be compatible with Shopware 5, we'll simply create the new directory structure. The current directory structure looks like this: -![Current structure](current-structure.png) - -Now we simply create two new directories inside of "Views": -__emotion__ and __responsive__. -Additionally, we move the existing "frontend" directory into the "emotion" directory. - -All we need to do for now is create some additional directories inside the __responsive__ directory. -Your directory structure should look like this: -``` -Views/ -|->responsive/ - |->frontend/ - |->_public/ - |->src/ - |->less/ -``` - -### Implement new Less structure - -As mentioned in the tutorial, our new CSS/Less, Javascript and images files will be placed inside the ___public__ directory, which contains the __src__ directory. The __src__ directory contains the __less__ directory. - -At first we start by creating a file called __all.less__ inside the __less__ directory, which has to be included in PHP now. This can be done using the new __"Theme_Compiler_Collect_Plugin_Less"__ event, which should be registered in the plugin's __install__ method: - -#### Registering the new event in the Bootstrap.php -```php -public function install() -{ - ... - // Register your custom Less files, so that they are processed into CSS and included on your template - $this->subscribeEvent( - 'Theme_Compiler_Collect_Plugin_Less', - 'addLessFiles' - ); - ... -} -... - -/** - * Provide the file collection for Less - */ -public function addLessFiles(Enlight_Event_EventArgs $args) -{ - $less = new \Shopware\Components\Theme\LessDefinition( - //configuration - array(), - //less files to compile - array( - __DIR__ . '/Views/responsive/frontend/_public/src/less/all.less' - ), - - //import directory - __DIR__ - ); - - return new Doctrine\Common\Collections\ArrayCollection(array($less)); -} -``` - -In the __addLessFiles__ method should return a Doctrine array collection containing the plugin's Less files. The Shopware 5 core will automatically process them into CSS files, and merge them together with the theme's standard CSS content (also processed from Less). This unified file will be minimized and served to the browser when a page is requested. After (re)installing the plugin and refreshing the theme cache, your plugin's __all.less__ file should be included in future requests. - -As mentioned in the tutorial, the __all.less__ is needed to include other Less files. Therefore, we new create a file called __modules.less__ and a directory called ___modules__. The newly created __modules.less__ file needs to be imported in __all.less__ - -#### all.less -```less -@import "modules"; -``` - -In turn, the __modules.less__ file should import all the .less files that will be created inside of the ___modules__ directory. There files will contain the actual Less code, and should be grouped by their usage in the frontend. -E.g. as we need styles for the detail page, we will create a new file called __detail.less__ inside of the ___modules__ directory. - -#### modules.less -```less -@import "_modules/detail"; -``` - -The __detail.less__ will contain the actual styling info. -In the old __example1.tpl__ were some inline styles for the img tag. -Let's replace them by using the new class of the img element: - -#### detail.less -```less -.own-topseller--img { - width: 50%; - display: block; -} -``` - -### Additional changes -The plugin is now Shopware 5 compatible and our template changes responsive. -The current implementation can be improved by wrapping the HTML code in Smarty blocks, so that these template changes can be extensible by other plugins. -By installing the plugin and opening a detail page, you will see the changes introduced by the plugin. - -Below is a list of all files that have been changed: - -#### Bootstrap.php -```php -subscribeEvent('Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail', 'onSecureDetailPostDispatch'); - - // Subscribe the needed event for less merge and compression - $this->subscribeEvent( - 'Theme_Compiler_Collect_Plugin_Less', - 'addLessFiles' - ); - - $form = $this->Form(); - $form->setElement( - 'mediaselection', - 'mediaselection', - ['label' => 'Media', 'value' => NULL] - ); - - return true; - } - - - public function onSecureDetailPostDispatch(Enlight_Event_EventArgs $arguments) - { - /**@var $controller Shopware_Controllers_Frontend_Listing*/ - $controller = $arguments->getSubject(); - - $controller->View()->addTemplateDir($this->Path() . 'Views/common/'); - - $template = Shopware()->Shop()->getTemplate(); - if ($template->getVersion() >= 3) { - $controller->View()->addTemplateDir($this->Path() . 'Views/responsive/'); - } else { - $controller->View()->addTemplateDir($this->Path() . 'Views/emotion/'); - $controller->View()->extendsTemplate('frontend/detail/example1.tpl'); - } - - $controller->View()->mediaSelection = $this->Config()->mediaselection; - } - - /** - * Provide the file collection for less - */ - public function addLessFiles(Enlight_Event_EventArgs $args) - { - $less = new \Shopware\Components\Theme\LessDefinition( - //configuration - array(), - //less files to compile - array( - __DIR__ . '/Views/responsive/frontend/_public/src/less/all.less' - ), - - //import directory - __DIR__ - ); - - return new Doctrine\Common\Collections\ArrayCollection(array($less)); - } -} -``` - -#### Views/responsive/frontend/_public/src/less/all.less -```less -@import "modules"; -``` - -#### Views/responsive/frontend/_public/src/less/modules.less -```less -@import "_modules/detail"; -``` - -#### Views/responsive/frontend/_public/src/less/_modules/detail.less -```less -.own-topseller--img { - width: 50%; - display: block; -} - -@media screen and (min-width: @phoneLandscapeViewportWidth) { - //Styles only being used when the size of the screen is at least 480px. - //All the sizes are mentioned in yourShopSystem/Themes/Frontend/Responsive/frontend/_public/src/less/_variables/structure.less -} - -@media screen and (min-width: @desktopViewportWidth) { - //Styles only being used when the size of the screen is at least 1260px. - //All the sizes are mentioned in yourShopSystem/Themes/Frontend/Responsive/frontend/_public/src/less/_variables/structure.less -} -``` - -#### SwagExample1/Views/common/frontend/swag_example1/detail_extension.tpl -```html -{block name="frontend_detail_example"} -
    - {block name="frontend_detail_example_headline"} -

    My topseller

    - {/block} - - {block name="frontend_detail_example_img"} - Test - {/block} - - {block name="frontend_detail_example_topseller"} -
    - {action module=widgets controller=listing action=top_seller sCategory=3} -
    - {/block} -
    -{/block} -``` - -#### SwagExample1/Views/emotion/frontend/detail/example1.tpl -```html -{block name="frontend_detail_index_detail"} - {include file="frontend/swag_example1/detail_extension.tpl"} -{/block} -``` - -#### SwagExample1/Views/responsive/frontend/detail/index.tpl -```html -{extends file="parent:frontend/detail/index.tpl"} - -{block name="frontend_detail_index_detail"} - {include file="frontend/swag_example1/detail_extension.tpl"} -{/block} -``` - -#### New directory structure -![New directory structure](new-structure.png) - +For more information about the searchBundle, have a look [here](https://developers.shopware.com/developers-guide/shopware-5-search-bundle/#search-results) ## Questions? For further questions you should read the complete Shopware 5 upgrade guide. diff --git a/source/developers-guide/shopware-5-search-bundle/index.md b/source/developers-guide/shopware-5-search-bundle/index.md index 325cdb68ce..8e5baa3859 100644 --- a/source/developers-guide/shopware-5-search-bundle/index.md +++ b/source/developers-guide/shopware-5-search-bundle/index.md @@ -39,12 +39,12 @@ This small example shows how the product number search can be used. ## Reference to SearchBundleDBAL As already described, the `Shopware\Bundle\SearchBundle` defines only how product lists are selected and which conditions and sorting criteria are used to restrict or modify the result set. -The `SearchBundle` contains no specify search engine implementation, like `Doctrine\ORM` or `PDO`, and can't be used as standalone search mechanism. +The `SearchBundle` contains no specific search engine implementation, like `Doctrine\ORM` or `PDO`, and can't be used as standalone search mechanism. As default for the `ProductNumberSearch`, Shopware uses `Shopware\Bundle\SearchBundleDBAL`, which implements the interfaces required by `Shopware\Bundle\SearchBundle` to use it as product search implementation. The `Shopware\Bundle\SearchBundleDBAL` is based on the `Doctrine\DBAL\Query\Builder`. ## ShopContextInterface -A `ShopContext` contains all shop related data for the current request. (shop id and details, customer group, ...) For more information, see `Shopware\Bundle\StoreFrontBundle\README.md` +A `ShopContext` contains all shop related data for the current request. (shop id and details, customer group, ...) For more information, see `Shopware\Bundle\SearchBundle\README.md` This context is used in all Shopware bundle services, and can be accessed by getting the `context_service` from the DI container. @@ -76,7 +76,7 @@ Shopware contains the following core conditions: - `CustomerGroupCondition` - *Products which are not blocked for the provided customer group* - `HasPriceCondition` - *Products which have a defined default customer group price* - `ImmediateDeliveryCondition` - *Products which are available for immediate delivery* -- `ManufacturerCondition` - *Products from one of the provided manufactures * +- `ManufacturerCondition` - *Products from one of the provided manufactures* - `PriceCondition` - *Products whose price is within the provided price range* - `ProductAttributeCondition` - *Dynamic condition which can be used to restrict the result by specify product attribute* - `PropertyCondition` - *Products which have one of the provided product property values* @@ -137,16 +137,15 @@ You can find a installable ZIP package of an attribute example plugin + + + ``` Each condition handler has to implement two methods: @@ -154,6 +153,19 @@ Each condition handler has to implement two methods: 2. `generateCondition` - Handles the condition and extends the provided query ```php +resetConditions(); + $reverted->resetSorting(); + + $query = $this->queryBuilderFactory->createQuery($reverted, $context); + //... +} +``` + +New implementation gets the "reverted" criteria as function parameter. This allows to switch between different filter modes. + +``` +public function generatePartialFacet( + FacetInterface $facet, + Criteria $reverted, + Criteria $criteria, + ShopContextInterface $context +) { + $query = $this->queryBuilderFactory->createQuery($reverted, $context); + //... +``` + +To support Shopware 5.3 and older versions inside the same plugin version, the following check has to be added at the top of the facet handler class: +``` +resetConditions(); + $reverted->resetSorting(); + + return $this->generatePartialFacet($facet, $reverted, $criteria, $context); + } + + public function generatePartialFacet( + FacetInterface $facet, + Criteria $reverted, + Criteria $criteria, + ShopContextInterface $context + ) { + $query = $this->queryBuilderFactory->createQuery($reverted, $context); + //... + } +} +``` + +## Optimized Batch Search + +As of Shopware 5.3, it is possible to search for multiple criteria objects at once. In addition, the fetch process for products will be minimized by fetching a product number only once. An optimization service will then try to optimize the search request by combining identical criteria objects into one. So there will be fewer search requests for identical criterias. + +Another advantage is that you won't get the same results with identical searches. + +### Example: Emotion Component Handler + +The emotion component handler make use of this service to increase the performance of loading a shopping world. Imagine you have three product boxes configured with topseller products of a category. Prior to 5.3, you will get the same product for each product box. As of 5.3, you will get different topseller products. + +### Usage + +You will still be working with criteria objects, but before sending the search request, you have to add them to a `Shopware\Bundle\SearchBundle\BatchProductSearchRequest`. + +```php +$criteria = new Critera(); +$criteria->addCondition(new CategoryCondition([3])); +$criteria->limit(3); + +$anotherCriteria = new Critera(); +$anotherCriteria->addCondition(new CategoryCondition([3])); +$anotherCriteria->limit(5); + +$request = new BatchProductNumberSearchRequest(); +$request->setProductNumbers('numbers-1', ['SW10004', 'SW10006']); +$request->setCriteria('criteria-1', $criteria); +$request->setCriteria('criteria-2', $anotherCriteria); + +$result = $this->container->get('shopware_search.batch_product_search')->search($request, $context); +``` + +In this case, there are two identical criteria objects which can be merged easily by adding the limit and work with the offset when returning the results. + +Every criteria object should be added with a unique key for the request. This is necessary to identify every search request and return the correct products. + +```php +$result->get('numbers-1'); // ['SW10004' => ListProduct, 'SW10006' => ListProduct] +$result->get('criteria-1'); // ['SW10006' => ListProduct, 'SW10007' => ListProduct, 'SW10008' => ListProduct] +$result->get('criteria-2'); // ['SW10009' => ListProduct, 'SW10010' => ListProduct, 'SW10011' => ListProduct, 'SW10012' => ListProduct, 'SW10013' => ListProduct] +``` + +The batch search will return a `Shopware\Bundle\SearchBundle\BatchProductSearchResult` which contains the results for every request. + +
    Hint! The batch product search is also available as batch product number search to only return found product numbers.
    diff --git a/source/developers-guide/shopware-5-upgrade-guide-for-developers/index.md b/source/developers-guide/shopware-5-upgrade-guide-for-developers/index.md index 28a8c76b60..0878e4658d 100644 --- a/source/developers-guide/shopware-5-upgrade-guide-for-developers/index.md +++ b/source/developers-guide/shopware-5-upgrade-guide-for-developers/index.md @@ -17,13 +17,1816 @@ including minor and bugfix releases, refer to the `UPGRADE.md` file found in you
    +## Shopware 5.7 + +### System requirements changes + +The **minimum PHP version** has been increased to **PHP 7.4 or higher**. We’ve also added support for **PHP 8.0** and +encourage you to use the latest version. + +The **minimum Elasticsearch version is 7.0**, support for Elasticsearch 6 has been dropped due to the +underlying library being used. + +### Core changes + +We have migrated the Session system from Zend Session to Symfony Session. This change is transparent to plugin +developers, the access using `Enlight_Session_Namespace` didn't change. The actual Symfony session is attached to the +`Request` (`$request->getSession()`) now. Apart from this we've changed the way plugins could influence the cores config +values, this is [not possible anymore](https://issues.shopware.com/issues/SW-24385), so please make sure that your +plugins do not rely on this behaviour. + +#### Deprecations + +We've [removed](https://github.com/shopware5/shopware/commit/b2709e5fd57432e92f6921dddddb76a2f7f5d0b2) all deprecations +marked for removal with v5.7.0. + +#### Development tools + +You may use composer v2 now, support has been added with this version. Furthermore we've removed the `psh.phar` tool for +Shopware development, it has been replaced by a +[Makefile](https://github.com/shopware5/shopware/blob/5.7/Makefile) which may now be used for development tasks like +starting the testsuite. + +#### Snippets + +Snippet names **can not** be defined without quotes `"` anymore. To refactor the snippets in your plugin, you may +use the following regular expression to search for the now invalid syntax: + +```regexp +\{s[\s.A-Za-z="_\/]*name=([A-Za-z\/_]*) +``` + +```diff +diff a/example.tpl b/example.tpl +--- a/example.tpl ++++ b/example.tpl +- {* Old syntax *} +- {s name=foo_bar} ++ {* New syntax *} ++ {s name="foo_bar"} +``` + +### Library updates + +* Updated `Symfony` to 4.4 LTS +* Updated `mpdf/mpdf` to 8.0.7 +* Updated `guzzlehttp/guzzle` to 7.0.1 +* Updated `monolog/monolog` to 2.0.1 +* Migrated components to Laminas + +### Changes due Library updates + +* All services are private by default. A workaround for this could be changing the default to public. + +## Shopware 5.6 + +### System requirements changes + +The **minimum PHP version** has been increased to **PHP 7.2 or higher**. We’ve also added support for **PHP 7.3** and +encourage you to use the latest version. Due to this change, Shopware will start using real types instead of typehints on new +services or interfaces, as well as on private methods. + +Existing services available for decoration, as well as hookable public or protected methods won't be strongly typed though, +to not break compatibility in plugins. + +The **minimum MySQL version is MySQL 5.7**, support for MySQL 5.5 and MySQL 5.6 has been dropped. + +The **minimum Elasticsearch version is 6.6**, support for Elasticsearch 2.0 and 5.0 has been dropped due to the +underlying library being used. Support for **Elasticsearch 7.0** has been added. + +### Library updates + +* Updated `Symfony` to 3.4.29 +* Updated `jQuery` to 3.4.1 +* Updated `doctrine/dbal` and `doctrine/orm` to 2.6.3 and `doctrine/common` to 2.10.0 +* Updated `mpdf/mpdf` to 7.1.9 +* Updated `league/flysystem` to 1.0.46 + +### Content Types + +Content Types are something similar to attributes, but for complete entities. You can create your own custom entities +with all necessary fields using an XML file (provided by a plugin) or all by yourself in the backend. +The main idea of this feature is to provide a possibility to easily create custom entities like recipes, store lists or +job listings without having to write any code. + +A new entity can be defined by a list of fields. Some of them are meta fields (like `name`, `description` or an icon), others +describe the essence of a content type, e.g. `Ingredients`, `Directions`, `Nutrition Facts`, `Preparation Time` and +an image for a recipe. + +Each defined entity comes with the following capabilities: + +- a table with `s_custom_` prefix +- a backend menu and controller for managing the entries (e.g. creating new or modifying existing recipes) +- ACL resources for this backend menu +- a repository service with `shopware.bundle.content_type.`**type_name** +- an API controller for all CRUD operations (Custom**type_name** e.g. `CustomRecipe`) +- (if explicitly enabled) a frontend controller with listing and detail views + +All custom entities are also accessible in templates using a new smarty function `fetchContent` + +Example +```html +{fetchContent type=recipe assign=recipes filter=[['property' => 'name', 'value' => 'Spaghetti Bolognese']]} + +{foreach $recipes as $recipe} + {$recipe.name} +{/foreach} +``` + +The backend fields and titles can be translated using snippet namespace `backend/customYOURTYPE/main`. + +Plugins can provide their own entities using an XML schema at `Resources/contenttypes.xml`: + +```xml + + + + + store + Stores + +
    + + + true + + + + false + + + + false + +
    +
    +
    +
    +
    +``` + +You can find more information and details (e.g. regarding available field types) in the [Developer Docs](https://developers.shopware.com/developers-guide/content-types/). + +### The request and response instances in Shopware now extend from Symfony Request / Response. + +`Enlight_Controller_Request_RequestHttp` is now extending `Symfony\Component\HttpFoundation\Request` and +`Enlight_Controller_Response_ResponseHttp` extends `Symfony\Component\HttpFoundation\Response`. + +This allows you to use the Symfony Request properties you might be more familiar with, like the `ParameterBag`s +`\Symfony\Component\HttpFoundation\Request::$attributes`, `\Symfony\Component\HttpFoundation\Request::$query`, +`\Symfony\Component\HttpFoundation\Request::$cookies`, `\Symfony\Component\HttpFoundation\Request::$headers` and many more. + +### Controller Registration using DI-Tag + +To allow for easier testing of controllers, Shopware now supports controllers as a service: they can be defined like any +other service in the DIC and registered as a controller using the DI tag `shopware.controller`. This DI tag needs the +attributes `module` and `controller` to map them into Shopware's routing infrastructure. These controllers are also lazy-loaded. + +#### Example DI + +```xml + + + + + +``` + +#### Example Controller + +```php +connection = $connection; + parent::__construct(); + } + + public function indexAction() + { + // Do something with $this->connection + } + + public function detailAction(int $productNumber = null, ListProductServiceInterface $listProductService, ContextServiceInterface $contextService) + { + if (!$productNumber) { + throw new \RuntimeException('No product number provided'); + } + + $this->View()->assign('product', $listProductService->getList([$productNumber], $contextService->getShopContext())); + } +} +``` +### Autowiring of controller actions parameters + +The new controllers tagged with `shopware.controller` tag, can now have parameters in action methods. Possible parameters are + +* Services (e.g `ListProductService $listProductService`) +* $request (e.g `Request $request`) +* Request parameters, e.g `myAction(int $limit = 0)`, filled by requesting `/myaction?limit=5` + + +### Custom validation of order numbers (SKU) + +Up to now, the validation of order numbers (or SKUs) was done in form of a Regex-Assertion in the Doctrine model at +`Shopware\Models\Article\Detail::$number`. That solution was not flexible and didn't allow any modifications of said +regex, let alone a complete custom implementation of a validation. + +Now, a new constraint `Shopware\Components\Model\DBAL\Constraints\OrderNumber` is used instead, which is a wrapper +around `\Shopware\Components\OrderNumberValidator\RegexOrderNumberValidator`. + +This way you can either change the regex which is being used for validation by defining one yourself in the `config.php`: +```php + [ + 'orderNumberRegex' => '/^[a-zA-Z0-9-_.]+$/' // This is the default + ], + 'db' => [...], +] +``` +Or you can create your own implementation of the underlying interface +`Shopware\Components\OrderNumberValidator\OrderNumberValidatorInterface` and use it for the validation by simply +decorating the current service with id `shopware.components.ordernumber_validator` and e.g. query some API. + +### Definition of MySQL version in config + +It is now possible to define the MySQL version being used in the `config.php` as part of the Doctrine default configuration. +The version can be determined by running the SQL query `SELECT version()`, the result needs to be provided in the `db.serverVersion` config: + +```php + [ + ... + 'serverVersion' => '5.7.24', + ], +]; +``` +Providing this value via config makes it unnecessary for Doctrine to figure the version out by itself, +thus reducing the number of database calls Shopware makes per request by one. + +If you are running a MariaDB database, you should prefix the `serverVersion` with `mariadb`- (e.g.: `10.4.7-MariaDB`). + +### Payment Token + +Some internet security software packages recognize requests to domains of payment providers and open a new clean browser +without cookies out of security concerns. After returning from the payment provider, the customer then will be +redirected to the home page, because this new browser instance does not contain the previous session. + +For this reason there is now a service to generate a token, which can be added to the returning url +(e.g `/payment_paypal/return?paymentId=test123&swPaymentToken=abc123def`). This parameter will be resolved in a +PreDispatch-event by the `\Shopware\Components\Cart\PaymentTokenSubscriber`: If the user is not logged in, but the URL +contains a valid token, the user will get back his former session and will be redirected to the original URL, +but without the token + +Example implementation: + +```php +get('shopware.components.cart.payment_token')->generate(); + + $returnParameters = [ + 'controller' => 'payment_paypal', + 'action' => 'return', + PaymentTokenService::TYPE_PAYMENT_TOKEN => $token + ]; + $returnLink = $this->router->assemble($returnParameters); + + $redirectUrl = $this->paymentProviderApi->createPayment($this->getCart(), $returnLink); + + $this->redirect($redirectUrl); + } +} +``` + +### Replaced Codemirror with Ace-Editor + +Codemirror has been replaced with the [Ace-Editor](https://ace.c9.io/). For compatibility reason, Ace-Editor supports all xtypes / classes from Codemirror. + +The following modes are available: +- css +- html +- javascript +- json +- less +- mysql +- php +- sass +- scss +- smarty +- sql +- text +- xml +- xquery + +The Ace-Editor has some advantages over the previous editor: +- It provides syntax validation (see [Improved ExtJs Error Reporter in Backend](#improved-extjs-error-reporter-in-backend)) +- It supports autocompletion +- It is faster + +You can see the autocompletion in action in the mail templates of Shopware 5.6: It autompletes the available Smarty +variables and tags like `if` or `foreach`. This might be extended to other areas as well in future releases. + +If you are interested in implementing this functionality in your own plugin, you'll first have to +[register a `completer`-callback](https://github.com/shopware5/shopware/blob/83b7f50837b134050a8d882cde1dbb3e66c61df9/themes/Backend/ExtJs/backend/mail/view/main/content_editor.js#L107) function. +This [`callback`](https://github.com/shopware5/shopware/blob/83b7f50837b134050a8d882cde1dbb3e66c61df9/themes/Backend/ExtJs/backend/mail/view/main/content_editor.js#L193) determines if an autocompletion could be possible (by performing some [sanity checks](https://github.com/shopware5/shopware/blob/83b7f50837b134050a8d882cde1dbb3e66c61df9/themes/Backend/ExtJs/backend/mail/view/main/content_editor.js#L197)) and doing an AJAX-request containing the relevant text portion of the editor. + +The backend now can respond with a data structure containing the relevant autocompletion suggestions. How these are +to be determined depends on your use case and underlying data structures. The `MailBundle` uses an +`\Shopware\Bundle\MailBundle\AutoCompleteResolver` that pipes the given text through multiple +`\Shopware\Bundle\MailBundle\AutocompleteResolver\Resolver`s, each checking for specific possible completions. + +### Improved ExtJs Error Reporter in Backend + +When an error occurred in the JavaScript of the Backend, the Error Reporter that pops up in these cases often wasn't +very helpful since the stacktrace being shown in such cases can be overwhelming. + +Thanks to the new [Ace-Editor](#replaced-codemirror-with-ace-editor), the Error Reporter can now show you the exact +position where the error occurred in the code and give you a hint what you might be able to do about it. + +### ExtJs Developer-Mode + +ExtJs developer mode loads a developer-version file of ExtJs to provide code documentation, warnings and better +error messages. This mode can be enabled using this snippet in the `config.php`: + +```php +'extjs' => [ + 'developer_mode' => true +] +``` + +### Improved Robots.txt + +The `robots.txt` generation has been reworked and now shows all links from all language shops. +To remove or add entries overwrite the blocks `frontend_robots_txt_disallows_output`, `frontend_robots_txt_allows_output` and call methods `setAllow`, `setDisallow`, `removeAllow`, `removeDisallow` on the `$robotsTxt` service. + +Example: + +```smarty +{block name="frontend_robots_txt_disallows_output"} + {$robotsTxt->removeDisallow('/ticket')} + {$smarty.block.parent} +{/block} +``` + +### Plugin specific logger + +There is a new logger service for each plugin. +The service ID of the plugin specific logger is a combination of the plugin's service prefix (lower case plugin name) and `.logger`. +For example: when a plugin's name is `SwagPlugin` the specific logger can be accessed via `swag_plugin.logger`. + +This logger will now write into the logs directory (`var/logs`) using a rotating file pattern like the other logger services. +The settings for the logger can be configured using the DI parameters `swag_plugin.logger.level`(defaults to shopware +default logging level) and `swag_plugin.logger.max_files` (defaults to 14 like other Shopware loggers). + +In our example, the logger would write into a file like `var/log/swag_plugin_production-2019-03-06.log`. + +Support for easier log message writing is enabled, so `key` => `value` arrays can be used like this: + +```php +fatal("An error is occurred while requesting {module}/{controller}/{action}", $controller->Request()->getParams()); +``` + +### Custom Sorting of products in categories + +Products in a category can now be sorted "by hand". This specific sorting can also be created using the categories API resource. + +They will be applied when the associated sorting has been selected in the storefront. Not manually sorted products will +use the configured normal fallback sorting. + +To create a custom sorting, find your category in the Category backend module and click the new tab `Custom Sorting`. + +Two display modes are available: a normal listview and a more frontend-like grid view. + +### HTTP2 Server Push Support + +HTTP2 Server Push allows Shopware to push certain resources to the browser without it even requesting them. To do so, +Shopware creates `Link`-headers for specified resources, informing Apache or Nginx to push these files to the browser. +Server Push is supported since +[Apache 2.4.18](https://httpd.apache.org/docs/2.4/mod/mod_http2.html#h2push) and +[nginx 1.13.9](https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/#http2_push). + +These resources are only pushed on the very first request of a client. After that, the files should be cached in the +browser and don't need to be transmitted anymore. The presence of a `session`-cookie is used to determine if a push is necessary. + +The Smarty function `{preload}` is used to define in the template which resource are to be pushed and as what. + +Example for CSS: +```html + +``` + +Example for Javascript: +```html + +``` + +Server Push can be enabled in the `Various` section of the `Cache/Performance` settings. Please do not enable +Server Push support if you are using Google's Pagespeed module: It creates custom CSS and Javascript files for the browser, +replacing the ones Shopware contains in the HTML. So pushing the original files to the browser leads to an unnecessary overhead. + +## Shopware 5.5 + +### System requirements changes + +
    + +The IonCube Loader requirement has been dropped! Starting with Shopware 5.5, only unencrypted plugins are supported. +Before removing the IonCube Loader from your server environment, please make sure no encrypted plugins are installed anymore. + +
    + +The minimum PHP version still is **PHP 5.6.4 or higher**, though **PHP 7.x is highly encouraged**. Please be aware that +PHP 5.6 and PHP 7.0 reach their "end of life" (the end of the official support by the PHP Group) by the end of the year. +Shopware will drop support for those versions of PHP with the next minor version. + +For performance and compatibility reasons, we recommend using **PHP 7.2**. Since encrypted plugins are no longer supported +and the IonCube Loader therefore is no longer a requirement, an update to the latest version of PHP is now possible. + +The minimum MySQL version still is MySQL 5.5, but **MySQL 8.0** is now supported as well. Since the extended support of +MySQL 5.5 ends in December 2018, Shopware will drop the support for this version soon thereafter. + +### Unencrypted Plugins + +Starting with Shopware 5.5, IonCube encryption of plugins is being discontinued and only unencrypted plugins are +supported in Shopware 5.5. + +The Shopware LicenseManager plugin still is compatible with Shopware 5.5 to not break any existing installations, it is +nevertheless being discontinued together with the encryption of plugins. So, if you are currently using the +Shopware LicenseManager in your plugin, you have to remove the license check in order to upload a Shopware 5.5 +compatible version of your plugin to the Shopware Store. + +### Removal of deprecated code + +Shopware 5.5 removes a lot of old, deprecated code. If you're updating from Shopware < v5.4.5, you might want to consider +updating on the latest version of 5.4 first: versions 5.4.5 onwards show deprecation warnings in development mode if a +deprecated function is being called. Alternatively you can change the loglevel in your `config.php` by configuring: + +```php +... +'logger' => [ + 'level' => \Shopware\Components\Logger::DEBUG +], +... +``` + +The most relevant changes are: + +- The methods `ModelManager::addAttribute` and `ModelManager::removeAttribute` were removed. Use `\Shopware\Bundle\AttributeBundle\Service\CrudService::update` instead. +- The legacy models `Shopware\Models\Customer\Billing` and `Shopware\Models\Customer\Shipping` aren't available anymore. +The same is true for their references in the `Customer` model (customer.billing, customer.shipping) and the repository +`Shopware\Models\Customer\BillingRepository`. The matching tables `s_user_billingaddress`, `s_user_shippingaddress` +and their attribute tables `s_user_billingaddress_attributes` and `s_user_shippingaddress_attributes` won't be removed +on upgrade until Shopware 5.6, but they won't be kept in sync anymore. +- Old conversion classes got removed: `Shopware_Components_Convert_Csv`, `Shopware_Components_Convert_Excel` and `Shopware_Components_Convert_Xml` +- The action `\Shopware_Controllers_Widgets_Listing::ajaxListingAction` was removed. Use `Shopware_Controllers_Widgets_Listing::listingCountAction` instead. + +For a complete overview check the **Removals** part of the
    Upgrade.md. + +### MySQL 8 workaround + +Due to a mixture of MySQL 8 and Doctrine constraints, the column `s_core_documents.ID` will be renamed to +`s_core_documents.id` on the fly if MySQL 8 is being used. To be able to do that, the service `\Shopware\Components\Compatibility\LegacyDocumentIdConverter` +was introduced, which is checked in the file `engine/Shopware/Models/Order/Document/Document.php` to determine if +a Doctrine model with uppercase or lowercase `id` needs to be used. + +If you need to reference this column in your own model, we recommend to use the same workaround there. You can use the +same service (see above) with id `legacy_documentid_converter` for that. + +The reason for this workaround is that MySQL 8 forces ids in foreign key constraints to be lower case. + +This is a problem in current systems since we have an uppercase `ID` in table `s_order_documents`. +MySQL doesn't care if we use `ID` in the table and `id` in the constraint, but Doctrine needs both to be written +in the same way. On new installations of Shopware 5.5 this is already the case, both are lowercase there. + +So in order to support MySQL 8 on updates from older Shopware versions we need to change the case of the `id` column +in `s_order_documents`, which breaks support of blue/green deployments as older versions of Shopware (< 5.5) need +that column to be uppercase. + +Since this change is only really necessary if you are using MySQL 8, it is only enforced when a MySQL 8 server is +detected. A downgrade to an older Shopware installation wouldn't be possible anyway in that case, as Shopware 5.4 +does not support MySQL 8 yet. + +If you want to make this migration offline, there is the command `sw:migrate:mysql8` to check if the migration was +executed and do so if you want. + +The column `s_core_documents.id` will be lowercase from Shopware 5.6 forward. + +### Library updates + +- Updated `Symfony` to version 3.4.15 LTS. Some nice, new features are Simpler injection of tagged services or Command Lazyloading. +- Updated `jQuery` to 3.3.1. You can see the jQuery update guide + for a list of important and breaking changes and links to the migration plugin. + In Shopware, the relevant changes were: + - Changing `.delegate()` to `.on()` + - Using `.prop()` instead of `.addAttribute()` or `.removeAttribute()` + - Using `JSON.parse()` instead of `$.parseJSON()` + - `$.ajax` is returning a `Deferred` object from now on and the callback method property for the failure behaviour is now called `error` instead of `failure` +- Updated `league/flysystem` to 1.0.45 +- Updated `Mpdf` to 7.0.3 +- Updated `doctrine/common` to 2.7.3 +- Updated `beberlei/assert` to 2.9.2 + +### Basket refactoring + +To improve basket performance and ease of use for plugin developers, the `sBasket` class was refactored slightly +while still being compatible with existing plugins as much as possible. + +The most relevant change is in regards to the following blocks in `themes/Frontend/Bare/frontend/checkout/cart_item.tpl` +which were moved to their own files: +* `frontend_checkout_cart_item_product` +* `frontend_checkout_cart_item_premium_product` +* `frontend_checkout_cart_item_voucher` +* `frontend_checkout_cart_item_rebate` +* `frontend_checkout_cart_item_surcharge_discount` + +These five blocks were moved to their own template files in `themes/Frontend/Bare/frontend/checkout/` to optimize the include process: +* `cart_item_product.tpl` +* `cart_item_premium_product.tpl` +* `cart_item_rebate.tpl` +* `cart_item_voucher.tpl` +* `cart_item_surcharge_discount.tpl` + +Also, the following templates no longer extend `cart_item.tpl` but include the logic themselves and have their own subtemplates: +* `confirm_item_premium_product.tpl` +* `confirm_item_product.tpl` +* `confirm_item_rebate.tpl` +* `confirm_item_surcharge_discount.tpl` +* `confirm_item_voucher.tpl` +* `finish_item_premium_product.tpl` +* `finish_item_product.tpl` +* `finish_item_voucher.tpl` + +If your theme extends one of the contained blocks, you'll have to change the filename it extends from. + +The following classes were added to simplify changes to the checkout process by plugins: +* Added struct `Shopware\Components\Cart\Struct\CartItemStruct` to represent items in the cart during calculation +* Added public function `sBasket::updateCartItems` to provide a new way of interacting with cart updates + +### Proportional tax calculation + +The proportional tax calculation allows to calculate multiple taxes for all fees, vouchers, discounts etc in baskets +containing items with e.g. 19% and 7% tax. This feature needs to be activated in *Settings*, *Checkout*, *Proportional calculation of tax positions*. +For the proportional tax calculation to work with vouchers and modes of dispatch, be sure to set the mode of tax calculation to "auto detection" in their settings + +The following changes were made to implement this feature: + +* Added `Shopware\Components\Cart\ProportionalTaxCalculator` to calculate proportional taxes for the cart items +* Added `Shopware\Components\Cart\BasketHelper` to to add items to the cart that need to be calculation in a proportional way +* Added `Shopware\Components\Cart\ProportionalCartMerger` to merge proportional cart items into one cart item +* Added new filter event to modify proportional vouchers `Shopware_Modules_Basket_AddVoucher_VoucherPrices` +* Added new column `invoice_shipping_tax_rate` to `s_order`, to save exact dispatch shipping tax rate. If the proportional +calculation is disabled, this field is `null` and shipping tax rate will be calculated like before +* Added new column `is_proportional_calculation` to `s_order`, which defines that the order is made with proportional items + +### Elasticsearch + +Shopware 5.5 supports Elasticsearch versions 2.x, 5.x and 6.x. + +
    + +Important: After the update to 5.5 you have to reindex your Elasticsearch indices. + +
    + +Shopware works with index aliases to allow multiple indices to exist in parallel and switch between them if necessary. +Should an index with the name of an alias (e.g. `shop_1`) already exist, it will now be deleted automatically so that +the new alias can be created without error. + +To support Elasticsearch 6 it was necessary to split the existing index into multiple indices for different document types +(product, property). If you're using `sw:es:analyze` and `sw:es:switch:alias`, you now need to also provide the parameter +document type that you want to analyze or switch the index of. + +### Elasticsearch backend + +A new `EsBackendBundle` was added to index and search for products, customers and orders in the backend. New `config.php` +parameters where added for that, the `es` array of parameters now contains a new key `backend`: + +``` +'es' => [ + ... + 'backend' => [ + 'write_backlog' => false, + 'enabled' => false, + ], + ... +], +``` + +To activate Elasticsearch in the backend, simply change `backend.enabled` to `true` and use the `sw:es:backend:index:populate` +command to populate the necessary indices. If you also enabled the `backend.write_log`, you can use `sw:es:backend:sync` +periodically after that to keep the index in sync. + +### Changed execution model of `replace` hooks + +When multiple `replace` hooks exist for one method, up to Shopware 5.4 (incl.) each of these hooks were called +sequentially with every hook having the opportunity to call `executeParent()`. Hence, if this happened, the original (parent) +implementation got called multiple times, leading to unexpected behaviours. + +In Shopware 5.5 the execution model was changed to a decorative type of implementation: If more than one `replace` hook +exists for a function, calling `executeParent()` inside the hook will execute the next `replace` hook of said function. Only +the last `replace` hook will then call the original implementation on `executeParent()`. + +### Filesystem abstraction layer + +As of Shopware 5.5, the use of direct file system access methods (like e.g. `fopen()`, `file()`, `is_readable()` and many more) +is being discouraged. Instead, we rely on the library FlySystem +as a generic file system abstraction layer. This allows a lot of flexibility in regards to where files are being stored. +E.g. Documents and Sitemap files can now also served from S3 or Google Cloud. We added support for Amazon Web Services +and Google Cloud Platform for the moment but this layer will help us in the future to support more cloud services. + +Multiple instances of this abstraction classes are available to plugins. The services with the ids `shopware.filesystem.public` +and `shopware.filesystem.private` are available to Shopware itself and to all plugins that are active. These services are +used to access a shared "folder" in which files can be stored that should be shared between Shopware and plugins. +The service `shopware.filesystem.public.url_generator` can be used for generating a URL to a file in the public filesystem. + +The `shopware.filesystem.public` file system is intended for files that need to be accessible to clients directly, e.g. +images, product manuals etc. while `shopware.filesystem.private` is for files that need to be available throughout Shopware +(e.g. invoice pdfs, ESDs like software), but are not to be accessed by a browser directly without any authentication. + +This is a simple example of how a file can be written using the new system: + +```php +$fileName = 'HelloWorld.txt'; +$contents = 'Uploaded by Shopware using FlySystem'; + +$filesystem = $this->container->get('shopware.filesystem.private'); +if ($filesystem->has($fileName)) { + $filesystem->delete($fileName); +} + +$filesystem->write($fileName, $contents); +``` + +Plugins can access their own filesystems which are instantiated and made available automatically by retrieving them from +the DIC: + * `plugin_name.filesystem.public` + * `plugin_name.filesystem.private` + +### Routing + +SEO support for some AJAX routes defined in the template `themes/Frontend/Bare/frontend/index/index.tpl` has been removed for performance reasons. If you need SEO URLs for the following routes, you can override the block +`frontend_index_header_javascript` and re-enable them by removing the `_seo=false`-attribute from the `{url controller=...}`-call. + +The affected routes are: + +- `/checkout/ajaxCart` +- `/register/index` +- `/checkout/addArticle` +- `/widgets/Listing/ajaxListing` +- `/checkout/ajaxAmount` +- `/address/ajaxSelection` +- `/address/ajaxEditor` + +### Sitemap + +
    + +Important: Large shops with many entities should consider switching to cronjob generation instead of live generation after the upgrade + +
    + +To support more than 50.000 URLs that are allowed in one `sitemap.xml` file, Shopware now adds a `sitemap_index.xml`, +which itself contains links to one or more `sitemap.xml`. The sitemap files can be created by cronjob, live or using +the command `sw:generate:sitemap`. + +If you need to add some links to the sitemap coming from a plugin, you can simply create a service implementing the interface +`\Shopware\Bundle\SitemapBundle\UrlProviderInterface` and give it the DIC tag `sitemap_url_provider`. The sitemap exporter +will collect your service using the tag and export the URLs it provides along with the others. + +### Dynamic cache invalidation + +The cache duration of the HTTP cache for shopping worlds, product detail pages, categories or blog pages is now being +calculated dynamically, should an entity be changed automatically at a certain point in time. An example would be a +new shopping world on the frontpage or a category that goes live at noon. Normally it wouldn't be visible till the +HTTP cache expires or is cleaned manually. + +Now, the cache time is calculated dynamically so that it expires at the correct point in time. + +If you want to implement this feature for your own element, you only need to provide a service that implements the +interface `Shopware\Components\HttpCache\CacheTimeServiceInterface` and then tag this service in the `services.xml` with +the tag `invalidation_date_provider`. It will then be picked up and called automatically. + +### Cache warmer + +Up to now, the cache warmer relied on seo urls, but that did only cover a small amount of possible urls which can be found +in shopware. With these changes, developers doesn't have to add unnecessary new seo urls to warm them for the cache. +In addition, the performance and amount of urls were greatly improved to cover the most content of shopware by itself. + +By implementing `HttpCache\UrlProvider\UrlProviderInterface` to a new service with the tag `cache_warmer.url_provider` +developers can now easily add their own url providers for the cache warmer. Note that CLI commands can't be extended by +Plugins, so adding UrlProviders can only be used by the `--extensions` parameter to only warm all non-Shopware extensions +or warming the full cache. To add these functions to the backend module, you have to extend the `httpCache` property like this: + +``` +//{block name="backend/performance/view/main/multi_request_tasks"} +//{$smarty.block.parent} +Ext.override(Shopware.apps.Performance.view.main.MultiRequestTasks, { + initComponent: function () { + this.httpCache.myNewProvider = { + providerLabel: 'myNewProvider', + requestUrl: '{url controller="Performance" action="warmUpCache" resource=myNewProvider}', + }; + + this.callParent(arguments); + } +}); +//{/block} +``` + +As mentioned in the change log, the CLI command also offers new parameters. They can be used combined to call the providers +independently. However, you still don't have to add any parameters to warm up everything. + +| Parameter | Short | Description | +| --------------------- | ----- | --------------------------------------------- | +| --category | -k | Warm up categories | +| --emotion | -o | Warm up emotions | +| --blog | -g | Warm up blog | +| --manufacturer | -m | Warm up manufacturer pages | +| --static | -t | Warm up static pages | +| --product | -p | Warm up products | +| --variantswitch | -d | Warm up variant switch of configurators | +| --productwithnumber | -z | Warm up products with number parameter | +| --productwithcategory | -y | Warm up producss with category parameter | +| --extensions | -x | Warm up all URLs provided by other extensions | + + +## Shopware 5.4 + +
    + +### SSL Encryption + +The mixed SSL encryption mode in the shop configuration has been removed. As of 5.4, the SSL encryption can only be +enabled or disabled globally. Shops using the mixed SSL encryption setting will automatically be upgraded to full +SSL encryption. Deprecated table columns and methods have been removed. + +To learn more about the server configuration changes to switch to the full SSL encryption, please refer to [Redirect all requests to equivalent HTTPS domain](http://en.community.shopware.com/_detail_1864.html). + +
    + +### System requirements changes + +The minimum PHP version still is **PHP 5.6.4 or higher**, though **PHP 7.x is highly encouraged**. + +### Removal of JSONP requests + +Shopware 5.4 removes all JSONP requests and replaces them with regular AJAX requests. The response type was changed to +standard HTML mostly (with one exception being JSON without the JSONP-callback, see below). + +These actions now return HTML directly: +- Shopware/Controllers/Frontend/AjaxSearch.php + - `indexAction` +- Shopware/Controllers/Frontend/Checkout.php + - `ajaxCartAction` +- Shopware/Controllers/Frontend/Compare.php + - `addArticleAction` + - `deleteArticleAction` + - `deleteAllAction` + - `getListAction` + - `indexAction` + - `overlayAction` +- Shopware/Controllers/Frontend/Note.php + - `ajaxAddAction` +- Shopware/Controllers/Widgets/Listing.php + - `ajaxListingAction` + +This action still returns JSON but doesn't use the JSONP-type function call. +- Shopware/Controllers/Frontend/Checkout.php + - `ajaxAmountAction` + +### POST on data modification + +Also, to be more HTTP compliant, all request that change any data on the server were changed to be made using the HTTP POST verb. +These methods are: + +* Basket actions + - `addArticle` + - `addAccessories` + - `addPremium` + - `changeQuantity` + - `deleteArticle` + - `setAddress` + - `ajaxAddArticle` + - `ajaxAddArticleCart` + - `ajaxDeleteArticle` + - `ajaxDeleteArticleCart` + +* Checkout actions: + - `finish` + +### Variants in listing + +It is now possible to allow displaying and filtering of variants in the listing. This new filter can be activated in the +backend filter settings and defines which groups are filterable and which of those will be expanded in the listing. + +The basic product box size is increased by 45px (from 231px to 276px) to allow the variants to display which of the +options filtered match for the current variant. If the customer e.g. filters for the colors red and blue and the +expanding of colors is active, each variant listed shows a little "**Color: red**" or "**Color: blue**" tag to identify +what variant this is. + +A new block `frontend_listing_box_variant_description` was added in file `themes/Frontend/Bare/frontend/listing/product-box/box-basic.tpl` +to allow modifications of the default way the options of the variant shown are displayed. + +### Sold out variants + +Shop owners can now mark individual variants as “sold out”. The flag `laststock` in table `s_articles` is still +available but a new column `laststock int(1) NOT NULL DEFAULT '0'` in table `s_articles_details` has been introduced +to allow selling off of variants. + +The current behaviour for main products stays the same, the new flag is being checked when variants are shown in listings +and on detail pages. + +This new flag is part of the configurator templates that are applied to variants, so regeneration of variants use the +default setting for this flag from the configurator template that is responsible for this product. + +Please make sure to also check for this new flag if you handle variants in your plugins or happen to read or write from +the `s_articles_details` table. Also take the flag into account when checking for stock quantity. + +### Smarty + +#### Security mode + +The `config.php` option `['template_security']['enabled'] => false` for disabling smarty security got removed. + +#### Link flags + +The flags `forceSecure` and `sUseSSL` for forcing the smarty `{url controller=...}`-helper to use SSL are deprecated. +They are now without function, whether the link uses SSL or not depends on the global "Use SSL" setting. + +### Routing + +Some AJAX routes generated in the template `themes/Frontend/Bare/frontend/index/index.tpl` check for existing SEO URLs. This +behaviour has been deprecated for performance reasons and SEO support for the routes defined in the template will be +removed in 5.5. If you want to disable SEO support for this routes, you can override the block +`frontend_index_ajax_seo_optimized` and set the variable `$ajaxSeoSupport` to `false`. + +The affected routes are: + +- `/checkout/ajaxCart` +- `/register/index` +- `/checkout/addArticle` +- `/widgets/Listing/ajaxListing` +- `/checkout/ajaxAmount` +- `/address/ajaxSelection` +- `/address/ajaxEditor` + +### DIC + +There have been some changes to underlying constants to be able to support Shopware as a [Composer](https://getcomposer.org/) +dependency. If you are interested in developing Shopware using Composer, have a look at the documentation + and the Shopware [Composer project](https://github.com/shopware5/composer-project). + +#### Shopware Version + +The usage of the constants `Shopware::VERSION`, `Shopware::VERSION_TEXT` and `Shopware::REVISION` has been deprecated. +They have been replaced with the following parameters in the DIC: + +- `shopware.release.version` + The version of the Shopware installation (e.g. '5.4.0') +- `shopware.release.version_text` + The version_text of the Shopware installation (e.g. 'RC1') +- `shopware.release.revision` + The revision of the Shopware installation (e.g. '20180081547') + +A new service was added in the DIC containing all parameters above +- `shopware.release` + A new struct of type `\Shopware\Components\ShopwareReleaseStruct` containing all parameters above + +To be compatible with most versions of Shopware, please use the `config` service from the DIC for the time being: +``` + $this->container->get('config')->get('version') === Shopware::VERSION; # => true + $this->container->get('config')->get('version_text') === Shopware::VERSION_TEXT; # => true + $this->container->get('config')->get('revision') === Shopware::REVISION; # => true +``` + +#### New paths + +Several paths have been added to the DIC: + +- `shopware.plugin_directories.projectplugins` + Path to project specific plugins, see [Composer project](https://github.com/shopware5/composer-project) +- `shopware.template.templatedir` + Path to the themes folder +- `shopware.app.rootdir` + Path to the root of your project +- `shopware.app.downloadsdir` + Path to the downloads folder +- `shopware.app.documentsdir` + Path to the generated documents folder +- `shopware.web.webdir` + Path to the web folder +- `shopware.web.cachedir` + Path to the web-cache folder + +These paths are configurable in the `config.php`, see `engine/Shopware/Configs/Default.php` for defaults + +### Mpdf + +Mpdf has been updated to v6.1.4 and it's namespace has been registered in the Composer autoloader. You don't need to +include the `mpdf.php` library as you used to in previous versions, you can just use `new mPDF();` to create a new instance. + +### Discard JavaScript/CSS from other Themes + +Since Shopware 5.4, it's possible to manipulate the chain of inheritance by discarding Less/JavaScript, defined by another theme. +Find out more at Custom theme configuration. + +## Shopware 5.3 + +
    + +### SSL Encryption + +The mixed SSL encryption mode in the shop configuration has been deprecated in 5.3 in favour of a stronger +security policy. + +As of 5.4, the SSL encryption can only be enabled or disabled globally. Shops using the mixed SSL encryption +setting will automatically be upgraded to full SSL encryption. We advise you to enable the full SSL encryption +for your shops as soon as possible to prevent negative future side-effects. + +To learn more about the server configuration changes to switch to the full SSL encryption, please refer to [Redirect all requests to equivalent HTTPS domain](http://en.community.shopware.com/_detail_1864.html). + +
    + +### System requirements changes + +The minimum PHP version still is **PHP 5.6.4 or higher**. + +### Internet Explorer 10 support + +Version 5.3 does not support IE10 anymore. + +### Smarty + +#### Security mode + +We have activated the Smarty security mode globally with 5.3: +[https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Components/DependencyInjection/Bridge/Template.php#L57](https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Components/DependencyInjection/Bridge/Template.php#L57) + +This means that certain PHP functions can no longer be used in Smarty. The available Smarty functions are stored in the following configuration file: +[https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Configs/smarty_functions.php](https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Configs/smarty_functions.php) + +This can be extended via the config.php as follows: + +``` + [ + //.... + ], + 'template_security' => [ + 'php_modifiers' => ['dirname'], + 'php_functions' => ['dirname', 'shell_exec'], + ] +]; +``` + +##### Disable template loading + +To disable the automatic template loading, the loadTemplate function was often used without parameters. +This does not work with version 5.3 anymore. To disable the automatic loading of templates, only the setNoRender function can be used: + +``` +View()->loadTemplate(''); + + //right way: no template loaded + $this->container->get('front')->Plugins()->ViewRenderer()->setNoRender(); + } +} +``` + +##### Load templates from non-registered directories + +In security mode, it is only possible to load templates from registered directories + +``` +View()->loadTemplate(__DIR__ . '/../Views/frontend/test.tpl'); + + //right way + $this->View()->addTemplateDir(__DIR__ . '/../Views/') + $this->View()->loadTemplate('frontend/test.tpl'); + } +} +``` + +###### Unknown {s} tag error +In the past, this error has occurred on some systems and results to an 500 internal server error. This is based on the problem that template files included, which are not inside a registered directory. In a production environment shopware prevents the exceptions to be displayed (config.php), which results that the rendering process are not aborted. +The following scenarios can occur: + +- Default config +No exceptions no errors should be displayed. This results to an apache error log entry with the unknown {s} tag error +- phpsettings.display_errors = 1 +The {s} tag error will be displayed in the store front +- front.throwExceptions = true +The original exception with the `unsecure template directory` will be displayed: +``` +Uncaught SmartyException: directory 'custom/plugins/.../Views/test_index.tpl' not allowed by security setting +``` + +To see all configurable options see [config.php settings documentation](https://developers.shopware.com/developers-guide/shopware-config/) + +#### Rendering + +##### Form module in the backend + +Smarty functions in form templates have been disabled. Also no new variables can be added to the template. + +**Example** + +``` +{sElement.name} // works + +{sElement.name|currency} // works, but does not execute the currency function + +{sElement.value[$key]|currency} // does not work +``` + +##### Tracking Code + +Smarty rendering has been disabled for this section. All variables have been removed with one exception. The variable `{$offerPosition.trackingcode}` is a placeholder now. To generate tracking urls, use the following pattern: + +``` +https://gls-group.eu/DE/de/paketverfolgung?match={$offerPosition.trackingcode} + +{$offerPosition.trackingcode} +``` + +##### Extending listing templates + +We've changed the way when product listing templates will be loaded. In order to respond with a JSON object, the template must be rendered even before the response will be sent. Therefore you have to subscribe to the `PreDispatch` event to register your templates in time. In case you are used to `extendsTemplate`, you have to update your plugin as this won't work anymore. Learn more on how to extends templates here. + +**Example Plugin** + +```php + 'onListing', + 'Enlight_Controller_Action_PreDispatch_Widgets' => 'onListing', + ]; +} + +public function onListing(\Enlight_Event_EventArgs $args) +{ + $this->container->get('template')->addTemplateDir(__DIR__ . '/Resources/views'); +} +``` + +### New basket signature + +Improvements in basket on security and query manipulation as described in [5.3 signature](/developers-guide/payment-plugin/#new-signature-in-shopware-5.3-and-later). + +### Product votes + +Added opportunity to display product votes only in sub shop where they posted. This behavior can be configured over the backend configuration module. + +### Attribute label translations + +Translations for different fields (help, support, label) may be configured via snippets. + +**Example: `s_articles_attributes.attr1`** + +| Field | Snippet name | +|-------|--------------| +| Snippet namespace | backend/attribute_columns | +| Snippet name label | s_articles_attributes_attr1_label | +| Snippet name support text | s_articles_attributes_attr1_supportText | +| Snippet name help text | s_articles_attributes_attr1_helpText | + +### Backend Components + +You may now define the expression for the comparison in SQL. For example `>=` can be defined like seen below: + +```javascript +Ext.define('Shopware.apps.Vote.view.list.extensions.Filter', { + extend: 'Shopware.listing.FilterPanel', + alias: 'widget.vote-listing-filter-panel', + configure: function() { + return { + controller: 'Vote', + model: 'Shopware.apps.Vote.model.Vote', + fields: { + points: { + expression: '>=', + } + } + }; + } +}); +``` + +### Captcha + +Captchas are now configurable via backend and can be added using the `shopware.captcha` dependency injection container tag. + +```xml + + + + + +``` + +For more information, please refer to our [Captcha Documentation](https://developers.shopware.com/developers-guide/implementing-your-own-captcha/). + +### Redis backend and doctrine cache +Redis may now be used as a cache provider for the backend and model caches. Here is an example: + +``` + 'model' => [ + 'redisHost' => '127.0.0.1', + 'redisPort' => 6379, + 'redisDbIndex' => 0, + 'cacheProvider' => 'redis', + ], + + 'cache' => [ + 'backend' => 'redis', + 'backendOptions' => [ + 'servers' => array( + array( + 'host' => '127.0.0.1', + 'port' => 6379, + 'dbindex' => 0, + ), + ), + ], + ], +``` + +### Asynchronous JavaScript +The concatenated main JavaScript file is now loaded asynchronously. This improves the first rendering of the page also known as page speed. If you are adding your files via the theme compiler you should not worry about a thing. Your script is loaded together with all other Shopware scripts. + +If there is a reason for you to implement your script in a different way, please be aware of possible race conditions that could occur. When you need some parts from the main script as a dependency (for example jQuery) there is a new callback method which you can use to wait for the main script to load. + +```javascript +document.asyncReady(function() { + // do your magic here +}); +``` + +### Select field replacement + +The replacement of the select field elements via JavaScript is deprecated and will be removed in a future release. You may create a styled select field with a simple CSS-only solution by adding a wrapper element. + +``` +
    + +
    +``` + +### Batch Product Search + +The Batch Product Search service works with request and results. You can add multiple criteria and/or product numbers to a request and resolve them in an optimized way. An optimizer groups multiple equal criteria into one and performs the search. + +```php +$criteria = new Criteria(); +$criteria->addCondition(new CategoryCondition([3])); +$criteria->limit(3); + +$anotherCriteria = new Criteria(); +$anotherCriteria->addCondition(new CategoryCondition([3])); +$anotherCriteria->limit(5); + +$request = new BatchProductNumberSearchRequest(); +$request->setProductNumbers('numbers-1', ['SW10004', 'SW10006']); +$request->setCriteria('criteria-1', $criteria); +$request->setCriteria('criteria-2', $anotherCriteria); + +$result = $this->container->get('shopware_search.batch_product_search')->search($request, $context); + +$result->get('numbers-1'); // ['SW10004' => ListProduct, 'SW10006' => ListProduct] +$result->get('criteria-1'); // ['SW10006' => ListProduct, 'SW10007' => ListProduct, 'SW10008' => ListProduct] +$result->get('criteria-2'); // ['SW10009' => ListProduct, 'SW10010' => ListProduct, 'SW10011' => ListProduct, 'SW10012' => ListProduct, 'SW10013' => ListProduct] +``` + +### Partial facets + +`\Shopware\Bundle\SearchBundleDBAL\FacetHandlerInterface` is marked as deprecated and replaced by `\Shopware\Bundle\SearchBundleDBAL\PartialFacetHandlerInterface`. +Each facet handler had to revert the provided criteria on their own to remove customer conditions. This behaviour is now handled in the `\Shopware\Bundle\SearchBundleDBAL\ProductNumberSearch::createFacets` + +Old implementation: +``` +/** + * @param FacetInterface $facet + * @param Criteria $criteria + * @param ShopContextInterface $context + * @return BooleanFacetResult + */ +public function generateFacet( + FacetInterface $facet, + Criteria $criteria, + ShopContextInterface $context +) { + $reverted = clone $criteria; + $reverted->resetConditions(); + $reverted->resetSorting(); + + $query = $this->queryBuilderFactory->createQuery($reverted, $context); + //... +} +``` + +New implementation: +``` +public function generatePartialFacet( + FacetInterface $facet, + Criteria $reverted, + Criteria $criteria, + ShopContextInterface $context +) { + $query = $this->queryBuilderFactory->createQuery($reverted, $context); + //... +``` + +#### Elasticsearch + +In the elastic search implementation the current filter behavior is controlled by the condition handlers. By adding a query as `post filter`, facets are not affected by this filter. +This behavior is checked using the the `Criteria->hasBaseCondition` statement: +``` +/** + * @inheritdoc + */ +public function handle( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context +) { + if ($criteria->hasBaseCondition($criteriaPart->getName())) { + $search->addFilter(new TermQuery('active', 1)); + } else { + $search->addPostFilter(new TermQuery('active', 1)); + } +} + +``` +This behavior is now controlled in the `\Shopware\Bundle\SearchBundleES\ProductNumberSearch`. To support the new filter mode, each condition handler has to implement the `\Shopware\Bundle\SearchBundleES\PartialConditionHandlerInterface`. +It is possible to implement this interface beside the original `\Shopware\Bundle\SearchBundleES\HandlerInterface`. +``` +namespace Shopware\Bundle\SearchBundleES; +if (!interface_exists('\Shopware\Bundle\SearchBundleES\PartialConditionHandlerInterface')) { + interface PartialConditionHandlerInterface { } +} + +namespace Shopware\SwagBonusSystem\Bundle\SearchBundleES; + +class BonusConditionHandler implements HandlerInterface, PartialConditionHandlerInterface +{ + const ES_FIELD = 'attributes.bonus_system.has_bonus'; + + public function supports(CriteriaPartInterface $criteriaPart) + { + return ($criteriaPart instanceof BonusCondition); + } + + public function handleFilter( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context + ) { + $search->addFilter( + new TermQuery(self::ES_FIELD, 1) + ); + } + + + public function handlePostFilter( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context + ) { + $search->addPostFilter(new TermQuery(self::ES_FIELD, 1)); + } + + public function handle( + CriteriaPartInterface $criteriaPart, + Criteria $criteria, + Search $search, + ShopContextInterface $context + ) { + if ($criteria->hasBaseCondition($criteriaPart->getName())) { + $this->handleFilter($criteriaPart, $criteria, $search, $context); + } else { + $this->handlePostFilter($criteriaPart, $criteria, $search, $context); + } + } +} +``` + +### CookiePermission + +Cookie permissions is now a part of shopware and you can configure it in the shop settings. + +We implement a basic cookie permission hint ***(see migration 910)***. If you want to change the decision whether the item is displayed or not, overwrite the jQuery plugin in the `jquery.cookie-permission.js`. + +### Shopping worlds + +Shopping worlds have been technically refactored from the ground up to improve the overall performance when adding several elements to a shopping world. It is now possible to export and import shopping worlds via the backend. +You can also convert shopping worlds to presets for reusability of configured shopping worlds. Please look at the "[Create custom emotion preset plugin](/developers-guide/emotion-preset-plugin/)" article for further information. + +#### Removed escaped_fragments + +In previous versions it was possible to request shopping worlds with parameter ```?_escaped_fragment_=1```. This provided direct loading of shopping worlds instead of ajax loading. This +parameter does not work anymore. + +#### ComponentHandler + +The processing of elements has been changed from events to component handler classes. + +**Before: Subscribe to an event and process element data in the callback method** + +```php +public static function getSubscribedEvents() +{ + return ['Shopware_Controllers_Widgets_Emotion_AddElement' => 'handleSideviewElement']; +} +``` + +**After: Create a new class and tag it as `shopware_emotion.component_handler` in your `services.xml`** + +```php +class SideviewComponentHandler implements ComponentHandlerInterface +{ + public function supports(Element $element) + { + return $element->getComponent()->getType() === 'emotion-component-sideview'; + } + + public function prepare(PrepareDataCollection $collection, Element $element, ShopContextInterface $context) + { + // do some prepare logic + } + + public function handle(ResolvedDataCollection $collection, Element $element, ShopContextInterface $context) + { + // do some handle logic and fill data + $element->getData()->set('key', 'value'); + } +} +``` + +#### Requesting items in ComponentHandler + +To make use of the performance improvement, you have to split your logic into a prepare step and handle step. The prepare step collects product numbers or criteria objects which will be resolved across all elements at once. The handle step provides a collection with resolved products and can be merged into your element. + +```php +public function prepare(PrepareDataCollection $collection, Element $element, ShopContextInterface $context) +{ + $productNumber = $element->getConfig()->get('selected_product_number'); + $collection->getBatchRequest()->setProductNumbers('my-unique-request', [$productNumber]); +} + +public function handle(ResolvedDataCollection $collection, Element $element, ShopContextInterface $context) +{ + $product = current($collection->getBatchResult()->get('my-unique-request')); + $element->getData()->set('product', $product); +} +``` + +Keep in mind to use a unique key for requesting and getting products. For best practise, use the element's id in your key (`$element->getId()`). + +### Grunt LiveReload mode & Modularized Grunt tasks +We worked on our Grunt integration and added two new features. The first one is a LiveReload mode which speeds up your theme development. The next big step forward is modularizing our Grunt tasks into separate files. Learn more on how to use these new features in our article on ["Using Grunt for theme development"](/designers-guide/best-practice-theme-development/). + +#### Import & Export of shopping worlds +It is now possible to import and export shopping worlds. To realize export of media for your own custom shopping world element, you can register custom handlers. +How to create such a handler is described in "[Adding a custom component handler for export](/developers-guide/custom-shopping-world-elements/#adding-a-custom-component-handler-for-export)" article. + +### Import / export module +The core import & export module got removed. It is replaced by the free [SwagImportExport](http://store.shopware.com/swagef36a3f0ee25/shopware-import/export.html) plugin which is is available in our community store. Installation +is also integrated via the backend. + +### New table for saving user settings + +A new table ```s_core_auth_config``` is added for storing MediaManager settings of an individual user. This table can also be used by third party plugins for storing other user related module configurations. + +### Library updates + +* Updated `FPDF` to 1.8.1 +* Updated `FPDI` to 1.6.1 +* Updated `flatpickr` to 2.4.7 +* Updated `jquery` to 2.2.4 +* Updated `grunt` to 1.0.1 +* Updated `grunt-contrib-clean` to 1.1.0 +* Updated `grunt-contrib-copy` to 1.0.0 +* Updated `Modernizr` to 3.5.0 + +### Deprecations + +* Deprecated `Shopware_Components_Convert_Csv` without replacement, to be removed with 5.4 +* Deprecated `Shopware_Components_Convert_Xml` without replacement, to be removed with 5.4 +* Deprecated `Shopware_Components_Convert_Excel` without replacement, to be removed with 5.4 +* Deprecated `\Shopware_Controllers_Widgets_Listing::ajaxListingAction`, use `\Shopware_Controllers_Widgets_Listing::listingCountAction` instead +* Deprecated method `sArticles::sGetAffectedSuppliers()` without replacement, to be removed with 5.5 +* Deprecated `Shopware\Models\Article\Element`, to be removed with 6.0 + +### Removals + + + +#### Article related +* Default plugin `LastArticle`, use `shopware.components.last_articles_subscriber` instead +* Session key `sLastArticle` +* View variable `sLastActiveArticle` from basket +* Join on `s_core_tax` in `Shopware\Bundle\SearchBundleDBAL\ProductNumberSearch` + +#### Methods +The following methods have been removed: + +* `Shopware\Models\Order\Repository::getBackendOrdersQueryBuilder()` +* `Shopware\Models\Order\Repository::getBackendOrdersQuery()` +* `Shopware\Models\Order\Repository::getBackendAdditionalOrderDataQuery()` +* `Shopware\Components\Model\ModelManager::__call()` +* `Shopware_Controllers_Widgets_Emotion::getEmotion()` +* `Shopware_Controllers_Widgets_Emotion::handleElement()`, use `Shopware\Bundle\EmotionBundle\ComponentHandler\ComponentHandlerInterface` instead +* `Shopware_Controllers_Widgets_Emotion::getRandomBlogEntry()` +* `Shopware_Controllers_Widgets_Emotion::getBlogEntry()`, has been replaced by `Shopware\Bundle\EmotionBundle\ComponentHandler\BlogComponentHandler` +* `Shopware_Controllers_Widgets_Emotion::getCategoryTeaser()`, has been replaced by `Shopware\Bundle\EmotionBundle\ComponentHandler\CategoryTeaserComponentHandler` +* `Shopware_Controllers_Widgets_Emotion::getBannerMappingLinks()`, has been replaced by `Shopware\Bundle\EmotionBundle\ComponentHandler\BannerComponentHandler` +* `Shopware_Controllers_Widgets_Emotion::getManufacturerSlider()`, has been replaced by `Shopware\Bundle\EmotionBundle\ComponentHandler\ManufacturerSliderComponentHandler` +* `Shopware_Controllers_Widgets_Emotion::getBannerSlider()`, has been replaced by `Shopware\Bundle\EmotionBundle\ComponentHandler\BannerSliderComponentHandler` +* `Shopware_Controllers_Widgets_Emotion::getArticleSlider()`, has been replaced by `Shopware\Bundle\EmotionBundle\ComponentHandler\ArticleSliderComponentHandler` +* `Shopware_Controllers_Widgets_Emotion::getHtml5Video()`, has been replaced by `Shopware\Bundle\EmotionBundle\ComponentHandler\Html5VideoComponentHandler` +* `Shopware\Models\Customer\Repository::getListQueryBuilder` +* `Shopware\Models\Customer\Repository::getListQuery` +* `Shopware\Models\Customer\Repository::getBackendListCountedBuilder` +* `hasLocalStorageSupport` from `jquery.storage-manager.js`. Use Modernizr to detect the feature +* `hasSessionStorageSupport` from `jquery.storage-manager.js`. Use Modernizr to detect the feature +* `Shopware\Bundle\StoreFrontBundle\Struct\Country::setShippingFree` and `Shopware\Bundle\StoreFrontBundle\Struct\Country::isShippingFree` +* `Shopware\Models\Country\Country::$shippingFree`, `Shopware\Models\Country\Country::setShippingFree` and `Shopware\Models\Country\Country::getShippingFree` + +#### Methods signature changes +* Removed parameter `$checkProxy` from `Enlight_Controller_Request_Request::getClientIp()` +* Removed parameter `sCategory` from search controller `listing/ajaxCount` requests + +#### Removed ExtJS components + +* Files and components + * `themes/Backend/ExtJs/backend/vote/view/vote/detail.js` + * `themes/Backend/ExtJs/backend/vote/view/vote/edit.js` + * `themes/Backend/ExtJs/backend/vote/view/vote/infopanel.js` + * `themes/Backend/ExtJs/backend/vote/view/vote/list.js` + * `themes/Backend/ExtJs/backend/vote/view/vote/toolbar.js` + * `themes/Backend/ExtJs/backend/vote/view/vote/window.js` + * `themes/Backend/ExtJs/backend/vote/controller/vote.js` + * `themes/Backend/ExtJs/backend/vote/controller/vote.js` + * `Shopware.apps.Customer.model.List` + * `Shopware.apps.Customer.store.List` + * `Shopware.apps.CanceledOrder.model.Customer` + +#### Database queries +* Field `attributes.search.cheapest_price` from DBAL search query +* Field `attributes.search.average` from DBAL search query +* `__country_shippingfree` field in `Shopware\Bundle\StoreFrontBundle\Gateway\DBAL\FieldHelper::getCountryFields` + +#### Database tables +* `s_core_auth_config` + * `user_id` + * `name` + * `config` + +#### Database columns +* `s_emarketing_lastarticles` + * `articleName` +* `s_emarketing_lastarticles` + * `img` +* `s_core_countries` + * `shippingfree` + +#### Classes +* `Enlight_Bootstrap` + +#### Smarty template files, blocks and their snippets +* Files + * `themes/Frontend/Bare/frontend/search/category-filter.tpl` + +* Snippets + * `frontend/checkout/actions/CheckoutActionsLinkLast` + +* Blocks + * `frontend_listing_filter_facet_media_list_flyout` + * `frontend_listing_filter_facet_media_list_title` + * `frontend_listing_filter_facet_media_list_icon` + * `frontend_listing_filter_facet_media_list_content` + * `frontend_listing_filter_facet_media_list_list` + * `frontend_listing_filter_facet_media_list_option` + * `frontend_listing_filter_facet_media_list_option_container` + * `frontend_listing_filter_facet_media_list_input` + * `frontend_listing_filter_facet_media_list_label` + * `frontend_listing_filter_facet_radio_flyout` + * `frontend_listing_filter_facet_radio_title` + * `frontend_listing_filter_facet_radio_icon` + * `frontend_listing_filter_facet_radio_content` + * `frontend_listing_filter_facet_radio_list` + * `frontend_listing_filter_facet_radio_option` + * `frontend_listing_filter_facet_radio_option_container` + * `frontend_listing_filter_facet_radio_input` + * `frontend_listing_filter_facet_radio_label` + * `frontend_listing_filter_facet_value_list_flyout` + * `frontend_listing_filter_facet_value_list_title` + * `frontend_listing_filter_facet_value_list_icon` + * `frontend_listing_filter_facet_value_list_content` + * `frontend_listing_filter_facet_value_list_list` + * `frontend_listing_filter_facet_value_list_option` + * `frontend_listing_filter_facet_value_list_option_container` + * `frontend_listing_filter_facet_value_list_input` + * `frontend_listing_filter_facet_value_list_label` + * `frontend_listing_actions_sort_field_relevance` + * `frontend_listing_actions_sort_field_release` + * `frontend_listing_actions_sort_field_rating` + * `frontend_listing_actions_sort_field_price_asc` + * `frontend_listing_actions_sort_field_price_desc` + * `frontend_listing_actions_sort_field_name` + * `frontend_search_category_filter` + * `frontend_listing_swf_banner` + +#### jQuery plugins +* Complete + * `src/js/jquery.selectbox-replacement.js` + * jQuery UI date picker integration in favour of a new global component +* Methods + * `showFallbackContent` in `jquery.emotion.js` + * `hideFallbackContent` in `jquery.emotion.js` + +* Events + * `plugin/swEmotionLoader/onShowFallbackContent` in `jquery.emotion.js` + * `plugin/swEmotionLoader/onHideFallbackContent`in `jquery.emotion.js` + +#### View variables +* Global + * `hasEscapedFragment` +* Forms + * `{$sShopname}`, use `{sShopname}` instead + +#### Frontend +* Support for `Internet Explorer < 11` +* Unneeded `css` classes +* Several `modernizr` options +* Meta tag `fragment` +* LESS variable `@zindex-fancy-select` +* LESS variable `@font-face` for `extra-bold` and `light` of the Open Sans font type +* LESS file `ie.less` from the Responsive Theme +* File `jquery.ie-fixes.js` from the Responsive Theme +* Several polyfills +* `html5shiv` +* Smarty modifier `rewrite` +* Vendor prefixes `-ms` and `-o` from all mixins in the Bare and Responsive Theme +* Scrollbar styling on filter-panels (Selector: `.filter-panel--content`) +* Support for `.swf` file type in the banner module +* `max-width` rule for `.filter--active` in `themes/Frontend/Responsive/frontend/_public/src/less/_components/filter-panel.less` + +### Other changes +* Added config element `displayOnlySubShopVotes` to display only shop assigned article votes +* Added parameter `displayProgressOnSingleDelete` to `Shopware.grid.Panel` to hide progress window on single delete action +* Added parameter `expression` in `Shopware.listing.FilterPanel` to allow definition of own query expressions +* Added parameter `splitFields` to `Shopware.model.Container` to configure fieldset column layout +* Added interface `Shopware\Components\Captcha\CaptchaInterface` +* Added method `Shopware\Models\Order\Repository::getList()` +* Added method `Shopware\Models\Order\Repository::search()` +* Added method `Shopware\Models\Order\Repository::getDocuments()` +* Added method `Shopware\Models\Order\Repository::getDetails()` +* Added method `Shopware\Models\Order\Repository::getPayments()` +* Added responsive helper css/less classes in `_mixins/visibility-helper.less` +* Added method `Shopware\Bundle\MediaBundle\MediaServiceInterface::getFilesystem()` for direct access to the media filesystem +* Added config element `liveMigration` to enable or disable the media live migration +* Added config element `displayListingBuyButton` to display listing buy button +* Added service `shopware_search.batch_product_search` and `shopware_search.batch_product_number_search` for optimized product queries +* Added support for callback methods and jQuery promises in `jQuery.overlay` and `jQuery.loadingIndicators` +* Added jQuery method `setLoading()` to apply a loading indicator to an element `$('selector').setLoading()` +* Added required attribute `data-facet-name` for filter elements +* Added type for the filter panels `value-list-single` +* Added smarty blocks for `frontend_listing_filter_facet_multi_selection` for unified filter panel +* Added service `Shopware\Bundle\StoreFrontBundle\Service\Core\CategoryDepthService` to select categories by the given depth +* Added jQuery event `plugin/swListing/fetchListing` which allows to load listings, facet data or listing counts +* Added config element `listingMode` to switch listing reload behavior +* Added jQuery event `action/fetchListing` which allows to load listings, facet data or listing counts +* Added property `path` to `Shopware\Bundle\StoreFrontBundle\Struct\Media` which reflects the virtual path +* Added service `Shopware\Bundle\StoreFrontBundle\Service\Core\BlogService` to fetch blog entries by id +* Added template `themes/Frontend/Bare/frontend/detail/content.tpl` +* Added template `themes/Frontend/Bare/frontend/detail/content/header.tpl` +* Added template `themes/Frontend/Bare/frontend/detail/content/buy_container.tpl` +* Added template `themes/Frontend/Bare/frontend/detail/content/tab_navigation.tpl` +* Added template `themes/Frontend/Bare/frontend/detail/content/tab_container.tpl` +* Added option to select variants in `Shopware.apps.Emotion.view.components.Article` and `Shopware.apps.Emotion.view.components.ArticleSlider` +* Added local path to `@font-face` integration of the Open Sans font +* Added smarty block `frontend_register_billing_fieldset_company_panel` for registration +* Added smarty block `frontend_register_billing_fieldset_company_title` for registration +* Added smarty block `frontend_register_billing_fieldset_company_body` for registration +* Added smarty block `frontend_register_billing_fieldset_panel` for registration +* Added smarty block `frontend_register_billing_fieldset_title` for registration +* Added smarty block `frontend_register_billing_fieldset_body` for registration +* Added smarty block `frontend_register_index_cgroup_header_title` for registration +* Added smarty block `frontend_register_index_cgroup_header_body` for registration +* Added smarty block `frontend_register_index_advantages_title` for registration +* Added smarty block `frontend_register_login_customer_title` for registration +* Added smarty block `frontend_register_personal_fieldset_panel` for registration +* Added smarty block `frontend_register_personal_fieldset_title` for registration +* Added smarty block `frontend_register_personal_fieldset_body` for registration +* Added smarty block `frontend_register_shipping_fieldset_panel` for registration +* Added smarty block `frontend_register_shipping_fieldset_title` for registration +* Added smarty block `frontend_register_shipping_fieldset_body` for registration +* Added global date picker component `frontend/_public/src/js/jquery.datepicker.js` to Responsive theme +* Added filter facets for date and datetime fields +* Added template `themes/Frontend/Bare/frontend/listing/filter/facet-date.tpl` for date and datetime facets +* Added template `themes/Frontend/Bare/frontend/listing/filter/facet-date-multi.tpl` for date and datetime facets +* Added template `themes/Frontend/Bare/frontend/listing/filter/facet-date-range.tpl` for date and datetime facets +* Added template `themes/Frontend/Bare/frontend/listing/filter/facet-datetime.tpl` for date and datetime facets +* Added template `themes/Frontend/Bare/frontend/listing/filter/facet-datetime-multi.tpl` for date and datetime facets +* Added template `themes/Frontend/Bare/frontend/listing/filter/facet-datetime-range.tpl` for date and datetime facets +* Added class `.filter-panel--radio` to `themes/Frontend/Responsive/frontend/_public/src/less/_components/filter-panel.less` +* Added class `.filter-panel--checkbox` to `themes/Frontend/Responsive/frontend/_public/src/less/_components/filter-panel.less` +* Added class `.radio--state` to `themes/Frontend/Responsive/frontend/_public/src/less/_components/filter-panel.less` +* Added class `.checkbox--state` to `themes/Frontend/Responsive/frontend/_public/src/less/_components/filter-panel.less` +* Added JavaScript method `document.asyncReady()` to register callbacks which fire after the main script was loaded asynchronously. +* Added missing dependency `jquery.event.move` to the `package.json` file. +* Added template switch for `listing/index.tpl` to `listing/customer_stream.tpl` in case that the category contains a shopping world which is restricted to customer streams +* Added database column `s_emarketing_vouchers.customer_stream_ids` to restrict vouchers to customer streams. +* Added database column `s_emotion.customer_stream_ids` to restrict shopping worlds to customer streams. +* Added database table `s_customer_streams` for a list of all existing streams (`Shopware\Models\Customer\CustomerStream`) +* Added database table `s_customer_search_index` for a fast customer search +* Added database table `s_customer_streams_mapping` for mappings between customer and assigned streams +* Added bundle `Shopware\Bundle\CustomerSearchBundle` which defines how customers can be searched +* Added bundle `Shopware\Bundle\CustomerSearchBundleDBAL` which allows to search for customers using DBAL +* Added console command `sw:customer:search:index:populate` to generate customer stream search index +* Added console command `sw:customer:stream:index:populate` to generate customer stream mapping table +* Added flag `$hasCustomerStreamEmotion` in `frontend/home/index.tpl` to switch between emotions restricted to customer streams and those which are unrestricted +* Added route `/frontend/listing/layout` which loads the category page layout for customer streams. This route is called using `{action ...}` in case that the category contains an emotion with customer streams +* Added route `/frontend/listing/listing` which loads the category product listing. This route is called using `{action ...}` in case that the category contains an emotion with customer streams +* Added entity `Shopware\Models\Customer\CustomerStream` for attribute single and multi selection. +* Added translations for attribute labels. See below for more information. +* Added database structure for new emotion preset feature: + * `s_emotion_presets` - contains all installed presets + * `s_emotion_preset_translations` - contains presets translations +* Added models for presets + * `Shopware\Models\Emotion\Preset` + * `Shopware\Models\Emotion\PresetTranslation` +* Added classes for handling emotion preset feature + * `Shopware\Components\Emotion\EmotionImporter` - handle emotion imports + * `Shopware\Components\Emotion\EmotionExporter` - handle emotion exports + * `Shopware\Components\Emotion\Preset\EmotionToPresetDataTransformer` - transform emotion to preset + * `Shopware\Components\Emotion\Preset\PresetDataSynchronizer` - uses component handlers to support import / export of emotions + * `Shopware\Components\Emotion\Preset\PresetInstaller` - installer for preset plugins + * `Shopware\Components\Emotion\Preset\PresetLoader` - loads presets and refreshes preset data to match current database + * `Shopware\Components\Emotion\Preset\PresetMetaDataInterface` - interface to use for preset plugin development +* Added API Resource for emotion presets `Shopware\Components\Api\Resource\EmotionPreset` +* Added backend controller for emotion presets `Shopware\Controllers\Backend\EmotionPresets` +* Added compiler pass to register emotion component handlers `Shopware\Components\DependencyInjection\Compiler\EmotionPresetCompilerPass` +* Added component handlers for asset import and export of shopping world elements + * `Shopware\Components\Emotion\Preset\ComponentHandler\BannderComponentHandler` + * `Shopware\Components\Emotion\Preset\ComponentHandler\BannerSliderComponentHandler` + * `Shopware\Components\Emotion\Preset\ComponentHandler\CategoryTeaserComponentHandler` + * `Shopware\Components\Emotion\Preset\ComponentHandler\Html5VideoComponentHandler` +* Added new ExtJs views for emotion presets under `themes\backend\emotion\view\preset` +* Added new service tag for registering emotion preset component handlers `shopware.emotion.preset_component_handler` +* Added actions to import and export shopping worlds in `Shopware_Controllers_Backend_Emotion` +* Added condition class `Shopware\Bundle\SearchBundle\Condition\WidthCondition` +* Added condition class `Shopware\Bundle\SearchBundle\Condition\HeightCondition` +* Added condition class `Shopware\Bundle\SearchBundle\Condition\LengthCondition` +* Added condition class `Shopware\Bundle\SearchBundle\Condition\WeightCondition` +* Added facet class `Shopware\Bundle\SearchBundle\Facet\CombinedConditionFacet` +* Added facet class `Shopware\Bundle\SearchBundle\Facet\WidthFacet` +* Added facet class `Shopware\Bundle\SearchBundle\Facet\HeightFacet` +* Added facet class `Shopware\Bundle\SearchBundle\Facet\LengthFacet` +* Added facet class `Shopware\Bundle\SearchBundle\Facet\WeightFacet` +* Added `Shopware\Bundle\SearchBundleDBAL\VariantHelper` which joins all variants for dbal search +* Added smarty blocks `frontend_checkout_shipping_payment_core_button_top` and `frontend_checkout_shipping_payment_core_button_top` for shipping +* Added new Interface for facet result template switch `Shopware\Bundle\SearchBundle\TemplateSwitchable` +* Added `selecttree` and `combotree` config elements for plugins +* Added backend configuration option for the newsletter to configure if a captcha is required to subscribe to the newsletter +* Added two new Smarty blocks for menu and menu item overwrite possibility to the account sidebar +* Added LiveReload mode for the default grunt which reloads your browser window automatically after the grunt compilation was successful +* Added `nofollow` attribute to all links in the block `frontend_account_menu` since these links are now visible in the frontend if the account dropdown menu is activated +* Added `type` parameter to `Shopware_Controllers_Widgets_Listing::productSliderAction` and `Shopware_Controllers_Widgets_Listing::productsAction` which allows to load product sliders or product boxes. +* Added new search builder class `Shopware\Components\Model\SearchBuilder` +* Added new search builder as `__construct` parameter in `Shopware\Bundle\AttributeBundle\Repository\Searcher\GenericSearcher` +* Added new `FunctionNode` for IF-ELSE statements in ORM query builder +* Added `/address` to robots.txt +* Added snippet `DetailBuyActionAddName` in `snippets/frontend/detail/buy.ini` +* Added `Shopware\Components\Template\Security` class for all requests. +* Added whitelist for allowed php functions and php modifiers in smarty + * `template_security.php_modifiers` + * `template_security.php_functions` +* Added new column `do_not_split` to table `s_search_fields`. Activate to store the values of this field as given into the search index. If not active, the default behaviour is used +* Added new service `shopware_storefront.price_calculator` which calculates the product price. Was formerly a private method in `shopware_storefront.price_calculation_service` +* Added service `shopware_media.extension_mapping` to provide a customizable whitelist for media file extensions and their type mapping +* Changed theme path for new plugins from `/resources` into `/Resources` +* Changed sorting of `Shopware.listing.FilterPanel` fields +* Changed database column `s_articles_vote`.`answer_date` to allow `NULL` values +* Changed `LastArticle` plugin config elements `show`, `controller` and `time` to be prefixed with `lastarticles_` +* Changed product listings in shopping worlds to only be loaded if `showListing` is true +* Changed sql query + in `sAdmin` queries which uses a sub query for address compatibility, following functions affected: + * `sAdmin::sGetDispatchBasket` + * `sAdmin::sGetPremiumDispatches` + * `sAdmin::sGetPremiumDispatchSurcharge` +* Changed attribute type `string` mapping to mysql `TEXT` type. String and single selection data type supports no longer a sql default value. +* Changed `roundPretty` value for currency range filter +* Changed `CategoryFacet` behavior to generate each time a tree based on the system category with a configured category depth +* Changed facet templates `facet-radio`, `facet-media-list` and `facet-value-list` into one template +* Renamed parameter `data-count-ctrl` on `#filter` form to `data-listing-url` +* Changed removal version of method `Shopware\Components\Model\ModelManager::addAttribute` to 5.4 +* Changed removal version of method `Shopware\Components\Model\ModelManager::removeAttribute` to 5.4 +* Changed template `component_article_slider.tpl` to show provided products instead of always fetching them via ajax +* Changed emotion preview to not save the current state before showing preview +* Changed command `sw:thumbnail:cleanup` to search the filesystem to remove orphaned thumbnails +* Changed configuration `defaultListingSorting` from the performance module to basic settings in `categories / listings` +* Changed the jQuery plugin `src/js/jquery.selectbox-replacement.js` to be used only as a polyfill. Use the CSS-only version for select fields instead. +* Changed template filename from `frontend/forms/elements.tpl` to `frontend/forms/form-elements.tpl` +* Changed smarty block from `frontend_forms_index_elements` to `frontend_forms_index_form_elements` +* Changed smarty blocks from `frontend_forms_elements*` to `frontend_forms_form_elements*` +* Changed template file `themes/Frontend/Bare/frontend/detail/index.tpl` to split it into separated files +* Changed property `linkDetails` of `$sArticle` +* Changed the article url to also contain the order number of the product +* Changed the product selection to variant selection in `Shopware.apps.Emotion.view.components.BannerMapping` +* Changed the integration of `modernizr.js` and added it to the compressed main JavaScript files +* Changed the script tag for the generated JavaScript file for asynchronous loading, can be changed in theme configuration +* Changed the inline script for the statistics update to vanilla JavaScript +* Changed event name from `plugin/swAjaxProductNavigation/onSetProductState` to `plugin/swAjaxProductNavigation/onGetProductState` +* Changed behavior of the smarty rendering in forms fields comment. See below for more information +* Changed behavior of the tracking url rendering. See below for more information +* Changed text color and height of `.filter--active` in `themes/Frontend/Responsive/frontend/_public/src/less/_components/filter-panel.less` +* Changed database column `s_articles_details.instock` to allow `NULL` values and default to `0` +* Changed return values so the array keys are now the respective country/state IDs in `\Shopware\Bundle\StoreFrontBundle\Service\Core\LocationService::getCountries` +* Moved the removal of the whole cache folder after the removal of the `.js` and `.css` files for better handling of huge caches in the `clear_cache.sh` script +* Changed `Shopware_Controllers_Widgets_Listing::streamSliderAction` to `Shopware_Controllers_Widgets_Listing::streamAction` +* Changed `Shopware_Controllers_Widgets_Listing::productSliderAction` to `Shopware_Controllers_Widgets_Listing::productsAction` +* Changed snippet `DetailBuyActionAdd` in `snippets/frontend/detail/buy.ini`, it now contains tags +* Changed snippet `ListingBuyActionAdd` in `snippets/frontend/listing/box_article.ini`, it now contains another tag +* Merged `account/sidebar.tpl` and `account/sidebar_personal.tpl` +* Moved snippets from `account/sidebar_personal.ini` to `account/sidebar.ini` +* Changed `Enlight_Hook_ProxyFactory` to use [ocramius/proxy-manager](https://github.com/Ocramius/ProxyManager) for generating proxy classes +* Backend customer listing is now loaded in `Shopware_Controllers_Backend_CustomerQuickView` +* Refactored backend customer module. Please take a look into the different template files to see what has changed. +* Removed import / export module +* Removed unused `Zend Framework Components` +* Removed alias support from `Enlight_Controller_Request_Request` (`getAlias`, `getAliases`, `setAlias`) +* Removed configuration option `sCOUNTRYSHIPPING` +* Removed constants of `\Shopware\Bundle\SearchBundle\CriteriaRequestHandler\CoreCriteriaRequestHandler` and `Shopware\Bundle\SearchBundle\StoreFrontCriteriaFactory`: + * `SORTING_RELEASE_DATE` + * `SORTING_POPULARITY` + * `SORTING_CHEAPEST_PRICE` + * `SORTING_HIGHEST_PRICE` + * `SORTING_PRODUCT_NAME_ASC` + * `SORTING_PRODUCT_NAME_DESC` + * `SORTING_SEARCH_RANKING` +* Removed route `/backend/performance/listingSortings` +* Removed route `/backend/customer/getList` +* Removed event `Shopware_Plugins_HttpCache_ShouldNotCache` +* Removed `eval` from block `frontend_forms_index_headline` in `index.tpl` of `themes\Frontend\Bare\frontend\forms` for `$sSupport.text` +* Removed cleanupPlugins from `Shopware\Bundle\PluginInstallerBundle\Service` + ## Shopware 5.2 ### System requirements changes The required PHP version is now **PHP 5.6.4 or higher**. Please check your system configuration and update your PHP version if necessary. If you are using a PHP version prior to 5.6.4 there will be errors. -The required IonCube Loader version was bumped to 5.0 or higher. +The required ionCube Loader version was bumped to 5.0 or higher. ### Shopping worlds @@ -92,16 +1895,16 @@ The account section and registration have been refactored to continue the refact * Uses the new service `shopware_account.register_service` * Methods of core class `\sAdmin` regarding the registration have been removed without substitution. * Templates may have been rewritten - * For a complete list of template and event changes, refer to the [UPGRADE.md](https://github.com/shopware/shopware/blob/5.2/UPGRADE-5.2.md). + * For a complete list of template and event changes, refer to the [UPGRADE.md](https://github.com/shopware5/shopware/blob/5.3/UPGRADE-5.2.md). ### Address management
    Important tasks after updating to Shopware 5.2
    -Existing adresses including their attributes have been migrated into s_user_addresses. Please verify that all addresses have been merged completely. Addresses have been read from s_user_billingaddress, s_user_shippingaddress, s_order_billingaddress, s_order_shippingaddress. +Existing addresses including their attributes have been migrated into s_user_addresses. Please verify that all addresses have been merged completely. Addresses have been read from s_user_billingaddress, s_user_shippingaddress, s_order_billingaddress, s_order_shippingaddress.
    -The address management allows a customer to manage more than only one address which gets changed with every order. The customer is now able to create more address, e.g. for home and work, and use them later on in an order without loosing all existing address data. He can just change the reference to the default billing address, instead of changing it entirely. +The address management allows a customer to manage more than only one address which gets changed with every order. The customer is now able to create more address, e.g. for home and work, and use them later on in an order without losing all existing address data. He can just change the reference to the default billing address, instead of changing it entirely. * Shopware versions prior to 5.2 were using the tables `s_user_billingaddress` and `s_user_shippingaddress` which have now been marked as deprecated as well as their associated models `\Shopware\Models\Customer\Billing` and `\Shopware\Models\Customer\Shipping`. Their association in `\Shopware\Models\Customer\Customer` will be removed with Shopware version 5.3. Please use `s_user_addresses` and it's model `\Shopware\Models\Customer\Address` instead. * Changes to the new model will automatically be synchronised with the old models. @@ -110,7 +1913,7 @@ The address management allows a customer to manage more than only one address wh * Selecting another address in the checkout results in a change of the session key `checkoutBillingAddressId` or `checkoutShippingAddressId` with the corresponding address id. After the order has been saved, the session keys will be reset. * The customer api endpoint now uses the structure of the address model, instead of the billing or shipping model * The checkout templates have been rewritten which results in changed and removed blocks. - * For a complete list of template changes, refer to the [UPGRADE.md](https://github.com/shopware/shopware/blob/5.2/UPGRADE-5.2.md). + * For a complete list of template changes, refer to the [UPGRADE.md](https://github.com/shopware5/shopware/blob/5.3/UPGRADE-5.2.md). To learn more about the new address service, refer to the [Address Management Guide](/developers-guide/address-management-guide). @@ -227,7 +2030,7 @@ attributeForm.saveAttribute(record.get('id'), function (successful) { This call will send a new request which saves all attributes for this item. -To learn more about the new attribute management, refer to the [README.md](https://github.com/shopware/shopware/blob/5.2/engine/Shopware/Bundle/AttributeBundle/README.md) file in the source code. +To learn more about the new attribute management, refer to the [README.md](https://github.com/shopware5/shopware/blob/5.3/engine/Shopware/Bundle/AttributeBundle/README.md) file in the source code. ### Library updates @@ -811,7 +2614,7 @@ public function install() public function onCollect() { return new ArrayCollection([ new \Shopware\Bundle\MediaBundle\Struct\MediaPosition('my_plugin_table', 'mediaID'), - new \Shopware\Bundle\MediaBundle\Struct\MediaPosition('my_other_plugin_table', 'mediaPath', 'path'); + new \Shopware\Bundle\MediaBundle\Struct\MediaPosition('my_other_plugin_table', 'mediaPath', 'path'), new \Shopware\Bundle\MediaBundle\Struct\MediaPosition('s_core_templates_config_values', 'value', 'path', MediaPosition::TYPE_SERIALIZE), ]); } @@ -861,7 +2664,7 @@ The **third** parameter selects the `s_media` column you are referencing to. The * Added library [zendframework/zend-escaper](https://github.com/zendframework/zend-escaper) * New interface: `\Shopware\Components\Escaper\EscaperInterface` * Default implementation: `\Shopware\Components\Escaper\Escaper`, uses `Zend\Escaper` - * Available in DI-Container: `shopware.escaper` + * Available in DI container: `shopware.escaper` * Smarty Modifiers: * escapeHtml * escapeHtmlAttr @@ -897,8 +2700,8 @@ The new responsive template is not supported in Internet Explorer 8 and below. T #### MySQL 5.1 MySQL 5.1 is no longer supported in Shopware 5. The required Version of MySQL for Shopware 5 is 5.5 or above. -#### IonCube Loader -IonCube Loader requirement has been upped to version 4.6.0. Notice that you only need the IonCube Loader if you are using plugins from the Shopware Store. +#### ionCube Loader +ionCube Loader requirement has been upped to version 4.6.0. Notice that you only need the ionCube Loader if you are using plugins from the Shopware Store. ### Major Breaks * `Street number` data was moved into the `Street`field @@ -929,7 +2732,7 @@ For this operation we recommend the console command `sw:thumbnail:generate` to a * `Shopware\Models\Article\Configurator\PriceSurcharged` replaced by `Shopware\Models\Article\Configurator\PriceVariation` * The new Shopware core selects all required data for `sGetArticleById`, `sGetPromotionById` and `sGetArticlesByCategory`. Several internal methods and events are no longer used by those functions. * Moved `engine/core/class/*` to `engine/Shopware/Core/*` -* Renamed `ENV` to `SHOPWARE_ENV` to avoid accidentally set `ENV` variable, please update your .htaccess if you use a custom envirenment or you are using the staging plugin +* Renamed `ENV` to `SHOPWARE_ENV` to avoid accidentally set `ENV` variable, please update your .htaccess if you use a custom environment or you are using the staging plugin * All downloaded dummy plugins are now installed in the engine/Shopware/Plugins/Community directory. * Replaced `orderbydefault` configuration by `defaultListingSorting`. The `orderbydefault` configuration worked with a plain sql input which is no longer possible. The `defaultListingSorting` contains now one of the default `sSort` parameters of a listing. If you want to reintegrate your old statement you can simple create a small plugin. You can find more information [here.](/developers-guide/shopware-5-search-bundle/) diff --git a/source/developers-guide/shopware-composer/index.md b/source/developers-guide/shopware-composer/index.md new file mode 100644 index 0000000000..4dad8694fd --- /dev/null +++ b/source/developers-guide/shopware-composer/index.md @@ -0,0 +1,137 @@ +--- +layout: default +title: Using Composer to install Shopware +github_link: developers-guide/shopware-composer/index.md +indexed: true +group: Developer Guides +subgroup: General Resources +menu_title: Using composer with Shopware +menu_order: 32 +--- + +
    + +## Introduction + +Starting with v5.4 Shopware supports installing a shop as a [Composer dependency](https://github.com/shopware5/composer-project) +out of the box. This helps you to professionalize development and deployments of Shopware shops by providing a reliable +versioning of Shopware itself and all the plugins required by your project. + +## What is Composer + +Composer defines itself as "[...] a tool for dependency management in PHP. It allows you to declare the libraries +your project depends on and it will manage (install/update) them for you" ([Composer website](https://getcomposer.org/)). + +Composer is the de-facto-standard for dependency management in the PHP community. You can think of it as the +apt/yum/npm/brew of the PHP world. + +## How to start + +To create a new Shopware project with Shopware as a dependency, all you need to do is install Composer +([download it here](https://getcomposer.org/download/) if you haven't yet) and run + +```bash +composer create-project shopware/composer-project my_project_name --no-interaction --stability=dev +``` + +This will clone the project repository with all necessary dependencies (including the latest released Shopware version) +into a new directory `my_project_name`. Shopware itself will be deployed under `vendor/shopware/shopware` like any other +dependency you might require later on. + +## Configuring Shopware + +The Composer Shopware project template relies on environment variables to configure your Shopware project. You can +either set those directly (this is recommended on production environments e.g. to not store credentials on disk) or +using a `.env` file in the project root. To see which variables are supported, have a look at the `.env.example` file. + +You can also have a `.env` file created for you! Simply run `./app/bin/install.sh` inside your new project directory to have a little installer-script ask you all necessary information. + +If you need to configure other values in the `config.php` (e.g. the error or session handler), you can find the file in +the `app/config/` directory. + +## Requiring plugins + +Given you want to require the SwagMediaSftp-plugin, all you need to do is run + +```bash +composer require shopwarelabs/swag-media-sftp:1.0.1 +``` + +This will add the plugin to your composer.json, download it and install it into the appropriate folder `custom/plugins`. +All you need to do afterwards is install and enable it in the Shopware backend. + +Composer knows where to install the plugin to because of the `type` defined in the `composer.json` of this plugin. For +a complete list of available types, see [Composer Installers](https://github.com/composer/installers). + +## Project specific plugins + +The path `custom/plugins` and all the old plugin directories were added to the `.gitignore` file to prevent plugins +required via Composer from being version controlled in the new project repository as well. We recommend to use Github +or some on-premise Git hosting solution to share plugins you use in more than one Shopware project between those shops. +You can then require those plugins in any of your shops using a Composer command like the one above. + +In case you want to create a project specific plugin which doesn't need to be shared, you can install it into the +`custom/project` directory. This directory is equivalent to `custom/plugins`, the only difference is that plugins added +here are version controlled together with the project. There is no equivalent alternative to the old-style plugin +directories, only 5.2 project specific plugins are supported out of the box. Of course you can modify your `.gitignore`- +file, but in that case you should add all plugins you might require via Composer into it. + +## Upgrading Shopware + +Update the version number of `shopware/shopware` in the `composer.json`, e.g. from `5.4.0` to `5.4.1` after this version +has been released: +```json + "require": { + "shopware/shopware": "5.4.1", + ... +``` +Then run `composer update shopware/shopware` to have Composer update the installed version of Shopware to the new version. +Do not forget to commit the new `composer.lock` file to your project afterwards. + +Currently every `composer update` triggers a `bin/console sw:migration:migrate` since it would be possible that +Shopware itself got updated and need a new schema version to run properly. If you want to disable this behaviour you can +remove the `post-update-cmd` hook in the `composer.json` or modify the `app/post-update.sh` according to your needs. + +
    +Though it is possible to define a more lax version constraint of a dependency in the `composer.json` (e.e.g `@stable` +to get the latest release or `^5.4` to get the latest release of the 5.4 minor version, +[see Composer documentation](https://getcomposer.org/doc/articles/versions.md) for details) it is nevertheless +recommended to define a specific, fixed version to not update by accident. +
    + +## Upgrading plugins + +### Upgrade project specific plugins + +As the code of project specific plugins (under `/custom/project/`) are part of your projects repository, these plugins +will always have the version that was committed with the version of your project you have checked out currently. + +### Upgrade required, external plugins + +If you want to upgrade (or downgrade) a plugin you required as described in `Requiring plugins` you can do it the same +way you upgrade Shopware itself: You change the version number specified in the `composer.json` and do a `composer update` +afterwards. + +If for example the plugin `shopwarelabs/swag-media-sftp` was to release a version `1.1.0` you would change this version +in the `composer.json`: +```json + "require": { + ... + "shopwarelabs/swag-media-sftp": "1.1.0", + ... +``` + +Afterwards you run `composer update shopwarelabs/swag-media-sftp` to have Composer check for the new version and commit +the updated `composer.lock` file to your version control software. + +
    +Please be aware that this only updates the source code of the plugins, it does not run any update-handlers this plugin +might contain (e.g. to upgrade some plugin-specific tables). +
    + +To let the plugin update itself, it is a good practice to run the following command to let the plugin update it's own +internal state to the new version: + +```bash +bin/console sw:plugin:update +``` diff --git a/source/developers-guide/shopware-config/index.md b/source/developers-guide/shopware-config/index.md index 5b4d0ecf6b..ca34e01ade 100644 --- a/source/developers-guide/shopware-config/index.md +++ b/source/developers-guide/shopware-config/index.md @@ -14,8 +14,8 @@ menu_order: 45 ## Introduction In this guide we will take a closer look at the configuration file `config.php`. -This file is in the root folder of a shopware installation. Normally it is generated during the -installation process and filled with your database credentials. +This file is in the root folder of a shopware installation. +Normally it is generated during the installation process and filled with your database credentials. It should look like this: ``` @@ -32,15 +32,23 @@ return [ ``` During this guide you will get to know some important options of the configuration. -For a complete list of options you can look at the `engine/Shopware/Configs/Default.php` file -which holds all possible configuration options and their default values. You only -need to specify options in your `config.php` if you want to override the defaults. -But keep in mind that most of these options should only be used for __debugging and testing__ -and should be removed for your live system. +For a complete list of options you can look at the `engine/Shopware/Configs/Default.php` file which holds all possible configuration options and their default values. +You only need to specify options in your `config.php` if you want to override the defaults. +But keep in mind that most of these options should only be used for __debugging and testing__ and should be removed for your live system. + +### Environment-specific config + +To be able to set different configs for different environments, you can place a file called `config_ENVIRONMENT.php` in the Shopware root directory. +`ENVIRONMENT` should be replaced with the environment the kernel gets initialized and defaults to `production`, e.g. `config_production.php`. +The environment-specific config file is preferred over the normal one. + +Blog post with a more advanced use case: [Configuring multiple Shopware environments](https://developers.shopware.com/blog/2016/01/26/configuring-multiple-shopware-environments/) ### Session locking -As of Shopware 5.2.13 session locking is enabled by default. This prevents unsuspected failures when concurrent ajax requests work with the same session variables. With enabled locking ajax requests are processed one after another. +As of Shopware 5.2.13 session locking is enabled by default. +This prevents unsuspected failures when concurrent ajax requests work with the same session variables. +With enabled locking ajax requests are processed one after another. ``` 'session' => [ @@ -58,9 +66,10 @@ As of Shopware 5.2.13 session locking is enabled by default. This prevents unsus ], ``` -With these options you can activate/deactivate the CSRF attack protection. By default, both options are set -to `true`. Deactivating them is for example necessary if you want to run mink tests -with behat. For more information take a look at the complete guide: [CSRF Protection](/developers-guide/csrf-protection/) +With these options you can activate/deactivate the CSRF attack protection. +By default, both options are set to `true`. +Deactivating them is for example necessary if you want to run mink tests with behat. +For more information take a look at the complete guide: [CSRF Protection](/developers-guide/csrf-protection/) ### PHP runtime settings @@ -74,8 +83,7 @@ with behat. For more information take a look at the complete guide: [CSRF Protec These PHP settings override the defaults of your `php.ini`. -`display_errors` is the only important option to change for debugging. Set this to `1` to enable the output -of low-level php errors. +`display_errors` is the only important option to change for debugging. Set this to `1` to enable the output of low-level php errors. The default value of `error_reporting` should be sufficient for developing. @@ -89,11 +97,12 @@ The default value of `error_reporting` should be sufficient for developing. ], ``` -The difference between `throwExceptions` and `showExceptions` is how an exception will be handled. +The difference between `throwExceptions` and `showException` is how an exception will be handled. The option `showException` keeps the Shopware error handler enabled, catches the PHP exception and prints the message instead of showing the generic "Oops! An error has occurred!" message. -In contrast, the option `throwExceptions` skips the Shopware error handler and outputs the pure PHP exception. This is important to understand, because some errors need to be catched by the Shopware error handler for self-healing processes e.g. CSRF Token invalidation. +In contrast, the option `throwExceptions` skips the Shopware error handler and outputs the pure PHP exception. +This is important to understand, because some errors need to be caught by the Shopware error handler for self-healing processes e.g. CSRF Token invalidation. ### Template @@ -105,7 +114,22 @@ In contrast, the option `throwExceptions` skips the Shopware error handler and o ], ``` -This option controls the smarty template caching. Normally you have to clear your cache after every change on the template, but if you set `forceCompile` to `true` your template will be compiled on every reload. This should be an essential option for every developer. Keep in mind that it does have a great impact on loading times and should never be used in production. +This option controls the smarty template caching. +Normally you have to clear your cache after every change on the template, but if you set `forceCompile` to `true` your template will be compiled on every reload. +This should be an essential option for every developer. +Keep in mind that it does have a great impact on loading times and should never be used in production. + +### Template security + +``` + 'template_security' => [ + 'php_modifiers' => ['shell_exec', 'strpos'], + 'php_functions' => ['shell_exec', 'strpos'], + ], +``` +This option is available since version 5.2.26 and controls the smarty security configuration. +Normally shopware has a whitelist of allowed php modifiers and functions for smarty template, +but if you need additional php function in your template, you can extend the whitelist by this configuration. ### Cache @@ -121,9 +145,12 @@ This option controls the smarty template caching. Normally you have to clear you ], ``` -These settings configure the caching implementation to be used inside of Shopware as well as everything necessary to set up that implementation. The `backend` option defines which cache implementation the cache should use, the available implementations can be found in `engine/Library/Zend/Cache/Backend`. +These settings configure the caching implementation to be used inside of Shopware as well as everything necessary to set up that implementation. +The `backend` option defines which cache implementation the cache should use, +the available implementations can be found in `engine/Library/Zend/Cache/Backend`. -The `backendOptions` configure the settings for the selected cache implementation. A list of available settings can be found at the `$_options` member of the main class `Zend_Cache_Backend` and the respective backend class. +The `backendOptions` configure the settings for the selected cache implementation. +A list of available settings can be found at the `$_options` member of the main class `Zend_Cache_Backend` and the respective backend class. The `frontendOptions` work similar to the `backendOptions`, you can find the available settings in the classes in `engine/Library/Zend/Cache/Frontend`. @@ -137,7 +164,90 @@ The `frontendOptions` work similar to the `backendOptions`, you can find the ava ], ``` -With these options you can set the HTTP Cache base configuration. For debugging we only take a look at the `debug` option and set it to `true`. If you want to learn more about the other options you can take a closer look on the complete guide: [HTTP cache](/developers-guide/http-cache/) +With these options you can set the HTTP Cache base configuration. +For debugging, we only take a look at the `debug` option and set it to `true`. +If you want to learn more about the other options you can take a closer look on the complete guide: [HTTP cache](/developers-guide/http-cache/) + +### Elasticsearch + +``` + 'es' => [ + 'prefix' => 'sw_shop', // set a prefix for the ES indices + 'enabled' => false, // enable ES + 'write_backlog' => true, // enable backlog + 'wait_for_status' => 'green', // wait until cluster is in the specified state + 'dynamic_mapping_enabled' => true, // define if properties should be mapped dynamically or not + 'batchsize' => 500, // set the documents batchsize + 'index_settings' => [ + 'number_of_shards' => 1, // set the number of shards + 'number_of_replicas' => 1, // set the number of replicase (e.g. 0 for development environments) + 'max_result_window' => 10000, // set the maximum number of results per window + 'mapping' => [ + 'total_fields' => [ + 'limit' => null, // set the maximum number of fields in an index + ], + ], + ], + 'backend' => [ + 'prefix' => 'sw_shop_backend_index_', // set a prefix for the ES indices + 'batchsize' => 500, // set the documents batchsize + 'write_backlog' => false, // enable backlog for the backend + 'enabled' => false, // enable ES for the backend + 'index_settings' => [ + 'number_of_shards' => 1, // set the number of shards + 'number_of_replicas' => 1, // set the number of replicase (e.g. 0 for development environments) + 'max_result_window' => 10000, // set the maximum number of results per window + 'mapping' => [ + 'total_fields' => [ + 'limit' => null, // set the maximum number of fields in an index + ], + ], + ], + ], + 'client' => [ + 'hosts' => [ + 'localhost:9200', // set the ES host + ], + ], + 'logger' => [ + 'level' => $this->Environment() !== 'production' ? Logger::DEBUG : Logger::ERROR, // set the logger level (production environments should use Logger::ERROR only!) + ], + 'max_expansions' => [ // set the max_expansions value of the phrase_prefix query .. + 'name' => 2, // .. for name + 'number' => 2, // .. for number + ], + 'debug' => false, // enable debug mode + ], +``` +With these options you can change the elasticsearch configuration. +Usually only the `enabled` and `client` options are needed to set up a runnable elasticsearch configuration. +The `max_result_window` option (since SW 5.5.2) can be useful if you're having more than 10000 products per category. +For this case you should increase the value to a bit more than the product amount of these categories. +The `max_expansions` option comes with SW 5.5.5 and allows you to change the ES expansions value of the `phrase_prefix` query for `name` and `number`. +This can be useful if you want to show more results while searching e.g. for a product number like "SW1000". +By default, only products with an up to two-digit longer number will be shown as well (e.g. SW1000XX). +Increase the value for `max_expansions` to also get products with more than a two-digit longer product number. +You can use this option to set own fields for a `phrase_prefix` query as well. +For example `'manufacturer.name' => 4` would be possible to search for products which start with the given manufacturer's name or an up to four characters longer name. +Since SW 5.7.18 it is possible to set up different indexer configuration for the storefront and the backend implementation within the `index_settings` keys. + +### Media whitelist + +In this setting, you can specify which file extensions are also permitted via a form so that they can be sent to your shop. + +``` + 'media' => [ + 'whitelist' => [], + ], +``` + +As an example, your configuration could look like this if you want that Text, CSV, Excel, Numbers, Word and Pages documents are allowed to be sent via a form. + +``` + 'media' => [ + 'whitelist' => ['txt', 'csv', 'xls', 'xlsx', 'numbers', 'csv', 'doc', 'docx', 'pages'], + ], +``` ## Example development config @@ -153,7 +263,7 @@ return [ ], 'front' => [ - 'throwException' => true, + 'throwExceptions' => true, 'showException' => true ], @@ -175,3 +285,44 @@ return [ ] ]; ``` + +## Redis configuration +With Shopware 5.3 it is possible to use [redis](https://redis.io/) as cache adapter: + +``` +'model' => [ + 'redisHost' => '127.0.0.1', + 'redisPort' => 6379, + 'redisDbIndex' => 0, + 'cacheProvider' => 'redis' +], +'cache' => [ + 'backend' => 'redis', // e.G auto, apcu, xcache + 'backendOptions' => [ + 'servers' => [ + [ + 'host' => '127.0.0.1', + 'port' => 6379, + 'dbindex' => 0, + 'redisAuth' => '' + ], + ], + ], +] +``` +Be aware, that for Zend_Cache::CLEANING_MODE_ALL the cache implementation will issue "FLUSHDB" and therefore clear the current redis db index. +For that reason, the db index for the cache should not be used for persistent data. + +## Changing MySQL Timezone +With Shopware 5.6.2 it is possible to define a custom timezone for the connection of your Shopware instance. + +``` + 'db' => [ + 'username' => 'someuser', + 'password' => 'somedb', + ... + timezone' => null, // Something like: 'UTC', 'Europe/Berlin', '-09:30', + ], + ... +``` +Please check the system info in your Shopware backend after changing this value to make sure the timezone is known to MySQL (use a relative offset if in doubt) and that there is no time difference between PHP and MySQL. diff --git a/source/developers-guide/tutorials.html b/source/developers-guide/tutorials.html index 5e665908cc..60b6ef2ee3 100644 --- a/source/developers-guide/tutorials.html +++ b/source/developers-guide/tutorials.html @@ -12,8 +12,10 @@ \ No newline at end of file + diff --git a/source/developers-guide/vagrant-phpstorm/index.md b/source/developers-guide/vagrant-phpstorm/index.md index 7f77e5c0d0..951640255d 100644 --- a/source/developers-guide/vagrant-phpstorm/index.md +++ b/source/developers-guide/vagrant-phpstorm/index.md @@ -13,7 +13,7 @@ menu_title: Vagrant and PHPStorm menu_order: 50 --- -We published a [Vagrant Setup](https://github.com/shopwareLabs/shopware-vagrant) that provides you with a basic Ubuntu 14.04 that contains everything that you needed to develop with Shopware. +We published a [Vagrant Setup](https://github.com/shopware5/shopware-vagrant) that provides you with a basic Ubuntu 16.04 that contains everything that you needed to develop with Shopware. It contains the Apache2 Web Server, MySQL Server as well as all required tools, like `ant`, `curl` and `git`. Please note that Vagrant setup does not contain a Shopware installation. The installation has to be done manually. @@ -25,7 +25,7 @@ Please note that Vagrant setup does not contain a Shopware installation. The ins Download the required software: - [VirtualBox](https://www.virtualbox.org/wiki/Downloads) - - [Vagrant](https://www.vagrantup.com/downloads) + - [Vagrant](https://www.vagrantup.com/downloads.html) Additional Information for VirtualBox setup (Windows), or you get errors like "error downloading the file" without any further explanation: - make sure VirtualBox supports 64bit Guest Systems, if only 32 bit is supported check if: @@ -38,14 +38,13 @@ The provisioning is done directly on your new virtual machine by [Ansible](http: ### 1. Clone the repository to your local machine ```bash -$ git clone https://github.com/shopwareLabs/shopware-vagrant +$ git clone https://github.com/shopware5/shopware-vagrant $ cd shopware-vagrant ``` ### 2. Boot up your Vagrant virtual machine ```bash -$ cd vagrant $ vagrant up ``` @@ -112,14 +111,14 @@ These directories will be marked red in your project and might not be visible an ### Coding Style -As mentioned in our [CONTRIBUTING.md](https://github.com/shopware/shopware/blob/5.1/CONTRIBUTING.md), you should follow the [PSR-1](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md) and [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standards. PhpStorm provides you with predefined settings in `Editor` -> `PHP` -> `CodeStyling`. +As mentioned in our [CONTRIBUTING.md](https://github.com/shopware5/shopware/blob/5.3/CONTRIBUTING.md), you should follow the [PSR-1](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md) and [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standards. PhpStorm provides you with predefined settings in `Editor` -> `PHP` -> `CodeStyling`. ### Deploy with PhpStorm -To deploy a locally installed project to the Vagrant server, you need to configure auto deployment in PhpStorm. Make sure you are using the developer version of Shopware which can be downloaded from [GitHub](https://github.com/shopware/shopware). +To deploy a locally installed project to the Vagrant server, you need to configure auto deployment in PhpStorm. Make sure you are using the developer version of Shopware which can be downloaded from [GitHub](https://github.com/shopware5/shopware). -`git clone https://github.com/shopware/shopware.git` +`git clone git@github.com:shopware5/shopware.git` #### Step 1 Open your Shopware project in PhpStorm. @@ -234,6 +233,8 @@ Download the test images and extract them into your Shopware's root directory. $ cd .. $ wget -O test_images.zip http://releases.s3.shopware.com/test_images.zip $ unzip test_images.zip +$ php bin/console sw:media:migrate +$ php bin/console sw:thumbnail:generate ``` Your Shopware installation is now complete and can be accessed at [http://192.168.33.10/shopware](http://192.168.33.10/shopware). diff --git a/source/favicon.ico b/source/favicon.ico deleted file mode 100644 index dc334d0873..0000000000 Binary files a/source/favicon.ico and /dev/null differ diff --git a/source/index.html b/source/index.html index e80749c998..1fd591dbcc 100644 --- a/source/index.html +++ b/source/index.html @@ -1,5 +1,5 @@ --- -layout: default +layout: start title: Shopware Developer Documentation generator: pagination pagination: @@ -8,33 +8,6 @@ - posts --- - - - -

    Recent blog posts

    @@ -51,35 +24,12 @@

    {% if post.meta.excerpt %} - {{ post.meta.excerpt }} + {{ post.meta.excerpt }} {% else %} - {% set summary_array = post.blocks.content|raw|striptags|split(' ')%} - {{ summary_array|slice(0,40)|join(' ') }}... + {% set summary_array = post.blocks.content|raw|striptags|split(' ')%} + {{ summary_array|slice(0,40)|join(' ') }}... {% endif %}
    {% endfor %}

    - -

    Community

    - - diff --git a/source/labs/3d-product-visualization/arc-rotate-camera.jpg b/source/labs/3d-product-visualization/arc-rotate-camera.jpg new file mode 100644 index 0000000000..4fd8b704a8 Binary files /dev/null and b/source/labs/3d-product-visualization/arc-rotate-camera.jpg differ diff --git a/source/labs/3d-product-visualization/illustration-3d-model.png b/source/labs/3d-product-visualization/illustration-3d-model.png new file mode 100644 index 0000000000..aa9e96f04b Binary files /dev/null and b/source/labs/3d-product-visualization/illustration-3d-model.png differ diff --git a/source/labs/3d-product-visualization/illustration-box.png b/source/labs/3d-product-visualization/illustration-box.png new file mode 100644 index 0000000000..b766c99f2b Binary files /dev/null and b/source/labs/3d-product-visualization/illustration-box.png differ diff --git a/source/labs/3d-product-visualization/illustration-bump-map.jpg b/source/labs/3d-product-visualization/illustration-bump-map.jpg new file mode 100644 index 0000000000..c81d094806 Binary files /dev/null and b/source/labs/3d-product-visualization/illustration-bump-map.jpg differ diff --git a/source/labs/3d-product-visualization/illustration-cylinder.png b/source/labs/3d-product-visualization/illustration-cylinder.png new file mode 100644 index 0000000000..0785c5d1e9 Binary files /dev/null and b/source/labs/3d-product-visualization/illustration-cylinder.png differ diff --git a/source/labs/3d-product-visualization/illustration-ground.png b/source/labs/3d-product-visualization/illustration-ground.png new file mode 100644 index 0000000000..1b85d37396 Binary files /dev/null and b/source/labs/3d-product-visualization/illustration-ground.png differ diff --git a/source/labs/3d-product-visualization/illustration-hemisphere-light.png b/source/labs/3d-product-visualization/illustration-hemisphere-light.png new file mode 100644 index 0000000000..93a978db63 Binary files /dev/null and b/source/labs/3d-product-visualization/illustration-hemisphere-light.png differ diff --git a/source/labs/3d-product-visualization/illustration-spot-light.png b/source/labs/3d-product-visualization/illustration-spot-light.png new file mode 100644 index 0000000000..4b0463d7b9 Binary files /dev/null and b/source/labs/3d-product-visualization/illustration-spot-light.png differ diff --git a/source/labs/3d-product-visualization/index.md b/source/labs/3d-product-visualization/index.md new file mode 100644 index 0000000000..7676bd0dcb --- /dev/null +++ b/source/labs/3d-product-visualization/index.md @@ -0,0 +1,328 @@ +--- +layout: labs +title: 3D Product visualization +github_link: labs/3d-product-visualization/index.md +shopware_version: X +indexed: true +tags: + - 3d + - backend + - frontend +group: Labs +menu_title: 3D Product visualization +menu_order: 0 +--- + +
    + +The 3D product visualization using our [SwagThreeSixty product viewer](https://github.com/shopware5/SwagThreeSixtyViewer) was one of the research topics we presented on the [SCD '17](https://en.shopware.com/news/community-day/2017/). In this document we will take a closer look at some of the aspects of it, like how to create your own 3D content as well as some useful tips & tricks on how to use the module. + +## Overview +The [SwagThreeSixty plugin](https://github.com/shopware5/SwagThreeSixtyViewer) provides you with a full-featured 3D scene editor inside your Shopware backend. The editor is built on top of [babylon.js](https://www.babylonjs.com/). Here's an overview of the features: + +- Import your own models (`*.obj`, `*.dae` & `*.babylon` are supported) + - Exports from [Unity](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/Unity%205), [Blender](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/Blender), [Maya](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/Maya/Tools), [3ds Max](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/3ds%20Max) are available + - A [command line tool](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/FBX) for converting `*.fbx` to `*.babylon` is also available +- Dynamic shadows generator +- Supports different types of lights + - Point light, Spot light & Hemisperic light +- Supports basic shapes + - Cylinder, Box & Ground Plane +- Supports different materials for basic shapes + - Diffuse, Specular & Ambient color / texture + - Bump map support +- Positioning, Scaling & Rotation of objects +- Scene Fog with different modes +- Highlight model on selection +- Debug mode for easier scene creation +- Two different anti-aliasing modes for your scene +- Different rendering modes - Wireframe, Point, Solid + +### Why have we created a 3D editor? +Before we're jumping straight into the plugin, I would like to explain why we've added a 3D editor instead of a simple upload ability for 3D models. A 3D model is just a single part of a 3D scene, next to things like the environment or the lighting, both of which are crucially important for a realistic display of your products. That is why we decided to provide a solution that seamlessly allows you to create everything necessary to present 3D representations of your products in your shop. + +A scene contains a camera which looks onto your product, you have different lights in place, for example a hemisphere light for environmental illumination and a spotlight which is giving your project the correct lighting, as well as a backdrop or even other 3D models which you're using to put your product into the right context. + +## Technical background +The editor is using [babylon.js](https://www.babylonjs.com/), a JavaScript framework for building 3D games with HTML5. WebGL, WebVR & Web Audio initially created by Microsoft. It helps you to streamline the development of 3D content in the browser. The framework itself it full-featured and includes a rich & in-depth documentation. + +### Browser support +It works on all WebGL platforms via a **specific modern shader architecture** and native touch support: + +- IE11 / MS Edge +- Google Chrome +- Mozilla Firefox +- Opera +- Safari +- iOS (iPad / iPhone) +- Android +- Windows Phone 8.1 / Mobile 10 +- Firefox OS +- Xbox One + + + + + + + + +## How to use the plugin +After you install the plugin using the installation guide in the GitHub repository you can find a new menu entry under **"Marketing"** called **"3D Product Viewer"**. + +When you create your first scene you'll notice the 3D view is empty. The reason is simple - you haven't added a camera to your scene yet. A camera is a mandatory part of the scene. It is the eye of your scene that allows you to see the 3D models. + +## Arc Rotate Camera + +In our 3D product viewer we've added a camera type, the so called **"Arc Rotate camera"** which rotates around a given pivot (e.g. your main 3D model). It can be controlled with mouse or touch events and is perfect for staging a product. + +![Arc Rotate camera illustration](arc-rotate-camera.jpg) + +*Illustration from the babylon.js documentation of the functionality of the Arc Rotate Camera* + +The camera is a little strange to use at first but it's quite easy when you're getting used to it. Basically you're controlling the radius property of the camera using the position of it. A good starting point is to set the *"Z"* value to *-15*. Please keep in mind that the value highly depends on the size of your model in the scene. If you're positioning the camera using the editor settings, the alpha and beta properties will be filled with the correct values right away, so you don't have to mess around and calculate them on your own. + +![Arc Rotate camera settings](settings-arc-camera.jpg) + +*Settings of the Arc Rotate Camera* + +Let us talk about the settings of the camera and take a deeper look at them. + +#### Zooming +The minimum and maximum zoom factors can be controlled using the *"lower radius limit"* and *"upper radius limit"*. The *"lower beta limit"* and *"upper beta limit"* control how much you can tilt the camera up and down. The setting *"Panning Sensibility"* defines the zoom speed when the user uses the mouse wheel to zoom. + +#### Auto rotating +The camera can automatically rotate around the given pivot. To do so, enable the checkbox *"Auto Rotate"*. The value of the *"Auto Rotate Speed"* defines the increment of the *"alpha"* value of the camera per frame. Please keep in mind we're aiming for 60 frames per second, so the value shouldn't be too high otherwise the camera would spin around the target like crazy. + +#### Anti-Aliasing +The camera also features anti-aliasing in 2 different intensities. The first option is *"FXAA"* (Fast Approximate Anti-Aliasing) which applies the smoothing as a per-pixel effect. In our internal tests the performance impact was low and we used it in almost every scene we've created. The second option is *"FSAA 4x"* (Full Scene Anti-Aliasing) with a 4x sampling of the scene. This type of anti-aliasing super-samples each full frame and renders it with four times the display resolution and then down-samples it to match the display resolution again. In our internal test we found out this type has a strong negative impact on performance and should only be used with very lightweight scenes and low poly 3D models, as otherwise the viewer would feel sluggish to the user, especially on lower end devices. + +## Lights +The next topic we are going to talk about are lights. We've added different types of lights which are having a different purpose. The following types are supported by our editor: + +- Directional light +- Point light +- Spot light +- Hemisphere light + +### Directional light + +![Directional light settings](settings-directional-light.jpg) + +*Settings of the Directional Light* + +As the name suggests this type of light is defined by a direction. It's emitted from everywhere towards a specific range. The most common types of directional light are front lighting, side lighting and back lighting. Diffused lighting occurs when light is scattered and does not seem to come from a discernible location - think of a cloudy day with dim light. + +The *"Intensity"* obviously provides you with the ability to change the intensity of the light. Usually you don't want a diffused light to blast on your product with 100%, so you'd rather dim it down to a certain degree. + +The option *"Shadows"* allows you to control if the light should be used as a source to generate dynamic shadows for your 3D objects in your scene. The shadow generator we're using to generate the dynamic shadows are using a soft shadows which lets the shadow looks better and removes hard edges. + +The *"Position"* fieldset can be used to position the light in the scene. It has no impact of the light itself and is just there to help you to organize your scene internally. + +The *"Direction"* fieldset contains the most important settings for the directional light. It lets you control the emitting direction of your light. + + +#### Colors +The *"Diffuse color"* lets you define the color of the light itself. + +On the other hand the *"Specular color"* lets you define the color produced by a light reflecting from a surface like the ground or another model in the scene. If you want to disable reflections all together you can set the color to `#000000`. + +### Point Light + +![Point light settings](settings-point-light.png) + +*Settings of the Point Light* + +A point light is a light defined by an unique point in world space. The light is emitted in every direction from this point. A good example of a point light is the sun. + +Lights are sharing the same settings most of the time, that's why I'm just covering the unique settings and point out when settings are having a different effect on your scene. + +The *"Range"* setting defines how far the light goes within your scene. This is useful when you're using a point light inside a model of a lamp for example. In this case you don't want the light to have an infinite range. + +The *"Position"* fieldset is important for this type of light. It defines the point in world space where your light will be placed. + +### Spot light + +![Spot light settings](settings-spot-light.png) + +*Settings of the Spot Light* + +A spot light is defined by a position, a direction, an angle, and an exponent. These values define a cone of light starting from the position, emitting toward the direction. + +The spot light has a bunch of settings other lights don't have. Before we're talking about the settings of the light, we should take a look what a spot light looks like to get a better understanding of the settings. + +![Spot Light illustration](illustration-spot-light.png) + +*Illustration of a Point Light* + +The *"Angle"* setting defines the size of the spotlight's conical beam in radians. + +The *"Exponent"* setting defines the speed of the decay of the light with distance. Imagine it like the lights max. distance. + +The *"Range"* setting defines the maximum range of the light before it doesn't reach an object anymore. + +The *"Direction"* fieldset is an important setting for the light. If you want your spot light to emit its light down for example you have to set the *"Y"* to *-1*. + +### Hemisphere Light + +![Hemisphere Light settings](settings-hemisphere-light.png) + +*Settings of the Hemisphere Light* + +A hemispheric light is an easy way to simulate realistic ambient environment light. It's fading the color from the diffuse color to the ground color. Please note that the hemisphere light is unable to create dynamic shadows. The light source is positioned above the scene by default, that's why you can't define the position of it. + +![Hemisphere Light illustration](illustration-hemisphere-light.png) + +*Illustration of the Hemisphere Light* + +The only unique setting for the Hemisphere Light is the property called *"Ground color"*. The hemisphere light fades the color from the diffuse color to the ground color. Imagine the Hemisphere light like a linear gradient. The diffuse color is the start color of the gradient and the ground color is the end value of the gradient. + + +## Basic geometry + +The editor provides you with 3 basic geometry elements you can use throughout your scene. These elements are provided by the framework and have a very light impact on performance. We're featuring the available elements in this section. + +The elements are sharing the same material settings. We'll cover these settings in a separate section. + +### Cylinder + +![Cylinder settings](settings-cylinder.png) + +*Settings of the Cylinder element* + +The cylinder in its simplest form is the surface formed by points at a fixed distance form a given straight line. The cylinder can also be used to create cones which is a cylinder which gets tighter either at the bottom or top of the element. + +![Cylinder illustration](illustration-cylinder.png) + +*Illustration of a Cylinder geometry* + +The setting *"Height"* defines the height of the element using a defined unit in three dimensional space. + +The *"Diameter Top"* property lets you define the width of the cylinder at the top of the element. + +The *"Diameter Bottom"* property lets you define the width of the cylinder at the bottom of the element. + +The property *"Tesselation"* is another interesting property of the cylinder. It defines how many sides should be created on the *Y*-axis. A lower value creates a hexagonal cylinder instead of a round one. + +The property *"Height Subdivs"* defines the resolution of the cylinder model. This property is mainly to optimize the performance and don't need to be changed by you in almost any case. + +### Box + +![Cylinder settings](settings-box.png) + +*Settings of the Box element* + +The box is another simple element available due to the nature of the geometry. A box is an element build out of 6 planes with a equal edge length. + +![Box illustration](illustration-box.png) + +*Illustration of a Box geometry* + +The box element just contains the setting *"Size"* which defines the edge length of the box. The value is using defined unit in three dimensional space. + +### Ground + +![Ground settings](settings-ground.png) + +*Settings of the Ground element* + +The ground / plane geometry element is the most simplest element available in the editor. It is more or less a two-dimensional surface with a defined size. + +![Ground illustration](illustration-ground.png) + +*Illustration of a Ground geometry* + +The *"Width"* property defines the size of the *X*-axis. The *"Height"* property defines the size of the ground on the *Z*-axis. It may be confusing up first cause the term "height" usually refers to the *Y*-axis, but not in this case. + +The setting *"Subdivs"* is similar to the *"Height Subdiv"* setting of the cylinder. It defines how many verticies are forming the ground plane. This is mainly for performance optimization and you don't have to change the value in most cases. + +### Basic geometry texture & color settings +The basic geometry elements are coming with a bunch of different settings you can adjust to get the perfect result for the texture. Let's take a closer look at those settings: + +#### Diffuse color / texture + +![Diffuse settings](settings-diffuse.png) + +*Diffuse color / texture settings* + +The diffuse color / texture will be displayed when a light shines on the element. You can either choose a color or a texture. When you're using a texture you can adjust the offset and the scale of the texture using the settings in the *"Texture offset & scale"* fieldset. This comes in handy when you're working with seamless textures and you want to scale it to fit the size of your object. + +#### Ambient color / texture + +![Ambient settings](settings-ambient.png) + +*Ambient color / texture settings* + +The ambient texture / color can be seen as a second level of diffuse. The produced color is multiplied to the diffuse color. This comes in handy when you want to stack textures onto each other. + +#### Specular color / texture + +![Specular settings](settings-specular.png) + +*Specular color / texture settings* + +The specular color is the color produced by a light reflecting from the surface of the element. Please note that the ambient color / texture has an unique property *"Power"* which allows you to set the intensity of the reflection. If you don't want to have any reflection on your model either set the color of the specular color to `#000000` or lower the *"Power"* value to zero. + +#### Bump map + +![Bump map settings](settings-bump.png) + +*Bump map settings* + +The bump texture simulates bumps and dents using an image texture that is also known as a normal map. There are plenty of online converters which can create normal maps based on your diffuse/color texture. Please note that the *"Texture offset & scale"* fieldset of the diffuse texture will be ignored when using a bump map. You have to adjust the values for the bump map instead. + +![Bump map illustration](illustration-bump-map.jpg) + +*Left side: Object with a diffuse texture, Right side: Object with diffuse texture & bump map* + +## Import your own 3D models +We talked a lot about basic geometry and your own 3D models in the scene but we haven't covered on how to use your own models. First of all, I would like to cover which formats can you use with our editor: + +- `*.obj`, `*.dae` & `*.babylon` are supported + - Export from [Unity](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/Unity%205), [Blender](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/Blender), [Maya](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/Maya/Tools), [3ds Max](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/3ds%20Max) are available + - A [command line tool](https://github.com/BabylonJS/Babylon.js/tree/master/Exporters/FBX) for converting `*.fbx` to `*.babylon` is also available + +### Preparing your model + +Before you can use your own 3D model on the web you have to prepare the model. First of all, we have to talk about file sizes. When you're 3D scanning your products, you're ending up with a highly detailed mesh and a file with a huge file size. Nobody want to load a 50 MB model + additional 15 MB for a texture. So you have to make sure to "scale down" the model. This technique is called "retopologizing". You're recalculating the mesh to a smaller polygon count and you have to bake the model to the new surface. Sketchfab has a great tutorial on [How to retopologize 3D scans into low poly game assets](https://blog.sketchfab.com/retopologise-3d-scans-low-poly-game-assets/) we used for our 3D scanned objects. + +When you're satisfied with the file size of your model and associated texture, we can move on with the next step before we can upload it to your web server. When you're 3D scanning objects or using photogrammetry to create models you have to align, scale and center the model otherwise you'll experience problems when trying to align it with other elements in the scene. + +In the following video I'm demonstrating how you can normalize a model in [blender](https://www.blender.org/): + + + +*Video on how to normalize a 3D model in Blender* + +### Setting up the model +![3D Model settings](settings-3d-model.png) + +*3D model settings* + +As you probably noticed in the screenshot above, the 3D model object in the editor doesn't contain a Media Manager field to select the model using the media manager. The supported 3D file are linking files inside the file to another file like textures, shaders and materials. To prevent issues with the file handling of our Media Manager we're using a absolute path for the *"Model"* setting. As an recommendation you should create a new folder inside the `files` directory to manage your models. + +To insert your model, just place the absolute path to your model in the *"Model"* field. If you haven't scaled your model down properly, you can use the fieldset *"Scaling"* to scale it accordingly. + +After you inserted the model, you unfortunately have to reload the module (i.e. close and open it up again) as we're using a download manager which manages the assets of your scene. + + +## Assets - 3D scanned shoe + +![3D Model illustration](illustration-3d-model.png) + +*Example image of the 3D scanned shoe* + +To get you a jump start for the 3D product viewer we're providing you with a free 3D scanned shoe we used for all our tests. The shoe was scanned using the [Artec Eva](https://www.artec3d.com/3d-scanner/artec-eva). + +- **Model**: Asics Gel-DS Trainer +- **File size**: 25.3 MB + 17.9 MB texture file in 4k +- **File format:** `.obj` + `.stl` +- **Verts**: 181,749 +- **Faces**: 363,512 +- **Tris**: 363,512 + + + Download 3D shoe model (23.5 MB) + diff --git a/source/labs/3d-product-visualization/settings-3d-model.png b/source/labs/3d-product-visualization/settings-3d-model.png new file mode 100644 index 0000000000..38ff7d566c Binary files /dev/null and b/source/labs/3d-product-visualization/settings-3d-model.png differ diff --git a/source/labs/3d-product-visualization/settings-ambient.png b/source/labs/3d-product-visualization/settings-ambient.png new file mode 100644 index 0000000000..f5fbe2f6ba Binary files /dev/null and b/source/labs/3d-product-visualization/settings-ambient.png differ diff --git a/source/labs/3d-product-visualization/settings-arc-camera.jpg b/source/labs/3d-product-visualization/settings-arc-camera.jpg new file mode 100644 index 0000000000..5dc2629ea7 Binary files /dev/null and b/source/labs/3d-product-visualization/settings-arc-camera.jpg differ diff --git a/source/labs/3d-product-visualization/settings-box.png b/source/labs/3d-product-visualization/settings-box.png new file mode 100644 index 0000000000..cf619297ab Binary files /dev/null and b/source/labs/3d-product-visualization/settings-box.png differ diff --git a/source/labs/3d-product-visualization/settings-bump.png b/source/labs/3d-product-visualization/settings-bump.png new file mode 100644 index 0000000000..ff212cbca3 Binary files /dev/null and b/source/labs/3d-product-visualization/settings-bump.png differ diff --git a/source/labs/3d-product-visualization/settings-cylinder.png b/source/labs/3d-product-visualization/settings-cylinder.png new file mode 100644 index 0000000000..c6afed3b4e Binary files /dev/null and b/source/labs/3d-product-visualization/settings-cylinder.png differ diff --git a/source/labs/3d-product-visualization/settings-diffuse.png b/source/labs/3d-product-visualization/settings-diffuse.png new file mode 100644 index 0000000000..bf72dce34c Binary files /dev/null and b/source/labs/3d-product-visualization/settings-diffuse.png differ diff --git a/source/labs/3d-product-visualization/settings-directional-light.jpg b/source/labs/3d-product-visualization/settings-directional-light.jpg new file mode 100644 index 0000000000..367ccd4d1e Binary files /dev/null and b/source/labs/3d-product-visualization/settings-directional-light.jpg differ diff --git a/source/labs/3d-product-visualization/settings-emissive.png b/source/labs/3d-product-visualization/settings-emissive.png new file mode 100644 index 0000000000..67525e254f Binary files /dev/null and b/source/labs/3d-product-visualization/settings-emissive.png differ diff --git a/source/labs/3d-product-visualization/settings-ground.png b/source/labs/3d-product-visualization/settings-ground.png new file mode 100644 index 0000000000..76d75979de Binary files /dev/null and b/source/labs/3d-product-visualization/settings-ground.png differ diff --git a/source/labs/3d-product-visualization/settings-hemisphere-light.png b/source/labs/3d-product-visualization/settings-hemisphere-light.png new file mode 100644 index 0000000000..3ed13ba33d Binary files /dev/null and b/source/labs/3d-product-visualization/settings-hemisphere-light.png differ diff --git a/source/labs/3d-product-visualization/settings-point-light.png b/source/labs/3d-product-visualization/settings-point-light.png new file mode 100644 index 0000000000..69432eaaf6 Binary files /dev/null and b/source/labs/3d-product-visualization/settings-point-light.png differ diff --git a/source/labs/3d-product-visualization/settings-specular.png b/source/labs/3d-product-visualization/settings-specular.png new file mode 100644 index 0000000000..1021f2962b Binary files /dev/null and b/source/labs/3d-product-visualization/settings-specular.png differ diff --git a/source/labs/3d-product-visualization/settings-spot-light.png b/source/labs/3d-product-visualization/settings-spot-light.png new file mode 100644 index 0000000000..e331182809 Binary files /dev/null and b/source/labs/3d-product-visualization/settings-spot-light.png differ diff --git a/source/labs/hello-human-fingerprint.svg b/source/labs/hello-human-fingerprint.svg new file mode 100644 index 0000000000..9c80b74ebd --- /dev/null +++ b/source/labs/hello-human-fingerprint.svg @@ -0,0 +1,151 @@ + \ No newline at end of file diff --git a/source/labs/index.html b/source/labs/index.html new file mode 100644 index 0000000000..ceb70b7c69 --- /dev/null +++ b/source/labs/index.html @@ -0,0 +1,12 @@ +--- +layout: default +title: Labs +github_link: labs/index.html +menu_title: Labs +menu_order: 40 +--- + +

    Experimental work

    + diff --git a/source/plugin-guide/index.html b/source/plugin-guide/index.html index d03d08e57d..0e5f5e6b45 100644 --- a/source/plugin-guide/index.html +++ b/source/plugin-guide/index.html @@ -9,19 +9,19 @@ menu_chapter: true --- - + \ No newline at end of file + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/account.md b/source/shopware-enterprise/b2b-suite/component-guide/account.md new file mode 100644 index 0000000000..3e764e8ce1 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/account.md @@ -0,0 +1,44 @@ +--- +layout: default +title: Account +github_link: shopware-enterprise/b2b-suite/component-guide/account.md +indexed: true +menu_title: Account Management +menu_order: 2 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- +Direct-Link to the module: `my-shop.de/registerFC/index/sValidation/H` + +### Registration + +As a b2b debtor, a special account is required to interact as partner. If you don't have an +active debtor account you can create a new one by filling out the registration form. +This account is the administrator for all data. + +### Request for Account activation + +After you have submitted the registration form the shop owner has to activate your account. +Furthermore he has to flag your new b2b account as a debtor account to grant you all necessary privileges. + + + +### Filter for B2B account + +After activating a customer as debtor or sales representative these customers can be filtered in the backend of Shopware 5.3.0 and newer. + + + +### Login + +After your account was activated you can login to your account directly from the frontend account page. +Direct Link: `my-shop.de/account` + + + +### Remove debitor + +Removing a debitor will remove all related data, including contacts, budgets, contingents, roles and the additional order data. + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/addresses.md b/source/shopware-enterprise/b2b-suite/component-guide/addresses.md new file mode 100644 index 0000000000..79364b0eaa --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/addresses.md @@ -0,0 +1,36 @@ +--- +layout: default +title: Addresses +github_link: shopware-enterprise/b2b-suite/component-guide/addresses.md +indexed: true +menu_title: Addresses +menu_order: 5 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +This component is managed in the company module, direct link to the company module: `my-shop.de/b2bcompany`. + +There are two different types of addresses: +* Billing addresses +* Shipping addresses + + + +### Add and edit an address +New addresses can be created by using the *Create address* button. +In the form you can decide between billing and shipping address. +All required fields are marked with a star. + +To edit an existing address you can click on the whole row to load the edit form. + + + +### Address Deletions +To remove addresses you can use the trash button to delete the address which is no longer required. + +### Default Addresses +The default addresses of the debtor are not showing in the company listing normally. +They have to be assigned by the debtor in the role details view. diff --git a/source/shopware-enterprise/b2b-suite/component-guide/alternative-listing-view.md b/source/shopware-enterprise/b2b-suite/component-guide/alternative-listing-view.md new file mode 100644 index 0000000000..e3c794a321 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/alternative-listing-view.md @@ -0,0 +1,24 @@ +--- +layout: default +title: Alternative Listing View +github_link: shopware-enterprise/b2b-suite/component-guide/alternative-listing-view.md +indexed: true +menu_title: Alternative Listing View +menu_order: 14 +menu_chapter: true +group: B2B-Suite +subgroup: Component Guide +--- + +The alternative listing view can be activated with the button in the left of the sorting field. This view is reduced on +the basic product information like ordernumber, name, price and quantity you want to buy. So it is possible +to add directly one or more products in your cart or to a selected order list. + +### Alternative View on Category Page + + + +### Alternative View on Search Result Page + + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/budgets.md b/source/shopware-enterprise/b2b-suite/component-guide/budgets.md new file mode 100644 index 0000000000..65e48fab4f --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/budgets.md @@ -0,0 +1,31 @@ +--- +layout: default +title: Budgets +github_link: shopware-enterprise/b2b-suite/component-guide/budgets.md +indexed: true +menu_title: Budgets +menu_order: 7 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +This component is managed in the company module, direct link to the company module: `my-shop.de/b2bcompany`. + +The budget module allows to manage your budgets. Budgets are used to define the amount users and roles can spend in a fiscal year. + +#### Feature Overview +* Create / Edit / Delete budgets +* Edit master data +* Get an overview about the remaining budget +* Send email to a responsible contact if the budget reaches a specific percentage +* Define a fiscal year + + + +Budgets define the amount of money, which a contact or role can spend. +You can set the period and start date of a fiscal year and whether +the responsible contact want to get a mail if the defined percentage is reached. + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/checkout.md b/source/shopware-enterprise/b2b-suite/component-guide/checkout.md new file mode 100644 index 0000000000..4f914f9bc0 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/checkout.md @@ -0,0 +1,52 @@ +--- +layout: default +title: Checkout +github_link: shopware-enterprise/b2b-suite/component-guide/checkout.md +indexed: true +menu_title: Checkout +menu_order: 12 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/checkout/confirm` + +The B2B-Suite adds two more functions to the checkout of Shopware: +* An order reference number +* A requested delivery date + +The fields have multiple features: +* Both fields are not affecting Shopware during the order process. +* Backend users can access these information in the order detail panel. +* Users can also add more specific information to these fields like "ES-271" or "only mondays". + + + + + +### Order Lists +If the cart is filled with an order list, the B2B-Suite will show the name of the list below every products. + +`my-shop.de/checkout/cart` + + +`my-shop.de/checkout/confirm` + + +`my-shop.de/checkout/finish` + + +### Budgets +Every b2b user, except debtors, must have a sufficient budget to place or accept an order. + +The confirm page includes a budget selection with a small overview of the selected budget. +Only sufficient budgets can be chosen. + + + +### Contingent Rules Messages +You can see all contingent rules and restrictions on the `my-shop.de/checkout/confirm` page if they are fulfilled or not + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/company.md b/source/shopware-enterprise/b2b-suite/component-guide/company.md new file mode 100644 index 0000000000..e865dc0800 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/company.md @@ -0,0 +1,29 @@ +--- +layout: default +title: Company +github_link: shopware-enterprise/b2b-suite/component-guide/company.md +indexed: true +menu_title: Company +menu_order: 3 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2bcompany` + + + +## General + +The company module allows you to manage your company and set permissions to roles and contacts. +The following entities will be managed in this module: + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/contacts.md b/source/shopware-enterprise/b2b-suite/component-guide/contacts.md new file mode 100644 index 0000000000..67b362c347 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/contacts.md @@ -0,0 +1,75 @@ +--- +layout: default +title: Contacts +github_link: shopware-enterprise/b2b-suite/component-guide/contacts.md +indexed: true +menu_title: Contacts +menu_order: 4 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +This component is managed in the company module, direct link to the company module: `my-shop.de/b2bcompany`. + +The contact module allows to manage your contacts. Contacts are allowed to login to the shop +frontend with their given credentials. Users with permissions can assign roles to the contact +which grant or revoke defined permissions. All role permissions can be extended by direct +contact permissions, but it is recommended to set the permissions via roles. + + + +#### Feature Overview +* Create / Edit / Delete contacts +* Edit master data +* Change billing address +* Set default billing address +* Change shipping address +* Set default shipping address +* Manage roles +* Manage permissions +* Manage contingents +* Manage order lists +* Manage budgets + +### Add and edit a Contact +To create a new contact you can press on the *Create contact* button. In the modal box you +have to fill in the form. All necessary fields are marked with a star. +After submitting the form you will be forwarded to the + + + +#### Change Contact Password +To send a change password mail to a contact, you have to confirm the password activation checkbox and save the data. + + + +After saving, a mail with a password activation link will be send. + +You can change the `b2bPasswordActivation` mail template in the Shopware Backend. + +Following the link in the mail, the customer could change the password of his account. + + + +### Contact details +To configure the contact settings you can press somewhere on the contact row. +In the detail window you can +* Edit master data +* Change billing address +* Change shipping address +* Manage contact visibility +* Manage role visibility +* Manage roles +* Manage permissions +* Manage contingents +* Manage Order lists +* Manage Budgets + +### Contact Deletions +To remove contacts you can use the trash button to delete the contact which is no longer required. +Since **version 3.0.5** and **version 4.2.1** of the B2B-Suite, removing the contact will also remove the shopware user data and transfer all offers and orders from the contact to the debtor. + +### Shopware Backend +The contacts are **not available** in the Backend. diff --git a/source/shopware-enterprise/b2b-suite/component-guide/contingents.md b/source/shopware-enterprise/b2b-suite/component-guide/contingents.md new file mode 100644 index 0000000000..1a0dd6ca0c --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/contingents.md @@ -0,0 +1,63 @@ +--- +layout: default +title: Contingents +github_link: shopware-enterprise/b2b-suite/component-guide/contingents.md +indexed: true +menu_title: Contingents +menu_order: 6 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +This component is managed in the company module, direct link to the company module: `my-shop.de/b2bcompany`. + +Contingents are used to limit the ability to order without clearance for specific contact accounts or whole roles. +The B2B-Suite defaults to disallow all direct orders and put them into the clearance process. + +A contingent group is a container for a specific rule set. + + + +### Add contingent groups + +In the contingent module you can create new contingent groups by clicking on the "Create contingent group" button. +After submitting the form you will be forwarded to the contingent detail page. + + + +### Add contingent rules + +After creating the contingent group you can define corresponding rules. You have to set *Contingent rules* in Order to allow contacts to direct order or clear orders themselves. + +There are three types of rules that can be configured. + +* Order amount +* Order item quantity +* Order quantity + +The rules supports three types of restrictions: + +#### Restriction Types + +**Order amount** + +Handles the maximum order amount net per time unit. + +**Order item quantity** + +Handles the maximum quantity of products per time unit. + +**Order quantity** + +Handles the maximum allowed orders per time unit. + + + +### Add contingent restrictions + +A contingent restriction disallows orders that are otherwise whitelisted through the contingents rule set. Currently you can directly disable products from certain categories or their subcategories. Also are restrictions based on product price and product ordernumber available. + +### Contingent group deletions +To remove contingent groups you can use the trash button to delete the selected group which is no longer required. diff --git a/source/shopware-enterprise/b2b-suite/component-guide/dashboard.md b/source/shopware-enterprise/b2b-suite/component-guide/dashboard.md new file mode 100644 index 0000000000..4d0271da5c --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/dashboard.md @@ -0,0 +1,39 @@ +--- +layout: default +title: Dashboard +github_link: shopware-enterprise/b2b-suite/component-guide/dashboard.md +indexed: true +menu_title: Dashboard +menu_order: 2 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2bdashboard` + +The dashboard offers a quick view over all B2B-Suite features. + + + +### Landingpage + +In the Shopware Backend, you can select a shopping world for every customergroup to display above the dashboard. + + + +You can select the shopping world in the Shopware Backend at the customergroup attributes. + + + +A direct declaration for the shopping worlds to debtors and contacts is possible through the tab `Debtor Landingpage` in the shopware backend customer detail view of a debtor. + + + +If the shopping worlds are not directly assigned the contacts inherit the shopping worlds from their debtor and the debtor inherit the shopping worlds from the customergroup. + +### Contingent Rules +You can see all rules and restrictions for the specific contact on the dashboard. + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/demo-environment.md b/source/shopware-enterprise/b2b-suite/component-guide/demo-environment.md new file mode 100644 index 0000000000..9f75a6f1cd --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/demo-environment.md @@ -0,0 +1,111 @@ +--- +layout: default +title: Demo Environment +github_link: shopware-enterprise/b2b-suite/component-guide/demo-environment.md +indexed: true +menu_title: Demo Environment +menu_order: 1 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +
    + +## Description + +In our demo environment, we have several demo accounts to test the B2B-Suite. In the following tables all described users +have the password "shopware". The debtor is the only user which have up to ten entries, so there is no pagination in the frontend. +
    contact1@example.com is the user with all possible rights +
    contact9@example.com is an inactive contact with no rights. + + +Debtor | Contact | Roles | Contingent-Groups +---|---|---|--- +debtor@example.com | contact1@example.com | 10.000 € each month | 10 orders each month + | contact2@example.com | 1/2 Billing Addresses | 10.000 € each month + | contact3@example.com | All Billing Addresses | 2 items, 2 orders and 2.000 € per day + | contact4@example.com | All Shipping Addresses | + | contact5@example.com | All listings, details, updates and assignments | + | contact6@example.com | All routes | + | contact7@example.com | All listings, details and updates | + | contact8@example.com | All listings and detail routes | + | contact9@example.com | All listing routes | + +## contact1@example.com +### Roles +* All Billing Addresses +* All Shipping Addresses +* All routes +* 10.000 € each month +### Orders +* 3 Orders with Status open, Clearance open and Clearance denied + +## contact2@example.com +### Roles +* All listing and detail routes +* 1/2 Billing Addresses +### Shipping-Address +* Musterstr. 2 +### Contingent-Group +* 2 items, 2 orders and 2.000 € per day + +## contact3@example.com +* no rights + + +The debtor 2 is the only user which have more than ten entries, so there is a pagination in the frontend. +
    contact11@example.com is the user with all possible rights +
    contact21@example.com is an inactive contact without any permissions. + +Debtor | Contact | Roles | Contingent-Groups +---|---|---|--- +debtor@example.com | contact11@example.com | All Day Contingents | 15.000 € | 15.000 € per quarter + | contact12@example.com | 1/2 Billing Addresses | 15 orders | 15 orders per quarter + | contact13@example.com | 1/2 Shipping Addresses | 15 items | 15 items per quarter + | contact14@example.com | All Billing Addresses | 5.000 € | 5.000 € per week + | contact15@example.com | All Shipping Addresses | 5 orders | 5 orders per week + | contact16@example.com | All routes | 5 items | 5 items per week + | contact17@example.com | All listings, details and updates | 1337 € | 1337 € per day + | contact18@example.com | All listings and detail routes | 2 orders per day | 2 orders per day + | contact19@example.com | All listing routes | 2 items | 2 items per day + | contact20@example.com | All Month Contingents | 10.000 € each month | + | contact21@example.com | All Quarter Contingents | 2 items, 2 orders and 2.000 € per day | + | | All listings, details, updates and assigns | | + +## contact11@example.com +### Roles +* All Billing Addresses +* All Shipping Addresses +* All Routes +### Contingent-Group +* 15.000 € per quarter +* 15 orders per quarter +* 15 items per quarter + +## contact12@example.com +### Roles +* All listing and detail routes +* 1/2 Billing Addresses +* 1/2 Shipping Addresses +### Contingent-Group +* 1337 € per day +* 2 orders per day +* 2 items per day + + +## contact13@example.com +### Roles +* All listing and detail routes +v1/2 Billing Addresses +* 1/2 Shipping Addresses +* All Quarter Contingents +* All Day Contingents +### Contingent-Group +* 1337 € per day +* 2 orders per day +* 2 items per day +* 15.000 € per quarter +* 15 orders per quarter +* 15 items per quarter diff --git a/source/shopware-enterprise/b2b-suite/component-guide/fastorder.md b/source/shopware-enterprise/b2b-suite/component-guide/fastorder.md new file mode 100644 index 0000000000..133e46994d --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/fastorder.md @@ -0,0 +1,29 @@ +--- +layout: default +title: Fast Order +github_link: shopware-enterprise/b2b-suite/component-guide/fastorder.md +indexed: true +menu_title: Fast Order +menu_order: 11 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +
    +You can download an example file here. +
    + +Direct-Link to the module: `my-shop.de/b2bfastorder` + +The fast order module allows to create an order or order list from *.csv, *.xls or *.xlsx files. + + + +#### Feature Overview +* Import *.csv, *.xls or *.xlsx file +* configuration of column to use (first column starts with 0), csv delimiter, csv enclosure and used headline +* overview of imported products +* fast order grid to add products by ordernumber to the basket +* autocompletion for product ordernumbers (deactivates autocompletion for given input field) diff --git a/source/shopware-enterprise/b2b-suite/component-guide/index.md b/source/shopware-enterprise/b2b-suite/component-guide/index.md new file mode 100644 index 0000000000..7c092d018e --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/index.md @@ -0,0 +1,36 @@ +--- +layout: default +title: Component Guide +github_link: shopware-enterprise/b2b-suite/component-guide/index.md +indexed: true +tags: [b2b, guide, start, begin] +menu_title: Component Guide +subgroup: B2B-Suite +menu_order: 2 +group: Shopware Enterprise +--- + + + +## General + +The B2B-Suite has different use cases for various types of users. +In the following document you can find several links for workflows. + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/offer.md b/source/shopware-enterprise/b2b-suite/component-guide/offer.md new file mode 100644 index 0000000000..658a79fe8d --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/offer.md @@ -0,0 +1,140 @@ +--- +layout: default +title: Offer +github_link: shopware-enterprise/b2b-suite/component-guide/Offer.md +indexed: true +menu_title: Offer +menu_order: 18 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2boffer` + +The B2B-Suite introduces the possibility to create offers. +With these your customers have the chance to ask for +* special discounts +* special prices +* free products + +As an example, you can grant a customer a discount of 500€ for an order or you sell your 1000 products for a price of 5€ per product instead of 6€. + +### Feature Overview +* Ask for an offer +* Get an overview of all your offers +* Edit / delete offers +* Create an order out of an offer +* Get an overview of offers from the admin view +* See the changes of an offer +* Get notified about changes + +## Ask for an offer +As a B2B-Customer, you can create an offer. + +This can be done through converting your basket into an offer from the checkout process. +Notice that you don't have to check the terms and conditions checkbox. + + + +After having taken these actions, you will be redirected to an overview of your new offer. + + + +There you can: +* Go back to cart +* Add / delete / edit products +* Add / delete / Edit discount prices +* Add / delete / edit a discount + +After processing your offer request you can send it to an admin user. + +## Get an overview of all your offers +You can access an overview of all offer requests. + + + +They can be searched and sorted by different criteria. + +Clicking on the row or detail button opens the detail view. + +## Detail view +The offer detail view provides three different views. + * An overview which shows the most important information. These are the original prices, the discount prices, the state and event dates. + * A grid view of all items. There you can edit, delete and add items to the offer + * A change view which provides a changelog of all actions done in the context of this offer e.g. adding items or comments. + + + +## Backend overview +Also, an additional view is added for the Shopware backend. + +It can be accessed through the new menu item in the customers index tab. + + + +This view shows a list of all offers which have to be processed by the admin. + + + +## Backend detail view +With the backend overview the admin can access the detail view of all offer requests. + + + +The admin can modify the positions or the total discount of an offer to create an counter-offer. +After the modification from the admin, the status will be automatically set to admin declined. +This can be done by changing the offer and sending it back. + +But there are also actions which can be performed all the time. +* Setting an expiration date for an offer +* Comment the offer to communicate with the customer + +## Create an Order +After an order has been accepted by the customer and the admin it can be converted to an order. + +view of the accepted offer request. This can be done with a button, which appears in the detail overview of the accepted offer request. + + + +It redirects you to the checkout page. + +You can still change the quantity to a value higher than the offer value and still add items to the cart. + +Here you can order it or create an order clearance if necessary. + +After creating the order it will be shown in the order overview. +Also the changelog of the offer will be displayed in the the order comment history. + +## See the changes of your offer + +This overview mainly serves the purpose to show you the current state and the history of your offer. +You will see if the offer already got approved, declined, or even sent. +Furthermore, all other changes will be shown there like changes of the discount, added items, removed items and price changes. + +You can access it from the history in the offer detail overview. + +There you can also find a button for creating comments. + + + + + + + +The same functionality can be accessed from the backend. + +The site can be accessed from the history tab of the offer detail view. + + + + + +## Get notified about changes + +Every time the status of an offer (associated with you) changes you will get notified by an email. + +Beeing a backend user you can choose to receive emails or not by marking the corresponding checkbox in the user administration. + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/order-list.md b/source/shopware-enterprise/b2b-suite/component-guide/order-list.md new file mode 100644 index 0000000000..7bf65d6895 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/order-list.md @@ -0,0 +1,79 @@ +--- +layout: default +title: Order List +github_link: shopware-enterprise/b2b-suite/component-guide/order-list.md +indexed: true +menu_title: Order List +menu_order: 9 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2borderlist` + +
    + +### Order List Overview +A list with all available order lists. +A user can delete, copy or add the items to the basket for every order list. + + + +### Create Order List +With a click on the `create order list` Button a user can create a new order list. +To create a list, a name is required. + + + +### Edit Order List +A click on the row or the edit button opens an Order List. + +### Master Data +After clicking, a modal window opens with the name of the list. +A User can always change the name of the order list. + + + +### Positions +All positions are listed in the modal window. + + + +With the arrow buttons, you can sort the order list positions. +Deleting is done by clicking the delete button in the row. +Through editing, a user can change a position´s quantity and write a comment. + + + +### Add Products +A user can add a position by clicking the add item Button. +After clicking the modal window ask the two **required values** `ordernumber` and `quantity`. +The `ordernumber` mus be a **valid** item order number from a normal item (`mode = 0`) in the shop. +The comment is an **optional** value. + + + +### Add Products from Detail Page +Optionally a user can add products to a specific order list on the product detail page. + + + + +If the product is already on the list, an error message will be shown. + + + +### Add Products from Cart +On the confirm `my-shop.de/checkout/confirm` and cart page `my-shop.de/checkout/cart`, a user can add all normal products (`modus = 0`), to a specific order list. + + + +The user can also create a new order list from the cart, by selecting **create new order list**. + + + +Every product position from a cart can also be particular added to an order list. + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/order.md b/source/shopware-enterprise/b2b-suite/component-guide/order.md new file mode 100644 index 0000000000..612c481e2b --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/order.md @@ -0,0 +1,157 @@ +--- +layout: default +title: Order +github_link: shopware-enterprise/b2b-suite/component-guide/order.md +indexed: true +menu_title: Order +menu_order: 8 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2border` + +
    + +## Order Overview + + + +The Order overview shows two distinct grids. +The first one is a list of all orders from your account and other accounts with fitting visibility rights. The second one is a list of all orders you are allowed to clear. + +### Order History + +If you are logged in as a Contact you will see all your personal orders, if the account is a Debtor you will see all orders triggered by your whole organization. +This overview mainly serves the purpose to show you th current state of past orders. You will see if the already got approved, declined, or even send. +From here you can: + +* Open the order details and see the whole history of the order. +* Create an order list for later uses. + +### Run into order clearance + +First of all you have to place a temporary order which is out of your allowed contingent rules or budgets. +In the confirm step instead of direct order placement you can only choose "request clearance". + +### Accept and Decline orders +Users with fitting contingent rules can now accept or decline the order which was placed before. +To accept the requested order you can use the check button. During the +accept process, the temporary order will be changed to a normal order. The shop administrator +is now able to deliver the placed order. + +If you want to decline the order there is a cross button which change the temporary order to a permanent +declined order. + + + +#### Decline +Triggering the decline button will open a modal box, where you can optionally add a comment. +This comment will be available to the user who started the order in the first place. + + + +#### Accept +Triggering the accept button, you will be redirected to the confirm page and have a cart filled with the items of the temporary order. +You can place the order, ask for an offer or cancel the clearance process with the `cancel Clearance` button. + + + +### Create Order List +With the order list create permission an order list can be created through every order by clicking the create order list button. +The detail window of the order list will open and the data of the order list can be changed. + + + +### Edit Order +A click on the row or the edit button opens an Order. + + + +### Master Data +After clicking, a modal window opens with all relevant data from the order. + +The order `reference number` and `requested delivery date` is editable for temporary orders. +The user can can always change the comment. + + + + + +If the user is placing the order with products from an order list, a short info will be shown. + + + +### Positions +In temporary orders, users can add, delete or modify the normal products (`mode = 0`) with the buttons. + + + +The button Add Line Item triggers the process to add an Product with a specific quantity and comment to the order clearance. + +Deleting is done by clicking the delete button in the row. +Through editing, a user can change a position´s quantity and write a comment. + + + +Positions from permanent orders cannot be changed. + + + +If the user is placing the order with products from an order list, a short info will be shown. + + + +### History +Every change from the order is logged and listed in the history. + + + +### Order reference number and requested delivery date in order confirmation mail +The B2B-Suite enriches orders with additional order data. +For example, it is possible to define a custom order reference number and requesting a delivery date in the checkout. +These data are also possible to view in the order confirmation mail. + +#### B2B-Suite for Shopware 5 +In this version, the mail confirmation template will be extended automatically. +If you use a custom mail template, you should check your template, because the structure could slip after running the migration, which extends the template. + +##### How to check and modify your template +Hover over the settings tab in the backend and click on "mail templates" at "mail management" to get a new window with all mail templates. +Then you click on "system mails" and select the "sOrder" template. +There you can modify the plain and HTML content to change the order or structure of your mail contents. +The B2B-Suite extension should look like: + + {if $orderReference} + Order reference number: {$orderReference} + {/if} + {if $requestedDeliveryDate} + Requested delivery date: {$requestedDeliveryDate} + {/if} + +##### How to modify your template +Click on the settings tab in the administration and choose "Email templates" at the "Shop" settings. +Then edit the mail template with the "Order confirmation" type. +There you can modify the content of your mail in plain and HTML text. + +For the B2B-Suite order template extension you have to add these snippets into your content depending on the type of text: + +###### Plain text + + {% verbatim %}{% if order.customFields.b2bOrderReferenceHolder is defined and order.customFields.b2bOrderReferenceHolder != '' %} + Order reference number: {{ order.customFields.b2bOrderReferenceHolder }} + {% endif %} + {% if order.customFields.b2bDeliveryDateHolder is defined and order.customFields.b2bDeliveryDateHolder != '' %} + Requested delivery date: {{ order.customFields.b2bDeliveryDateHolder }} + {% endif %}{% endverbatim %} + +###### HTML text + + {% verbatim %}{% if order.customFields.b2bOrderReferenceHolder is defined and order.customFields.b2bOrderReferenceHolder != '' %} + Order reference number: {{ order.customFields.b2bOrderReferenceHolder }}
    + {% endif %} + {% if order.customFields.b2bDeliveryDateHolder is defined and order.customFields.b2bDeliveryDateHolder != ''%} + Requested delivery date: {{ order.customFields.b2bDeliveryDateHolder }} + {% endif %}{% endverbatim %} diff --git a/source/shopware-enterprise/b2b-suite/component-guide/ordernumber.md b/source/shopware-enterprise/b2b-suite/component-guide/ordernumber.md new file mode 100644 index 0000000000..408c3f224c --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/ordernumber.md @@ -0,0 +1,37 @@ +--- +layout: default +title: Custom ordernumber +github_link: shopware-enterprise/b2b-suite/component-guide/ordernumber.md +indexed: true +menu_title: Custom ordernumber +menu_order: 10 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2bordernumber` + +### Custom ordernumber overview +The custom ordernumber overview is splitted into two parts. +The `custom ordernumber file upload` and the `custom ordernumber grid`. +The custom ordernumbers can be used instead of the shop ordernumbers. + +#### Custom ordernumber file upload + +The custom ordernumber upload allows to import ordernumbers from *.csv, *.xls or *.xlsx files +and **replace** them with existing. + +##### Upload feature overview +* Import *.csv, *.xls or *.xlsx file +* configuration of column to use (first column starts with 0), csv delimiter, csv enclosure and used headline + +#### Custom ordernumber grid + +The grid shows a list of all custom ordernumbers. +The empty line can be used, to add new custom ordernumbers. + +##### Feature overview +* Create / Edit / Delete custom ordernumbers +* autocompletion for product ordernumbers diff --git a/source/shopware-enterprise/b2b-suite/component-guide/payment.md b/source/shopware-enterprise/b2b-suite/component-guide/payment.md new file mode 100644 index 0000000000..88423a59d6 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/payment.md @@ -0,0 +1,37 @@ +--- +layout: default +title: Payment +github_link: shopware-enterprise/b2b-suite/component-guide/payment.md +indexed: true +menu_title: Payment +menu_order: 17 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +### Clearance Payment Method +For order clearance, a payment method with the internal name `b2b_order_clearance_payment` will be created. + + + +The payment method **has to be active** and will be **automatically selected** before placing an order for the clearance process. + +`my-shop.de/checkout/shippingPayment` + + + +`my-shop.de/checkout/confirm` + + + +### Risk Management Rule +A Shopware backend user can block a payment method for b2b or normal users with the risk management rule `B2B Account`. + +Value `1` is for blocking the payment for b2b user and the value `0` for normal user. + + + +### Payment Inheritance +Every contact inherits the debtor payment after login. You can change it for an order. After another login the contact inherits the debtor payment again. diff --git a/source/shopware-enterprise/b2b-suite/component-guide/permissions.md b/source/shopware-enterprise/b2b-suite/component-guide/permissions.md new file mode 100644 index 0000000000..a7e62bf753 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/permissions.md @@ -0,0 +1,34 @@ +--- +layout: default +title: Permissions +github_link: shopware-enterprise/b2b-suite/component-guide/permissions.md +indexed: true +menu_title: Permission Management +menu_order: 10 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2bcontact` + +The contact module also allows to define permissions for contacts. The permissions can be set by component. +Notice: The Debtor account does not experience any ACL behaviour. A Debtor is always the Superadmin. + +The default behaviour is to block all access. Which means that all access control panels are +whitelisting additional resources or actions. + +To grant privileges you have to assign a role or a permission to a contact. Important to know is, that +direct granted privileges to a contact ranked higher than role privileges. + + + +## Assignment Types +* Grant privilege +* Inherit privilege + +We decide between two types of privilege. To grant access to a specific component there is the checkbox +which is flagged by a checkmark. +The second option allows the user to grant his privilege to other users. This option ist flagged by the +forward icon. diff --git a/source/shopware-enterprise/b2b-suite/component-guide/price.md b/source/shopware-enterprise/b2b-suite/component-guide/price.md new file mode 100644 index 0000000000..cd8cee3270 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/price.md @@ -0,0 +1,35 @@ +--- +layout: default +title: Price +github_link: shopware-enterprise/b2b-suite/component-guide/price.md +indexed: true +menu_title: Price +menu_order: 13 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- +
    + This feature got replaced by the Pricing Engine within Version 3.0. +
    + +The B2B-Suite also adds individual prices for debtors which you can define over the API. +You can find them in the swagger.json. + +For example if you want to add a price to a product SW10009. + + + +First you get the id of the product from the s_articles_details table. +The id is 15 in our case. + +With the identifier you are able to fire the API call. + + + +And then you see the price update. + + + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/profile.md b/source/shopware-enterprise/b2b-suite/component-guide/profile.md new file mode 100644 index 0000000000..c126069bd7 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/profile.md @@ -0,0 +1,26 @@ +--- +layout: default +title: Profile Page +github_link: shopware-enterprise/b2b-suite/component-guide/profile.md +indexed: true +menu_title: Profile Page +menu_order: 2 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- +Direct-Link to the module: `my-shop.de/b2baccount` + +## Profile Page + +With an existing b2b account you can access your profile page by pressing on your own name. +Here you can change your account password. Additionally you have access to the order lists if your account +have the required permissions. + +### Profile Avatar + +You are able to upload your own profile picture (Square). If you don't setup a picture we use [Gravatar](http://gravatar.com) +as a fallback. If you would like to change your fallback avatar you have to define the avatar on [gravatar.com](http://gravatar.com) + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/quick_start.md b/source/shopware-enterprise/b2b-suite/component-guide/quick_start.md new file mode 100644 index 0000000000..363fef859e --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/quick_start.md @@ -0,0 +1,44 @@ +--- +layout: default +title: Quick Start +github_link: shopware-enterprise/b2b-suite/component-guide/quick_start.md +indexed: true +menu_title: Quick start +menu_order: 17 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +This document describes, how to quick start, if you have a demo package of the B2B-Suite. If you don't have +such a package yet, please contact us via [this form](https://enterprise.shopware.com/en/b2b-signup). + +## Installation +We will provide the B2B-Suite as typical Shopware plugin. In your project, you can make use of the full framework architecture, +of course. But for a quick demo, the plugin is much easier to use. + +The B2B-Suite requires the following plugins to be installed: +* [Cron](http://en.community.shopware.com/Plugin-Cron_detail_1606.html) + +To [run the cronjob](http://en.community.shopware.com/Cronjobs_detail_1103.html), it has to be configured in your system. + +You can install and activate the plugin from the [Plugin Manager in the Shopware backend](http://en.community.shopware.com/Plugin-Manager-from-Shopware-5_detail_1858.html#Installed). + +Please clear the caches afterwards and reload the backend. + +In Shopware 5.3.x or lower the B2B Suite requires always SSL or no SSL configuration. In Shopware 5.4.x the partial SSL option is no longer available. + +In Shopware 5.4.x or newer, the B2B Suite can also be installed by the composer installation of shopware with the store-plugin-installer module. More detailed information can be found [here](https://github.com/shyim/store-plugin-installer). + +## Creating a frontend user account +In order to test the B2B-Suite in the Shopware Frontend, [create a customer account first](http://en.community.shopware.com/Create_detail_1180_681.html). This can be done from the menu +`Customers->Create`. Now create a customer as usual. In order to upgrade this customer to a debtor account and grant access to the features of the +B2B-Suite, just tick the "Mark the account as debtor" box: + + + +## Using the B2B-Suite +Now visit your shop's frontend and log in as the customer you just created. After login, all B2B features as described are available. + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/roles.md b/source/shopware-enterprise/b2b-suite/component-guide/roles.md new file mode 100644 index 0000000000..2afa2f21bc --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/roles.md @@ -0,0 +1,61 @@ +--- +layout: default +title: Roles +github_link: shopware-enterprise/b2b-suite/component-guide/roles.md +indexed: true +menu_title: Roles +menu_order: 6 +menu_chapter: true +group: B2B-Suite +subgroup: Component Guide +--- + +This component is managed in the company module, direct link to the company module: `my-shop.de/b2bcompany`. + +The role module allows to define different roles for specific contact types. E.g. for different +departments. In contrast to contacts roles have the possibility to inherit from other roles. + + + +#### Feature overview +* Create / Edit / Delete roles +* Edit master data +* Manage permissions +* Change billing address +* Change shipping address +* Manage Budgets + +### Add and edit a role + +To add a role you can click on the "Create role" button which will open a new form. +After submitting the creation form you will be forwarded to the role configuration. + + + +### Role detail + +After you created the role you can access multiple settings for the created role: +* Master data +* Manage permissions +* Manage contact visibility +* Manage role visibility +* Billing addresses +* Shipping addresses +* Manage contingents +* Manage order lists +* Manage budgets + + + +### Role deletions +To remove roles you can use the trash button to delete the role which is no longer required. + +### Inheritance + + + +The permissions of a role will be implicit inherit from other roles. +E.g. (compared to the screenshot) The Role "Research & Development" inherits implicit the permissions of the roles +"IT-Administration", "Technical Engineering" and "Product Development". If the "Technical Engineering" has the +permission to edit order lists, the "Research & Development" role has no need to get these permission explicit. +It is possible to drag'n'drop a role before, after or as a child role. diff --git a/source/shopware-enterprise/b2b-suite/component-guide/sales-representative.md b/source/shopware-enterprise/b2b-suite/component-guide/sales-representative.md new file mode 100644 index 0000000000..ebf9a2ca63 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/sales-representative.md @@ -0,0 +1,44 @@ +--- +layout: default +title: Sales Representative +github_link: shopware-enterprise/b2b-suite/component-guide/sales-representative.md +indexed: true +menu_title: Sales Representative +menu_order: 15 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- +Direct-Link to the module: `my-shop.de/b2bsalesrepresentative` + +### Client overview + +After login as a sales representative, you can see an overview of all your clients. +You are not able to visit other sites of the shop, if you are not logged in as a client. +The list can be sorted or filtered. +You can log in as a client with the log in button. + + + +### Logged in as a client + +After log in as a client, a top bar will be displayed. +The name of the client is displayed on the left and the client overview link is on the right of the top bar. +You have the same rights as the client and are able to place orders or configure other things. + + + +### Backend sales representative + +Every Shopware customer can be declared as a sales representative by checking the sales representative checkbox in the customer detail module. + + + +An image for the sales representative can be selected at the customer attributes. + + + +After a reload of the customer module, the clients for the declared "sales representative" can be chosen in a new tab. + + diff --git a/source/shopware-enterprise/b2b-suite/component-guide/statistics.md b/source/shopware-enterprise/b2b-suite/component-guide/statistics.md new file mode 100644 index 0000000000..5404539343 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/component-guide/statistics.md @@ -0,0 +1,25 @@ +--- +layout: default +title: Statistics +github_link: shopware-enterprise/b2b-suite/component-guide/statistics.md +indexed: true +menu_title: Statistics +menu_order: 3 +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Component Guide +--- + +Direct-Link to the module: `my-shop.de/b2bstatistic` + +The statistics offers the following functionality: +* quick view of relevant order statistics with available filter for a more detailed view +* viewed statistics can be exported to disk as csv or xls file +* grid view of filtered orders + + + +### Grid view of selected orders + + diff --git a/source/shopware-enterprise/b2b-suite/download/index.html b/source/shopware-enterprise/b2b-suite/download/index.html new file mode 100644 index 0000000000..35efb1ab32 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/download/index.html @@ -0,0 +1,31 @@ +--- +layout: default +title: Download +github_link: b2b-suite/download/index.md +indexed: true +tags: [b2b, download] +menu_title: Download +menu_order: 5 +group: Shopware Enterprise +subgroup: B2B-Suite +--- + + + +
    + +
    + +
    +

    + Latest Development Build: +

    +
    + +
    + +
    +
    diff --git a/source/shopware-enterprise/b2b-suite/download/releases/SwagB2bPlugin_0.1.0-dev.zip b/source/shopware-enterprise/b2b-suite/download/releases/SwagB2bPlugin_0.1.0-dev.zip new file mode 100644 index 0000000000..5b3dc1f218 Binary files /dev/null and b/source/shopware-enterprise/b2b-suite/download/releases/SwagB2bPlugin_0.1.0-dev.zip differ diff --git a/source/shopware-enterprise/b2b-suite/example-plugins/index.md b/source/shopware-enterprise/b2b-suite/example-plugins/index.md new file mode 100644 index 0000000000..cef8eb5128 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/example-plugins/index.md @@ -0,0 +1,32 @@ +--- +layout: default +title: Example Plugins +github_link: b2b-suite/example-plugins/index.md +indexed: true +tags: [b2b, example plugins] +menu_title: Example Plugins +menu_order: 5 +group: Shopware Enterprise +subgroup: B2B-Suite +--- + +## General + +In our documentation, we provide several example plugins how to extend the b2b-suite. In the following you can find a list with further descriptions: + +Plugin | Description +---------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------- +[B2bAcl](/exampleplugins/B2bAcl.zip) | The most advanced plugin to show our ACL implementation incl. frontend usage. It also shows the CRUD and listing usage +[B2bAjaxPanel](/exampleplugins/B2bAjaxPanel.zip) | Small example to show our Ajax Panels and how to use them +[B2bAuditLog](/exampleplugins/B2bAuditLog.zip) | A very basic implementation of our audit log component +[B2bAuth](/exampleplugins/B2bAuth.zip) | Login as a certain user +[B2bContingentRuleItem](/exampleplugins/B2bContingentRuleItem.zip) | This plugins adds an own contingent restriction type to the default contingent system +[B2bContingents](/exampleplugins/B2bContingents.zip) | An small `CartAccessStrategy` example. Allowing orders on specific Weekdays +[B2bCustomerFrontendApi](/exampleplugins/B2bCustomerFrontendApi.zip) | An example to show how to create an end user API +[B2bLogin](/exampleplugins/B2bLogin.zip) | Exchange the E-Mail login with a staff-number login +[B2bPrice](/exampleplugins/B2bPrice.zip) | A small implementation of another debtor price data source +[B2bRestApi](/exampleplugins/B2bRestApi.zip) | Small entry level plugin to show the `RestApi` Routing +[B2bServiceExtension](/exampleplugins/B2bServiceExtension.zip) | Small entry level plugin to show how to extend a Service +[B2bSalesRepresentativePlugin](/exampleplugins/B2bSalesRepresentativePlugin.zip) | Small entry level plugin to show how to extend the sales-representative grid with customer-number and company +[B2bContingentsBudgets](/exampleplugins/B2bContingentsBudgets.zip) | Small entry level plugin to make budgets and contingents optional +[B2bThemeInheritance](/exampleplugins/B2bThemeInheritance.zip) | Small theme to show how to extend the **SwagB2bPlatform** templates diff --git a/source/shopware-enterprise/b2b-suite/index.html b/source/shopware-enterprise/b2b-suite/index.html new file mode 100755 index 0000000000..932a3601bd --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/index.html @@ -0,0 +1,56 @@ +--- +layout: default +title: B2B-Suite +github_link: shopware-enterprise/b2b-suite/index.html +indexed: true +menu_title: B2B-Suite +group: Shopware Enterprise +menu_order: 20 +menu_style: bullet +--- + + +

    B2B-Suite

    + +

    The following B2B-Suite documentation is intended for the Shopware 5 context. For Shopware 6 information, please refer to the B2B-Suite (Shopware 6) Documentation

    + +

    Installation

    + + +

    Technical Documentation

    + + +

    Component Guide

    + + +

    Example Plugins

    + diff --git a/source/shopware-enterprise/b2b-suite/installation/index.md b/source/shopware-enterprise/b2b-suite/installation/index.md new file mode 100644 index 0000000000..a19440df8c --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/installation/index.md @@ -0,0 +1,101 @@ +--- +layout: default +title: Installation Guide +github_link: b2b-suite/installation/index.md +indexed: true +tags: [b2b, installation] +menu_title: Installation Guide +menu_order: 3 +group: Shopware Enterprise +subgroup: B2B-Suite +--- + +
    + +## General + +We provide two kinds of virtualization, a vagrant and a docker based solution. Our developers use the docker containers mainly. These containers are also used in our continuous integration process. The supported functions are for both systems equal if the host systems is based on Linux. Instead of testing windows in combination with docker we evaluated the vagrant setup. + +If you want to install the B2B-Suite for production environment your system must fit with the defined requirements from the [Shopware core](https://developers.shopware.com/sysadmins-guide/system-requirements/). + +### Minimum Requirements + +The B2B Suite is based on the minimum requirements of the Shopware core. + +#### For Shopware 5 +These requirements apply from **B2B Suite 3.1.0 and above**. + +* Shopware 5.6 +* PHP 7.2 +* MySQL 5.7 +* No IE 10 support + +**Like in the Shopware 5 core PHP 7.2.20, 7.3.7, 7.4.14 and MySQL 8.0.20 - 8.0.21 are not supported.** + +## Installation on a Linux based system +### Docker (recommended) +As minimum requirement, we need a docker runtime with version 1.12.* or higher. [psh.phar](https://github.com/shopwareLabs/psh) provides the following available docker commands: + +```bash +./psh.phar docker:start # start & build containers +./psh.phar docker:ssh # ssh access web server +./psh.phar docker:ssh-mysql # ssh access mysql +./psh.phar docker:status # show running containers and network bridges +./psh.phar docker:stop # stop the containers +./psh.phar docker:destroy # clear the whole docker cache +``` + +To start the docker environment just type +```bash +./psh.phar docker:start +``` +on your command line. The several containers are booted and afterwards you can login into your web container with +```bash +./psh.phar docker:ssh +``` +After that, you can start the initialization process by typing +```bash +./psh.phar init +``` + +After a few minutes, our test environment should be available under the address [10.100.200.46](http://10.100.200.46). + +To get a full list of available commands, you can use +```bash +./psh.phar +``` + +## Installation on a OS X based system +The following commands are available to create a mac setup. Apache, MySQL and ant are +required. You can use brew package manager to install them. + +```bash +./psh.phar mac:init # build installation +./psh.phar mac:start # start apache, mysql +./psh.phar mac:stop # stop apache, mysql +./psh.phar mac:restart # restart apache, mysql +``` +You can change the database configuration in your own .psh.yaml file. +```bash +mac: + paths: + - "dev-ops/mac/actions" + const: + DB_USER: "USERNAME" + DB_PASSWORD: "PASSWORD" + DB_HOST: "DB_HOST" + SW_HOST: "SWHost" +``` +For a better explanation, use the provided .psh.yaml.dist file as an example. + +#### Common +Once the environment has booted successfully, you can use the common scripts to setup shopware + +```bash +./psh.phar clear # remove vendor components and previously set state +./psh.phar init # init composer, install plugins +./psh.phar unit # execute test suite +``` + +## Quick Start Guide +A [quick start guide](/shopware-enterprise/b2b-suite/component-guide/quick_start/#installation) for the B2B-Suite is in the component-guide. diff --git a/source/shopware-enterprise/b2b-suite/technical/acl-routes.md b/source/shopware-enterprise/b2b-suite/technical/acl-routes.md new file mode 100644 index 0000000000..49271202a7 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/acl-routes.md @@ -0,0 +1,123 @@ +--- +layout: default +title: ACL & Routing +github_link: shopware-enterprise/b2b-suite/technical/acl-routes.md +indexed: true +menu_title: ACL & Routing +menu_order: 20 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Introduction + +The ACL Routing component allows you to block Controller Actions for B2B-Users. It relies on and extends the technologies +already defined by the ACL component. To accomplish this, the component directly maps an `action` in a given `controller` +to a `resource` (= entity type) and `privilege` (= class of actions). There are two core actions you should know about. + +## Registering routes + +All routes that need access rights need to be stored in the database. The B2B-Suite provides a service too simplify this process. For the service to work correctly you need an array in a specific format. This array needs to be structured like this: + +```php +$myAclConfig = [ + 'contingentgroup' => //resource name + [ + 'B2bContingentGroup' => // controller name + [ + 'index' => 'list', // action name => privilege name + [...] + 'detail' => 'detail', + ], + ], +]; +``` + +This configuration array can then be synced to the database by using this service during installation: + +```php +Shopware\B2B\AclRoute\Framework\AclRoutingUpdateService::create() + ->addConfig($myAclConfig); +``` + +This way you can easily create and store the resources. Of course in order to show a nice frontend you need to provide snippets for translation too. The snippets get automatically created from resource and privilege names and are prefixed with `_acl_`. So the resource `contingentgroup` needs a translation named `_acl_contingentgroup`. + +## Privilege names + + The default privileges are: + +| Privilege Name | What it means | +| :-------------------- |:-------------------------------------------------------------------------------------------- | +| list | entity listing, (e.g. indexActions, gridActions) | +| detail | disabled forms, lists of assignments, but only the inspection, not the modification | +| create | creation of new entities | +| delete | removal of existing entities | +| update | updating/Changing existing entities | +| assign | changing the assignment of the entity | +| free | no restrictions | + +It is quite natural to map CRUD Actions like this. However, assignment is a little less intuitive. This should help: + +* All assignment controllers belong to the resource of the right side of the assignment (e.g. `B2BContactRole` controller is part of the `role` resource). +* All assignment listings have the detail privilege (e.g. `B2BContactRole:indexAction` is part of the `detail` privilege). +* All actions writing the assignment are part of the assign privilege (e.g. `B2BContactRole:assignAction` is part of the `assign` privilege). + +## Automatic generation + +You can autogenerate this format with the `RoutingIndexer`. This service expects a format that is automatically created by the IndexerService. This could be a part of your deployment or testing workflow. + +```php +require __DIR__ . '/../B2bContact.php'; +$indexer = new Shopware\B2B\AclRoute\Framework\RoutingIndexer(); +$indexer->generate(\Shopware_Controllers_Frontend_B2bContact::class, __DIR__ . '/my-acl-config.php'); +``` + +The generated file looks like this: + +```php +'NOT_MAPPED' => //resource name + array( + 'B2bContingentGroup' => // controller name + array( + 'index' => 'NOT_MAPPED', // action name => privilege name + [...] + 'detail' => 'NOT_MAPPED', + ), + ), +``` + +If you spot a privilege or resource that is called `NOT_MAPPED`, the action is new and you have to update the file to add the correct privilege name. + +## Template extension + +The ACL implementation is safe at the php level. Any route you have no access to will automatically be blocked but for a better user experience you should also extend the template to hide inaccessible actions. + + ```html + + ``` + +This will add a few vital css classes: + +Allowed actions: +```html + +``` +Denied actions: +```html + +``` + +The default behaviour is then just to hide the link by setting its display property to `display: none;`. + +But there are certain specials to this: + +* applied to a `form` tag it will remove the submit button and disable all form items. +* applied to a table row in the b2b default grid it will mute the applied ajax-panel action. + +## Download + A simple example plugin can be found here diff --git a/source/shopware-enterprise/b2b-suite/technical/acl.md b/source/shopware-enterprise/b2b-suite/technical/acl.md new file mode 100644 index 0000000000..a3d4484637 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/acl.md @@ -0,0 +1,427 @@ +--- +layout: default +title: Entity based ACL +github_link: shopware-enterprise/b2b-suite/technical/acl.md +indexed: true +menu_title: Entity ACL +menu_order: 19 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Introduction + +One of the core concepts of the B2B-Suite is that all entities can be restricted through ACL settings. Therefore the package contains a component named ACL which provides a common base implementation for access restriction. + +To guarantee a high level of flexibility the acl component has next to no dependencies to other parts of the framework. **At its core ACL are an implementation of a M:N relation management** in the database. They provide the means of creating the tables, storing and removing the relation and reading the information. This is implemented in a way that multiple relations (eg. user and role) can be resolved to a single `true`/`false` result or joined in a query. + +## Architecture + +In Order to understand the design decisions of the ACL component we first take a look at the different requirements imposed on ACL. As you can see in the graphic below access control is basically a concern of every technical layer of the application. + +![acl adresses](/assets/img/b2b/acl-architecture.svg) + +The base ACL component, which is described in this document provides functionality for repository filtering and service checks. The authentication component provides the context for the currently logged in user and the acl route component then provides the ability to secure routes and means of inspection for allowed routes. + +## Naming + +| Name | Description | +| ---- | ----------- | +| Context | The user or role | +| Subject | The entity that is allowed / denied | + +## Datastructure + +#### Description + +The ACL are represented as M:N relation tables in the database and always look like this: + +```sql +CREATE TABLE `b2b_acl_*` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `entity_id` INT(11) NOT NULL, + `referenced_entity_id` INT(11) NOT NULL, + `grantable` TINYINT(4) NOT NULL DEFAULT '0', + + [...] +); +``` + +| Case | Description | +|------|-------------| +| No record exists | The referenced entity is not accessible for the given context | +| A record exists | The referenced entity is accessible for the given context | +| Grantable is `1` | The context may grant access to the referenced entity for other contexts | + + +#### Address ACL example + +As an example let's take a look at the schema part that is responsible for storing the address access rights. + +![acl adresses](/assets/img/b2b/acl-address-schema.svg) + +As you can see the addresses (subject) can be allowed in two distinct contexts. Either through a *role* or through a *contact*. So in between these entities there are two acl tables holding the M:N relations. On the left you see the *ContactRole* table. This table holds the information which contact is assigned to what roles. + +This allows for a single query to select all allowed addresses of a particular user combined from role and direct assignments. + +## Usage + +#### Description + +For this part we stay at the address example. Since the ACL are directly implemented through the storage layer there is no service but just a repository for access and data manipulation. So we need an instance of `Shopware\B2B\Acl\Framework\AclRepository`. The address ACL repository can be retrieved through the DIC by the `b2b_address.acl_repository` key. + +The repository then provides the following methods. If you are already familiar with other ACL implementations most methods will look quite familiar. + +```php +namespace Shopware\B2B\Acl\Framework; + +class AclRepository +{ + /** + * @param object $context + * @param int $subjectId + * @param bool $grantable + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + */ + public function allow($context, int $subjectId, bool $grantable = false) { [...] } + + /** + * @param $context + * @param array $subjectIds + * @param bool $grantable + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + */ + public function allowAll($context, array $subjectIds, bool $grantable = false) { [...] } + + /** + * @param object $context + * @param int $subjectId + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + */ + public function deny($context, int $subjectId) { [...] } + + /** + * @param object $context + * @param array $subjectIds + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + */ + public function denyAll($context, array $subjectIds) { [...] } + + /** + * @param object $context + * @param $subjectId + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + * @return bool + */ + public function isAllowed($context, $subjectId): bool { [...] } + + /** + * @param object $context + * @param $subjectId + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + * @return bool + */ + public function isGrantable($context, $subjectId): bool { [...] } + + /** + * @param object $context + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + * @return int[] + */ + public function getAllAllowedIds($context): array { [...] } + + /** + * @param object $context + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + * @return array + */ + public function fetchAllGrantableIds($context): array { [...] } + + /** + * @param $context + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + * @return array + */ + public function fetchAllDirectlyIds($context): array { [...] } + + /** + * @param object $context + * @throws \Shopware\B2B\Acl\Framework\AclUnsupportedContextException + * @return AclQuery + */ + public function getUnionizedSqlQuery($context): AclQuery { [...] } +} +``` + +Important commonalities are: + +All methods act on a context. This context must be one of the following types: +* `Shopware\B2B\Contact\Framework\ContactEntity` +* `Shopware\B2B\StoreFrontAuthentication\Framework\Identity` +* `Shopware\B2B\StoreFrontAuthentication\Framework\OwnershipContext` +* `Shopware\B2B\Role\Framework\RoleEntity` +* `Shopware\B2B\Role\Framework\RoleAclGrantContext` +* `Shopware\B2B\Contact\Framework\ContactAclGrantContext` + +The AclGrantContext and it's accompanied AclContextProvider allows a component to use and select arbitrary ACL targets without depending on the explicit implementation. + +Depending on the provided context the methods decide whether they utilize both tables or just one. +* Reading usually utilizes both. +* Writing utilizes only the directly related table. + +If the provided context is not supported a `\Shopware\B2B\Acl\Framework\AclUnsupportedContextException` is thrown. +* Debtors for example are unknown to the ACL, so all debtor identities will trigger the exception. + +#### Modifying entity access + +A standard use case is to allow records to a user, this can be done by this simple code snippet: + +```php +$aclAdressRepository = Shopware()->Container()->get('b2b_address.acl_repository'); +$contactRepository = Shopware()->Container()->get('b2b_contact.repository'); + +$contact = $contactRepository->fetchOneById(1); + +$aclAdressRepository->allow( + $contact, // the contact + 22, // the id of the adress + true // whether the contact may grant access to other contacts +); +``` + +We can then deny tha access just by this + +```php +$aclAdressRepository->deny( + $contact, // the contact + 22, // the id of the adress +); +``` + +or just set it not grantable, by + +```php +$aclAdressRepository->allow( + $contact, // the contact + 22, // the id of the adress + false // whether the contact may grant access to other contacts +); +``` + +#### Reading entity access + +If you want to know whether a certain contact can access a entity you can call `isAllowed`. + +```php +$aclAdressRepository->isAllowed( + $contact, // the contact + 22, // the id of the adress +); +``` + +Or you just want to check whether an entity can be granted by a contact. + +```php +$aclAdressRepository->isGrantable( + $contact, // the contact + 22, // the id of the adress +); +``` + +One of the more complex problems you might face is that you want to filter a query by acl assignments (frontend listing). This can be achieved by this snippet: + +```php + +use Doctrine\DBAL\Query\QueryBuilder; +use Shopware\B2B\Acl\Framework\AclUnsupportedContextException; +use Shopware\B2B\StoreFrontAuthentication\Framework\OwnershipContext; + +protected function applyAcl(OwnershipContext $context, QueryBuilder $query) +{ + try { + $aclQuery = $this->aclRepository->getUnionizedSqlQuery($context); + + $query->innerJoin( + self::TABLE_ALIAS, + '(' . $aclQuery->sql . ')', + 'acl_query', + self::TABLE_ALIAS . '.id = acl_query.referenced_entity_id' + ); + + foreach ($aclQuery->params as $name => $value) { + $query->setParameter($name, $value); + } + } catch (AclUnsupportedContextException $e) { + // nth + } +} +``` + +The `getUnionizedSqlQuery` method returns a `Shopware\B2B\Acl\Framework\AclQuery` instance that can then be used as a join in the DBAL `QueryBuilder`. If you want to inspect the query yourself be warned: It might look a little strange due to some performance tuning for MySQL. + +## Extending the ACL + +#### Add a new Subject + +The most common use case will be that you want to extend the ACL to span around your own entity. How this is done can be observed in many places throughout the B2B Suite. So let's take a look at the addresses again. + +The first thing you need is to define a the relations from role and contact to your entity. This is achieved by creating small classes that contain the particular information: + +```php +namespace Shopware\B2B\Address\Framework; + +use Shopware\B2B\Acl\Framework\AclTable; +use Shopware\B2B\Contact\Framework\AclTableContactContextResolver; + +class AddressContactAclTable extends AclTable +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + parent::__construct( + 'contact_address', // name suffix + 'b2b_debtor_contact', // context table + 'id', // context primary key + 's_user_addresses', // subject table name + 'id' // subject primary key + ); + } + + /** + * {@inheritdoc} + */ + protected function getContextResolvers(): array + { + return [ + new AclTableContactContextResolver(), + ]; + } +} +``` +This is the implementation utilized to set up the `contact<->address` relation. In `__construct` we set up the table and relation properties. The `getContextResolver` method returns an utility class that is responsible for extracting the `id` from different context objects. See further down below for additional information on this interface. + +A identical class exists for the `role<->address` relation. + +Now we need to tell the B2B-Suite to create the necessary tables. In Shopware this must be done during the plugin installation process. Because the container is not yet set up with the B2B-Suite services, we use a static factory method in the following code: + +```php +use Shopware\B2B\Acl\Framework\AclDdlService; +use Shopware\B2B\Address\Framework\AddressContactTable; + +AclDdlService::create()->createTable(new AddressContactTable()); +``` + +Now the table exists, but we must still make the table definition accessible through the DIC, so the ACL component can set up appropriate repositories. This is achieved through a tag in the service definition: + +```xml + + + +``` + +Finally we need to register the service in the DIC. This is done by this xml snippet: + +```xml + + + s_user_addresses + +``` + +Et voilà the addresses are an acl-ified entity! + +#### Add a new context + +Since the ACL are so loosely coupled with the B2B Suite it is possible to create your own complete subset of restrictions based on other contexts then contact and role. For this you have to create a different `Shopware\B2B\Acl\Framework\AclContextResolver`. An `AclContextResolver` is responsible for extracting the primary key out of a given context object and produces a query that joins the main acl table. This is done by implementing `getQuery`, `isMainContext` and `extractId`. + +```php +class MyContextResolver extends AclContextResolver +{ + + /** + * @param string $aclTableName + * @param int $contextId + * @param QueryBuilder $queryContext + * @return AclQuery + */ + public function getQuery(string $aclTableName, int $contextId, QueryBuilder $queryContext): AclQuery + { + // your implementation here + } + + /** + * @param $context + * @return int + */ + public function extractId($context): int + { + // your implementation here + } + + /** + * @return bool + */ + public function isMainContext(): bool + { + // your implementation here + } +} +``` + +An rather generic implementation for `getQuery` that just filters for a given context id looks like this: + +```php +/** + * {@inheritdoc} + */ +public function getQuery(string $aclTableName, int $contextId, QueryBuilder $queryBuilder): AclQuery +{ + $mainPrefix = $this->getNextPrefix(); + + $queryBuilder + ->select($mainPrefix . '.*') + ->from($aclTableName, $mainPrefix) + ->where($mainPrefix . '.entity_id = :p_' . $mainPrefix) + ->setParameter('p_' . $mainPrefix, $contextId); + + return (new AclQuery())->fromQueryBuilder($queryBuilder); +} +``` + +Notice the `getMainPrefix` call. This allows the ACL component to be joined without conflicting SQL aliases. + +And a implementation of extract id usually looks like this: + +```php +public function extractId($context): int +{ + if ($context instanceof ContactIdentity) { + return $context->getId(); + } + + if ($context instanceof OwnershipContext && is_a($context->identityClassName, ContactIdentity::class, true)) { + return $context->identityId; + } + + if ($context instanceof ContactEntity && $context->id) { + return $context->id; + } + + throw new AclUnsupportedContextException(); +} +``` + +Make sure to throw a `UnsupportedContextException` if no id can be produced. + +The `isMainContext` method finally just returns true or false. Since it is possible to have more then one ContextResolver that can extract a valid id, one context resolver must be responsible for the writes, this is the flag that notifies the `AclRepository`. + +## Security + +The nature of this implementation is that you as a developer have the greatest degree of freedom in using the ACL. This of course means that you are responsible for securing the workflow yourself. The ACL component is just a collection of commonly used functions not a automatically wired security layer! The core suite secures the workflows through it's test suite, feel free to join us :). diff --git a/source/shopware-enterprise/b2b-suite/technical/advanced-cart.md b/source/shopware-enterprise/b2b-suite/technical/advanced-cart.md new file mode 100644 index 0000000000..27043be375 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/advanced-cart.md @@ -0,0 +1,25 @@ +--- +layout: default +title: Advanced Cart +github_link: shopware-enterprise/b2b-suite/technical/advanced-cart.md +indexed: true +menu_title: Advanced Cart +menu_order: 21 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +## Compatibility + +At the moment Advanced Cart won't be applied when you are logged in as a B2B-User. +This means that no items were saved in the basket when a user has logged out. + +The reason for this is that the order clearance is not compatible with Advanced Cart. +Advanced Cart adds items to the basket of a user. +But it also adds items from an order clearance after a user log out from the clearance process. + +We use the AdvancedCartSubscriber to disable the AdvancedCart feature. + diff --git a/source/shopware-enterprise/b2b-suite/technical/ajax-panel.md b/source/shopware-enterprise/b2b-suite/technical/ajax-panel.md new file mode 100644 index 0000000000..6d4f356779 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/ajax-panel.md @@ -0,0 +1,138 @@ +--- +layout: default +title: Ajax panel +github_link: shopware-enterprise/b2b-suite/technical/ajax-panel.md +indexed: true +menu_title: Ajax panel +menu_order: 8 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    +You can download a plugin showcasing the topic here. +
    + +
    + +## Description + +`AjaxPanel` is a jQuery based extension to the Shopware Frontend Framework. It mimics `iFrame` behaviour by integrating content from different controller actions through ajax into a single view and intercepting, usually page changing, events and transforming them into XHR-Requests. + +The diagram below shows how this schematically behaves: + +![image](/assets/img/b2b/ajax-panel-abstract.svg) + +## Basic usage + +The `AjaxPanel` plugin is part of the b2b frontend and will scan your page automatically for the trigger class `b2b--ajax-panel`. The most basic ajax panel looks like this: + +```html +
    + +
    +``` + +After `$(document).ready()` is triggered it will trigger a XHR GET-Request and replace it's inner html with the responses content. Now all clicks on links and form submits inside the container will be changed to XHR-Requests. A streamlined example of this behaviour can be found in the [B2BAjaxPanel Example Plugin](/exampleplugins/B2bAjaxPanel.zip), but it is used across the B2B-Suite. + +## Extended usage + +### Make links clickable + +Any HTML element can be used to trigger a location change in a ajax panel, just add a class and set a destination: + +```html + +``` + +### Ignore links + +It might be necessary that certain links in a panel really trigger the default behaviour, you just have to add a class to the link or form: + +```html +Go to Shopware Home + +
    + [...] +
    +``` + +### Link to a different panel + +One panel can influence another one by defining and linking to an id. + +```html +
    + Open in another component +``` + + +## Ajax Panel Plugins + +The B2B-Suite comes with a whole library of simple helper plugins to add behaviour the ajax panels. + +![image](/assets/img/b2b/ajax-panel-structure.svg) + +As you can see, there is the `AjaxPanelPluginLoader` responsible for initializing and reinitializing plugins inside b2b-panels. Let's take our last example and extend it with a form plugin. + +```html +
    + +
    +``` + +This will disable all form elements inside the panel during panel reload. + +While few of them add very specific behaviour to the grid or tab's views. There are also a few more commonly interesting plugins. + +### Modal + +The `b2bAjaxPanelModal` plugin helps opening ajax panel content in a modal dialog box. Let's extend our initial example: + +```html +
    + +
    +``` + +This will open the content in a modal box. + +### TriggerReload + +Sometimes change in one panel needs to trigger a reload in another panel. This might be the case if you are editing in a dialog and displaying a grid behind it. In this case you can just trigger a reload on other panel id's, just like that: + +```html +
    + +
    + +
    + +
    +``` + +Now every change in the form view will trigger a reload in the grid view. + +### TreeSelect + +This `TreeSelect` plugin allows to display a tree view with enabled drag and drop. In the view the `div`-element needs the class `is--b2b-tree-select-container` and the data attribute `data-move-url="{url action=move}"`. The controller have to implement a move action, which accepts the `roleId`, `relatedRoleId` and the `type`. + +Possible types: +* prev-sibling +* last-child +* next-sibling + diff --git a/source/shopware-enterprise/b2b-suite/technical/architecture.md b/source/shopware-enterprise/b2b-suite/technical/architecture.md new file mode 100644 index 0000000000..10b0092e8d --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/architecture.md @@ -0,0 +1,76 @@ +--- +layout: default +title: System architecture +github_link: shopware-enterprise/b2b-suite/technical/architecture.md +indexed: true +menu_title: System architecture +menu_order: 1 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Description + +The B2B-Suite is a collection of loosely coupled mostly uniform **components** packaged with a small example plugin and a common library. + +## Component layering + +A single component with all layers and the maximum of allowed dependencies looks like this: + +![image](/assets/img/b2b/architecture-component.svg) + +The responsibilities from bottom to top: + + Layer | Description + :------------ | ----------- + Shop-Bridge | Bridges the broad Shopware interfaces to the specific framework requirements
    • Implements interfaces provided by the framework
    • Subscribes to shopware events and calls framework services
      • + Framework | Contains the B2B specific Domain Requirements
        • CRUD and assignment service logic
        • The specific use cases of the component
        + REST-API | REST access to the services + Frontend | Controller as a service for frontend access + B2B-Plugin | Store front access to the services + +> Please notice: Apart from framework all other layers and dependencies are optional. + +## Component dependencies + +At the time of this writing there are 18 different components, all build with the same structure. We sorted these components into four different complexes: + +#### Common - The one Exception + +There is a small library of shared functionality. It contains a few commonly used **technical** implementations that are shared between most components like exception classes, repository helpers, a dependency manager, or a REST-API router. + +#### User-Management + +The user management is based on the `StoreFrontAuthentication` component and then provides `Contact` and `Debtor` entities which have `Address`es and `Role`s. These entities are mostly informational and CRUD based. Other parts of the system only depend on the `StoreFrontAuthentication` component but not the specific implementations as debtor or contact. + +![image](/assets/img/b2b/architecture-users.svg) + +#### ACL + +The `Acl` implementation is connected to most other entities provided by the B2B-Suite. + +![image](/assets/img/b2b/architecture-acl.svg) + +#### Order and Contingent Management + +`ContingentGroups`s are connected to `Debtor`s and can have `Acl` settings based on `Role`s or `Contact`s. `Order`s are personalized through the `StoreFrontAuthentication`. + +![image](/assets/img/b2b/architecture-order.svg) + +#### The whole picture + +Most dependencies are directly derived from requirements. So, the dependency flow of the components should follow the basic business needs. There are a few exception, mainly the M:N assignment components each representing a reset in complexity where a complex feature just resolves itself into a context object for another use case. You can think of it like that. + +* A Debtor has can be created and updated through a service **=>** _The debtor is an **entity**_ +* A Debtor may be an entity connected to many workflows by it's id **=>** _The Debtor is just the **context**_ + +So - for the sake of completeness - this is the whole picture: + +![image](/assets/img/b2b/architecture-components-complete.svg) + +Everything you should get from that is, that there is a left to right propagation of dependencies. The components on the left side can be used and even useful entirely without the components on the right side. diff --git a/source/shopware-enterprise/b2b-suite/technical/as-a-framework.md b/source/shopware-enterprise/b2b-suite/technical/as-a-framework.md new file mode 100644 index 0000000000..5904ce234e --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/as-a-framework.md @@ -0,0 +1,104 @@ +--- +layout: default +title: Use the B2B-Suite as a framework +github_link: shopware-enterprise/b2b-suite/technical/as-a-framework.md +indexed: true +menu_title: Standalone Framework +menu_order: 18 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +Although the B2B-Suite comes with an example implementation in form of the SwagB2bPlugin it is also intended to be used as a framework. Referring to the [System Architecture](/shopware-enterprise/b2b-suite/technical/architecture/) guide you can use the different layers of the components separately. + +## Installation + +In order to access the B2B-Framework you need to make the components reachable through the autoloader. The easiest way to accomplish this is to just **install the plugin without activating it**. The installation process creates all necessary database tables and includes the framework through the autoloader. You can even update the framework through this process which will guarantee that all changes to the database will automatically be migrated. + +## Requiring a Component + +The B2B-Suite comes with a custom `B2BContainerBuilder` class that is used to resolve each components dependencies in the framework. For example the [Contact-Component](/shopware-enterprise/b2b-suite/technical/architecture/#the-whole-picture) depends on StoreFrontAuthentication and ACL, which will be automatically loaded by `B2BContainerBuilder`. So the first step must be always to instantiate this class. + +```php +use Shopware\B2B\Common\B2BContainerBuilder; + +$b2bContainerBuilder = B2BContainerBuilder::create(); +``` + +Now you need to identify which component(s) you want to use. Each component layer has a directory named `DependencyInjection` that at least contains a `Configuration` suffixed PHP class. For our example we want to use the contact framework the configuration then must be located in `components/Contact/Framework/DependencyInjection/` and called `ContactFrameworkConfiguration`. All these classes can be instantiated without arguments so you can always write: + +```php +use Shopware\B2B\Common\B2BContainerBuilder; +use Shopware\B2B\Contact\DependencyInjection\ContactFrameworkConfiguration; + +$b2bContainerBuilder = B2BContainerBuilder::create(); +$contactConfiguration = new ContactFrameworkConfiguration(); +``` + +and add it to the `B2BContainerBuilder` + +```php +use Shopware\B2B\Common\B2BContainerBuilder; +use Shopware\B2B\Contact\DependencyInjection\ContactFrameworkConfiguration; + +$b2bContainerBuilder = B2BContainerBuilder::create(); +$contactConfiguration = new ContactFrameworkConfiguration(); +$b2bContainerBuilder->addConfiguration($contactConfiguration); +``` + +Now the B2BContainerBuilder knows that it has to load the contact framework and all contact framework dependencies but we still have to make this available through the Shopware container. We do this by passing the [ContainerBuilder](https://developers.shopware.com/developers-guide/plugin-system/#extended-container-configuration) to it. + +```php +use Shopware\B2B\Common\B2BContainerBuilder; +use Shopware\B2B\Contact\DependencyInjection\ContactFrameworkConfiguration; + +$b2bContainerBuilder = B2BContainerBuilder::create(); +$contactConfiguration = new ContactFrameworkConfiguration(); +$b2bContainerBuilder->addConfiguration($contactConfiguration); +$b2bContainerBuilder->registerConfigurations($shopwareContainerBuilder); +``` + +The easiest way to accomplish this is by overwriting the `build()` method in your plugin class. + + +```php +addConfiguration($contactConfiguration); + $b2bContainerBuilder->registerConfigurations($shopwareContainerBuilder); + + parent::build($shopwareContainerBuilder); + } +} +``` + +## Using the Framework + +From now on you can use the components services in your own custom B2B implementation by loading them through the Shopware container, either in your own services file (recommended) or directly. + +```php +$crudService = Shopware()->Container()->get('b2b_contact.crud_service') +``` + +```xml + + +``` + +To exemplify a little bit more what can be done with the framework please refer to our collection of [Example Plugins](/b2b-suite/example-plugins/). diff --git a/source/shopware-enterprise/b2b-suite/technical/assignment-service.md b/source/shopware-enterprise/b2b-suite/technical/assignment-service.md new file mode 100644 index 0000000000..c3482a6810 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/assignment-service.md @@ -0,0 +1,104 @@ +--- +layout: default +title: Assignment service +github_link: shopware-enterprise/b2b-suite/technical/assignment-service.md +indexed: true +menu_title: Assignment service +menu_order: 7 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
        + +## The Pattern + +A repeating pattern used throughout the B2B-Suite are Assignment Services. The B2B-Suite ships with many new entities, and therefore provides the means to connect them to each other. This is done through M:N assignments in for that purpose alone created components. + +The Diagram below shows the usually implemented objects with their outside dependencies. + +![image](/assets/img/b2b/assignment-service.svg) + + +## The Repository + +Again the repository is the exclusive access layer to the storage engine. Contrary to CRUD operations there is no object, but just plain integers (The primary keys). The default repository will have these three methods relevant for assignment: + +```php + +You can download a plugin showcasing the topic here. +
    + +
    + +## Description + +The B2B-Suite provides a general audit log which can be implemented in every component. The audit log component +can save different log types, author information like firstname, lastname and email, and provides a one to many +association index. The database structure is described in the graphic below: + +![image](/assets/img/b2b/audit_log_structure.svg) + +As you can see, the database structure is very flat. In the `b2b_audit_log` table we save a log type and a serialized +AuditLogValueEntity. All required author information is saved in the `b2b_audit_log_author` table. + +The `b2b_audit_log_index` saves all association data between an audit log and affected entities. As an example, if you + change an order position it would be nice to show this information in the main order view. + +## A simple Example +In this example we will increase the quantity of an order position. To create an audit log you can use the following +snippet: + +```php +$auditLogValue = new AuditLogValueEntity(); +$auditLogValue->comment = 'We neeed more items'; +$auditLogValue->newValue = 15; +$auditLogValue->oldValue = 10; + +$auditLog = new AuditLogEntity(); +$auditLog->logValue = $auditLogValue->toDatabaseString(); +$auditLog->logType = 'changeOrderPosition'; + +$orderReferenceIndex = new AuditLogIndexEntity(); +$orderReferenceIndex->referenceId = 10; +$orderReferenceIndex->referenceTable = 's_order'; + +$orderDetailReferenceIndex = new AuditLogIndexEntity(); +$orderDetailReferenceIndex->referenceId = 20; +$orderDetailReferenceIndex->referenceTable = 's_order_details'; + +$indexGroup = [ + $orderReferenceIndex, + $orderDetailReferenceIndex, +]; + +$this->auditLogService + ->createAuditLog($auditLog, $identity, $indexGroup); + +``` + +With the following snippet you can get all available audit logs: + +```php +$auditLogSearchStruct = new AuditLogSearchStruct(); +$auditLogs = $this->auditLogService + ->fetchList('s_order', 10, $auditLogSearchStruct); + +``` + +If you want to access only audit logs from a specific order position look at the example below: + + ```php + $auditLogSearchStruct = new AuditLogSearchStruct(); + $auditLogs = $this->auditLogService + ->fetchList('s_order_details', 20, $auditLogSearchStruct); + ``` diff --git a/source/shopware-enterprise/b2b-suite/technical/basic-conventions.md b/source/shopware-enterprise/b2b-suite/technical/basic-conventions.md new file mode 100644 index 0000000000..b0315eb148 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/basic-conventions.md @@ -0,0 +1,39 @@ +--- +layout: default +title: Basic conventions +github_link: shopware-enterprise/b2b-suite/technical/basic-conventions.md +indexed: true +menu_title: Basic conventions +menu_order: 2 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +This is the list of easy - mostly naming - conventions that the B2B-Suite complies to: + +Group | Practice +---|--- +DI Container | All container ids look like `b2b_*.*` + | The first asterisk is the component name + | The second asterisk is a class name abbreviation +Database | All table names start with `b2b_` + | All table names are in **singular** + | All field and table names are in snake case +Attributes | All attribute names start with `swag_b2b_` +Subscriber | All subscriber methods are named in accordance to their function, not to the event. +Tests | All test methods are in snake case + | All test methods start with `test_` +Controller | All controller names start with `B2b` + | View assignment should be done through the assign method eg `$this->View()->assign('foo', 'bar');` +Templates | All new layout modules are wrapped in `b2b--*` class containers + | modules reuse the template style of shopware + | CSS: 3 levels of selector depth as max + | `{block name="swag_b2b_*"}{/block}` empty blocks are in one line +JavaScript | jQuery plugins are prefixed with `b2b` + | jQuery plugins are written for the `StateManager` +Snippets | Namespace in CamelCase + | First line of every template: `{namespace name="frontend/plugins/b2b_debtor_plugin"}` + | Use snippets with english defaults `{s name="TestSnippet"} Test snippet {/s}` diff --git a/source/shopware-enterprise/b2b-suite/technical/company.md b/source/shopware-enterprise/b2b-suite/technical/company.md new file mode 100644 index 0000000000..eca3a81eae --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/company.md @@ -0,0 +1,39 @@ +--- +layout: default +title: Company +github_link: shopware-enterprise/b2b-suite/technical/company.md +indexed: true +menu_title: Company +menu_order: 19 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Introduction + +The company component acts as a container for role related entities by providing a minimalistic interface to the different components. This ensures shared functionality. The following graph shows components which are managed in this component: + +![image](/assets/img/b2b/company-management.svg) + +## The Context + +A shared context for entity creation and update is provided via the `AclGrantContext` concept. Therefore the components do not have to depend on roles directly but rather on the company context. (see [acl](/shopware-enterprise/b2b-suite/technical/acl/)) + +## Create entity + +To create a new entity (which is managed in the company component), you have to pass the parameter `grantContext` with an identifier of an `AclGrantContext`. The newly created entity is automatically assigned to the passed role. + +## Company filter + +The `CompanyFilterStruct` is used by company module to filter and search for the entities. It extends the `SearchStruct` by the `companyFilterType` and `aclGrantContext`. The correct filter type can be applied by the `CompanyFilterHelper`, possible filter types see in the list below. + +| Filter name | What it applies | +| :---------- | :------------------------------------------------------------------------- | +| acl | shows only entities which are visible to this `grantContext` | +| assignment | shows only entities assigned to this `grantContext` | +| inheritance | shows only entities which are visible to this or inherited `grantContext`s | diff --git a/source/shopware-enterprise/b2b-suite/technical/complex-views.md b/source/shopware-enterprise/b2b-suite/technical/complex-views.md new file mode 100644 index 0000000000..c0df70f046 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/complex-views.md @@ -0,0 +1,148 @@ +--- +layout: default +title: Complex views +github_link: shopware-enterprise/b2b-suite/technical/complex-views.md +indexed: true +menu_title: Complex views +menu_order: 9 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Description + +The B2B-Suite comes with a whole User Interface providing administration like features in the frontend. The structure is reflected in the naming of the several controller classes. Each controller then uses a canonical naming scheme. The example below shows the *ContactController* with all its assignment controllers. + +![image](/assets/img/b2b/contact-controller-complex-example.svg) + +As you can see, every controller is associated with one specific component. + + +## Controller structure + +The controller naming is very straightforward. It always looks like this: + +```sh +B2bContact - contact listing +├── B2bContactRole - role <-> contact assignment +├── B2bContactAddress - address <-> contact assignment +├── B2bContactContingent - contingent <-> contact assignment +├── B2bContactRoute - route <-> contact assignment +``` + +We distinguish here between *root controller* and *sub controller*. A root controller does not require parameters to be passed to it, it provides a basic page layout and CRUD actions on a single entity. Contrary a sub controller depends on a context (usually a selected id) from requests and provides auxiliary actions, like assignments, in this context. + +## Root controller + +The root controller usually looks like this: + +```php +B2B-Suite Modal Component diff --git a/source/shopware-enterprise/b2b-suite/technical/crud-service.md b/source/shopware-enterprise/b2b-suite/technical/crud-service.md new file mode 100644 index 0000000000..ef38171349 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/crud-service.md @@ -0,0 +1,357 @@ +--- +layout: default +title: CRUD service +github_link: shopware-enterprise/b2b-suite/technical/crud-services.md +indexed: true +menu_title: CRUD service +menu_order: 6 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    +You can download a plugin showcasing the topic here. +
    + +
    + +## The Pattern + +A repeating pattern used throughout the B2B-Suite are CRUD services. The B2B-Suite ships with it's own entities, and therefore provides the means to **cr**eate **u**pdate and **d**elete them. Although these entities may have special requirements, there is a exclusively used naming convention and pattern used to implement all CRUD operations. + +The Diagram below shows the usually implemented objects with their outside dependencies. + +![image](/assets/img/b2b/crud-service.svg) + +## The Entity + +There always is an entity representing the data that has to be written. Entities are uniquely identifiable storage objects, with public properties and only a few convenience functions. An example entity looks like this: + +```php +id; + } + + /** + * @return array + */ + public function toDatabaseArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 's_user_debtor_email' => $this->debtorEmail + ]; + } + + /** + * @param array $roleData + * @return CrudEntity + */ + public function fromDatabaseArray(array $roleData): CrudEntity + { + $this->id = (int) $roleData['id']; + $this->name = (string) $roleData['name']; + $this->debtorEmail = (string) $roleData['s_user_debtor_email']; + + return $this; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + foreach ($data as $key => $value) { + if (!property_exists($this, $key)) { + continue; + } + + $this->{$key} = $value; + } + } + + /** + * @return array + */ + public function toArray(): array + { + return get_object_vars($this); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } +} +``` + +The convenience interface `Shopware\B2B\Common\CrudEntity` is not required to assign context to the object. Furthermore, the definition whether an entity can be stored or retrieved from storage can only securely be determined if corresponding repository methods exist. + +## The Repository + +There always is a repository, that handles all storage and retrieval functionality. Contrary to Shopware default repositories they do not use the ORM and do not expose queries. A sample repository might look like this: + +```php +connection = $connection; + } + + /** + * @param int $id + * @return CrudEntity + * @throws \Shopware\B2B\Common\Repository\NotFoundException + */ + public function fetchOneById(int $id): CrudEntity + { + [...] + } + + /** + * @param RoleEntity $role + * @return RoleEntity + * @throws \Shopware\B2B\Common\Repository\CanNotInsertExistingRecordException + */ + public function addRole(RoleEntity $role): RoleEntity + { + [...] + } + + /** + * @param RoleEntity $role + * @return RoleEntity + * @throws \Shopware\B2B\Common\Repository\CanNotUpdateExistingRecordException + */ + public function updateRole(RoleEntity $role): RoleEntity + { + [...] + } + + /** + * @param RoleEntity $roleEntity + * @return RoleEntity + * @throws \Shopware\B2B\Common\Repository\CanNotRemoveExistingRecordException + */ + public function removeRole(RoleEntity $roleEntity): RoleEntity + { + [...] + } +} +``` + +Since it seams to be a sufficient workload for a single object to just interact with the storage layer, there is no additional validation of any sort. Everything that is solvable in PHP only is not part of this object. Notice that the exceptions are all typed and can be caught easily by the implementation code. + +## The Validation Service + +Every entity has a corresponding `ValidationService` + +```php +validationBuilder = $validationBuilder; + $this->validator = $validator; + } + + /** + * @param RoleEntity $role + * @return Validator + */ + public function createInsertValidation(RoleEntity $role): Validator + { + + [...] + + } + + /** + * @param RoleEntity $role + * @return Validator + */ + public function createUpdateValidation(RoleEntity $role): Validator + { + + [...] + + } +``` + +It provides assertions that can be evaluated by a controller and printed to the user. + +## The CRUD Service + +Services are the real entry point to an entity. They are reusable and not dependant of any specific I/O mechanism. + +They are not allowed to depend on HTTP implementations directly, and therefore provide their own request classes that contain the source independent required raw data. Notice that they are also used to initially filter a possibly larger request and they allow just the right data points to enter the service, although the contents is validated by the `ValidationService`. + +```php + + +## Introduction + +The Currency component provides the means for currency calculation in the B2B-Suite. The following graph shows components depending on this component: + +![image](/assets/img/b2b/currency-usage.svg) + + +## The Context + +The Currency component provides an additional Context object (`\Shopware\B2B\Currency\Framework\CurrencyContext`) containing a currency factor. +You can retrieve the default context which always contains the currently selected currency factor through the `\Shopware\B2B\Currency\Framework\CurrencyService`. + +```php +use Shopware\B2B\Currency\Framework\CurrencyContext; +use Shopware\B2B\Currency\Framework\CurrencyService; + +class TestController +{ + /** + * @var CurrencyService + */ + private $currencyService; + + /** + * @param CurrencyService $currencyService + */ + public function __construct( + CurrencyService $currencyService + ) { + $this->currencyService = $currencyService; + } + + /** + * @return array + */ + public function testAction(): array + { + $currencyContext = $this->currencyService + ->createCurrencyContext(); + } + +``` + +This way you can either store the currency factor with a newly provided amount or retrieve recalculated data from your repository. + +## The Entity + +All recalculatable entities must implement the interface `\Shopware\B2B\Currency\Framework\CurrencyAware`. + +```php +use Shopware\B2B\Currency\Framework\CurrencyAware; + +class MyEntity implements CurrencyAware +{ + /** + * @var float + */ + public $amount1; + + /** + * @var float + */ + public $amount2; + + /** + * @var float + */ + private $factor; + + /** + * @return float + */ + public function getCurrencyFactor(): float + { + return $this->factor; + } + + /** + * @param float $factor + * @return float + */ + public function setCurrencyFactor(float $factor) + { + $this->factor = $factor; + } + + /** + * @return string[] + */ + public function getAmountPropertyNames(): array + { + return [ + 'amount1', + 'amount2', + ]; + } +} +``` + +Which provides the means to access the currency data. + +## The Repository + +The Repository has to guarantees that every entity retrieved from storage has valid and if necessary recalculated money values. The Currency component provides `\Shopware\B2B\Currency\Framework\CurrencyCalculator` to help with this promise. +So a typical repository looks like this: + +```php + +use \Shopware\B2B\Currency\Framework\CurrencyCalculator; + +class Repository +{ + /** + * @var CurrencyCalculator + */ + private $currencyCalculator; + + /** + * [...] + * @param CurrencyCalculator $currencyCalculator + */ + public function __construct( + [...] + CurrencyCalculator $currencyCalculator $calculator + ) { + $this-currencyCalculator = $calculator; + } +} +``` + +#### Calculating in PHP (prefered) + +To recalculate an entity amount the calculator provides two convenient functions. + +`recalculateAmount` for a single entity: +```php + /** + * [...] + * @param CurrencyContext $currencyContext + * @return CurrencyAware + */ + public function fetchOneById(int $id, CurrencyContext $currencyContext): CurrencyAware + { + [...] // load entity from Database + + $this->currencyCalculator->recalculateAmount($entity, $currencyContext); + + return $entity; + } +``` + +And `recalculateAmounts` to recalculate an array of entities: + +```php + /** + * [...] + * @param CurrencyContext $currencyContext + * @return CurrencyAware[] + */ + public function fetchList([...], CurrencyContext $currencyContext): array + { + [...] // load entities from Database + + //recalculate with current amount + $this>currencyCalculator->recalculateAmounts($entities, $currencyContext); + + return $entities; + } + +``` + +#### Calculating in SQL + +Although calculation in PHP is the preferred way, it may sometimes be necessary to recalculate the amounts in SQL. This is the case if you for example use a `GROUP BY` statement and try to create a sum. For this case the Currency component creates a SQL calculation snippet. + +So if your original snippet looked like this: + + +```php + + /** + * @param int $budgetId + * @return float + */ + public function fetchAmount(int $budgetId): float + { + return (float) $this-connection->fetchColumn( + 'SELECT SUM(amount) AS sum_amount FROM b2b_budget_transaction WHERE budget_id=:budgetId', + ['budgetId' => $budgetId] + ) + } +``` + +It really should look like this: + +```php + /** + * @param int $budgetId + * @param CurrencyContext $currencyContext + * @return float + */ + public function fetchAmount(int $budgetId, CurrencyContext $currencyContext): float + { + $transactionSnippet = $this->currencyCalculator + ->getSqlCalculationPart('amount', 'currency_factor', $currencyContext); + + return (float) $this-connection->fetchColumn( + 'SELECT SUM(' . $transactionSnippet . ') AS sum_amount FROM b2b_budget_transaction WHERE budget_id=:budgetId', + ['budgetId' => $budgetId] + ) + } +``` diff --git a/source/shopware-enterprise/b2b-suite/technical/dependency-injection.md b/source/shopware-enterprise/b2b-suite/technical/dependency-injection.md new file mode 100644 index 0000000000..e376abd57e --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/dependency-injection.md @@ -0,0 +1,80 @@ +--- +layout: default +title: Dependency injection +github_link: shopware-enterprise/b2b-suite/technical/dependency-injection.md +indexed: true +menu_title: Dependency injection +menu_order: 3 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Shopware DIC + +The B2B-Suite registers with the DIC from [Shopware](https://developers.shopware.com/developers-guide/shopware-5-core-service-extensions/). Be sure you are familiar with the basic usage patterns and practices. Especially service decoration is an equally important extension point. + +## Dependency Injection Extension B2B + +The B2B-Suite provides an abstract `DependencyInjectionConfiguration` class, that is used throughout the Suite as an initializer of DI-Contents across all components. + +```php +addConfiguration(new Shopware\B2B\Contact\Framework\DependencyInjection\ContactFrameworkConfiguration()); + $containerBuilder->registerConfigurations($container); + } +} +``` + +## Tags + +Additionally the B2B-Suite makes heavy use of service tags as a more modern replacement for collect events. They are used to help you extend central B2B services with custom logic. Please take a look at the example plugins and there usage of that extension mechanism. Be sure you know [the basics](http://symfony.com/doc/current/service_container/tags.html). diff --git a/source/shopware-enterprise/b2b-suite/technical/exception.md b/source/shopware-enterprise/b2b-suite/technical/exception.md new file mode 100644 index 0000000000..be227a0769 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/exception.md @@ -0,0 +1,78 @@ +--- +layout: default +title: Exception +github_link: shopware-enterprise/b2b-suite/technical/exception.md +indexed: true +menu_title: Exception +menu_order: 16 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +## Translatable Exception + +To show the customer a translated exception message in the shopware error controller, the exception must implement the `B2BTranslatableException` Interface. + +```php +... + +class NotAllowedRecordException extends \DomainException implements B2BTranslatableException +{ + /** + * @var string + */ + private $translationMessage; + + /** + * @var array + */ + private $translationParams; + + /** + * @param string $message + * @param string $translationMessage + * @param array $translationParams + * @param int $code + * @param Throwable|null $previous + */ + public function __construct( + $message = '', + string $translationMessage = '', + array $translationParams = [], + $code = 0, + Throwable $previous = null + ) { + parent::__construct($message, $code, $previous); + + $this->translationMessage = $translationMessage; + $this->translationParams = $translationParams; + } + + /** + * {@inheritdoc} + */ + public function getTranslationMessage(): string + { + return $this->translationMessage; + } + + /** + * {@inheritdoc} + */ + public function getTranslationParams(): array + { + return $this->translationParams; + } +} +``` +The snippet key is a modified `translationMessage`. + +```php +preg_replace('([^a-zA-Z0-9]+)', '', ucwords($exception->getTranslationMessage())) +``` + +Variables in the message will be replaced by the `string_replace()` method. +The identifiers are the keys of the `translationParams` array. diff --git a/source/shopware-enterprise/b2b-suite/technical/how-to-overload-classes.md b/source/shopware-enterprise/b2b-suite/technical/how-to-overload-classes.md new file mode 100644 index 0000000000..afcdaa3213 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/how-to-overload-classes.md @@ -0,0 +1,157 @@ +--- +layout: default +title: How to overload classes +github_link: shopware-enterprise/b2b-suite/technical/how-to-overload-classes.md +indexed: true +menu_title: How to overload classes +menu_order: 14 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    +You can download a plugin showcasing the topic here. +
    + +
    + +## Description + +To add new functionality or overload existing classes to change functionality, +the B2B-Suite uses the Dependency Injection +as an extension system instead of events and hooks, which shopware uses. + +### How does a services.xml look like + +In the release package, our service.xml looks like this + +```xml + + + + + Shopware\B2B\Role\Framework\RoleRepository + [...] + + + + + + + [...] + + + [...] + + + +``` + +For development (Github) it looks like this + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +We generate the new services.xml files for our package automatically. + +### How do I use it + +The whole system works exactly like this. + +You only have to change the parameter or overload the service id. + +Your service file could look like this + +```xml + + + + + + + [...] + + +``` + +Just define a class with the same service id as our normal class and add our abstract class as the parent. +After that, add your own arguments or override ours. + +An example of your class could look like this: + +```php + +myService = array_pop($args); + + parent::__construct(... $args); + } + + public function updateRole(RoleEntity $role): RoleEntity + { + [your stuff] + } +} + +``` + +You extend the B2B class and just change any action you need. + +### What is the profit + +By building our extension system in this way, we can still add and delete constructor arguments without breaking your plugins. +Also, we don't have to add too many interfaces to the B2B-Suite. + +### What are the problems with this approach + +Since we don't know which plugin is loaded first, we can't say which class overload another one. +To prevent any random errors, you should only overload each class once. diff --git a/source/shopware-enterprise/b2b-suite/technical/index.md b/source/shopware-enterprise/b2b-suite/technical/index.md new file mode 100644 index 0000000000..235f609d57 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/index.md @@ -0,0 +1,38 @@ +--- +layout: default +title: Technical Documentation +github_link: shopware-enterprise/b2b-suite/technical/index.md +indexed: true +tags: [b2b, guide, technical, features, php] +menu_title: Technical Documentation +menu_order: 4 +menu_style: numeric +group: Shopware Enterprise +subgroup: B2B-Suite +--- + + diff --git a/source/shopware-enterprise/b2b-suite/technical/line-item-list.md b/source/shopware-enterprise/b2b-suite/technical/line-item-list.md new file mode 100644 index 0000000000..961db13078 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/line-item-list.md @@ -0,0 +1,52 @@ +--- +layout: default +title: Line Item List +github_link: shopware-enterprise/b2b-suite/technical/line-item-list.md +indexed: true +menu_title: Line Item List +menu_order: 12 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Description + +The LineItemList component is the central representation of product lists in the B2B-Suite. The main design choices are: + +* Central abstraction of product lists +* Minimal knowledge and inheritance of Shopware core services and data structures +* Persistable lists of products +* Guaranteed audit logging + +The component is used across a multitude of different child components throughout the B2B-Suite. + +![image](/assets/img/b2b/line-item-list-outer-dependencies.svg) + +The yellow colored blocks represent components, the smaller green ones are context objects that contain the component specific information. + +## Internal data structure + +The component provides LineItemList and LineItemReference as its central entities. As the name suggests a LineItemReference references line items. In most cases these line items will be products, but may include other types (eg. vouchers) that are valid purchasable items. + +To make this work with the Shopware cart, order and product listing the LineItemReferences themselves can be set up by different entities. Schematically a list that is not yet ordered looks like this: + +![image](/assets/img/b2b/line-item-list-with-listing.svg) + +Whereas an ordered list looks like this: + +![image](/assets/img/b2b/line-item-list-with-order.svg) + +As you can see, each LineItemReference borrows data from Shopware data structures, but an user of these objects can solely depend on the LineItemReference and LineItemList objects for a unified access. + +This basic pattern revolves against other data structures in the component as well. + +![image](/assets/img/b2b/line-item-list-with-order-context.svg) + +As you can see the specific data is abstracted away through the order context object. An object that can either be generated during the Shopware checkout process or be created dynamically through the API. Here the rule applies: *The B2B-Suite may store or provide ID's, without having an actual concept on what they refer to.* + +These central data containers help provide a forward compatible structure for many B2B components. diff --git a/source/shopware-enterprise/b2b-suite/technical/listing-service.md b/source/shopware-enterprise/b2b-suite/technical/listing-service.md new file mode 100644 index 0000000000..0c2a42d574 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/listing-service.md @@ -0,0 +1,168 @@ +--- +layout: default +title: Listing service +github_link: shopware-enterprise/b2b-suite/technical/listing-service.md +indexed: true +menu_title: Listing service +menu_order: 5 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    +You can download a plugin showcasing the topic here. +
    + +
    + +## The Pattern + +A repeating pattern used throughout the B2B-Suite are listing services. The B2B-Suite ships without an ORM but still has use for semi automated basic listing and filtering capabilities. To reduce the necessary duplications, there are common implementations for this. + +The diagram below shows the usually implemented objects with their outside dependencies. + +![image](/assets/img/b2b/listing-service.svg) + +## The Search Struct + +The globally used `SearchStruct` is a data container moving the requested filter, sorting and pagination data from HTTP request to the repository/query. + +```php + + +## Replaceable functions + +Almost every function in the B2B-Suite is replaceable but not all are guaranteed to be compatible to every version change. +Only the framework domain has guaranteed rules to limit the changes of each method per release version. +The methods in other domains have dependencies on the Shopware core and have to be adjusted if changes are made. + +### Protected functions in framework + +Protected functions with an `@internal` comment are **not** guaranteed to be compatible or changed to minor versions changes. + +Example: +```php +offset = $request->getParam('offset', null); + $struct->limit = $request->getParam('limit', null); + } + + [...] +} +``` + +### Public functions in framework + +Public functions are made to be compatible and not be changed until major version changes. diff --git a/source/shopware-enterprise/b2b-suite/technical/migration.md b/source/shopware-enterprise/b2b-suite/technical/migration.md new file mode 100644 index 0000000000..7a53913e75 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/migration.md @@ -0,0 +1,275 @@ +--- +layout: default +title: Migration of the hybrid plugin +github_link: shopware-enterprise/b2b-suite/technical/migration.md +indexed: true +menu_title: Migration +menu_order: 24 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +# B2B the hybrid plugin +Recently we launched the nearly feature-complete version of the B2B-Suite. +Hence, in this article, we want to take a look at the changes which were made to support Shopware 6. +Thereby we want to focus on the breaking changes and the resulting changes for plugin developers. + +## Introduction to hybrid plugins + +In the blog article "[Large Scale Plugin Architecture](https://developers.shopware.com/blog/2016/12/05/large-scale-plugin-architecture/)", my colleague Jan Philipp talked about the macro architecture of hybrid plugins like the B2B-Suite. +Sadly, we still had to introduce breaking changes to make the B2B-Suite compatible with both Shopware 6 and Shopware 5. +This is especially important for migrating an existing B2B-Suite project from Shopware 5 to Shopware 6. + +More importantly, we want to list up the different hard breaks, which were not deprecated previously. + +## The new "bridge" layer + +As described in the previously mentioned blog post, we use the dependency inversion principle to depend on an interface to wrap the data access, which is owned by our domain. + +While just working with Shopware 5, the architecure was built like shown in the image below. + +![image](/assets/img/b2b/DIP_SW5.svg) + +Because of this, we only had to duplicate our bridge layer and adapt it accordingly to Shopware 6. + +It was mostly rewriting the bridge or moving existing framework logic to the bridge layer, which is already implemented in Shopware 6. + +![image](/assets/img/b2b/DIP_sw6.svg) + + +## The frontend bridge + +Also, because of the usage of a frontend bridge, it was possible to do the same. + +![image](/assets/img/b2b/DIP_SW5_Front.svg) + +![image](/assets/img/b2b/DIP_SW6_Front.svg) + +## The Plugin + +As shown in our component guide, each components consist of multiple layers. + +![image](/assets/img/b2b/component-layers.svg) + +- Plugin +- Controllers +- Framework +- Shop bridge + +Due to the changes in the plugin system, it was not possible to reuse the plugin itself. Therefore we had to rewrite it. + +It resulted in the following layering: + +![image](/assets/img/b2b/components-layers-sw6.svg) + +## IdValue + +As seen in the previously shown graphics, we had to change multiple places to support Shopware 6, but this is not all. + +Due to the field changes of ids from `int` to `binary`, we had to change our internal logics as well. + +Unfortunately, we could not deprecate them, because it would require to deprecate all repositories as well, to be consistent. + +E.g., one of our main tables, which is used to assign contacts to debtors, is `b2b_store_front_auth` table. + +It uses a combination between `provider_key` and `provider_context`. + +Thereby the `context` shows the table and the `key` is the corresponding id. + +We had to introduce a way to use our current architecture with both id types for a hybrid plugin. + +The new `IdValue` is an abstraction of id values. + +It can represent three different values: + +``` +IntIdvalue -> Int +UUIdValue -> UUID +NullIdValue -> Null +``` + +If you would like to create a new `IdValue`, you just have to provide the `value` as a parameter to `Showpare\B2B\Common\Idvalue::create()`. + +Thereby the IdValue creates the corresponding class based on the Shopware version and the value type. + +Afterwards, you can receive the internal value via `$idValue->getValue()` and the storage value `$idVlaue->getStorageValue();` + +For example a `UUIdValue->getValue()` would return the `Hex` value of the UUID and `UUIdValue->getStorageValue()` the corresponding `binary` value. + +In contrast `IntIdValue->getValue()` and `IntIdValue->getStorageValue()` return `int` values. + +### Changes in the Repository + +So, but how does it look like in practice? + +Often we use functions like `fetchOneById(int $id, ...): Entity;` in our repositories. + +These had to chang to: + +```php +public function fetchOneById(IdValue $id, OwnershipContext $ownershipContext): ContactEntity +{ + $query = $this->connection->createQueryBuilder() + ->select('*') + ->from(self::TABLE_NAME, self::TABLE_ALIAS) + ->where(self::TABLE_ALIAS . '.id = :id') + ->setParameter('id', $id->getStorageValue()); + + $this->filterByContextOwner($ownershipContext, $query); + + $contactData = $query->execute()->fetch(PDO::FETCH_ASSOC); + + return $this->createContactByContactData($contactData, (string) $id->getValue()); +} +``` + +The most important part here is the change of the parameter `fetchOneById(IdValue $id, ...)` and that the usage of the storage value for filtering + +```php + ->setParameter('id', $id->getStorageValue()); +``` + +### In your plugins + +Since these changes are hard breaks, you have to change your code accordingly. + +Let's take a look at a controller action that used the `fetchOneById` in the past. + +The function `Shopware\B2B\Contact\Frontend\ContactCrontroller:detailAction`. + +```php +public function detailAction(Request $request): array +{ + $id = (int) $request->requireParam('id'); + $ownershipContext = $this->authenticationService->getIdentity()->getOwnershipContext(); + + return ['contact' => $this->contactRepository->fetchOneById($id, $ownershipContext)]; +} +``` + +Here, you just had to change the `$id` variable to an `IdValue`, and we are done. + +```php +public function detailAction(Request $request): array +{ + $id = IdValue::create($this->requireParam('id')); + $ownershipContext = $this->authenticationService->getIdentity()->getOwnershipContext(); + + return ['contact' => $this->contactRepository->fetchOneById($id, $ownershipContext)]; +} +``` + +### Views + +The introduction of the IdValues does not only affect the PHP-Code but also the usage in the templates. + +Therefore it is not possible to just use the id. + +You have to access the value via the function `IntIdValue->getValue()`. + +In twig templates, it looks like this: + +```html + +``` + +In smarty templates: + +```html + +``` + +## Views + +The new Shopware version involves Twig as the template engine. +Therefore we changed our template accordingly to twig. +Smarty is still used for Shopware 5. + +### JavaScript + +The Shopware 5 plugin system had a state manager capable of handling jQuery plugins provided by a plugin. + +Since Shopware 6 is just using jQuery slim, it does not include the complete functionality which we require. + +Therefore we added different jQuery polyfills to complete the functionality. +Also, we added a new `PluginInstance`, which provides the prototype of the Shopware 5 plugins. + +Currently, we are working on replacing these plugins with a TypeScript approach to ensure a higher quality and stability - while making full use of the new storefront plugin system of Shopware 6. +The existing plugins can be extended simply by accessing the jQuery object, which contains each plugin instance as an object. +```JavaScript +$.ajaxPanel.init = () => { + // Your code here +}; +``` +In the future, the new TypeScript based plugins can be extended just by following the [official Shopware 6 documentation](https://docs.shopware.com/en/shopware-platform-dev-en/how-to/extend-core-js-storefront-plugin?category=shopware-platform-dev-en/how-to). + +## Database + +With Shopware 6, a new custom field management was introduced, so we had to change the storage of our customer-related attributes. + +For this, we introduced the new table `b2b_customer_data` for the customer's attributes. + +It is the counterpart to the added `s_user_attributes` fields in Shopware 5 and shows debtors and sales representatives. + +## Administration/Backend + +Since the administration interface implementation is related to Shopware, we had to recreate the administration module with Vue. + +For the implementation, we added a new controller layer that depends on Shopware for the administration. + +It behaves like the Shopware 5 backend controllers. + +It is shown in the [plugin chapter](#the-plugin) of this article. + +## PHP version + +With the new B2B-Suite release, we dropped some PHP version. So the minimal PHP version is 7.2. + +With the drop, we also applied some of the latest PHP features. + +The most recognizable change is the appliance of the `void` return type. + +We know that this is a breaking change, but because of the amount of deprecations this would involve, because of the already existing breaking changes and because of improvement of security, we think this is bearable. + +This change results in a fatal error if you extend an existing function, which got a new `void` return. + +``` +FATAL ERROR Declaration of bar::foo() must be compatible with betterBar::foo(): void on line number x +``` + +### How to fix the fatal error + +For plugin developers, fixing the problem should be easy as long you have programmed according to our doc types. + +So + +```php +public function foo() +{ +} +``` + +becomes + +```php +public function foo(): void +{ +} +``` + +# Conclusion + +As seen in this article, the work for migrating the B2B-Suite got much easier because of our chosen architecture. + +Nevertheless, we couldn't do it without breaking changes. + +Hopefully, this article helped you to overcome the changes which were introduced with the migration. diff --git a/source/shopware-enterprise/b2b-suite/technical/modal.md b/source/shopware-enterprise/b2b-suite/technical/modal.md new file mode 100644 index 0000000000..14aeadb251 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/modal.md @@ -0,0 +1,157 @@ +--- +layout: default +title: Modal Component +github_link: shopware-enterprise/b2b-suite/technical/modal.md +indexed: true +menu_title: Modal Component +menu_order: 17 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    + +## Description + +In this article we explain the B2B modal component. We are using the modal view for an entity detail information window +which holds additional content for the selected grid item. We use two different templates for this approach. +The base modal template `(_base/modal.tpl)` is responsible for the base structure of the modal box. In this template you +can find multiple smarty blocks which are for the navigation inside the modal and the content area. + +In the B2B-Suite the content block will be extended with the second modal template `(_base/modal-content.tpl)`. The content +template can be configured with different variables to improve the user experience with a fixed top and bottom bar. We are using +this bars for filtering, sorting and pagination. + +There are many advantages to extend both templates instead of building your own modal view. +* Responsive styling for all viewports +* Same experience for every view +* No additional CSS classes required +* Easy modal adaptions because every view using the same classes + + +The modal component comes with different states: + +* Simple content holder +* Content delivered by an ajax panel +* Split view with sidebar navigation and an ajax ready content +* Fixed top and bottom bar for action buttons and pagination + +## Modal with simple content + +``` +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{extends file="frontend/_base/modal.tpl"} + +{block name="b2b_modal_base" prepend} + {$modalSettings.navigation = false} +{/block} + +{block name="b2b_modal_base_navigation_header"} + Modal Title +{/block} + +{block name="b2b_modal_base_content_inner"} + Modal Content +{/block} +``` + +## Modal with Navigation + +If you would like to have a navigation sidebar inside the modal window you can set the navigation variable to `true`. + +``` +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{extends file="frontend/_base/modal.tpl"} + +{block name="b2b_modal_base" prepend} + {$modalSettings.navigation = true} +{/block} + +{block name="b2b_modal_base_navigation_header"} + Modal Title +{/block} + +{block name="b2b_modal_base_navigation_entries"} +
  • + + Navigation Link + +
  • +{/block} + +{block name="b2b_modal_base_content_inner"} + Modal Content +{/block} +``` + +## Modal with Navigation and Ajax Panel Content + +``` +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{extends file="frontend/_base/modal.tpl"} + +{block name="b2b_modal_base" prepend} + {$modalSettings.navigation = true} +{/block} + +{block name="b2b_modal_base_navigation_header"} + Modal Title +{/block} + +{block name="b2b_modal_base_navigation_entries"} +
  • + + Navigation Link + +
  • +{/block} + +{block name="b2b_modal_base_content_inner"} +
    +{/block} +``` + +### Ajax Panel template for modal content + +The modal content template has different options for fixed inner containers. The top and bottom bar can be enabled or disabled. +The correct styling for each combination of settings will be applied automatically so u dont have to take care of styling. +We use the topbar always for action buttons like "Create element". The bottom bar could be used for pagination for example. + +``` +{namespace name=frontend/plugins/b2b_debtor_plugin} + +{extends file="parent:frontend/_base/modal-content.tpl"} + +{block name="b2b_modal_base_settings"} + {* Enables actions topbar inside the content area of a grid modal component *} + {$modalSettings.actions = true} + + {* Enables content padding inside the inner content area of a grid modal component *} + {$modalSettings.content.padding = true} + + {* Enables bottom actions inside the content area of a grid modal component *} + {$modalSettings.bottom = true} +{/block} + +{block name="b2b_modal_base_content_inner_topbar_headline"} + Modal Content Headline +{/block} + +{block name="b2b_modal_base_content_inner_scrollable_inner_actions_inner"} + Modal Actions +{/block} + +{block name="b2b_modal_base_content_inner_scrollable_inner_content_inner"} + Modal Content +{/block} + +{block name="b2b_modal_base_content_inner_scrollable_inner_bottom_inner"} + Modal Bottom +{/block} +``` diff --git a/source/shopware-enterprise/b2b-suite/technical/product-search.md b/source/shopware-enterprise/b2b-suite/technical/product-search.md new file mode 100644 index 0000000000..fd9614a318 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/product-search.md @@ -0,0 +1,30 @@ +--- +layout: default +title: Product Search +github_link: shopware-enterprise/b2b-suite/technical/product-search.tpl +indexed: true +menu_title: Product Search +menu_order: 8 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + + +## Description + +Our product search is a small jQuery Plugin which allows you to create input fields with autocompletion for products. A small example is shown below. The plugin deactivates the default autocompletion for this field from your browser. + +```html +
    + +
    +``` + +### Elasticsearch + +While using Elasticsearch you have to enable the variants filter in the filter menu of the basic settings to show all variants in the product search. + +![image](/assets/img/b2b-suite/features/variant-filter.png) diff --git a/source/shopware-enterprise/b2b-suite/technical/rest-api.md b/source/shopware-enterprise/b2b-suite/technical/rest-api.md new file mode 100644 index 0000000000..3c7ece23d1 --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/rest-api.md @@ -0,0 +1,119 @@ +--- +layout: default +title: REST api +github_link: shopware-enterprise/b2b-suite/technical/rest-api.md +indexed: true +menu_title: REST api +menu_order: 4 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    +You can download a plugin showcasing the topic here. +
    + +
    +We use swagger.io for the documentation of our B2B-Suite endpoints. The created swagger.json file can be displayed with swagger ui. +
    + +
    + +## Description + +The B2B-Suite comes with its own extension to the REST-API. Contrary to Shopwares own implementation that makes heavy use of the Doctrine ORM the B2B-Suite reuses the same services defined for the Storefront and therefore provides a controller format that is more reminiscent of Symfony 2. + +## A Simple Example + +A REST-API Controller is just a plain old PHP-Class, registered to the DIC. An action is a public method suffixed with `Action`. It always gets called with the request implementation derived from Shopwares default `\Enlight_Controller_Request_Request` as a parameter. + +```php + 'hello']; // will automatically be converted to JSON + } +} +``` + +## Adding the route + +Contrary to the default Shopware API, the B2B API provides deeply nested routes. All routes can be found in `http://my-shop.de/api/b2b`. If you want to register your own routes you have to add a `RouteProvider` to the routing service. + +First we create the routing provider containing all routing information. Routes themselves are defined as simple arrays, just like this: + +```php + + + + + +``` + +Notice that the route provider is tagged as a `b2b_common.rest_route_provider`, this tag triggers that the route is registered. + +## Complex routes + +The used route parser is [FastRoute](https://github.com/nikic/FastRoute#defining-routes) which supports more powerful features that can also be used by the B2B API. Please take a look at the linked documentation to learn more about placeholders and placeholder parsing. + +If you want to use parameters, you have to define an order in which the parameters should be passed to the action: + +```php +[ + 'GET', // the HTTP method + '/my/hello/{name}', // the subroute will be concatenated to http://my-shop.de/api/b2b/my/hello/world + 'my.api_controller', // DIC controller id + 'hello' // action method name, + ['name'] // define name as first argument +] + +``` + +And now you can use the placeholders value as a parameter: + +```php + 'hello' . $name]; // will automatically be converted to JSON + } + +``` diff --git a/source/shopware-enterprise/b2b-suite/technical/store-front-authentication.md b/source/shopware-enterprise/b2b-suite/technical/store-front-authentication.md new file mode 100644 index 0000000000..f5a4b683fa --- /dev/null +++ b/source/shopware-enterprise/b2b-suite/technical/store-front-authentication.md @@ -0,0 +1,256 @@ +--- +layout: default +title: Store Front Authentication +github_link: shopware-enterprise/b2b-suite/technical/store-front-authentication.md +indexed: true +menu_title: Authentication +menu_order: 11 +menu_style: numeric +menu_chapter: true +group: Shopware Enterprise +subgroup: B2B-Suite +subsubgroup: Technical Documentation +--- + +
    +You can download a plugin showcasing how to add a provider here.
    +And you can download a plugin which exchange the login value here. +
    + +
    + +## Description + +The Store front authentication component provides a common B2B interface for login, ownership and authentication processes. It extends the Shopware default authentication component and provides several benefits for developers: + +* Use multiple different source tables for authentication +* Provide an unified Identity interface +* Provide a context for ownership + +A schematic overview of the central usage of the Authentication component looks like this: + +![image](/assets/img/b2b/authentication-overview.svg) + +Color | Type | Description +--- | --- | --- +green | Provider | Provides user identities. For example a contact and a debtor are both valid B2B-Accounts that log in through the same user interface, but do not share a common storage table. +yellow | Context | Uses the `Identity` as a context to determine what data should be shown. Usually a simple debtor or tenant like filter. +blue | Owner | Uses the `Identity` to store the specific owner of a record. + +## Working with the Identity as a Context + +The `StoreFrontAuthentication` component provides an identity representing the currently logged in user, that can easily be retrieved and inspected through `\Shopware\B2B\StoreFrontAuthentication\Framework\AuthenticationService`. + +Typically you want to use the identity as a global criteria to secure that data does not leak from one debtor to another. Therefore you should add a `context_owner_id` to your mysql table design. + +```sql +CREATE TABLE IF NOT EXISTS `b2b_my` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `context_owner_id` INT(11) NOT NULL, + [...] + + PRIMARY KEY (`id`), + + INDEX `b2b_my_auth_owner_id_IDX` (`context_owner_id`), + + CONSTRAINT `b2b_my_auth_owner_id_FK` FOREIGN KEY (`context_owner_id`) + REFERENCES `b2b_store_front_auth` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE +) + +``` + +This modifier column allows you to store the context owner independent of the actual source table of the context owner. You can access the current context owner always through the Identity. + +```php + [...] + + /** @var AuthenticationService $authenticationService */ + $authenticationService = Shopware()->Container()->get('b2b_front_auth.authentication_service'); + + if (!$authenticationService->isB2b()) { + throw new \Exception('User must be logged in'); + } + + $ownershipContext = $authenticationService + ->getIdentity() + ->getOwnershipContext(); + + echo "The context owner id " . $ownershipContext->contextOwnerId . "\n" + + [...] +``` + +You can even load the whole Identity through the `AuthenticationService`. + +```php + [...] + + $ownerIdentity = $authenticationService->getIdentityByAuthId($contextOwnerId); + + [...] +``` + +## Working with the Identity as an Owner + +Sometimes you want to flag records to be owned by certain identities. + +```sql +CREATE TABLE IF NOT EXISTS `b2b_my` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `auth_id` INT(11) NULL DEFAULT NULL, + + [...] + + PRIMARY KEY (`id`), + + CONSTRAINT `b2b_my_auth_user_id_FK` FOREIGN KEY (`auth_id`) + REFERENCES `b2b_store_front_auth` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE +) +``` + +To fill this column we again access the current identity but instead of the `contextOwnerId` we access the `authId` + +```php + [...] + + $ownershipContext = $authenticationService + ->getIdentity() + ->getOwnershipContext(); + + echo "The common identity id " . $ownershipContext->authId . "\n" + + [...] +``` + +The B2B-Suite views the context owner as some kind of admin that - from the perspective of the authentication component - owns all individual users and their data *(Of course the ACL component may overwrite this.)*. + +Therefore commonly used queries are: + +```php +/** @var Connection $connection */ +$connection = Shopware()->Container()->get('dbal_connection'); +/** @var Identity $identity */ +$identity = Shopware()->Container()->get('b2b_front_auth.authentication_service') + ->getIdentity(); + +// get all records relative to the user +$connection->fetchAll( + 'SELECT * FROM b2b_my my WHERE my.auth_id = :authId', + [ + 'authId' => $identity->getOwnershipContext()->authId + ] +); + +// get all records relative to the users context owner +$connection->fetchAll( + 'SELECT * FROM b2b_my my WHERE my.auth_id IN (SELECT auth_id FROM b2b_store_front_auth WHERE context_owner_id = :identityContextOwnerId)', + [ + 'identityContextOwnerId' => $identity->getOwnershipContext()->contextOwnerId + ] +); + +// get all records relative to the current user or if the owner is logged in to the owner +$connection->fetchAll( + 'SELECT * FROM b2b_my my WHERE my.auth_id IN (SELECT auth_id FROM b2b_store_front_auth WHERE auth_id = :authId OR context_owner_id = :identityContextOwnerId)', + [ + 'authId' => $identity->getOwnershipContext()->authId, + 'identityContextOwnerId' => $identity->getOwnershipContext()->authId, + ] +); + +``` + +## Working with the Identity as a Provider + +If you need another type of user you can follow the `Contact` and `Debtor` implementations. This guide will show you which classes need to be extended. + +#### Implement your own Identity + +The B2B-Suites `\Shopware\B2B\StoreFrontAuthentication\Framework\Identity` is an interface which means that every user has to reimplement it. + +The interface acts as a factory for different contexts that are used throughout the B2B-Suite. It contains: + +* B2B-Suite ids and data (eg. auth id, context owner) +* Shopware glue (eg. customer group id, password hash) + +Therefore it can be seen as a *man in the middle* between Shopware and the B2B-Suite. + +Example implementations are either `\Shopware\B2B\Debtor\Framework\DebtorIdentity` or `\Shopware\B2B\Contact\Framework\ContactIdentity`. + +#### Implement your own CredentialsBuilder + +In the CredentialsBuilder you create the CredentialsEntity witch is used to login in the B2B-Suite. + +```php + +/** + * @param Enlight_Event_EventArgs $args + * @return CredentialsEntity + */ +public function createCredentials(Enlight_Event_EventArgs $args): CredentialsEntity +{ + $entity = new CredentialsEntity(); + + $entity->email = $args->get('post')['email']; + + return $entity; +} + +``` + +The CredentialsEntity represents the data which are used to login with. + +#### Implement your own AuthenticationIdentityLoader + +Next you have to provide the means to register your identity on login this is done through implementing `\Shopware\B2B\StoreFrontAuthentication\Framework\AuthenticationIdentityLoaderInterface`. + +The LoginContextService is passed as an argument to help you retrieving and creating the appropriate auth and context owner ids. Notice that the interface is designed to be chained, so dependant auth ids can be created on the fly. + +```php +[...] +/** + * @param string $email + * @param LoginContextService $contextService + /** + * {@inheritdoc} + */ + public function fetchIdentityByCredentials(CredentialsEntity $credentialsEntity, LoginContextService $contextService, bool $isApi = false): Identity + { + if (!$credentialsEntity->email) { + throw new NotFoundException('Unable to handle context'); + } + + $entity = $this->yourEntityRepository->fetchOneByEmail($email); + + /** @var DebtorIdentity $debtorIdentity */ + $debtorIdentity = $this + ->debtorRepository + ->fetchIdentityById($entity->debtor->id, $contextService); + + $authId = $contextService->getAuthId(YourEntityRepository::class, $entity->id, $debtorIdentity->getAuthId()); + + $this->yourEntityRepository->setAuthId($entity->id, $authId); + + return new YourEntityIdentity($authId, (int) $entity->id, YourEntityRepository::TABLE_NAME, $entity, $debtorIdentity); + } +[...] +``` + +Finally you register your authentication provider (in our case a repository) as a tagged service through the DIC. + +```xml + + [...] + + + +``` +## Sales Representative + +Both sales representative identities extend the debtor identity. +The sales representative identity is to log in as clients (debtors). + +After log in, the sales representative gets the sales representative debtor identity. +With this structure, the original sales representative identity can be identified, when logged in as a client. + +As a sales representative debtor, he is actually logged in as the client with additional possibilities. diff --git a/source/shopware-enterprise/index.html b/source/shopware-enterprise/index.html new file mode 100644 index 0000000000..b4a33bcb87 --- /dev/null +++ b/source/shopware-enterprise/index.html @@ -0,0 +1,23 @@ +--- +layout: default +title: Shopware Enterprise +github_link: shopware-enterprise/index.html +menu_title: Shopware Enterprise +menu_order: 110 +menu_style: bullet +--- + + + +

    Latest topics

    + + diff --git a/source/shopware-enterprise/performance/essentials.md b/source/shopware-enterprise/performance/essentials.md new file mode 100644 index 0000000000..e1c296e59a --- /dev/null +++ b/source/shopware-enterprise/performance/essentials.md @@ -0,0 +1,609 @@ +--- +layout: default +title: SwagEssentials +github_link: shopware-enterprise/performance/essentials.md +indexed: true +menu_title: Essentials +group: Shopware Enterprise +subgroup: Performance +menu_order: 2 +shopware_version: Shopware 5.3.0 +--- + +Shopware Essentials is a tool collection for developers. It provides components such as + +* additional, low level cache layers +* cache invalidation for multi appserver environments +* read / write query splitting +* high concurrency number incrementer + +
    +SwagEssentials is a toolset for developer that helps you to tackle more sophisticated Shopware projects. We only +recommend it for experienced developers as it requires advanced knowledge of Shopware and the toolset itself which cannot +be imparted in the context of the shopware support. +
    + +
    +SwagEssentials requires advanced administrative tasks such as setting up and managing a redis server and a primary/replica database cluster. +Setting up, managing and supporting these environments is the responsibility of the customer / implementing partner. +
    + + +
    + +## Using SwagEssentials +SwagEssentials can be installed just as any other Shopware plugin. However, the developer selects the components he wants +to use for the Shopware environment. You can activate and configure SwagEssentials via parameters in the `config.php`. +If you have all modules activated your config file should look like this: + +```php +require_once __DIR__ . '/custom/plugins/SwagEssentials/Redis/Store/RedisStore.php'; +require_once __DIR__ . '/custom/plugins/SwagEssentials/Redis/Factory.php'; +require_once __DIR__ . '/custom/plugins/SwagEssentials/Redis/RedisConnection.php'; +return [ +'db' => + [ + 'host' => 'mysql', + 'port' => '3306', + 'username' => 'app', + 'password' => 'app', + 'dbname' => 'shopware', + 'factory' => '\SwagEssentials\PrimaryReplica\PdoFactory', + 'replicas' => [ + 'replica-backup' => [ + 'username' => 'app', + 'password' => 'app', + 'dbname' => 'shopware', + 'host' => '10.123.123.41', + 'port' => '', + ] + ] + ], + 'swag_essentials' => + [ + 'modules' => + [ + 'CacheMultiplexer' => false, + 'Caching' => false, + 'PrimaryReplica' => false, + 'RedisNumberRange' => true, + 'RedisPluginConfig' => false, + 'RedisProductGateway' => false, + 'RedisStore' => true, + 'RedisTranslation' => true, + ], + 'redis' => + [ + 0 => + [ + 'host' => 'app_redis', + 'port' => 6379, + 'persistent' => true, + 'dbindex' => 0, + 'auth' => 'app', + ], + ], + 'cache_multiplexer_hosts' => + [ + 0 => + [ + 'host' => 'http://10.123.123.31/api', + 'user' => 'demo', + 'password' => 'demo', + ], + 1 => + [ + 'host' => 'http://10.123.123.32/api', + 'user' => 'demo', + 'password' => 'demo', + ], + ], + 'caching_enable_urls' => true, + 'caching_enable_list_product' => true, + 'caching_enable_product' => true, + 'caching_ttl_urls' => 3600, + 'caching_ttl_list_product' => 3600, + 'caching_ttl_product' => 3600, + 'caching_ttl_plugin_config' => 3600, + 'caching_ttl_translation' => 3600, + ], + 'httpcache' => + [ + 'storeClass' => 'SwagEssentials\\Redis\\Store\\RedisStore', + 'redisConnections' => + [ + 0 => + [ + 'host' => 'app_redis', + 'port' => 6379, + 'persistent' => true, + 'dbindex' => 0, + 'auth' => 'app', + ], + ], + ], +]; +``` + +## Enterprise Cache / Redis Cache + +**What it does**: Uses Redis for HTTP caching. This way, multiple appserver do share the same cache. + +**Needed for**: Cluster setups, with more than one appserver + + +### Abstract +Usually Shopware can be used with either the built in Cache or Varnish. Both have their pros and cons. With our Redis Cache +for Enterprise environments, we also provide one solution, that combines the benefits of both existing cache alternatives + +| | Built in| Varnish | Enterprise Cache | +|:-:|:-:|:-:|:-:| +| Easy to setup / maintain| ✓ | | ✓ | +| Central cache for multiple appservers | | ✓ | ✓ | +| Cluster setup| | Varnish Plus | ✓ | +| Optimized handling of ESI tags| ✓ | Varnish Plus | ✓ | + +Using the Enterprise Cache, all Shopware appserver are able to share the same cache. This increases the general cache hit rate +massively. + +### Setup +Assuming that SwagEssentials is available at `custom/plugins/SwagEssentials` in your Shopware root directory, change your config.php like this: + +``` + [...], + 'swag_essentials' => + [ + 'modules' => + [ + ... + 'RedisStore' => true, + ], + 'redis' => + [ + 0 => + [ + 'host' => 'app_redis', + 'port' => 6379, + 'persistent' => true, + 'dbindex' => 0, + 'auth' => 'app', + ], + ], + ], + 'httpcache' => + [ + 'storeClass' => 'SwagEssentials\\Redis\\Store\\RedisStore', + 'keyPrefix' => '', //this is only needed when running multiple shops on one Redis-Cluster + 'compressionLevel' => 9, + 'redisConnections' => + [ + 0 => + [ + 'host' => 'app_redis', + 'port' => 6379, + 'persistent' => true, + 'dbindex' => 0, + 'auth' => 'app', + ], + ], + ], + + // rest of your config.php +]; +``` + +New are the both configuration keys `storeClass` and `redisConnections` in the `httpcache` config array. +`storeClass` configures the cache backend, in this case, you can just use `require_once 'custom/plugins/SwagEssentials/RedisStore/loader.php'` +in order to setup the cache correctly. You can provide a custom compression level for cached pages to control the tradeoff between redis memory usage (with low compression level) and time consumption for compression (with high compression level) when warming up the cache. The default value of 9 yields maximum compression, a value of 0 no compression at all. +`redisConnections` defines the redis connections you want to use for your HTTP cache. Available options are: + +* `persistent`: If the redis connection should be persistent (default) +* `port`: Redis port, default 6379 +* `host`: IP / host name of your redis server +* `timeout`: Timeout for the connection, default 30s +* `auth`: your authentification key, default empty +* `dbindex`: The redis database to be used, default 0 + +### Warming the cache + +Shopware provides a script for warming your HTTP caches: + +``` +php bin/console sw:warm:http:cache +``` + +It allows you to configure the number of parallel workers to warm the HTTP cache: + +``` +php bin/console sw:warm:http:cache -b10 +``` + +For Shopware-Instances below v5.5.0 please use the script which SwagEssentials provides you: + + +``` +php bin/console sw:cache:siege +``` + +It also allows you to configure the number of parallel workers to warm the HTTP cache: + +``` +php bin/console sw:cache:siege -c10 +``` + +Depending on the number of workers and the performance of your system, this will massively decrease the time needed to warm all shop pages. + +
    +Please notice: This command requires siege to be available. On debian based distributions it can be +installed using sudo apt-get install siege +
    + +## CacheMultiplexer +**What it does**: Multiplexes cache invalidation (e.g. from the cache/performance module) to multiple instances of shopware. + +**Needed for**: Cluster setups, where you need to invalidate multiple appservers at once + +### How to configure: +#### How to enable +In order to enable the submodule you have to import the parameters in your `config.php`: + +```php +'db' => [...], +'swag_essentials' => + [ + 'modules' => + [ + ..., + 'CacheMultiplexer' => true, + ], + 'cache_multiplexer_hosts' => + [ + [ + 'host' => 'http://10.123.123.31/api', + 'user' => 'demo', + 'password' => 'demo', + + ], + [ + 'host' => 'http://10.123.123.32/api', + 'user' => 'demo', + 'password' => 'demo', + + ], + ], + ], +``` + +#### Configuration +In the following example you can see how to configure an appserver. The credentials are used for the shopware API + +```php +'cache_multiplexer_hosts' => + [ + [ + 'host' => 'http://10.123.123.31/api', + 'user' => 'demo', + 'password' => 'demo', + + ], + ], +``` + +## Primary / replica +**What it does**: Use multiple databases for shopware. Will split write queries to primary connection and read queries to replica connections. + +**Needed for**: Cluster setups and setups with high load on the primary database connection + +### How to configure +#### How to enable +Install the SwagEssentials plugin and enable `PrimaryReplica` in your config.php and enable the primary/replica setup in two steps: + + 1. `require_once __DIR__ . '/custom/plugins/SwagEssentials/PrimaryReplica/PdoFactory.php'`; + 2. Add `'factory' => '\SwagEssentials\PrimaryReplica\PdoFactory',` to the `db` array + 3. configure at least one replica database in the `db.replicas` array + +The result could look like this: + +``` + [ + 'username' => 'root', + 'password' => 'root', + 'dbname' => 'training', + 'host' => 'localhost', + 'factory' => '\SwagEssentials\PrimaryReplica\PdoFactory', + 'port' => '', + 'replicas' => [ + 'replica-backup' => [ + 'username' => 'root', + 'password' => 'root', + 'dbname' => 'training', + 'host' => '192.168.0.30', + 'port' => '', + ], + 'replica-redundancy' => [ + 'username' => 'root', + 'password' => 'root', + 'dbname' => 'training', + 'host' => '192.168.0.31', + 'port' => '', + ] + ], + 'modules' => + [ + ... + 'PrimaryReplica' => true, + ], + ] +]; +``` + + +#### Additional Configuration: +In the main `db` array of your `config.php` you can set additional options: + * `includePrimary`: Also make the primary connection part of the "read" connection pool. Default: `false` + * `stickyConnection`: Within a request, choose one random read connection from the replica pool and stick to that connection. If disabled, for every request a new random connection will be chosen. Default: `true` + +Furthermore you can set a `weight` for every connection (also for the primary connection). This way you can define, +how often a connection should be choosen in comparison to other connections. + +#### Using a proxy for replica connections +In more advanced setups, you probably don't want to maintain a list of all database replicas in the application itself. If you have some sort of load balancer / proxy for your database replicas in place, you can just configure it as (the only) replica connection. +This has several advantages: + + * the proxy takes care of query distribution acrooss the replica pool + * only the proxy needs to "know" of all replicas + * the proxy can take care of e.g. health checks etc. + * solutions with haproxy or nginx are quite common + +## NumberRange +**What it does**: Allows you to remove the s_order_number ussage via mysql + +**Needed for**: Cluster setups and setups with high load on the primary database connection + +### How to configure: +#### How to enable +In order to enable the submodule, import it in your `config.php`: + +```php +'db' =>[...], +'swag_essentials' => + [ + 'modules' => + [ + ... + 'RedisNumberRange' => true, + ], + ], +``` + +#### Configuration +To activate the NumberRange export the existing numbers via the following cli command: + +```bash +./bin/console numberrange:sync --to-redis +``` + +To save the incrementions from redis to the database you can use this command: + +```bash +/bin/console numberrange:sync --to-shopware +``` + + +### Importing the numbers +As soon as the component is activated, Shopware will use Redis for storing the number ranges. In order to import your +current number ranges to Redis, please run: + +``` +php ./bin/console numberrange:sync --to-redis +``` + +in your Shopware root. In order to import your Redis number ranges back to Shopware, just run + +``` +php ./bin/console numberrange:sync --to-shopware +``` + +in your Shopware root. + + +## Caching +**What it does**: Allows you to cache additional resources in Shopware + +**Needed for**: Uncached pages, Shopware instances without HTTP cache + +### How to configure: +#### How to enable +In order to enable the submodule, import it in your `config.php`: + +```php +'db' =>[...], +'swag_essentials' => + [ + 'modules' => + [ + ... + 'Caching' => true, + ], + 'caching_enable_urls' => true, + 'caching_enable_list_product' => true, + 'caching_enable_product' => true, + 'caching_ttl_urls' => 3600, + 'caching_ttl_list_product' => 3600, + 'caching_ttl_product' => 3600, + ], +``` + +#### Configuration +Generally, you can configure the submodule for the following resources: + + * `urls`: Caching of generated SEO urls + * `list_product`: Caching for listings + * `product`: Caching for detail pages + + Each of these resources can be enabled / disabled separately: + +```php + 'caching_enable_urls' => true, + 'caching_enable_list_product' => true, + 'caching_enable_product' => true, +``` + +Also each of these resources can have an individual TTL (caching time): + +```php + 'caching_ttl_urls' => 3600, + 'caching_ttl_list_product' => 3600, + 'caching_ttl_product' => 3600, +``` + + +## PluginConfigCaching +**What it does**: Allows you to cache the Shopware Plugin Configuration + +**Needed for**: Uncached pages, Shopware instances without HTTP cache + +### How to configure: +#### How to enable +In order to enable the submodule, import it in your `config.php`: + +```php +'db' =>[...], +'swag_essentials' => + [ + 'modules' => + [ + ... + 'RedisPluginConfig' => true, + ], + ... + 'caching_ttl_plugin_config' => 3600, + ], +``` + +#### Configuration +You can configure the cache TTL (time to live) for this module: + +```php + 'caching_ttl_plugin_config' => 3600, +``` + +## ProductGatewayCaching +**What it does**: Allows you to cache the ListProduct Structs from Shopware in Redis + +**Needed for**: Uncached pages, Shopware instances without HTTP cache + +### How to configure: +#### How to enable +In order to enable the submodule, import it in your `config.php`: + +```php +'db' =>[...], +'swag_essentials' => + [ + 'modules' => + [ + ... + 'RedisProductGateway' => true, + ], + ... + ], +``` + + +## RedisHttpCaching +**What it does**: The Redis HTTP Cache allows to store the complete HTTP cache inside Redis. + +**Needed for**: Cluster setups and setups with high load + +### How to configure: +#### How to enable +In order to enable the submodule, import it in your `config.php`: + +```php +require_once __DIR__ . '/custom/plugins/SwagEssentials/Redis/Store/RedisStore.php'; +require_once __DIR__ . '/custom/plugins/SwagEssentials/Redis/Factory.php'; +require_once __DIR__ . '/custom/plugins/SwagEssentials/Redis/RedisConnection.php'; +return [ + 'db' => [...], + 'swag_essentials' => + [ + 'modules' => + [ + ... + 'RedisStore' => true, + ], + 'redis' => + [ + 0 => + [ + 'host' => 'app_redis', + 'port' => 6379, + 'persistent' => true, + 'dbindex' => 0, + 'auth' => 'app', + ], + ], + ], + 'httpcache' => + [ + 'storeClass' => 'SwagEssentials\\Redis\\Store\\RedisStore', + 'redisConnections' => + [ + 0 => + [ + 'host' => 'app_redis', + 'port' => 6379, + 'persistent' => true, + 'dbindex' => 0, + 'auth' => 'app', + ], + ], + ], +]; +``` + +## TranslationCaching +**What it does**: Allows you to cache the translation calls against the mysql db + +**Needed for**: Uncached pages, Shopware instances without HTTP cache, Cluster setups and setups with high load on the primary database connection + +### How to configure: +#### How to enable +In order to enable the submodule, import it in your `config.php`: + +```php +'db' =>[...], +'swag_essentials' => + [ + 'modules' => + [ + ... + 'RedisTranslation' => true, + ], + ... + 'caching_ttl_translation' => 3600, + ], +``` + +#### Configuration +You can configure the cache TTL (time to live) for this module: + +```php + 'caching_ttl_translation' => 3600, +``` + + +## Github repository +Access to the [Github repository](https://gitlab.com/shopware/shopware/enterprise/swagessentials) is granted on request. diff --git a/source/shopware-enterprise/performance/index.html b/source/shopware-enterprise/performance/index.html new file mode 100755 index 0000000000..05b72cfa63 --- /dev/null +++ b/source/shopware-enterprise/performance/index.html @@ -0,0 +1,32 @@ +--- +layout: default +title: Performance +github_link: shopware-enterprise/performance/index.html +indexed: true +menu_title: Performance +group: Shopware Enterprise +--- + + +

    Shopware Enterprise Performance

    +

    Shopware Enterprise provides tools to measure and improve the performance of your shop. While our performance +whitepaper gives an overview how Shopware can be scaled, our JMeter scripts will help you to examine the limits of your +individual shop setup. SwagEssentials will then help you to improve the scalability of your shop.

    + + +

    Performance Whitepaper

    + + +

    JMeter

    + + +

    SwagEssentials

    + diff --git a/source/shopware-enterprise/performance/jmeter.md b/source/shopware-enterprise/performance/jmeter.md new file mode 100644 index 0000000000..9846989795 --- /dev/null +++ b/source/shopware-enterprise/performance/jmeter.md @@ -0,0 +1,144 @@ +--- +layout: default +title: JMeter +github_link: shopware-enterprise/performance/jmeter.md +indexed: true +menu_title: JMeter +group: Shopware Enterprise +subgroup: Performance +menu_order: 1 +--- + +JMeter is a load testing tool which helps you to analyse the performance and scalability a web application. It does so +by simulating multiple concurrent users browsing the web application. This way you are not only able to estimate the +numbers of users your web application can handle, but also where bottlenecks are and perhaps how to fix them. For that +reason JMeter is usually used in addition with other monitoring tools and profilers such as [tideways](https://tideways.io/), +for example. + +## Shopware JMeter Scripts +Load testing is a common requirement in e-commerce projects, especially when challenging performance requirements needs to be addressed. +For that reason Shopware provides a basic set of JMeter scripts for Enterprise customers, in order to be able to +estimate the scalability of the system before going live. + +### Setup +In order to use the Shopware JMeter Scripts you need to run Linux / Mac and have a recent version of Java installed. The +Shopware JMeter Scripts are available on [github](https://gitlab.com/shopware/shopware/enterprise/jmeter)(access on request). First +clone the repository: + +``` +git clone git@gitlab.com:shopware/shopware/enterprise/jmeter.git +``` + +No you'll find the following directories: + + * `assets`: The Shopware JMeter Scripts will generate testing assets in here + * `config`: JMeter configuration + * `scripts`: Scripts to generate the required assets and run / edit JMeter + * `var`: Logs and result files + * `vendor`: JMeter itself + + +### Editing the test plan + + + +In order to edit the test plans, just run `./scripts/jm-edit.sh`. It will open the JMeter application. The Shopware +JMeter Scripts generally defines 9 different components (2) you can use in your test plans: + + * `Frontend - Start`: A call to the start page + * `Frontend - Category`: A call to a random category page + * `Frontend - Search`: Performs a random search + * `Frontend - Random`: A request to a random static page + * `Frontend - addToBasket`: Add a product to the shopping cart + * `Frontend - ArticleDetail`: A call to a random product detail page + * `Frontend - Register`: Registers a new customer + * `Frontend - Login`: Logs in as an existing customer + * `Frontend - Checkout`: Calls the checkout confirm and the checkout finish page (thus performs an order) + +Most of these components use certain assets (1): For the `Frontend - Category` component, for example, a random category URL is required - +and provided by the corresponding asset. + +The actual test plans are defined in so called "thread groups": Each thread group copies or links one or more components +and therefor "tells a user story" such as "a user visits the start page, searches for 5 items and then adds two items to cart" or +"a user browses 3 categories, adds 1 item to cart, registers and performs an order". This way you can try to model real +user's behaviour in your test plans. In production, for example, just 3 to 5% of your users will actually perform a checkout. +So it makes sense to create one thread group only browsing the shop and one thread group which actually performs checkouts. + +The Shopware JMeter Scripts define 9 thread groups which you can change to your needs: E.g. one group for users mostly +searching, one group for users mostly browsing, one group for registered users and one group for users not yet registered. +Copy / link the pre-defined components into each thread group as needed. After saving you can close the window again and +configure, how many threads / users are active for each thread group. + +### Test plan configuration +First edit the file `config/testplan_config.properties`. By default it will look like this: + +``` +server.hostname = shopware.local +server.protocol = http + +execution.delay = 10000 + +execution.deviation = 5000 + +execution.rampup = 30 + +execution.duration = 120 + +threads.group_1.users = 100 +threads.group_2.users = 3 +threads.group_3.users = 0 +threads.group_4.users = 0 +threads.group_5.users = 0 +threads.group_6.users = 0 +threads.group_7.users = 0 +threads.group_8.users = 0 +threads.group_9.users = 0 +``` + + * ` server.hostname`: Hostname of the server you want to load test, e.g. `my-production-shop.example.com` + * `server.protocol`: The protocol you want to use, usually `http` or `https` + * `execution.delay`: How many ms should pass between each action of a thread group + * `execution.deviation`: The `execution.delay` will randomly be increased / reduced by `execution.deviation` + * `execution.rampup`: JMeter will take up to `execution.rampup` seconds until all threads are started + * `execution.duration`: Number of seconds the load test should run + * `threads.group_NUMER.users`: Number of concurrent users you want to simulate for a thread group + +The above configuration, for example, will run for 2 minutes and spawn 100 threads ("users") for the first thread group +and 3 threads ("users") for the second thread group. Between each request of a thread ("user") a 10 second delay is defined. + +### Generating the assets +In order to run JMeter some assets needs to be generated, so that JMeter knows which URLs to call and which products +to buy. All assets are generated directly from Shopware's database. For that reason, you need to specify your database +credentials in `scripts/credentials.sh`. A template for that file can be found in `scripts/credentials.dist.sh`. + +Furthermore you need to specify `SHOP_USER_PASSWORD`: JMeter will use this password when logging in as a user. If you are +load testing an existing database dump, you will usually need to set the passwords in the database: + +`UPDATE s_user SET password="$2y$10$PeSQ3o7F0hocHKH.1CvUCexZ/qernZ4wUC4cbGj2a3jLgLCcvwMRm"` + +This will set all passwords to "shopware". JMeter will now be able to log in as any of these users. As an alternative +you can also generate dummy data automatically by using the [Shopware CLI tools](https://github.com/shopwareLabs/sw-cli-tools). + +In order to generate all assets, just run `./scripts/all-uris.sh`. If you just want to (re-)generate a part of the assets, +there are specific scripts such as `scripts/uri-accounts.sh` or `scripts/uri-search.sh`. + +### Running the load test and inspecting the results +In order to execute the load test, just run `scripts/jm-run.sh`. It will execute the test plan for the configured +`execution.duration`: + + + +While the test plans run, its usually helpful to watch the load of the application servers and the database: If disc load, +RAM usage or CPU usage are extremely high, you might have found a bottleneck. But also keep in mind, that the number +of connections can be a limiting factor for all of these servers. + +After the test plan exited, you can run `./scripts/jm-edit.sh` again in order to open JMeter. At the bottom of the navigation +on the left side you find "Summary Report" (1). Click on it and open the CSV file of your JMeter run (2): + + + +JMeter will present you all URLs which have been called, the average response time, number of samples, percentage of +errors and other useful metrics. You can even inspect every single request by selecting "View Result Tree" from the +navigation. After opening the `result.csv` file, JMeter will show you every request with the corresponding headers and +body. This is especially useful, if you want to debug errors. + diff --git a/source/shopware-enterprise/pricing-engine/index.html b/source/shopware-enterprise/pricing-engine/index.html new file mode 100644 index 0000000000..0698366c51 --- /dev/null +++ b/source/shopware-enterprise/pricing-engine/index.html @@ -0,0 +1,29 @@ +--- +layout: default +title: Pricing Engine +github_link: pricing-engine/index.html +indexed: true +menu_title: Pricing Engine +group: Shopware Enterprise +--- + + +

    Pricing Engine

    +

    + You can individually customise your price lists with numerous rules, which can include criteria such as delivery country, customer group, currency, shop, or customer stream. The Pricing Engine enables a seamless connection with your existing ERP or PIM systems and optimises performance through batch processing. +

    + +

    Features

    + +The Pricing Engine provides different features. The prices can be stored as gross prices and supports quantity prices. Furthermore the prices can have a datetime validity. At least it is possible to use calculation terms based on product properties instead of using normal prices. +

    +As an example: (%height% * %width% * %length%) * 0.1 +

    +All prices are stored in pricelists. The pricelists can have context based conditions like Shippingcountry, Shop, Customer, Customergroup, Customerstream and Currency. If all conditions fits the prices from the list will override the default prices from the specific shop. + +

    + + diff --git a/source/shopware-enterprise/pricing-engine/installation/index.md b/source/shopware-enterprise/pricing-engine/installation/index.md new file mode 100644 index 0000000000..e3cb784430 --- /dev/null +++ b/source/shopware-enterprise/pricing-engine/installation/index.md @@ -0,0 +1,52 @@ +--- +layout: default +title: Installation Guide +github_link: pricing-engine/installation/index.md +indexed: true +tags: [pricing engine, installation] +menu_title: Installation Guide +menu_order: 2 +group: Shopware Enterprise +subgroup: Pricing Engine +--- + +
    + +## General + +At the moment we provide only a docker based virtualization solution. Our developers use the docker containers mainly. These containers are also used in our continuous integration process. The supported functions are for both systems equal if the host systems is based on Linux. + +If you want to install the Pricing Engine for production environment your system must fit with the defined requirements from the [Shopware core](https://developers.shopware.com/sysadmins-guide/system-requirements/). In contrast to the Shopware core requirements we need php 7.0 or higher and MySQL 5.7.0 or higher. + +## Installation on a Linux based system +### Docker (recommended) +As minimum requirement, we need a docker runtime with version 1.12.* or higher and a [phive](https://phar.io/#Install) installation. Before you can use [psh](https://github.com/shopwareLabs/psh) you have to execute phive install in the root directory. After that psh provides the following available docker commands: + +```bash +./psh docker:start # start & build containers +./psh docker:ssh # ssh access web server +./psh docker:ssh-mysql # ssh access mysql +./psh docker:status # show running containers and network bridges +./psh docker:stop # stop the containers +./psh docker:destroy # clear the whole docker cache +``` + +To start the docker environment just type +```bash +./psh docker:start +``` +on your command line. The several containers are booted and afterwards you can login into your web container with +```bash +./psh docker:ssh +``` +After that, you can start the initialization process by typing +```bash +./psh init +``` + +After a few minutes, our test environment should be available under the address [http://10.222.222.30](http://10.222.222.30). + +To get a full list of available commands, you can use +```bash +./psh +``` diff --git a/source/shopware-enterprise/pricing-engine/rest-api/index.md b/source/shopware-enterprise/pricing-engine/rest-api/index.md new file mode 100644 index 0000000000..37b0beb61c --- /dev/null +++ b/source/shopware-enterprise/pricing-engine/rest-api/index.md @@ -0,0 +1,295 @@ +--- +layout: default +title: REST API +github_link: pricing-engine/rest-api/index.md +indexed: true +tags: [pricing engine, rest api] +menu_title: REST API +menu_order: 3 +group: Shopware Enterprise +subgroup: Pricing Engine +--- + +
    +You can also find a fully comprehensive and up-to-date Swagger documentation here. +
    + +
    + +## API Structure + +In order to maintain your custom pricing lists, the Pricing Engine offers a range of RESTful endpoints. In the following examples is http://10.222.222.30/api our local endpoint for the shopware api. You can find examples written with guzzle in the official git repo. If you don't have access yet, please create a ticket via your shopware account. You will receive an invitation soon. + +### Price lists + +`/api/PriceList` + +* `GET` Get all available price lists + + - `limit`, `offset` + +```http request +GET http://10.222.222.30/api/PriceList +Authorization: Digest demo demo +Content-Type: application/json + +{ + "filters": [ + { + "field-name": "id", + "value": 6, + "type": "eq" + } + ] +} +``` + +* `POST` Create a price list + + - `priceList` + +```http request +POST http://10.222.222.30/api/PriceList +Authorization: Digest demo demo +Content-Type: application/json + +{ + "name": "Create new PriceList", + "priority": 0 +} +``` + +* `UPDATE` Update a price list + +```http request +PUT http://10.222.222.30/api/PriceList/11 +Authorization: Digest demo demo +Content-Type: application/json + +{ + "id": 11, + "name": "Update PriceList", + "priority": 0 +} +``` + +* `DELETE` Delete a price list + +```http request +DELETE http://10.222.222.30/api/PriceList/11 +Authorization: Digest demo demo +Content-Type: application/json +``` + +### Single price list + +`/api/PriceList/{id}` + +* `GET` Get one price list + + - `id` + +```http request +GET http://10.222.222.30/api/PriceList/11 +Authorization: Digest demo demo +Content-Type: application/json +``` + +* `PUT` Update one price list + + - `id`, `priceList` + +```http request +PUT http://10.222.222.30/api/PriceList/11 +Authorization: Digest demo demo +Content-Type: application/json + +{ + "name": "Update PriceList", + "priority": 0 +} +``` + +* `DELETE` Delete one price list + + - `id` + +```http request +DELETE http://10.222.222.30/api/PriceList/11 +Authorization: Digest demo demo +Content-Type: application/json +``` + +### Conditions + +`/api/PriceListCondition` + +* `GET` Returns all configurable conditions + +```http request +GET http://10.222.222.30/api/PriceListCondition +Authorization: Digest demo demo +Content-Type: application/json +``` + +* `POST` Create one price list condition + + - `priceListCondition` + +```http request +POST http://10.222.222.30/api/PriceListCondition +Authorization: Digest demo demo +Content-Type: application/json + +{ + "priceListId": 1, + "type": "SwagEnterprisePricingEngine\\Source\\PriceListCondition\\Conditions\\CurrencyCondition", + "value": "EUR" + +} +``` + +* `UPDATE` Create one price list condition + + - `priceListCondition` + +```http request +PUT http://10.222.222.30/api/PriceListCondition/16 +Authorization: Digest demo demo +Content-Type: application/json + +{ + "priceListId": 1, + "type": "SwagEnterprisePricingEngine\\Source\\PriceListCondition\\Conditions\\CurrencyCondition", + "value": "EUR" + +} +``` + +* `DELETE` Delete + +### Single condition + +`/api/PriceListCondition/{id}` + +* `GET` Get one price list condition + + - `id` + +```http request +GET http://10.222.222.30/api/PriceListCondition/1337 +Authorization: Digest demo demo +Content-Type: application/json +``` + + +### Prices in all lists + +`/api/PriceListPrices` + +* `GET` Get all available price list prices + + - `limit`, `offset` + +```http request +GET http://10.222.222.30/api/PriceListPrices?limit=10 +Authorization: Digest demo demo +Content-Type: application/json + +{ + "filters": [ + { + "field-name": "id", + "value": 6, + "type": "eq" + } + ] +} +``` + +* `POST` Create one or more price list prices + + - `priceListPrices` + +```http request +POST http://10.222.222.30/api/PriceListPrices +Authorization: Digest demo demo +Content-Type: application/json + +{ + "prices": [ + { + "priceListId": 1, + "orderNumber": "SW10010", + "price": 3.78151260504201, + "pseudoPrice": 5.8823529411764595, + "from": 1, + "to": 1, + "validFrom": null, + "validTo": null, + "calculation": null, + "gross": false + } + ] +} +``` + +* `PUT` Update one or more price list prices + + - `priceListPrices` + +```http request +PUT http://10.222.222.30/api/PriceListPrices +Authorization: Digest demo demo +Content-Type: application/json + +{ + "prices": [ + { + "id": 1867, + "priceListId": 1, + "orderNumber": "SW10010", + "price": 3.78151260504201, + "pseudoPrice": 5.8823529411764595, + "from": 1, + "to": 1, + "validFrom": null, + "validTo": null, + "calculation": null, + "gross": true + } + ] +} +``` + +* `DELETE` Remove an amount of price list prices + + - `priceListPrices` + +```http request +DELETE http://10.222.222.30/api/PriceListPrices +Authorization: Digest demo demo +Content-Type: application/json + +{ + "prices": [ + { + "id": 1867 + } + ] +} +``` + +### Prices in a single list + +`/api/PriceListPrices/{id}` + +* `GET` get one price list prices + + - `id` + +```http request +GET http://10.222.222.30/api/PriceListPrices/1867 +Authorization: Digest demo demo +Content-Type: application/json +``` diff --git a/source/shopware-enterprise/pricing-engine/technical/index.md b/source/shopware-enterprise/pricing-engine/technical/index.md new file mode 100644 index 0000000000..3d71bd55c5 --- /dev/null +++ b/source/shopware-enterprise/pricing-engine/technical/index.md @@ -0,0 +1,165 @@ +--- +layout: default +title: Extension Guide +github_link: pricing-engine/technical/index.md +indexed: true +tags: [pricing engine, extension] +menu_title: Extension Guide +menu_order: 2 +group: Shopware Enterprise +subgroup: Pricing Engine +--- + +## General + +The Pricing Engine is designed to be highly flexible and extendable. So based on this purpose you have only to do three steps do create your own condition: + +* extends the CustomerContext +* decorate the ContextFactory +* create your own Condition and register it in the dependency injection container + +In the following example we will add a condition with is based on the firstname of the logged in customer. + +## Extending +First we have to extend the CustomerContext: + +```php +originalCustomerContext = $originalCustomerContext; + } + + public function getCustomerId(): int + { + return $this->originalCustomerContext->getCustomerId(); + } + // and so on to implement the original interface + + // our own condition method to provide the firstname + public function getCustomerFirstName(): string + { + $userData = Shopware()->Modules()->Admin()->sGetUserData(); + + return $userData['billingaddress']['firstname'] ?? ''; + } +} +``` + +After that we will decorate the ContextFactory to return our new `CustomerContextExtended` + +```php +firstName = $firstName; + } + + /** + * {@inheritdoc} + */ + public function checkValidity(ShopContextInterface $shopContext, CustomerContextInterface $customerContext): bool + { + return $customerContext->getCustomerFirstName() === $this->firstName; + } +} +``` + +At least we have to register our components in the dependency injection container: + +```xml + + + + + + + + + + + + + + + +``` + +## Conclusion +To add a own condition is quite simple. If you need the whole plugin, please have a look into our [example-plugins](https://gitlab.com/shopware/shopware/enterprise/swagenterprisepricingengine/-/tree/master/example-plugins/) directory. diff --git a/source/shopware-enterprise/pricing-engine/user-guide/index.md b/source/shopware-enterprise/pricing-engine/user-guide/index.md new file mode 100644 index 0000000000..6f93cb2cb2 --- /dev/null +++ b/source/shopware-enterprise/pricing-engine/user-guide/index.md @@ -0,0 +1,39 @@ +--- +layout: default +title: User Guide +github_link: pricing-engine/user-guide/index.md +indexed: true +tags: [pricing engine, guide, start, begin] +menu_title: User Guide +group: Shopware Enterprise +subgroup: Pricing Engine +menu_order: 1 +--- + +
    + +## General + +The Pricing Engine stores several prices in price lists which can be assigned to different contexts with conditions. The whole architecture is designed for use cases where an ERP or PIM system is managing the prices. So no backend view is present for adding, inserting or deleting prices. So the backend offers only a view to manage the conditions for the imported price lists. For controlling purposes there is a separate tab added in the product detail view. In this view you can see all configured prices for this product and filter it by the existing price lists. + +To import price lists and prices there is a powerful REST-Api present. You can find all definitions in our swagger.json [here](https://gitlab.com/shopware/shopware/enterprise/swagenterprisepricingengine/-/blob/master/swagger.json) + + +## Backend View +You can find the Pricelist backend view under Items -> Pricelists. + + +Now you will see an overview of all imported pricelists. All information about each pricelist is present there like name, priority, price and condition count. + + +With clicking on the edit button the Pricelistdetailview will open and you can see the created conditions for this list. + + + +In this view you are able to add, update and delete conditions for the previous selected price list. Below you can see an example how to add a new condition: + + + +If you want to check which prices are imported for a product, the Pricing Engine adds a new tab to the product detail view. In this tab you can search for prices and filter by given price lists: + + diff --git a/source/shopware-enterprise/search/configuration.md b/source/shopware-enterprise/search/configuration.md new file mode 100644 index 0000000000..465b3f0b13 --- /dev/null +++ b/source/shopware-enterprise/search/configuration.md @@ -0,0 +1,92 @@ +--- +layout: default +title: Configuration +github_link: search/configuration.md +indexed: true +menu_title: Configuration +group: Shopware Enterprise +subgroup: Enterprise Search +menu_order: 3 +--- + +Shopware Enterprise Search has comprehensive configuration options to configure the search as needed. + +
    + +## Basic settings +The basic settings allow some general configuration of the search: + +* Highlight search terms: Will highlight the search term in the search result +* Find products for the first suggestion: In addition to products matching the actual search term, SES will also show +products matching the first suggestion for the search term. So even if SES cannot find products for the search term +based on the search configuration, it will still be able to find products for a similar search term. This makes it less +likely that no products can be found. +* Content search - Number of results on search result page: Configure the number of search results per content type +* Content search - Number of results for ajax search: Configure the number of search results for the quick search +* Min score value for ajax and content search + +## Synonyms +Synonyms allow you to configure search terms which are considered to be synonym or should match the same result set, +e.g. "shoe" and "sneaker". Synonyms are managed in synonym groups, where all search terms in a synonym group are considered +synonyms. + +Additionally you can limit synonyms to certain shops and add a shopping world to the search result page of search terms +belonging to that synonym group. That way you can have your "shoe shopping world" appear on searches for "shoe" as well +as "sneaker" and "boot". + +## Relevance +Relevance defines the fields ElasticSearch will consider during a search as well as the relevance the have. Additionaly +you can modify how ElasticSearch handles those fields. + +* Name: A meaningful name for the relevance definition, e.g. "rank exact matches higher" +* Relevance: The boost ElasticSearch will apply to products matching the search term in the given field (1-100) +* Field: The field this definition applies for, e.g. "name" for the product name. Most of a product's fields are available +here, some might even appear multiple times, e.g. `name`, `name.german_analyzer` and `name.raw`. The suffix indicates, +how ElasticSearch handles the field: `name` might have some special characters removed, `name.german_analyzer` will +apply some optimizations for german language, `name.raw` does not have any optimizations. In addition to that +there are special fields you should consider: `attributes.enterprise.product_suggestion` will split compound words +such as "skyscraper" into separate words such as "sky" and "scraper". This is especially useful for language like e.g. german. +Also there is `attributes.enterprise.product_suggestion_compound` which will search the ngram index and also match "skyscraper" +for searches like "ysc". As this might massively lower the quality of the suggestions, we usually recommend to not use +this field, if it can be avoided. +* Fuzzyness: Number of letters that will be ignored / corrected by the search: + * `Auto`: Automatically determine the value depending on the length of the search string. Usually: 0 for 1-2 characters, 1 + for 3-5 characters and 2 for anything longer then that. + * `None`: No correction of letters + * `One`: One letter will be corrected + * `Two`: Two letters are corrected + * `Three`: Three letters are corrected +* Operator: Defined how the search term is compared to the field: `AND` will only match, if all search terms occur in the field, +`OR` matches, once one search term can be found in the field. `AND` is less likely - and therefor usually will get a higher +boosting than `OR`. E.g. By defining a higher rating for `AND`, a customer searching for "sky scraper" will first see +"sky scraper" and then "skyway" or "whisky". +* Type: Defines what is considered a match. `Default` will apply the default mechanisms of ElasticSearch, `phrase` will +force a whole word to match. `Phrase prefix` will allow to match a word, even if just the beginning matches, e.g. "skyscraper" for "sky". +* Maximum expansion: Only available for `phrase prefix`. Defines how many letters may follow after the beginning of the +word and still are considered a match. Searching "sky" will only match "skyscraper", if maximum expansion is at least 7 +(the number of letters in "scraper"). + +Generally you will always need multiple relevance definitions for several fields. Think of the relevance definitions as +usage scenarios: + +* If a word matches multiple terms in the name, rank it highest: `field: name, operator: and, relevance: 100` +* If one word matches exactly, rank it high: `field: name, operator: or, relevance: 90` +* Match medium, if part of a compound word matches: `field: attributes.enterprise.product_suggestion, relevance: 50` +* Match medium, if manufacturer matches partially: `field: manufacturer.name, operator: or, relevance: 50, type: phrase_prefix` + +Usually this is the most relevant part when setting SES for a shop: Usually customers want both: Very precise and good +rankings for products matching the term properly as well as fuzzy matches for less relevant searches. + + +## Boost +Boost behaves similar to the relevance configuration. They allow to define additional boosts for products which meet +certain criteria. This way you can configure, that a product is especially interesting, if it is e.g. a topsellet product. + +* Boosting name: Meaningful name for the boost definition, e.g. "boost topseller products" +* Relevance value: The value of the boosting (1-100) +* Table field: The boost will only apply, if the defined field of the product is `true`. E.g. `isTopSeller` or `isNew`. + +By default SES will only allow you to choose from boolean product and attributes fields, as these can easily be checked +for their state. In future versions this might be extended to also support arbitrary fields and apply rules on them, such +as `product.release_date < 2017/02/03`. Feel free to give us feedback, if this might be relevant for you. + diff --git a/source/shopware-enterprise/search/developer/and_or.md b/source/shopware-enterprise/search/developer/and_or.md new file mode 100644 index 0000000000..9cb45dbaef --- /dev/null +++ b/source/shopware-enterprise/search/developer/and_or.md @@ -0,0 +1,62 @@ +--- +layout: default +title: AND / OR search +github_link: search/developer/and_or.md +indexed: true +menu_title: AND / OR +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 4 +--- + +AND / OR searches are two typical search configurations: Given a search with multiple words (e.g. "samsung screen") an AND +search will only find products which matches both teams. An OR search, however, will find all products which match +at least one of these terms. +In practice an AND search is useful to find high quality matches: A customer searching for "samsung screen" does not +want to find all screens and all products from Samsung: He wants to find all screens from the brand Samsung. OR searches +on the other hand might be useful for searches such as "display monitor" where people want to find all products matching +one of the terms. + +## AND / OR searches in SES +The default configuration of SES includes both: Given a search like "samsung screen" SES will give products matching both +terms (AND) a relevance of 100. Products matching only one of the terms (OR) will get a relevance of 80. This way you +get best of both worlds: High quality matches will always be ranked first, less quality matches can still be found. + +## Changing the configuration +In the following examples two products "chair" and "clock" have been defined. +### OR +In the SES backend module only one rule is defined. The `operator` configuration is set to "OR": + + + +Searching in the frontend for "clock chair" both products can be found. + + + +### AND +Now only an AND rule is configured by setting the `operator` configuration to "AND": + + + +Now the search will not return any results, as there are no products with both words in the name: + + + +If there was a product such as "clock with chair motive" it would be found. + +### Recommendation +As described above, pure AND / OR searches for products can easily be configured in SES. In most cases, however, +the combined approach is more useful: It makes sure that AND matches are preferred while OR matches can still be found +as fallback. +Also notice that AND rules will only apply per field: So the "samsung screen" example will only apply as long as both terms are part +of the same field (e.g. the name). An AND search over multiple fields could be implemented, however, by decorating +`\SwagEnterpriseSearch\Bundle\SearchBundleES\SearchQueryBuilder::buildQuery` where the actual search queries are created. +Also you could index the both fields in question as one field and then configure it from the backend module. + +### Other entities +Content related search results (e.g. blog, categories, shopping worlds etc) are implemented using the `completion_suggestion` +functionality of ElasticSearch and cannot be configured from the backend module. The relevant logic for these search types can +be found in `\SwagEnterpriseSearch\Bundle\SearchBundle\SuggestionFacet` as well as `\SwagEnterpriseSearch\Bundle\SearchBundleES\SuggestionHandler`. + + diff --git a/source/shopware-enterprise/search/developer/boosting.md b/source/shopware-enterprise/search/developer/boosting.md new file mode 100644 index 0000000000..e206ae0354 --- /dev/null +++ b/source/shopware-enterprise/search/developer/boosting.md @@ -0,0 +1,187 @@ +--- +layout: default +title: Boosting +github_link: search/developer/boosting.md +indexed: true +menu_title: Boosting +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 3 +shopware_version: 1.2.0 +--- + +"Boosting" is the process on prioritizing certain search results over others. For example a shop owner might +want to boost products in order to free the stock for new products. Or during a promotion products from a certain +manufacturer should be boosted. + +
    + +## Boosting in SES + +Rules for boosting can be created from the SES backend module. The screenshot below shows the boosting configuration: + + + +The follwing fields are available: + + * `Boosting name`: Usually there will be multiple boosting, for each use case a seperate one. In order to tell apart the + various boostings, its recommended to choose a speaking name for each rule + * `Active`: SES will only apply boostings which are marked as "active" + * `Relevance`: The boost each product matching the rules will get. Usually a value between 1 and 100, higher values are also possible + * `Boosting rule`: Defines, which products should get the defined boosting. In the image above all products having the + "comming soon" flag set will get a boost of 10 + * `Valid from… till…`: Defines from when to when the boosting applies. This allows preparing future boostings / promotions + +## Technical overview + +Boostings are applied during query time, the main entry point is `\SwagEnterpriseSearch\Bundle\SearchBundleES\SearchQueryBuilder::applyBoosting`. +This will read all configured, active and valid boosting definitions from the database and apply them after they have been +converted to ElasticSearch queries by `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\BoostingQueryBuilder::getBoostingQuery`. +All rule types that are available in the backend module (`AND`, `OR`, `PRODUCT_COMPARE` and `TRUE`) have separate +handlers, such as `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\RuleHandler\AndRuleHandler` or +`\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\RuleHandler\ProductRuleHandler`. These will convert a definition +such as `['ProductCompareRule' => ['name', '=', 'table']` to an ElasticSearch query such as `BoolQuery(TermQuery('name', 'table'), BoolQuery::should)`. + +### Own rule types + +Own rule types can simply be registered through the DI container: + +``` + + + +``` + +The tag `enterprise_search.boosting.rule` will register the given service as a rule handler. + +The service now just needs to implement the interface `SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\OperatorResolverInterface`: + +``` +namespace SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\RuleHandler; + +use ONGR\ElasticsearchDSL\Query\MatchQuery; +use SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\OperatorResolverInterface; + +class MyType implements RuleHandlerInterface +{ + public function supports($name) + { + return $name === 'my_rule'; + } + + public function handle($name, $data, $nextRecursion, SearchContextInterface $context) + { + list($field, $operator, $value) = $data; + + // logic to convert the current instance into an ES query + + return $query; + } +} + +``` + +Your `supports` method should return `true`, if your service is able to handle the given rule type - `AND`, `OR` and +`PRODUCT_COMPARE` are the default ones. If the service returned `true` for a given rule type, the `handle` method will be called: + + * `name`: The rule type, "my_rule" in the above case + * `data`: Usually a tuple of three values: The field, the operator and the value. If your rule type is more like a container + type (such as `AND`), `$data` will just contain more nested definitions. + * `nextRecursion`: A callback method you can call in order to let other handlers handle the inner data of your rule type. Again + this mostly applies for container types (such as `AND`). + * `$context`: A context object with the current search term and the `ShopContext` object + +The following examples show, how SES uses the `handle` method by itself: + +``` +// \SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\RuleHandler\AndRuleHandler +public function handle($name, $data, $nextRecursion, SearchContextInterface $context) +{ + $boolQuery = new BoolQuery(); + $result = $nextRecursion($data); + foreach ($result as $item) { + $boolQuery->add($item, BoolQuery::MUST); + } + if (count($boolQuery->getQueries())) { + $boolQuery->addParameter('boost', 1 / (count($boolQuery->getQueries()))); + } + + return $boolQuery; +} +``` + +The `AND` rule handler will create an ElasticSearch `BoolQuery`. Then it will call `$nextRecursion` so that all data +inside the `AND` rule is resolved. The return values are than added to `BoolQuery`. The definition `BoolQuery::MUST` +defines, that all sub-conditions need to evaluate to `true` if the current rule should apply. + +Please notice, that ElasticSearch will add the boosting to each matching subrule. For that reason you need to devide `boost` +by the number of subrules: + +``` +if (count($boolQuery->getQueries())) { + $boolQuery->addParameter('boost', 1 / (count($boolQuery->getQueries()))); +} +``` + +### Own operators +Operators (such as `=` or `>=`) are handled by the `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\OperatorResolver`. +The `OperatorResolver` will translate all operator definitions to the corresponding ElasticSearch queries: +The `>=` operator, for example, is converted to a `new RangeQuery($field, ['gte' => $value])` object. More complex +operators such as "notcontains" will be converted into a `MUST_NOT` `BoolQuery` with an inner `MatchQuery`: + +``` +$query = new BoolQuery(); +$query->add(new MatchQuery($field, $value), BoolQuery::MUST_NOT); +``` + +Own operators can simply be implemented by decorating `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\OperatorResolver` +and implementing `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\OperatorResolverInterface`. +In your decorator just overwrite the method `getQuery`: If your own operator is passed, handle it by yourself, if another +operator is passed, just pass it to the decorated service. + +### ValueSearch +In the SES boosting dialog the shop owner is able to search values for the boosting rules. This way he does not need +to remember IDs or exact product names - he can just search them in the dialog. +During this search SES will pass the current field name (e.g. "name") and the search term (e.g. "summer") to the +`\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\ValueSearch\ValueSearch` service. The `ValueSearch` +service will then iterate all registered services implementing the interface `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Boosting\ValueSearch\ValueSearchInterface`. + +Again each of this services has a `supports` method which sould return `true`, if the given field (e.g. "name") is +supported. Then the `findValues` method is called. The service should return a list of values for the given field +which match the given search term. + +``` +public function findValues($field, $searchQuery): array +{ + switch ($field) { + case 'categoryIds': + $sql = 'SELECT `id`, `description` FROM s_categories WHERE `description` LIKE :search'; + break; + case 'id': + $sql = 'SELECT `id`, `name` FROM s_articles WHERE `name` LIKE :search'; + break; + } + + + $result = $this->connection->fetchAll($sql, ['search' => '%' . $searchQuery . '%']); + + return $result; +} +``` + +The returned array should look like this: + +``` +[ + [1, "name"], + [2, "other name"] +] +``` + +The first value *must* be the value of the field requested, in this case the ID of the category or product. All other +fields have just informational purpose for the user and should usually include the searched fields as well as other +relevant information such as the product number etc. + +Own handlers for certain value types can be registered using the tag `enterprise_search.value_search`. Optionally +a `priority` can be set if you need to overwrite some default handlers, for example. diff --git a/source/shopware-enterprise/search/developer/compound_words.md b/source/shopware-enterprise/search/developer/compound_words.md new file mode 100644 index 0000000000..0c9f5a7673 --- /dev/null +++ b/source/shopware-enterprise/search/developer/compound_words.md @@ -0,0 +1,87 @@ +--- +layout: default +title: Handling Compound Words +github_link: search/developer/compound_words.md +indexed: true +menu_title: Handling Compound Words +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 4 +--- + +Compound words (such as "skyscraper") are words actually compound from two or more other words ("sky" and "scraper"). +For searching these kind of words are quite relevant, as most search engines will easily be able to look up words +beginning with a string ("sky") but not with a string at other places (such as "scraper" in "skyscraper"). + +For SES there are several mechanisms in place to handle this kind of words. + +
    + +## Dictionaries +When indexing your product catalogue, SES will search for language dictionaries at several places. By default +it will handle dictionaries in `/usr/share/hunspell/` and `%PLUGIN_DIR%/Resources/assets/`. Furthermore +you are able to maintain custom dictionaries in `%PLUGIN_DIR%/Resources/assets/custom_dictionaries/`. + +During the indexing process, SES will build the [ngram](https://en.wikipedia.org/wiki/N-gram) of each indexable field +and will compare each ngram with the dictionary. This will make indexing a bit slower - but makes sure that only high +quality search terms are indexed. With this mechanism, "skyscaper" can be decompound into "sky" and "scraper" and therefore +be indexed. + +### Dictionary format +By default SES expects the dictionaries to be available in the three folders mentioned above. Each dictionary should +be named by the convention `%LANGUAGE_CODE%.dic`, for example `de_DE.dic`. Within the dictionary SES expects the words +to be line separated, for example: + +``` +sky +scraper +``` + +### Extending the dictionary +The default dictionaries SES considers are the dictionaries provided by hunspell. SES also comes with dictionaries +for de_DE and en_GB if hunspell is not available on your system. In order to extend these dictionaries or add custom +ones, you can just create the corresponding files in `%PLUGIN_DIR%/Resources/assets/custom_dictionaries/`. SES will +handle those files as *additions* to the default dictionaries. For that reason there should be no need to modify +any other dictionary files in SES directly. + +### Custom dictionary handler +If you want to provide dictionaries in another format, you can do so by implementing the interface `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Dictionary\DictionaryInterface`. +Then just set your implementation into `\SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\Dictionary\DictionaryManager` using +`setDictionaries`. `DictionaryManager` will - for every given language key - search for an implementation supporting +the given language key. The first implementation matching, will be responsible for handling the language in question. + +If you want your implementation to be considered before SES's default dictionary implementation, you should make it the +first one in the array passed to `setDictionaries`. + +### Using the dictionary index +In order to use the decompound words from the dictionaries, make use of the search field `attributes.enterprise.product_suggestion` +in the relevance configuration of SES. + +## n-gram +ElasticSearch also allows to index [n-grams](https://en.wikipedia.org/wiki/N-gram) directly. On the one hand, this +will not require an additional lookup against dictionaries while indexing, on the other hand, this will massively bloat +the search index with non meaningful search terms such as "ysc" or "ape" for "skyscraper". +By default Shopware will index ngrams with a width from 3 to 8. If you want to change this (to have larger ngrams or to +turn them of generally in order to speed up indexing), you can do this as follows: + +``` +// config.php +return [ + 'db' => // your database config, + 'es' => [ + // your default ES config + 'compound_filter' => [ + 'enabled' => true, // enable / disable ngrams + 'type' => 'ngram', // type of ngram, e.g. 'ngram' or 'edge_ngram' + 'min_gram' => 3, // minimum width + 'max_gram' => 8, // maximum width + 'token_chars' => ['letter'], // characters to tokenize + ], + ] +]; + +``` + +### Using n-grams +In order to use the n-gram index, make use of the `*.ngram` fields in the relevance configuration of SES. diff --git a/source/shopware-enterprise/search/developer/environment.md b/source/shopware-enterprise/search/developer/environment.md new file mode 100644 index 0000000000..e059993a85 --- /dev/null +++ b/source/shopware-enterprise/search/developer/environment.md @@ -0,0 +1,40 @@ +--- +layout: default +title: The Development Environment +github_link: search/developer/environment.md +indexed: true +menu_title: Development Environment +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 2 +--- + +## The development environment +The [Github repo of SES](https://gitlab.com/shopware/shopware/enterprise/swagenterprisesearch) contains the full development stack for SES. This +is useful for developing SES itself - or just for trying it out quickly. + +After checking out the repository, step into the SES directory: + +``` +git clone git@github.com:shopware/SwagEnterpriseSearch.git +cd swagenterprisesearch +``` + +Now setup the docker environment using the build script: + +```./psh.phar docker:start``` + +This process might take several minutes and will setup docker containers for shopware, MySQL and ElasticSearch. In +order to install shopware with SES in docker, you need to SSH into the container and run the corrsponding build script: + +``` +./psh.phar docker:ssh +./psh.phar init +``` + +After this, populate the ES index with `./psh.phar es-index`. After doing so the shop can be reached at `10.100.150.46` +on your local machine. + +### Unit tests +In order to run unit tests, execute `./psh.phar unit-fast`. The coverage is built using `./psh.phar coverage`. diff --git a/source/shopware-enterprise/search/developer/index.html b/source/shopware-enterprise/search/developer/index.html new file mode 100644 index 0000000000..12b7e31cc1 --- /dev/null +++ b/source/shopware-enterprise/search/developer/index.html @@ -0,0 +1,19 @@ +--- +layout: default +title: Developer +github_link: search/developer +indexed: true +menu_title: Developer +menu_order: 3 +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +--- + + diff --git a/source/shopware-enterprise/search/developer/indexing.md b/source/shopware-enterprise/search/developer/indexing.md new file mode 100644 index 0000000000..b4643ccd00 --- /dev/null +++ b/source/shopware-enterprise/search/developer/indexing.md @@ -0,0 +1,18 @@ +--- +layout: default +title: Indexing Additional Data +github_link: search/developer/indexing.md +indexed: true +menu_title: Indexing Additional Data +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 3 +--- + +For content search Shopware Enterprise Search does index additional data such as blogs, categories, static pages, +shopping worlds and manufacturers. In order to index additional entities, please follow the [corresponding guide](https://developers.shopware.com/developers-guide/elasticsearch/#indexing-additional-data) +in out devdocs. + +After doing so, the default search extension system of Shopware applies: By adding a custom condition to your search +you will be able to perform additional queries to ElasticSearch with your custom content pages as [described here](https://developers.shopware.com/developers-guide/elasticsearch/#extend-product-search-query). diff --git a/source/shopware-enterprise/search/developer/overview.md b/source/shopware-enterprise/search/developer/overview.md new file mode 100644 index 0000000000..14d14bd02f --- /dev/null +++ b/source/shopware-enterprise/search/developer/overview.md @@ -0,0 +1,134 @@ +--- +layout: default +title: Overview +github_link: search/developer/overview.md +indexed: true +menu_title: Overview +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 1 +--- + +This document will present the big picture of how data is indexed and searched in SES. + +
    + + +## Bundle Structure +SES replicates the Shopware Bundle structure to a certain level: + + * `ESIndexingBundle`: Services for indexing data + * `SearchBundle`: General components which are not necessarily bound to ElasticSearch such as `Facet` and `FacetResult` objects + * `SearchBundleES`: Components specific to ES, such as `FacetHandler` + +Services specific to SES can be found in `EnterpriseSearchBundle`: + + * `AlternativeTerm`: Performs an additional ES search in order to find alternative search terms for the given search term + * `Dictionary`: Provides language specific dictionaries used during indexing + * `Explain`: Enables the ES "Explain" functionality for the preview search in Shopware's backend + * `HistoryBoosting`: Defines the amount of boost for certain fields + * `ImportExport`: Import/Export functionality for settings + * `SearchConfig`: Representation of the backend search configuration (e.g. relevance, boosting…) + * `Session`: Session Wrapper for the Shopware Session which is easier to inject / test + * `SynonymSearch`: Perform searches for synonyms + + +## Indexing data +Indexing describes the process of making data from Shopware available in ElasticSearch and keeping it up to date. + +### Indexing components +SES adds various content pages to the Shopware search. For that reason, it provides additional indexer for blogs, +shopping worlds, categories, static pages, manufacturers and synonyms. All of these services can be found in +`SwagEnterpriseSearch\Bundle\ESIndexingBundle`. The main entry point of each of these components is the so called `DataIndexer`, +which is registered in the DI container with the tag `shopware_elastic_search.data_indexer`. It will either index all entities +of a given type (e.g. blogs) in the `populate` method for full updates or just index certain entities of a given type in +the `index` method for partial updates. + +Usually every `DataIndexer` will have a method called `createQuery` which reads all affected IDs for the full index. The +`Provider` service is then used to read the actual data for that entity, e.g. "name", "author" and "content" for a blog. +At this point every `Provider` needs to make sure, that all relevant information for the frontend are indexed into ElasticSearch, +so that no additional queries are needed in the frontend in order to fetch e.g. URLs, images etc. + +Also every component has a `Mapping` service registered to the DI tag `shopware_elastic_search.mapping`. It provides +the ElasticSearch mapping data, such as "id is an integer" or "description is an english text field". + +The so called `SuggestionBuilder` of each component is responsible for providing the search terms each entity will match +to and also provides the suggestions being shown as "search term suggestions" of the ajax search. Usually the `SuggestionBuilder` +will make use of the `SuggestionStringExploder` service, which will split compound words into individual words [based +on dictionaries](/search/developer/compound_words/). + + +### How indexing is triggered +By default Shopware provides commands such as `sw:es:index:populate` (full index) and `sw:es:backlog:sync` (partial update +usually run by a cronjob). These will automatically apply for SES as well. Full indexes are handled by the `populate` method +of the `DataIndexer` services, partial updates are handled by the `synchronize` method of the `Synchronizer` services. +Shopware will pass all current indexing backlog entries to these services which will then extract the IDs of those entries, +which are handled by the current services. So the `BlogSynchronizer` will only handle backlogs of the type `blog`. The +extracted IDs will then be passed to the `index` method of the `DataIndexer`. + +In order to recognize which entities needs to be re-indexed, the class `SwagEnterpriseSearch\Subscriber\ORMBacklogSubscriber` +will register to all lifecycle events of the handled content types (blogs, categories etc) and write the corresponding +backlog entries for those. + +### Immediate Indexing +If immediate indexing is enabled, SES will index changed entities right away +and not wait for a cronjob to run. + +```php +return [ + 'db' => [...], + 'es' => [ + 'immediate_index' => true; + ... + ], +]; +``` + +## Search +The following section describes, how SES extends Shopware in order to make the content search available and how +SES applies the search configuration. + +### General pattern +All additional information provided by SES (such as suggestions and content pages) are added by the `SwagEnterpriseSearch\Bundle\SearchBundle\SuggestionFacet` +and its handler `SwagEnterpriseSearch\Bundle\SearchBundleES\SuggestionHandler`. The handler adds +the corresponding suggestions queries to the main ElasticSearch query in the `handle` method and hydrates the +results into `SwagEnterpriseSearch\Bundle\SearchBundle\SuggestionFacetResult`. For that reason `SuggestionFacetResult` +contains all non-product search results such as search suggestions, blogs, manufacturers, categories, shopping worlds +and static pages. + +The backend configuration of the search (such as relevance fields and boostings) are applied by `SwagEnterpriseSearch\Bundle\SearchBundleES\SearchQueryBuilder`. +This service decorates the default `shopware_search_es.search_term_query_builder` service and also adds the "auto suggest" +and the "history boosting" functionality. + +So roughly speaking the `SuggestionFacet` and its handler are responsible for the suggest search (including content suggestions), +the `SearchQueryBuilder` is responsible for extending the default product search by the SES features and configurations. + +### Ajax search +The main entry point of the ajax search is `SwagEnterpriseSearch/Controllers/Widgets/Suggest.php`. It triggers +a search using `Shopware\Bundle\SearchBundle\ProductNumberSearchInterface::search` after it added the `SuggestionFacet` +to the `Condition` object. The rest is handled by `SearchQueryBuilder` and `SuggestionHandler` as described above. +As quick responses are key for a "search as you type" functionality, the ajax search disables the template engine and prints +out a JSON representation of the search directly. The actual rendering of the results to the search overlay is performed +by the JavaScript stack. + +### Search result page +The search result page generally operates by the same patterns: The `SuggestionFacet` is added by the +`SwagEnterpriseSearch\Bundle\SearchBundle\CriteriaRequestHandler`, so all content hits are also available by the `SuggestionFacetResult`. +Additionally it adds a `SwagEnterpriseSearch\Bundle\SearchBundle\SynonymFacet` to the `Criteria` object. The corresponding +`SwagEnterpriseSearch\Bundle\SearchBundleES\SynonymHandler` will then add a `SwagEnterpriseSearch\Bundle\SearchBundle\SynonymFacetResult` +to the result, if a matching synonym group for the current search term was found. The `SynonymFacetResult` is then used +to display shopping worlds or product streams for the current search, if configured. + +You should notice, however, that SES replaces the default search controller in `SwagEnterpriseSearch/Controllers/Frontend/Search.php`. +This is needed, as configured SynonymGroups might replace the entire search result with a product stream or redirect the +user to another page, if `RedirectURL` was defined. For that reason, SES will perform a lookup for a matching SynonymGroup +before hands and only triggers the default search as described above, if no redirect and no product stream are configured +for the current SynonymGroup. + + +### Index time vs search time +Dealing with ElasticSearch there are usually concerns handled while indexing and concerns handled during the actual search. +Roughly speaking you rather want your indexing to be slow than your search. For that reason, compound words, ngrams and +synonyms are dealt with while indexing. +Concerns such as relevance, boosting, auto suggest and history boosting are applied while searching as described above. diff --git a/source/shopware-enterprise/search/developer/redirects.md b/source/shopware-enterprise/search/developer/redirects.md new file mode 100644 index 0000000000..217fef53da --- /dev/null +++ b/source/shopware-enterprise/search/developer/redirects.md @@ -0,0 +1,178 @@ +--- +layout: default +title: Search Redirects +github_link: search/developer/redirects.md +indexed: true +menu_title: Search Redirects +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 5 +--- + +SES will perform redirects for search terms, if + + * the search term belongs to an active synonym group with a redirect defined + * the search term is the product number of an existing product + +You can easily add additional logic to also perform redirects e.g. for EAN or catalog numbers. + +## General mechanism + +SES has a redirect handler, which is called in the frontend search controller: + +``` +class SearchRedirectHandler +{ + public function getRedirect($searchTerm, SynonymStruct $synonym = null) + { + /** @var SearchRedirectInterface $redirect */ + foreach ($this->redirectServices as $redirect) { + + if ($url = $redirect->getRedirect($searchTerm, $synonym)) { + return $url; + } + + } + + return null; + } +} +``` + +It will iterate all redirect implementations and return the first URL which is returned by one of the implementations. +Each implementation has to implement `SearchRedirectInterface`: + +``` +interface SearchRedirectInterface +{ + /** + * Returns a redirect URL if a redirect should be performed - or NULL if no redirect can be determined in the + * current implementation + * + * @param $searchTerm + * @param SynonymStruct|null $synonym + * @return string|null + */ + public function getRedirect($searchTerm, SynonymStruct $synonym = null); +} +``` + +The synonym redirect implementation, for example, is quite simple: + +``` +// \SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\SearchRedirect\SynonymRedirect +class SynonymRedirect implements SearchRedirectInterface +{ + public function getRedirect($searchTerm, SynonymStruct $synonym = null) + { + if ($synonym && $synonym->getRedirectUrl()) { + return $synonym->getRedirectUrl(); + } + } +} +``` + +It will just check, if a valid synonym was passed and if it contains a redirect - and return that if it does. +Other implementations could be more complex, such as the `ProductNumberRedirect`: + +``` +// \SwagEnterpriseSearch\Bundle\EnterpriseSearchBundle\SearchRedirect\ProductNumberRedirect +class ProductNumberRedirect implements SearchRedirectInterface +{ + private $connection; + private $router; + private $contextService; + + public function __construct(Connection $connection, $router, ContextServiceInterface $contextService) { … } + + public function getRedirect($searchTerm, SynonymStruct $synonym = null) + { + $result = $this->getArticleByNumber($searchTerm); + + if (!$result) { + return; + } + + $assembleParams = [ + 'sViewport' => 'detail', + 'sArticle' => $result['articleId'], + ]; + + // if variant is not the main variant, add the number to the URL + if ($result['kind'] != 1) { + $assembleParams['number'] = $result['number']; + } + + return $this->router->assemble($assembleParams); + } + + private function getArticleByNumber($searchTerm) + { + $result = $this->connection->fetchAll( + 'SELECT + ad.ordernumber as number, + ad.articleID as articleId, + ad.kind + FROM s_articles_details ad + + INNER JOIN s_categories c + ON c.id = :mainCategory + AND c.active = 1 + + INNER JOIN s_articles_categories_ro ro + ON ro.articleID = ad.articleID + AND ro.categoryID = c.id + + + WHERE `ordernumber` LIKE :number + LIMIT 1', + ['number' => $searchTerm, 'mainCategory' => $this->contextService->getShopContext()->getShop()->getCategory( + )->getId()] + ); + + return array_shift($result); + } +} +``` + +In this case the service will query the database in order to find a matching product and then assemble a SEO url which is returned. + +## Implementing own redirect services +If you need to implement own redirect services (e.g. for catalog numbers which are stored as product attributes), +you just need to create a service implementing `SearchRedirectInterface` as described above. + +Then register it in your `services.xml` using the tag `enterprise_search.redirect`. This could look like this: + +``` + + + + +``` + +The tag will make sure, that your service is handled by `SearchRedirectHandler`. + +## Priority +As `SearchRedirectHandler` will only handle the first URL returned by one of the implementations, the order of the services +is critical: Do you want product numbers or catalog numbers to be more important? +For that reason, the `enterprise_search.redirect` tag support priorities: The higher the priority is, the earlier the +corresponding handler will be executed. + +``` + + + + + + + + + + + +``` + +As you can see, `ProductNumberRedirect` has a priority of 20, `SynonymRedirect` has a priority of 10. In order to make +your `CatalogRedirect` more important thant the `ProductNumberRedirect` you need to give it at least a priority of 21. +If you want to place it between `ProductNumberRedirect` and `SynonymRedirect`, it needs a priority between 10 and 20. diff --git a/source/shopware-enterprise/search/developer/stopwords_stemmer.md b/source/shopware-enterprise/search/developer/stopwords_stemmer.md new file mode 100644 index 0000000000..da26898c69 --- /dev/null +++ b/source/shopware-enterprise/search/developer/stopwords_stemmer.md @@ -0,0 +1,86 @@ +--- +layout: default +title: Stopwords and Stemmer +github_link: search/developer/stopwords_stemmer.md +indexed: true +menu_title: Stopwords & Stemmer +group: Shopware Enterprise +subgroup: Enterprise Search +subsubgroup: Developer +menu_order: 2 +shopware_version: 1.0.1 +--- + +Stopwords are common words in a language, that to not add any relevance to a document but will bloat the search index +and return less meaningful results for searches. Examples are "the" or "a" in english language. +Stemmer are language specific rules that reduce a word to its basic form, such as "men => man" or "houses => house". Stemmer +usually ensures, that searches will find results, even if the grammatical number or case of the search term does not match +the grammatical number / case of the indexed term. + +## Modify stemmer / stopwords +Stemmer as well as stopwords are configured while indexing in `\SwagEnterpriseSearch\Bundle\ESIndexingBundle\IndexingSettings\Settings`. +For each shop SES will find the corresponding language and choose stopwords / stemmer based on the language locale. ElasticSearch +has a lot of stemmer / stopword filters built in. In `\SwagEnterpriseSearch\Bundle\ESIndexingBundle\IndexingSettings\ElasticMapping` +SES maintains a mapping list in order to map language locales to the corresponding ElasticSearch stopword / stemmer configuration: + +``` +class ElasticMapping implements ElasticMappingInterface +{ + public function getStopwordMapping(): array + { + return [ + 'de' => '_german_', + 'en' => '_english_', + 'fr' => '_french_', + 'nl' => '_dutch_', + 'it' => '_italian_', + ]; + } + + public function getStemmerMapping(): array + { + return [ + 'de' => 'light_german', + 'en' => 'english', + 'fr' => 'light_french', + 'nl' => 'dutch', + 'it' => 'light_italian', + + ]; + } +} +``` + +In order to change or extend this mapping list, you can decorate the `ElasticMapping` service and modify the stopword +mapping `getStopwordMapping` or the stemmer mapping `getStemmerMapping`. + +## Own stopwords list +By default SES will use the default stopword list of ElasticSearch as mapped in `ElasticMapping`. However, you are able +to overwrite stopwords per language. In order to do so, configure a stopword directory in your `config.php`: + +``` +// your default config.php content +// … +'es' => [ + // your default ES configuration + // … + 'stopword_directory' => '/var/www/stopwords/' +], +``` + +In this directory now create per-language stopwords files such as `en.txt`. Each line should hold one stopword: + +``` +# file: /var/www/stopwords/en.txt + +these +are +four +stopwords +``` + +For more details of the implementation see `\SwagEnterpriseSearch\Bundle\ESIndexingBundle\IndexingSettings\Stopwords`. + +
    +Notice: As stopwords and stemmer are configured while indexing, changes will only apply after a full re-index. +
    diff --git a/source/shopware-enterprise/search/example-plugins/index.md b/source/shopware-enterprise/search/example-plugins/index.md new file mode 100644 index 0000000000..f95e046b18 --- /dev/null +++ b/source/shopware-enterprise/search/example-plugins/index.md @@ -0,0 +1,19 @@ +--- +layout: default +title: Example Plugins +github_link: search/example-plugins/index.md +indexed: true +tags: [search, example plugins] +menu_title: Example Plugins +menu_order: 5 +group: Shopware Enterprise +subgroup: Enterprise Search +--- + +## General + +In our documentation, we provide several example plugins. In the following you can find a list with further descriptions: + + Plugin | Description + ------------------------------------------------------------------- |:-------------------------------------------------------------------------------------------- + [SesVariantSearch](/exampleplugins/SesVariantSearch.zip) | Show Variants in the search diff --git a/source/shopware-enterprise/search/features.md b/source/shopware-enterprise/search/features.md new file mode 100644 index 0000000000..c468998f4b --- /dev/null +++ b/source/shopware-enterprise/search/features.md @@ -0,0 +1,129 @@ +--- +layout: default +title: Features +github_link: search/features.md +indexed: true +menu_title: Features +group: Shopware Enterprise +subgroup: Enterprise Search +menu_order: 1 +--- + +Overview of the feature set of Shopware Enterprise Search (SES) + +
    + +## Content search +SES does not only searches the default product catalog: It will also search content pages such as: + +* Shopping worlds +* Static pages +* Manufacturers +* Categories +* Blog postings + +Content pages matching the user's search term will be shown in the quick search as well as on the search result page. + + + +## Performance +SES makes use of ElasticSearch - a well known high performing, scalable search engine. For that +reason Shopware Enterprise Search will search million of products in short time. This will take load from your database +and therefore improve the scalability of your overall server setup. + +The good performance of Shopware Enterprise Search especially can be seen in our quick search functionality: Every key +stroke will show the users better suggestions, content pages and products for his search - with almost no impact to +the database. + +## Behavioural boosting +Behavioural boosting will track the customer journey and boost products depending on the customer's behaviour. +Viewing a product, browsing a category or filtering for manufacturers or properties will in future boost products, +which matches these criteria. So if a customer filtered for t-shirts in "XL" and "red", products matching these criteria +will get an improved ranking. + + +## Synonyms +Synonyms allow you to define groups of words, which are considered to be synonym: So searching for "car" will also find +products which are only optimized for "vehicle". + + + +## Shopping worlds +With SES you can define shopping worlds for certain searches. Therefore you are able to prepare high quality search +result pages in order to optimize conversion for certain parts of your catalogue. + + + +## Understanding your words +In addition to fuzzy searches (searches being tolerant to typos to a certain degree) SES is also able to split compound +words into individual words, which is especially relevant for german languages. Imagine a product called "skyscraper": +SES "understands", that this word is compound from "sky" and "scraper" - and will find it without falling back to fuzzy +mechanisms. This massively improves the quality of your search results in comparison to mechanisms such as +[n-gram](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-ngram-tokenizer.html). + +## Helps you optimizing your searches +The preview functionality will not only allow you to quickly check certain searches - it also helps you to understand, +why certain results are ranked higher than others. + + + +## Highly configurable +With SES the search can be configured for the customer's needs. Highlighting of search terms and number of results +per content type can be configured, of course. Also the minimum ranking value can be determined. + + + +A suggestion blacklist and the minimum word length for suggestions can be configured in the indexing settings tab. + + + + +### Relevance +Additionally you can define the relevance of each +product's field individually and specify fuziness, search operators and type of search to be performed. So a product +matching a search term perfectly will get a better ranking than a product where the search term needs to substitute a letter. +Products matching multiple search terms will get a better ranking than products matching just a few words. And a product +where the search term matches manufacturer as well as product name will get a better ranking than products only matching +one of both. + + + + +### Boosting +Usually you want to boost certain groups of items. In addition to the relevance system, you can boost items matching +certain criteria, e.g. topseller items, items from certain manufacturers or categories etc. Of course custom attributes / free +text fields are also supported. + + + +Boosting rules can even be nested, so that you are able to create boosting rules such as: + +``` +IF + product.manufacturer = Samsung + AND + product.name CONTAINS "Smartphone" +OR + product.highlight = TRUE + OR + product.attr17 = promote + AND + product.inStock > 100 +``` + +Furthermore each boosting can be applied for certain period of times, so that you are able to create boostings +for e.g. black friday promotions beforehand and only have them active from friday to saturday evening, for example. + +### Per-entity optimization +In addition to that, any searchable content (products, shopping worlds, static pages, categories etc) can be optimized +individually: So you can define, that your product "red glove" will get an especially good ranking for "glove", e.g. for +sale. On the other hand you can also exclude content individually - for example, if you are having a sales promotion on +"red glove" and don't want other gloves to show up in the search. + + + +### Profiles +Profiles help you to organize sets of configuration: You can have different profiles for different shops or even prepare +configurations for certain promotions and activate them with just a single mouse click. + + diff --git a/source/shopware-enterprise/search/index.html b/source/shopware-enterprise/search/index.html new file mode 100755 index 0000000000..a8079f3ce8 --- /dev/null +++ b/source/shopware-enterprise/search/index.html @@ -0,0 +1,43 @@ +--- +layout: default +title: Enterprise Search +github_link: search/index.html +indexed: true +menu_title: Enterprise Search +group: Shopware Enterprise +menu_order: 20 +menu_style: bullet +menu_chapter: true +--- + + + +

    Enterprise Search

    +

    Shopware Enterprise Search is an advanced search module using ElasticSearch. In addition to a high performance +product search, it also offers search for content pages such as shopping worlds, blogs, manufacturers, static pages and +categories. The simple backend module allows quick and easy configuration of the search.

    + + +

    Features

    + + +

    Setup

    + + + +

    Developer

    + diff --git a/source/shopware-enterprise/search/installation.md b/source/shopware-enterprise/search/installation.md new file mode 100644 index 0000000000..ae2e790758 --- /dev/null +++ b/source/shopware-enterprise/search/installation.md @@ -0,0 +1,25 @@ +--- +layout: default +title: Installing the SwagEnterpriseSearch +github_link: search/installation.md +indexed: true +menu_title: Installation +group: Shopware Enterprise +subgroup: Enterprise Search +menu_order: 2 +--- + +## Installation + +##### Requirements + +- PHP 7 or later. +- Shopware version 5.2.19 or later +- Elasticsearch 5.* (Elasticsearch 6.0 since Shopware version 5.5.0 or later) + +##### Installation + +First you have to configure your elasticsearch like it is described [here](https://developers.shopware.com/sysadmins-guide/elasticsearch-setup/#elasticsearch-installation-and-configuration). +After configuration, SES can be installed as every other plugin in Shopware. You can find the plugin's source code [here](https://gitlab.com/shopware/shopware/enterprise/swagenterprisesearch/-/tree/major/components/SwagEnterpriseSearch). + +After indexing the product catalogue with `php bin/console sw:es:index:populate` the search can be used. diff --git a/source/sysadmins-guide/elasticsearch-setup/index.md b/source/sysadmins-guide/elasticsearch-setup/index.md index 3efffd7fce..91772c59d7 100644 --- a/source/sysadmins-guide/elasticsearch-setup/index.md +++ b/source/sysadmins-guide/elasticsearch-setup/index.md @@ -6,52 +6,94 @@ shopware_version: 5.1.0 tags: - performance - elasticsearch -redirect: - - sysadmins-guide/elastic-search-setup/ indexed: true group: System Guides menu_title: Elasticsearch setup -menu_order: 50 +menu_order: 60 ---
    ## Introduction -For shops that contain millions of different products, Shopware 5 requires an alternative to MySQL to be able to provide an optimal user experience in terms of both functionality and speed. +For shops that contain millions of different products, +Shopware 5 requires an alternative to MySQL to be able to provide an optimal user experience in terms of both functionality and speed. -[Elasticsearch](https://www.elastic.co/products/elasticsearch) is an open source search engine, built to handle such scenarios, where a set of millions of entries needs to be queried in just a few milliseconds. +[Elasticsearch](https://www.elastic.co/products/elasticsearch) is an open source search engine, +built to handle such scenarios, where a set of millions of entries needs to be queried in just a few milliseconds. Shopware 5.0 is able to provide a seamless Elasticsearch integration that will greatly benefit those shops. ## Installation and configuration To enable Elasticsearch integration, you must configure both your server and your Shopware 5 installation. -Elasticsearch 2.0 or newer is required. +Since Shopware 5.7 a minimum Version of Elasticsearch 7 is required. ### Elasticsearch installation and configuration -Elasticsearch installation and configuration greatly depends on your operating system and hosting provider. You will find extensive documentation online regarding the installation and configuration of Elasticsearch on most common Linux distributions. Some hosting providers might also provide specific documentation regarding this subject. Installation on Mac OSX or Windows is also possible, but not officially supported. +Elasticsearch installation and configuration greatly depends on your operating system and hosting provider. +You will find extensive documentation online regarding the installation and configuration of Elasticsearch on most common Linux distributions. +Some hosting providers might also provide specific documentation regarding this subject. +Installation on Mac OSX or Windows is also possible, but not officially supported. -The current Shopware 5 integration is designed to work with the out-of-the-box configuration of Elasticsearch. This does not mean, of course, that these are the best settings for a production environment. Although they will affect performance and security, the settings you choose to use on your Elasticsearch setup will be mostly transparent to your Shopware installation. The best setting constellation for your shop will greatly depend on your server setup, number and structure of products, replication requirements , to name a few. These settings fall out of the scope of this document, but you can refer to the official [Elasticsearch documentation page](https://www.elastic.co/guide/index.html) for more info. +The current Shopware 5 integration is designed to work with the out-of-the-box configuration of Elasticsearch. +This does not mean, of course, that these are the best settings for a production environment. +Although they will affect performance and security, the settings you choose to use on your Elasticsearch setup will be mostly transparent to your Shopware installation. +The best setting constellation for your shop will greatly depend on your server setup, number and structure of products, replication requirements , to name a few. +These settings fall out of the scope of this document, but you can refer to the official [Elasticsearch documentation page](https://www.elastic.co/guide/index.html) for more info. ### Shopware configuration -By default, Elasticsearch integration is disabled in Shopware, as most shops won't benefit from it. Like mentioned before, Elasticsearch should only be used in shops containing a large set of items. +By default, Elasticsearch integration is disabled in Shopware, as most shops won't benefit from it. +Like mentioned before, Elasticsearch should only be used in shops containing a large set of items. To enable Elasticsearch (provided it's already installed, configured and running), edit your `config.php` file, adding the following array: - +```php + // ... + 'es' => [ + 'enabled' => true, + 'index_settings' => [ + 'number_of_replicas' => 1, + 'number_of_shards' => 1, + ], + 'dynamic_mapping_enabled' => true, + 'client' => [ + 'hosts' => [ + 'localhost:9200' + ] + ] + ], + // Other configuration settings... + // ... ``` -[ + +Your config.php should look like this now: +```php + [ + 'username' => 'dbuser', + 'password' => 'dbpw', + 'dbname' => 'dbname', + 'host' => 'localhost', + 'port' => '3306', + ], 'es' => [ 'enabled' => true, - 'number_of_replicas' => null, - 'number_of_shards' => null, + 'index_settings' => [ + 'number_of_replicas' => 1, + 'number_of_shards' => 1, + ], + 'dynamic_mapping_enabled' => true, 'client' => [ 'hosts' => [ 'localhost:9200' @@ -59,83 +101,138 @@ To enable Elasticsearch (provided it's already installed, configured and running ] ], // Other configuration settings... -] +]; ``` -Shopware 5 communicates with Elasticsearch using the latter's REST API. The `hosts` array accepts multiple address syntaxes, about which you can read more [here](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/_configuration.html#_host_configuration). The `number_of_shards` and `number_of_replicas` parameter provided to the generated index. A `null` configuration allows to use the elastic search server configuration of this parameters. +Shopware 5 communicates with Elasticsearch using the latter's REST API. +The `hosts` array accepts multiple address syntax, about which you can read more [here](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/_configuration.html#_host_configuration). +The `number_of_shards` and `number_of_replicas` parameter provided to the generated index. +### Define Elasticsearch Dynamic Mapping + +By default, Shopware enables the dynamic mapping option. +By this, fields can be added dynamically to a document or to inner objects within a document, just by indexing a document containing the new field. + +The problem with this option is, that Shopware provides floats and integers to Elasticsearch. +So Elasticsearch uses the first detect field mapping for dynamic fields and can't change it then. +This is the reason why sometimes products are indexed and sometimes not. + +With disabling dynamicMapping, you have to provide a mapping for every Field which is used in Elasticsearch. +Only then the field will be indexed. +This will fix the type issues and product will get indexed. + +Take a look [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic.html) for more information. + +```php + [ + ... + 'dynamic_mapping_enabled' => true, + ] +]; +``` ### Initial data import Once both Elasticsearch and Shopware 5 are configured and running, you should execute the following Shopware CLI command: -``` +```shell php bin/console sw:es:index:populate ``` -This command will reindex your shops data in Elasticsearch. Keep in mind that, should your shops already have a large amount of article data, this process can take a considerable amount of time. If you wish, you can limit this process per shop. Refer to the command's documentation for more info. +This command will reindex your shops data in Elasticsearch. +Keep in mind that, should your shops already have a large amount of product data, this process can take a considerable amount of time. +If you wish, you can limit this process per shop. Refer to the command's documentation for more info. ### Maintaining data consistency -In order to ensure data consistency between your MySQL database and your Elasticsearch indexes, there are two CLI commands you need to periodically execute: - +In order to ensure data consistency between your MySQL database and your Elasticsearch indexes, +there are two CLI commands you need to periodically execute: #### Live synchronization -The `sw:es:backlog:sync` command ensures your latest changes are propagated into Elasticsearch. It uses a queueing system, and it's execution time may greatly vary, depending on the pending operation list content. This command should be executed periodically to ensure data consistency. +The `sw:es:backlog:sync` command ensures your latest changes are propagated into Elasticsearch. +It uses a queueing system, and it's execution time may greatly vary, depending on the pending operation list content. +This command should be executed periodically to ensure data consistency. -``` +```shell php bin/console sw:es:backlog:sync ``` #### Data reindexing -To ensure your MySQL database and your Elasticsearch instance are synchronized, we recommend running a complete reindexing of your articles daily. This can be done by setting up a cron job that runs the following command: +To ensure your MySQL database and your Elasticsearch instance are synchronized, +we recommend running a complete reindexing of your products daily. +This can be done by setting up a cron job that runs the following command: -``` +```shell php bin/console sw:es:index:populate ``` -We recommend running this command every 24 hours, at a time when you server expects less traffic (typically during the night). There will be no downtime for both Shopware and Elasticsearch during its execution. - +We recommend running this command every 24 hours, at a time when your server expects less traffic (typically during the night). +There will be no downtime for both Shopware and Elasticsearch during its execution. ## Elasticsearch integration details -While this guide is not meant to cover the technical details of the integration implementation in depth, there are some concepts that you need to keep in mind when configuring the integration between Shopware and Elasticsearch. The first, and probably most important, is that Elasticsearch is NOT a MySQL replacement. Although they provide, to some extent, similar features, Elasticsearch is and should be seen as a complement to a DBMS, not as a replacement. As such, you will still require a running MySQL instance, and its configuration will still greatly affect Shopware's performance in most actions. +While this guide is not meant to cover the technical details of the integration implementation in depth, +there are some concepts that you need to keep in mind when configuring the integration between Shopware and Elasticsearch. +The first and probably most important, is that Elasticsearch is NOT a MySQL replacement. +Although they provide, to some extent, similar features, Elasticsearch is and should be seen as a complement to a DBMS, not as a replacement. +As such, you will still require a running MySQL instance, and its configuration will still greatly affect Shopware's performance in most actions. -Another vital detail you should keep in mind when using Elasticsearch with Shopware 5.1 is that the data stored in Elasticsearch is a duplicate of the data already present in your Shopware 5 database. Whenever changes are made to your data (for example, you edit an article description), that information is saved to MySQL and, only later, to Elasticsearch. +Another vital detail you should keep in mind when using Elasticsearch with Shopware is, +that the data stored in Elasticsearch is a duplicate of the data already present in your Shopware 5 database. +Whenever changes are made to your data (for example, you edit a product description), +that information is saved to MySQL and, only later, to Elasticsearch. ### Using and understanding the data synchronization mechanism -Some changes shop owners perform in the backend may affect a great number of entries on your database. For example, a change in your default tax rate could theoretically affect all your products. MySQL data is not greatly affected by this, as the gross value calculation is done when the article is loaded from the database. However, Elasticsearch may store gross values, to speed up performance. This has the obvious downside that, if your tax rate changes, you need to update all your products prices. If your shop has hundreds of thousands or millions of products, this operation can take a significant amount of time, even on a high performance application like Elasticsearch. To handle these scenarios, the Elasticsearch integration in Shopware 5 includes support for asynchronous data propagation to Elasticsearch. +Some changes shop owners perform in the backend may affect a great number of entries on your database. +For example, a change in your default tax rate could theoretically affect all your products. +MySQL data is not greatly affected by this, as the gross value calculation is done when the product is loaded from the database. +However, Elasticsearch may store gross values, to speed up performance. +This has the obvious downside that, if your tax rate changes, you need to update all your products prices. +If your shop has hundreds of thousands or millions of products, this operation can take a significant amount of time, even on a high performance application like Elasticsearch. +To handle these scenarios, the Elasticsearch integration in Shopware 5 includes support for asynchronous data propagation to Elasticsearch. #### The queueing system -The asynchronous propagation of change to Elasticsearch is implemented using an operation queuing system. Operations are stored in the `s_es_backlog` by hooking into Doctrine's `postPersist`, `postUpdate` and `postRemove` events for selected entities. The events store the entity type, its ID, the operation to be executed. +The asynchronous propagation of change to Elasticsearch is implemented using an operation queuing system. +Operations are stored in the `s_es_backlog` by hooking into Doctrine's `postPersist`, `postUpdate` and `postRemove` events for selected entities. +The events store the entity type, its ID, the operation to be executed. #### The synchronization process -These events are handled by a CLI command included in Shopware 5.1, which you can execute using the following line: +These events are handled by a CLI command included in Shopware, which you can execute using the following line: -``` +```shell php bin/console sw:es:backlog:sync ``` -When executed, this command loads all operations from the queue which have not yet been executed. It then executes them, in the order in which they were queued. +When executed, this command loads all operations from the queue which have not yet been executed. +It then executes them, in the order in which they were queued. -It's highly recommended that you set up a cron job in your system to periodically execute this command, in order to ensure your MySQL database and Elasticsearch have consistent data. The configuration of this cron task will depend on your system's operating system, and is not part of the scope of this guide. +It's highly recommended that you set up a cron job in your system to periodically execute this command, +in order to ensure your MySQL database and Elasticsearch have consistent data. +The configuration of this cron task will depend on your system's operating system, and is not part of the scope of this guide. #### Event handling workflow -Shopware detects data changes over the doctrine ORM event system. This events triggered if doctrine persist changes into the database. +Shopware detects data changes over the doctrine ORM event system. +Those events are triggered if doctrine persist changes into the database. Traced models: - `Shopware\Models\Article\Supplier` @@ -147,44 +244,137 @@ Traced models: - `Shopware\Models\Article\Detail` - `Shopware\Models\Article\Price` -When changes to entities belonging to these models are done, they are immediately saved in the MySQL database, but propagation to Elasticsearch is delayed until the next execution of the `sw:es:sync:backlog` command. +When changes to entities belonging to these models are done, +they are immediately saved in the MySQL database, but propagation to Elasticsearch is delayed until the next execution of the `sw:es:backlog:sync` command. ### Rebuilding the search index -As discussed before, the synchronization process between Shopware/MySQL and Elasticsearch traces only changes of defined entities. Global system changes like `created a new shop`, `created new customer groups` are not detected by the synchronisation and only be considered at indexing time. The `sw:es:index:populate` command does just that. +As discussed before, the synchronization process between Shopware/MySQL and Elasticsearch traces only changes of defined entities. +Global system changes like `created a new shop`, `created new customer groups` are not detected by the synchronization and only be considered at indexing time. +The `sw:es:index:populate` command does just that. -Internally, Elasticsearch uses [indexes](https://www.elastic.co/guide/en/elasticsearch/reference/current/glossary.html#glossary-index) to store your data. If you are new to Elasticsearch, think of indexes as MySQL databases: they contain your data, and you can read from and write to them. Fully regenerating an index can take a significant amount of time, just as loading a database from an `.sql` file can, during which your data is unavailable. +Internally, Elasticsearch uses [indexes](https://www.elastic.co/guide/en/elasticsearch/reference/current/glossary.html#glossary-index) to store your data. +If you are new to Elasticsearch, think of indexes as MySQL databases: they contain your data, and you can read from and write to them. +Fully regenerating an index can take a significant amount of time, just as loading a database from an `.sql` file can, during which your data is unavailable. #### Cycling indexes -To avoid downtime, even when you are reindexing your shop's data, Shopware uses multiple indexes instead of just one. When you trigger the reindexing process, Shopware will index your data into a completely new index. While your shop is being indexed, if a customer queries your shop, the old index will be used to provide the results. As such, there's no downtime. Once your indexing process is finished, Shopware will automatically start using the new one. The old index is not deleted. Should you detect a problem with the new index, you can revert to the old one, instead of having to wait for a full reindexing of your shop. Shopware provides a tool to switch back to this old indices: -```bash +To avoid downtime, even when you are reindexing your shop's data, Shopware uses multiple indexes instead of just one. +If you trigger the reindexing process, Shopware will index your data into a completely new index. +While your shop is being indexed, if a customer queries your shop, the old index will be used to provide the results. +As such, there's no downtime. +Once your indexing process is finished, Shopware will automatically start using the new one. +The old index is not deleted. +Should you detect a problem with the new index, you can revert to the old one, instead of having to wait for a full reindexing of your shop. +Shopware provides a tool to switch back to this old indices: + +```shell php bin/console sw:es:switch:alias --shopId=1 --index=sw_shop1_TIMESTAMP ``` + Again, this is done so that, no matter what happens, there's no downtime for your customers. #### Concurrent usage -While your data is being indexed, the shop owner might make some changes to its products. This could cause your new index to be inconsistent, but Shopware handles this too, ensuring your newly created index includes even the changes made while it was being created. +While your data is being indexed, the shop owner might make some changes to its products. +This could cause your new index to be inconsistent, but Shopware handles this too, ensuring your newly created index includes even the changes made while it was being created. -As mentioned before, Shopware uses a queuing system to asynchronously handle changes to your data. But this queue is not used only for this. When the indexing process starts, Shopware records the current position of the queue end. As the indexing takes place, the shop owner may manipulate data, resulting in operations being added to the queue. When the indexing is finished, Shopware is able to determine if new operations were carried out while the data was being indexed. Should that be the case, the new operations are re-executed on the new index, ensuring consistency. +As mentioned before, Shopware uses a queuing system to asynchronously handle changes to your data. +But this queue is not used only for this. +When the indexing process starts, Shopware records the current position of the queue end. +As the indexing takes place, the shop owner may manipulate data, resulting in operations being added to the queue. +When the indexing is finished, Shopware is able to determine if new operations were carried out while the data was being indexed. +Should that be the case, the new operations are re-executed on the new index, ensuring consistency. #### Index cleanup -After a new index is created, the old one is no longer used, but is not deleted. Should your new index be corrupted, you can just replace it with the old one, and have your shop running again without downtime. However, as new indexes should be created often (recommended every 24 hours), old indexes can accumulate and start taking up a significant amount of resources. Shopware provides a tool to cleanup those old indexes: +After a new index is created, the old one is no longer used, but is not deleted. +Should your new index be corrupted, you can just replace it with the old one, and have your shop running again without downtime. +However, as new indexes should be created often (recommended every 24 hours), old indexes can accumulate and start taking up a significant amount of resources. +Shopware provides a tool to clean up those old indexes: -```bash +```shell php bin/console sw:es:index:cleanup ``` -This command will delete every old version of an index, but keep the latest. As such, it's safe to run this command even on production environments, provided you have ensured that your current index is working as intended. +This command will delete every old version of an index, but keep the latest. +As such, it's safe to run this command even on production environments, +provided you have ensured that your current index is working as intended. #### Backlog cleanup -Like the old indices, the processed backlog queue is never deleted which allows to reproduce data changes of the system in case of a index rollback. Shopware provides a tool to cleanup those processed backlog queue: +Like the old indices, the processed backlog queue is never deleted which allows reproducing data changes of the system in case of an index rollback. +Shopware provides a tool to clean up those processed backlog queue: -```bash +```shell php bin/console sw:es:backlog:clear ``` -As such, it's safe to run this command even on production environments, provided you have ensured that your current index is working as intended. +As such, it's safe to run this command even on production environments, +provided you have ensured that your current index is working as intended. + +## Elasticsearch in Backend + +Since Shopware 5.5 it is also possible to use Elasticsearch in the backend for listing and search operations for products, orders and customers. +To use this you need to adjust your `config.php` to + +```php +return [ + 'db' => [ + 'username' => 'dbuser', + 'password' => 'dbpw', + 'dbname' => 'dbname', + 'host' => 'localhost', + 'port' => '3306', + ], + 'es' => [ + 'enabled' => true, + 'index_settings' => [ + 'number_of_replicas' => 1, + 'number_of_shards' => 1, + ], + 'client' => [ + 'hosts' => [ + 'localhost:9200' + ] + ], + 'backend' => [ + 'enabled' => true, + 'write_backlog' => true, + 'index_settings' => [ + 'number_of_replicas' => 1, + 'number_of_shards' => 1, + ], + ], + ], +]; +``` + +### Initial data import + +Once Shopware is configured for using Elasticsearch in the backend, you should execute the following Shopware CLI command: + +```shell +php bin/console sw:es:backend:index:populate +``` + +#### Live synchronization + +The `sw:es:backend:sync` command ensures your latest changes are propagated into Elasticsearch. +It uses a queueing system, and it's execution time may greatly vary, depending on the pending operation list content. +This command should be executed periodically to ensure data consistency. + +```shell +php bin/console sw:es:backend:sync +``` + +#### Index cleanup + +After a new index is created, the old one is no longer used, but is not deleted. +Should your new index be corrupted, you can just replace it with the old one, and have your shop running again without downtime. +However, as new indexes should be created often (recommended every 24 hours), old indexes can accumulate and start taking up a significant amount of resources. +Shopware provides a tool to clean up those old indexes: + +```shell +php sw:es:backend:index:cleanup +``` diff --git a/source/sysadmins-guide/index.html b/source/sysadmins-guide/index.html index 12ecbb6d48..915ec60b70 100644 --- a/source/sysadmins-guide/index.html +++ b/source/sysadmins-guide/index.html @@ -11,10 +11,11 @@

    Guides

    diff --git a/source/sysadmins-guide/installation-guide/index.md b/source/sysadmins-guide/installation-guide/index.md index 71f3dd64a7..eb34c79d92 100644 --- a/source/sysadmins-guide/installation-guide/index.md +++ b/source/sysadmins-guide/installation-guide/index.md @@ -1,54 +1,24 @@ --- layout: default -title: Shopware 5 Installation Guide +title: Shopware Installation Guide github_link: sysadmins-guide/installation-guide/index.md indexed: true group: System Guides menu_title: Installation Guide menu_order: 20 --- -## Shopware 5 Installer -Shopware 5 features a redesigned installer. If you have used the Shopware 4 installer, you will find the new version features a familiar workflow, while improving the previous experience. +Welcome to the Shopware developer community! Follow these steps for a successful start into Shopware development. -In the welcome step, you can choose the language you wish to use during the installation process. On a later step, you will be able to configure the shop's language. +### System Requirements +Before you start, you should check if your environment meets the requirements. -![Installer](screen-installer-step1.png) +### Installation +Simply follow the instructions on our README.md file for installing the Shopware developer version. -The installer will automatically check your system configuration to determine if it meets all of Shopware's requirements and recommendations. +If you want to install the latest stable release of Shopware, download it here and follow these instructions. -![Installation requirements](screen-installer-requirements.png) - -After accepting Shopware's license, you will be asked to provide your database's access details. You should have previously created an empty database. - -![Database configuration](screen-installer-db-config.png) - -After the database has been successfully created, you can select which license you will be using with your Shopware installation. If unsure, use the default value. - -![License selection](screen-installer-license.png) - -In the last step of the installer you will be asked to configure your default shop and user account. - -![Shop configuration](screen-installer-shop-config.png) - -## Shopware 5 First Run Wizard - -Shopware 5 includes a new First Run Wizard, that will assist you in installing new plugins and translations, and doing additional basic adjustments to your shop settings. The First Run Wizard can be skipped at any moment, and can be re-enabled after the first execution in the Basic Settings. - -The first step welcomes you to your Shopware installation and checks connectivity to shopware's servers, required to provide plugin suggestions. No information about you or your shop is transmitted during this process. - -![Shop configuration](screen-frw-welcome.png) - -During the following steps, you will be able to install and activate several plugins that will help you localize your shop for other markets. You will also have access to demo data sets, as well as plugins suggested by shopware. - -![Shop configuration](screen-frw-plugin.png) - -The First Run Wizard also allows you to configure additional details about your shop, like your address or the shop's logo. You can change these values later, in the backend's Basic Settings or Theme Manager. - -![Shop configuration](screen-frw-config.png) - -You can also login or register your Shopware ID, required to have access to the complete Shopware plugin catalogue. You can also automatically register your shop's domains and associate it to your Shopware ID. - -![Shop configuration](screen-frw-swid.png) - -Once you finish the last step of the First Run Wizard, you will be taken to your shop's backend, and have access to all of Shopware's features. +### IDE +To make it easy as possible for you to develop with and for Shopware, a good IDE is one of the most important things. +Our recommendation here is PhpStorm. +Have a look at this article so see why. diff --git a/source/sysadmins-guide/installation-guide/screen-frw-config.png b/source/sysadmins-guide/installation-guide/screen-frw-config.png deleted file mode 100644 index 06daa28d3d..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-frw-config.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-frw-plugin.png b/source/sysadmins-guide/installation-guide/screen-frw-plugin.png deleted file mode 100644 index 4c4b5a4a07..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-frw-plugin.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-frw-swid.png b/source/sysadmins-guide/installation-guide/screen-frw-swid.png deleted file mode 100644 index 513b9c07cd..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-frw-swid.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-frw-welcome.png b/source/sysadmins-guide/installation-guide/screen-frw-welcome.png deleted file mode 100644 index ecce0e9cd1..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-frw-welcome.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-installer-db-config.png b/source/sysadmins-guide/installation-guide/screen-installer-db-config.png deleted file mode 100644 index fc1c2ebb02..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-installer-db-config.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-installer-license.png b/source/sysadmins-guide/installation-guide/screen-installer-license.png deleted file mode 100644 index 4219c490bd..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-installer-license.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-installer-requirements.png b/source/sysadmins-guide/installation-guide/screen-installer-requirements.png deleted file mode 100644 index c545e511b6..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-installer-requirements.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-installer-shop-config.png b/source/sysadmins-guide/installation-guide/screen-installer-shop-config.png deleted file mode 100644 index 9725de7ddc..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-installer-shop-config.png and /dev/null differ diff --git a/source/sysadmins-guide/installation-guide/screen-installer-step1.png b/source/sysadmins-guide/installation-guide/screen-installer-step1.png deleted file mode 100644 index 2e7c29eec6..0000000000 Binary files a/source/sysadmins-guide/installation-guide/screen-installer-step1.png and /dev/null differ diff --git a/source/sysadmins-guide/professional-deployments/index.md b/source/sysadmins-guide/professional-deployments/index.md new file mode 100644 index 0000000000..cd970c79c0 --- /dev/null +++ b/source/sysadmins-guide/professional-deployments/index.md @@ -0,0 +1,86 @@ +--- +layout: default +title: Professional Deployments +github_link: sysadmins-guide/professional-deployments/index.md +indexed: true +group: System Guides +menu_title: Professional Deployments +menu_order: 40 +--- + +## Professional Deployments + +This guide is intended as a list of best practices to follow when professionally deploying Shopware shops. + +### Development best practices + +Professional deployment begins with professional development. The time when everybody edited files on a central +FTP server are long gone. + +You should be using a version control system (VCS), preferably [git](https://git-scm.com/) to allow parallel development +on local machines while integrating these parts in a central point. See the developers guide. + +If you find yourself reusing plugins, themes or configuration, think about using [Composer](https://getcomposer.org/) and +the [Shopware Composer Project](https://github.com/shopware5/composer-project) for your new projects. It will help you +to require plugins or libraries you repeatedly use. + +### Development, staging and production systems + +Even if some feature works perfectly on the developers own machine, there is always a chance that some setting or the +integration with some feature of another developer breaks the shop. So it is a good idea to have one or more stages of +integrations; a common pattern is a three level system of development, staging and production systems. Of course all +these are just a recommendation - all of this can and should be modified to your situation, use cases +and workflows. + +- The development system is where a first check is possible or where internal stakeholders can get a preview or glimpse + of how a feature is about to be developed. + +- The staging system is where a thoroughly tested feature is being shown to an external customer and features are combined + into releases. + +- The production system is what the end customer gets to see. If a staging system contains all relevant features + for e.g. a certain milestone, these can be deployed together to this system. + +### Deployments + +Deployments should be as automatic as possible to prevent any chance of human error. It is generally a good idea to +not simply replace or update your webroot on the production system. Rather create a fresh clone of the latest production +release from your VCS, maybe run some automated tests to make sure no basic errors occurred and just switch the webroot +of your webserver to point to your new release. Shared resources (like images, documents etc.) can be offloaded to a +CDN or themselves be included in your webroot by file system links. + +Should anything be wrong with this release, you can still switch back to the old version (given the database wasn't +modified in a destructive way). This way you can minimize the downtime, or even eliminate it all together if you're +working in a cluster setup and are able to update machine by machine. + +### Blue-Green-Deployments + +The ability to upgrade/downgrade your production environment is commonly known as "Blue-Green Deployment". Starting with +v5.4 forward, Shopware commits itself to be able to support this kind of deployment out-of-the-box for succeeding +minor versions. This means that you can confidently deploy a coming v5.5 release, knowing you can always switch back to +your current proven v5.4. Of course this presumes that all installed plugins support this as well. + +So any deprecations to the database that were announced in v5.4 will only be applied in a v5.6 release. That way the +intermediate v5.5 database can and must be usable with both a v5.4 and a v5.5. Database deprecations in v5.5 will be +applied in a v5.7 and so on. + +### Best practices + +Try to not store credentials on disk to minimize the information an attacker might gain in case of file system access. +Rather use environment variable set e.g. in the webserver. The [Shopware Composer Project](https://github.com/shopware5/composer-project) +supports environment variable for the setup of e.g. the database out of the box. + +You could start your automated deployments with some simple shell scripts to e.g. checkout a specific tag on the +server or switch the currently productive version by changing the target of the webroot-link. You could re-utilize these +scripts later by integrating them into a CI/CD system like Jenkins, Bamboo or others. + +It is often also a good idea to redefine basic settings which could be changed by users to sane defaults with +each deploy. Examples could be the mailer configuration, thumbnail sizes or controller caching times for the HTTP cache. + +In Shopware 5.5.2, there is a new config value which allows you to have the plugins loaded in a predictable way during the DI container build process. This is important if you use multiple app servers because the current behavior is based on the underlying file system and might differ between machines. Add the following code snippet to your config.php file and the plugins are loaded alphabetical: + +```php + 'backward_compatibility' => [ + 'predictable_plugin_order' => true + ], +``` diff --git a/source/sysadmins-guide/sessions/index.md b/source/sysadmins-guide/sessions/index.md index 3d220eaf13..ab30fb8988 100644 --- a/source/sysadmins-guide/sessions/index.md +++ b/source/sysadmins-guide/sessions/index.md @@ -10,13 +10,10 @@ tags: - redis - lock - transaction -redirect: - - /sysadmins-guide/memcached-as-session-handler/ - - /sysadmins-guide/memcached-as-session-handler/index.html indexed: true group: System Guides menu_title: Sessions -menu_order: 70 +menu_order: 80 --- Shopware uses the database for session handling by default. This article will explain configuration options and @@ -54,9 +51,9 @@ controller by calling the function `session_write_close()` manually, before doin ## Available session adapters The following list will explain the session adapters Shopware supports by default -### Memcache -Memcache is a popular cache server, that also can be used for sessions. As it supports session locking as well, we -recommend Memcache for bigger setups with high traffic. +### Memcached +Memcached is a popular cache server, that also can be used for sessions. As it supports session locking as well, we +recommend Memcached for bigger setups with high traffic. #### Install @@ -68,11 +65,11 @@ sudo apt-get install memcached Also the [PHP memcached extension](https://pecl.php.net/package/memcached) has to be installed. -For Debian/Ubuntu based distributions you can just install the `php5-memcached` package: +For Debian/Ubuntu based distributions you can just install the `php-memcached` package: ```bash -sudo apt-get install php5-memcached +sudo apt-get install php-memcached ``` For other distributions you can [compile](http://php.net/manual/en/memcached.installation.php) the extension by yourself. @@ -84,20 +81,21 @@ In this example the memcache server was installed locally ("localhost") on the a have a stand alone memcache instance in place: ```php -'session' => array( +'session' => [ 'save_handler' => 'memcached', 'save_path' => "localhost:11211", -), +], -'backendsession' => array( +'backendsession' => [ 'save_handler' => 'memcached', 'save_path' => "localhost:11211", -), +], ``` ### Redis Redis is a popular key/value storage, that easily can be clustered for redundancy. It does not support session locking, however. +When using a single redis instance with multiple Shopware installations (e.g. for staging environments) it would be wise to use separate prefixes for each installation. Otherwise, your session keys could be re-used between your installations and race conditions or strange session-related behavior may occur. A prefix can be configured using the connection uri. Please consult the official [phpredis documentation](https://github.com/phpredis/phpredis#php-session-handler). #### Install @@ -119,34 +117,15 @@ sudo apt-get install php-redis In this example the redis server is running locally (127.0.0.1) on port 6379: ```php -'session' => array( +'session' => [ 'save_handler' => 'redis', 'save_path' => "tcp://127.0.0.1:6379", -), +], -'backendsession' => array( +'backendsession' => [ 'save_handler' => 'redis', 'save_path' => "tcp://127.0.0.1:6379", -), -``` - - -### File -The "file" session adapter also supports session locking and will create sessions on the file system of each app server. -For that reason, it is not recommended to be used in cluster setups, as it will then require mechanisms for syncing or -session stickiness. Also ever read / write of session data will access the hard drive of the server - and might therefore -slow down response times. - -#### Configuration - -```php -'session' => array( - 'save_handler' => 'file', -), - -'backendsession' => array( - 'save_handler' => 'file', -), +], ``` ### Database @@ -155,23 +134,23 @@ The database session handler is Shopware's default session handler. #### Configuration ```php -'session' => array( +'session' => [ 'save_handler' => 'db', -), +], -'backendsession' => array( +'backendsession' => [ 'save_handler' => 'db', -), +], ``` #### Disable locking As of Shopware 5.2.13, you can disable the session locking for the database handler: ```php -'session' => array( +'session' => [ 'save_handler' => 'db', 'locking' => false -) +] ``` #### Blocking transactions diff --git a/source/sysadmins-guide/shopware-5-performance-for-sysadmins/index.md b/source/sysadmins-guide/shopware-5-performance-for-sysadmins/index.md index cb2f59d9ef..ad674dc194 100644 --- a/source/sysadmins-guide/shopware-5-performance-for-sysadmins/index.md +++ b/source/sysadmins-guide/shopware-5-performance-for-sysadmins/index.md @@ -10,153 +10,258 @@ tags: - php - apc - cache -redirect: - - /sysadmins-guide/optimize-performance/ group: System Guides menu_title: Performance Guide -menu_order: 40 +menu_order: 50 --- -In this document we detail performance related settings that you can set in your server to get the most out of it. Some of them were already part of previous Shopware releases, which we complemented with new addictions, for optimized performance and scalability. In most cases, it's assumed that you have already installed and configured Shopware on your server, and that it's running properly. This document does not cover configuration options *needed* by Shopware (for example, increasing PHP's `memory_limit` to an acceptable level), and focus only on making an already working system perform better. +In this document we detail performance related settings that you can set in your server to get the most out of it. +Some of them were already part of previous Shopware releases, which we complemented with new dependencies, for optimized performance and scalability. +In most cases, it's assumed that you have already installed and configured Shopware on your server, and that it's running properly. +This document does not cover configuration options *needed* by Shopware (for example, increasing PHP's `memory_limit` to an acceptable level), and focus only on making an already working system perform better.
    -Note: This guide only covers system configuration optimizations, and does not cover Shopware's configuration itself. However, there are several configuration options inside Shopware itself that you can use to improve you shop's performance. Please refer to the Shopware 5 performance guide for developers for more details on this subject. +Note: This guide only covers system configuration optimizations, and does not cover Shopware's configuration itself. +However, there are several configuration options inside Shopware itself that you can use to improve you shop's performance. +Please refer to the Shopware 5 performance guide for developers for more details on this subject.
    ## Server -Shopware performance optimization should start long before you install Shopware itself. If needed, you can contact one of our partners, that will help you determine which server requirements you will have, depending on your estimated shop size and incoming traffic. There is no rule of thumb for this, as each shop is unique and should be analysed on a case-by-case basis. There are, however, some rules you should observe when choosing your hosting provider: +Shopware performance optimization should start long before you install Shopware itself. +If needed, you can contact one of our partners, that will help you determine which server requirements you will have, depending on your estimated shop size and incoming traffic. +There is no rule of thumb for this, as each shop is unique and should be analysed on a case-by-case basis. +There are, however, some rules you should observe when choosing your hosting provider: -- `Processor`: Different hosting providers and plans provide different options. More than speed, the key value here is the number of cores. Each core works separately from each other, meaning that the more you have, the more concurrent requests your shop will be able to handle. This is particularly relevant if you expect periods of high traffic on your shop (for example, a highly anticipated product release that causes an abnormal flow of requests to your shop) or you expect to frequently have multiple dozens of simultaneous requests on your shop. As a rule of thumb, even for small shops, a dual core processor is recommended. +- `Processor`: Different hosting providers and plans provide different options. +More than speed, the key value here is the number of cores. +Each core works separately from each other, meaning that the more you have, the more concurrent requests your shop will be able to handle. +This is particularly relevant if you expect periods of high traffic on your shop (for example, a highly anticipated product release that causes an abnormal flow of requests to your shop) or you expect to frequently have multiple dozens of simultaneous requests on your shop. +As a rule of thumb, even for small shops, a dual-core processor is recommended. -- `Memory`: Memory is used by all parts of your system. Not only does Shopware consume memory, but so does your server's operating system, your database server, your web server, and any other application your server might be running. Additionally, in the sections bellow, we will cover different caching configurations, that you can use to speed up Shopware, at the expense of additional memory. So, even if your Shopware installation runs with minimal memory configurations like 1 or 2 GBs, it's recommended that you consider adding additional memory to your setup, especially if you plan on using other applications on your server simultaneously, or if you want to configure the caching features described below. +- `Memory`: Memory is used by all parts of your system. +Not only does Shopware consume memory, but so does your server's operating system, your database server, your web server, and any other application your server might be running. +Additionally, in the sections below, we will cover different caching configurations, that you can use to speed up Shopware, at the expense of additional memory. +So, even if your Shopware installation runs with minimal memory configurations like 1 or 2 GBs, it's recommended that you consider adding additional memory to your setup, especially if you plan on using other applications on your server simultaneously, or if you want to configure the caching features described below. -- `Hard drive`: Aside from disk space, which does not affect performance (unless the disk is full or close to it), hard drives are differentiated by their nature into one of two categories: `hard disc drive` (HDD) and `solid state drives` (SSD). The former are more commonly available, especially in entry level hosting solutions, but are gradually being phased out by most providers in favor of the latter, which are usually more expensive but offer significantly better performance. As the price difference between the two has been declining over the last few years, it's now possible to find hosting solution that use the faster SSD technology even for budget-level hosting solutions. +- `Hard drive`: Aside from disk space, which does not affect performance (unless the disk is full or close to it), hard drives are differentiated by their nature into one of two categories: `hard disc drive` (HDD) and `solid state drives` (SSD). +The former are more commonly available, especially in entry level hosting solutions, but are gradually being phased out by most providers in favor of the latter, which are usually more expensive but offer significantly better performance. +As the price difference between the two has been declining over the last few years, it's now possible to find hosting solution that use the faster SSD technology even for budget-level hosting solutions. ## Database configuration - MySQL -The following variables are the most relevant when it comes to fine tuning MySQL's performance: +The following variables are the most relevant when it comes to fine-tuning MySQL's performance: -- `innodb_buffer_pool_size`: The larger you set this value, the less disk I/O is needed to access the same data in tables more than once. On a dedicated database server, you might set this to up to 80% of the machine physical memory size. +- `innodb_buffer_pool_size`: The larger you set this value, the less disk I/O is needed to access the same data in tables more than once. +On a dedicated database server, you might set this to up to 80% of the machine physical memory size.
    *Source: dev.mysql.com* - `query_cache_size`: The amount of memory allocated for caching query results. **By default, the query cache is disabled**.
    *Source: dev.mysql.com* -The MySQL documentation has a [dedicated page](http://dev.mysql.com/doc/refman/5.6/en/optimization.html) that covers optimization and performance improvements. For more details about the above mentioned variables, or other potential improvements, please refer to it. +The MySQL documentation has a [dedicated page](http://dev.mysql.com/doc/refman/5.6/en/optimization.html) that covers optimization and performance improvements. +For more details about the above-mentioned variables, or other potential improvements, please refer to it. ### MariaDB users -The above settings are also applicable when using MariaDB with InnoDB or XtraDB engines. You can read more about MariaDB performance settings [on their website](https://mariadb.com/kb/en/mariadb/optimization-and-tuning/). Please keep in mind that, although you might be able to run Shopware while using MariaDB, no official support is provided. +The above settings are also applicable when using MariaDB with InnoDB or XtraDB engines. +You can read more about MariaDB performance settings [on their website](https://mariadb.com/kb/en/mariadb/optimization-and-tuning/). +Please keep in mind that, although you might be able to run Shopware while using MariaDB, no official support is provided. ## Web server -Improving your web server's configuration can also help your Shopware installation perform better, especially under heavy load periods, when multiple simultaneous requests are made to your server. +Improving your web server's configuration can also help your Shopware installation perform better, +especially under heavy load periods, when multiple simultaneous requests are made to your server. ### Apache -Apache web server is the officially supported web server for hosting Shopware. While Shopware might also work in other web servers (nginx, for example), they are not officially supported. Apache uses extension modules that cover a multitude of tasks, ranging from URL rewriting for SEO purposes to increased security. These modules, many of them optional for Shopware's operation, might affect performance as well. Each module usually has a list of configuration option, which associated documentation you should read to optimize their performance. +Apache web server is the officially supported web server for hosting Shopware. +While Shopware might also work in other web servers (nginx, for example), they are not officially supported. +Apache uses extension modules that cover a multitude of tasks, ranging from URL rewriting for SEO purposes to increased security. +These modules, many of them optional for Shopware's operation, might affect performance as well. +Each module usually has a list of configuration option, which associated documentation you should read to optimize their performance. #### Prefork MPM vs Worker MPM -Apache is the application responsible for handling all incoming requests to your server. If your request asks for a static file (an image or a CSS file, for example), Apache handles that request alone. If you ask for a dynamic page (for example, a Shopware article listing page), Apache handles that request over to PHP, which returns a response to Apache which, in turn, returns that response to the user's browser. As you can imagine, these processes have to be very fast and, more important, have to happen concurrently for multiple requests. +Apache is the application responsible for handling all incoming requests to your server. +If your request asks for a static file (an image or a CSS file, for example), Apache handles that request alone. +If you ask for a dynamic page (for example, a Shopware article listing page), Apache handles that request over to PHP, +which returns a response to Apache which, in turn, returns that response to the user's browser. +As you can imagine, these processes have to be very fast and, more important, have to happen concurrently for multiple requests. -To do this, Apache uses one of several existing `Multi-Processing Modules` (MPMs), a module responsible for deciding how each concurrent request is handled. While several different MPMs exist, `Prefork` and `Worker` are the most commonly used. While usually considered faster, `Worker` is also not thread safe, meaning it does not meet the requirements to execute Shopware. As such, you need to use `Prefork` to run Shopware. +To do this, Apache uses one of several existing `Multi-Processing Modules` (MPMs), a module responsible for deciding how each concurrent request is handled. +While several MPMs exist, `Prefork` and `Worker` are the most commonly used. +While usually considered faster, `Worker` is also not thread safe, meaning it does not meet the requirements to execute Shopware. +As such, you need to use `Prefork` to run Shopware. #### Configuring Prefork MPM -Prefork settings determine how many simultaneous requests Apache (and thus your server) will handle and how many requests will queue once it can no longer handle more simultaneous requests. These settings greatly impact your Apache's performance and system resource usage, so you might need to do a bit of trial and error before finding which setup offers better performance without overwhelming your server's processor and/or memory. Remember that these settings apply to the web server as a whole, meaning that they will also affect other content served by Apache. You should also take that into consideration when setting these values. - -Apache configuration settings is often split into multiple files, to improve readability and maintainability, so you might have to search different files until you find the one that contains Prefork's configuration values. While not required, you will usually find these values inside a ` ... ` block, to ensure that these values are not loaded if you decide to use another MPM. This configuration block may contain all or some of the following variables: - -- `StartServers`: This number controls the number of server processes created when the server (Apache) is started. As the number of processes is later on handled dynamically, this setting has little impact on performance. - -- `MinSpareServers` and `MaxSpareServers`: These settings control the number of idle server processes that are allowed to exist. Idle processes are processes that exist but are not actually doing any work. They consume resources (although, as they are idle, this consumption is relatively low) but are immediately available to answer a request once it comes in. The creation of these processes take time, which is why Apache keeps a few always available. The default value for these settings are usually 5 and 10 respectively, which is adequate for smaller servers. Increase them to scale performance on more powerful servers. Note that its generally recommended to use the same value for `MinSpareServers` and `StartServers`. - -- `MaxRequestWorkers` and `ServerLimit`: The maximum number of active server processes that are allowed to simultaneously exist on your server. Setting these values too low may cause simultaneous requests to be queued or ignored, decreasing response time during times of heavy load. Setting them too high may exhaust your server memory, causing memory swaps and decreasing performance or even crashing the server altogether. These two values should be equal. - -- `MaxConnectionsPerChild`: This value represents the number of connections each thread will accept and handle before its terminated by Apache. This setting is mostly used to prevent memory leakage from consuming a significant amount of server memory. The default 0 prevents processes from ever being terminated (unless they are idle and `MaxSpareServers` has been reached, or Apache itself is terminated). If you suspect your server is affected by memory leakage, set a different value to this variable. +Prefork settings determine how many simultaneous requests Apache (and thus your server) will handle +and how many requests will queue once it can no longer handle more simultaneous requests. +These settings greatly impact your Apache's performance and system resource usage, +so you might need to do a bit of trial and error before finding which setup offers better performance without overwhelming your server's processor and/or memory. +Remember that these settings apply to the web server as a whole, meaning that they will also affect other content served by Apache. +You should also take that into consideration when setting these values. + +Apache configuration settings is often split into multiple files, to improve readability and maintainability, +so you might have to search different files until you find the one that contains Prefork's configuration values. +While not required, you will usually find these values inside a ` ... ` block, +to ensure that these values are not loaded if you decide to use another MPM. +This configuration block may contain all or some of the following variables: + +- `StartServers`: This number controls the number of server processes created when the server (Apache) is started. +As the number of processes is later on handled dynamically, this setting has little impact on performance. + +- `MinSpareServers` and `MaxSpareServers`: These settings control the number of idle server processes that are allowed to exist. +Idle processes are processes that exist but are not actually doing any work. +They consume resources (although, as they are idle, this consumption is relatively low) but are immediately available to answer a request once it comes in. +The creation of these processes take time, which is why Apache keeps a few always available. +The default value for these settings are usually 5 and 10 respectively, which is adequate for smaller servers. +Increase them to scale performance on more powerful servers. +Note that it's generally recommended using the same value for `MinSpareServers` and `StartServers`. + +- `MaxRequestWorkers` and `ServerLimit`: The maximum number of active server processes that are allowed to simultaneously exist on your server. +Setting these values too low may cause simultaneous requests to be queued or ignored, decreasing response time during times of heavy load. +Setting them too high may exhaust your server memory, causing memory swaps and decreasing performance or even crashing the server altogether. +These two values should be equal. + +- `MaxConnectionsPerChild`: This value represents the number of connections each thread will accept and handle before its terminated by Apache. +This setting is mostly used to prevent memory leakage from consuming a significant amount of server memory. +The default 0 prevents processes from ever being terminated (unless they are idle and `MaxSpareServers` has been reached, or Apache itself is terminated). +If you suspect your server is affected by memory leakage, set a different value to this variable. ### Nginx -Nginx has been known to run Shopware on several server setups. If you wish to, you can use this web server instead of Apache. Note that this setup is not supported, so our support team will not be able to assist you should you run into problems with it. +Nginx has been known to run Shopware on several server setups. +If you wish to, you can use this web server instead of Apache. +Note that this setup is not supported, so our support team will not be able to assist you should you run into problems with it. -The [following post](http://nginx.com/blog/tuning-nginx/) on the official nginx blog might be a good place to start if you are looking for performance tips for your nginx configuration. You can also use the [nginx configuration for Shopware](https://github.com/bcremer/shopware-with-nginx) provided by [Benjamin Cremer](https://github.com/bcremer). +The [following post](http://nginx.com/blog/tuning-nginx/) on the official nginx blog might be a good place to start if you are looking for performance tips for your nginx configuration. +You can also use the [nginx configuration for Shopware](https://github.com/bcremer/shopware-with-nginx) provided by [Benjamin Cremer](https://github.com/bcremer). ## PHP -Shopware 5.0 has PHP 5.4 as minimum requirement, which is, at the time of the release, the oldest supported PHP version. However, PHP 5.4 support will be dropped during Shopware 5's lifetime, and the minimum requirement will be raised to PHP 5.5. As such, we recommend using, whenever possible, PHP 5.5 or higher, not only for performance reasons, but also to ensure your system will support future releases of Shopware 5. - -At the time of this publication, the latest stable PHP version was 5.6, which includes several performance optimizations over PHP 5.5. As such, we recommend that you use PHP 5.6 whenever possible. +At the time of this publication, the latest stable PHP version was 7.3, which includes several performance optimizations over PHP 7.0 and PHP 5.6. +As such, we recommend that you use PHP 7.3 whenever possible, though Shopware currently still supports PHP 7.2. +Please check the supported PHP versions of your Shopware version before updating PHP. ### Opcode cache -Shopware's PHP code (and all PHP code) needs to be transformed from the format you see and understand into machine code your computer can actually execute. This process is complicated, and you don't need to know how it's done, but it is important to understand that it's executed on every incoming request to your server, meaning it can have a significant performance impact. +Shopware's PHP code (and all PHP code) needs to be transformed from the format you see and understand into machine code your computer can actually execute. +This process is complicated, and you don't need to know how it's done, but it is important to understand that it's executed on every incoming request to your server, +meaning it can have a significant performance impact. -An Opcode cache can be used during this code transformation process, caching the resulting machine code so it's reused across multiple requests. Skipping that transformation process will, naturally, result in better performance in all requests after the first one. +An Opcode cache can be used during this code transformation process, caching the resulting machine code, so it's reused across multiple requests. +Skipping that transformation process will, naturally, result in better performance in all requests after the first one. #### Opcode cache in PHP 5.5 and later - OPcache -One of the main changes in PHP 5.5 was the addition of Zend Optimiser+ opcode cache, now known as OPcache extension. This means that the opcode cache is installed and enabled by default (on most systems), speeding up your shop. Shopware will automatically clear this cache when needed, in some situations (i.e. when a new plugin is installed). Please keep in mind that, in some situations and depending on your system configuration, you might need to manually clear this cache. Refer to OPcache extension documentation for more information. +One of the main changes in PHP 5.5 was the addition of Zend Optimiser+ opcode cache, now known as OPcache extension. +This means that the opcode cache is installed and enabled by default (on most systems), speeding up your shop. +Shopware will automatically clear this cache when needed, in some situations (i.e. when a new plugin is installed). +Please keep in mind that, in some situations and depending on your system configuration, you might need to manually clear this cache. +Refer to OPcache extension documentation for more information. -The inclusion of OPcache extension in PHP 5.5 was one of the big performance improvements added in that version, but not the only one. Other features were added that will make Shopware (and most PHP projects) perform better in the newer version. PHP 5.6 also includes performance improvements over PHP 5.5, and it's safe to assume that future releases will continue to provide faster results over previous versions. +The inclusion of OPcache extension in PHP 5.5 was one of the big performance improvements added in that version, but not the only one. +Other features were added that will make Shopware (and most PHP projects) perform better in the newer version. +PHP 5.6 also includes performance improvements over PHP 5.5, and it's safe to assume that future releases will continue to provide faster results over previous versions. #### OPcache configuration -Depending on your system and PHP installation, OPcache might not be installed, installed but not enabled or enabled by default. Check your `phpinfo()` output for more info on your current settings. The [official project documentation page](http://php.net/manual/en/book.opcache.php) details all the configuration options you can set in order to fine tune OPcache's behaviour. Below we document some of these settings. +Depending on your system and PHP installation, OPcache might not be installed, installed but not enabled or enabled by default. +Check your `phpinfo()` output for more info on your current settings. +The [official project documentation page](http://php.net/manual/en/book.opcache.php) details all the configuration options you can set in order to fine tune OPcache's behaviour. +Below we document some of these settings. -- `opcache.max_accelerated_files`: The maximum number of PHP files OPcache will handle. A typical clean Shopware 5 installation has around 7000 PHP files, but that does not include generated cache files, 3rd party or custom plugin files or other PHP files your web server might host simultaneously. Make sure that the value you put here is high enough to include all your PHP files (unless, for some reason, you have many files and not enough memory). +- `opcache.max_accelerated_files`: The maximum number of PHP files OPcache will handle. +A typical clean Shopware 5 installation has around 7000 PHP files, but that does not include generated cache files, 3rd party or custom plugin files or other PHP files your web server might host simultaneously. +Make sure that the value you put here is high enough to include all your PHP files (unless, for some reason, you have many files and not enough memory). -- `opcache.memory_consumption`: How much memory you wish to allocate to OPcache. The default value is 64MB, which should be enough for smaller shop installations. [Rasmus Lerdorf](https://github.com/rlerdorf) created a [simple PHP script](https://github.com/rlerdorf/opcache-status) that you can use to check your current OPcache settings and status, including hit/miss rates and memory status. Use this tool to monitor your cache status and decide if you need to fine tune this setting. As before, this value is server-wide, meaning that you might need to increase it if you host more PHP projects on your server or if your Shopware installation includes a significant amount of 3rd party or custom plugins. +- `opcache.memory_consumption`: How much memory you wish to allocate to OPcache. +The default value is 64MB, which should be enough for smaller shop installations. +[Rasmus Lerdorf](https://github.com/rlerdorf) created a [simple PHP script](https://github.com/rlerdorf/opcache-status) that you can use to check your current OPcache settings and status, including hit/miss rates and memory status. +Use this tool to monitor your cache status and decide if you need to fine tune this setting. +As before, this value is server-wide, meaning that you might need to increase it if you host more PHP projects on your server or if your Shopware installation includes a significant amount of 3rd party or custom plugins. -- `opcache.revalidate_freq`: This setting tells OPcache how often (in seconds) it should check your files for changes. The default value is 2 (seconds), but you can safely increase it to a bigger value once your shop is in production and you don't expect frequent code changes +- `opcache.revalidate_freq`: This setting tells OPcache how often (in seconds) it should check your files for changes. +The default value is 2 (seconds), but you can safely increase it to a bigger value once your shop is in production, +and you don't expect frequent code changes - `opcache.fast_shutdown`: This optimizes memory handling when deconstructing objects, resulting in improved performance. -- `opcache.save_comments` This setting defaults to 1 and must not be changed. Some Shopware code relies on annotations that don't work properly if this configuration value is set to 0. +- `opcache.save_comments` This setting defaults to 1 and must not be changed. +Some Shopware code relies on annotations that don't work properly if this configuration value is set to 0. #### Opcode cache in PHP 5.4 - APC -PHP 5.4 does not include a opcode cache out of the box, but you can (and we recommend) that you install APC opcode cache. APC is and stands for Alternative PHP Cache, and will allow you to increase your server performance if you are using PHP 5.4 +PHP 5.4 does not include an opcode cache out of the box, but you can (and we recommend) that you install APC opcode cache. +APC is and stands for Alternative PHP Cache, and will allow you to increase your server performance if you are using PHP 5.4 #### APC configuration -You should check your `phpinfo()` output to determine if APC is installed and enabled. Installing and enabling APC depends on your system, but you can find information about this on related support sites or forums. You can find more details about APC configuration on the [project configuration page](http://php.net/manual/en/apc.configuration.php). Bellow you can see some of the most commonly customized values: +You should check your `phpinfo()` output to determine if APC is installed and enabled. +Installing and enabling APC depends on your system, but you can find information about this on related support sites or forums. +You can find more details about APC configuration on the [project configuration page](http://php.net/manual/en/apc.configuration.php). +Below you can see some of the most commonly customized values: -- `apc.shm_segments`: The number of memory segments allocated to APC. Typically you want 1 (default value). Increasing this value will multiply the memory consumption of APC, so you should be careful if you decide to change this value +- `apc.shm_segments`: The number of memory segments allocated to APC. Typically, you want 1 (default value). +Increasing this value will multiply the memory consumption of APC, so you should be careful if you decide to change this value -- `apc.shm_size`: The memory size allocated to each segment. The default is 32M, which you should increase to at least 64M or more, depending on your system and expected workload. +- `apc.shm_size`: The memory size allocated to each segment. +The default is 32M, which you should increase to at least 64M or more, depending on your system and expected workload. -- `apc.num_files_hint`: The number of files that APC will handle. A typical clean Shopware 5 installation has around 7000 PHP files, but that does not include generated cache files, 3rd party or custom plugin files or other PHP files your web server might host simultaneously. Make sure that the value you put here is high enough to include all your PHP files (unless, for some reason, you have many files and not enough memory). +- `apc.num_files_hint`: The number of files that APC will handle. +A typical clean Shopware 5 installation has around 7000 PHP files, but that does not include generated cache files, 3rd party or custom plugin files or other PHP files your web server might host simultaneously. +Make sure that the value you put here is high enough to include all your PHP files (unless, for some reason, you have many files and not enough memory). -- `apc.ttl` and `apc.user_ttl`: Once the cache fills up and new entries come in, some old entries need to be removed. These TTLs (in seconds) determine how long an old entry is allowed to stay in cache until its removed. Using 0 means that old entries will never be removed. The values for this settings greatly depend on your system setup, but it's recommended to not set them to 0. +- `apc.ttl` and `apc.user_ttl`: Once the cache fills up and new entries come in, some old entries need to be removed. +These TTLs (in seconds) determine how long an old entry is allowed to stay in cache until its removed. +Using 0 means that old entries will never be removed. +The values for these settings greatly depend on your system setup, but it's recommended to not set them to 0. ### User data cache - APCu -While OPcache and APC speed up PHP code processing, APCu does the same for user data. If available, it's used by Shopware to store in memory commonly accessed data that would otherwise be stored in disk, and slower to read. For that reason, it's recommended that you install APCu on your production environment. +While OPcache and APC speed up PHP code processing, APCu does the same for user data. +If available, it's used by Shopware to store in memory commonly accessed data that would otherwise be stored in disk, and slower to read. +For that reason, it's recommended that you install APCu on your production environment.
    -Note: Be careful not to confuse APC (opcode cache) with APCu (user data cache). At the time, APC is not compatible with PHP 5.5 or greater, but APCu is. APCu's configuration variables are set in the `apc` namespace, and not in `apcu`, as you might expect. This means to abstract differences between the two libraries, and is intended. Your `phpinfo()` should display `APC support: Emulated`, meaning you are correctly using APCu but not APC. If you are using PHP 5.4, you might want to use APC to replace OPcache, which is not bundled into PHP itself in this version. +Note: Be careful not to confuse APC (opcode cache) with APCu (user data cache). +At the time, APC is not compatible with PHP 5.5 or greater, but APCu is. +APCu's configuration variables are set in the `apc` namespace, and not in `apcu`, as you might expect. +This means to abstract differences between the two libraries, and is intended. +Your `phpinfo()` should display `APC support: Emulated`, meaning you are correctly using APCu but not APC. +If you are using PHP 5.4, you might want to use APC to replace OPcache, which is not bundled into PHP itself in this version.
    APCu uses the same configuration variables as APC, so you can see the above APC configuration section for more details. - Please keep in mind that you need to change `apc.num_files_hint` and/or `apc.shm_size` depending on if you are using only APC, only APCu or both simultaneously. +Please keep in mind that you need to change `apc.num_files_hint` and/or `apc.shm_size` depending on if you are using only APC, only APCu or both simultaneously. ## Cron -Some tasks associated with your shop can be executed in the background, like sending emails or refreshing search indexes. For these tasks, you can use Cron jobs. These execute certain processes in the background, automatically, at certain intervals, indirectly contributing to increased usage speed when a customer visits your shop. To find out more about Cron jobs in Shopware, read [the following wiki page](http://en.wiki.shopware.com/_detail_1103.html). +Some tasks associated with your shop can be executed in the background, like sending emails or refreshing search indexes. +For these tasks, you can use Cron jobs. +These execute certain processes in the background, automatically, at certain intervals, indirectly contributing to increased usage speed when a customer visits your shop. +To find out more about Cron jobs in Shopware, read [the following wiki page](https://docs.shopware.com/en/shopware-5-en/settings/system-cronjobs). ## HTTP Cache -One of the most commonly used tools for speeding up websites is an HTTP cache. These work by storing previously generated responses and returning them immediately when a similar requests comes in, preventing the server from generating a new, equal response, and the time and resource consumption associated with that job. +One of the most commonly used tools for speeding up websites is an HTTP cache. +These work by storing previously generated responses and returning them immediately when a similar request comes in, +preventing the server from generating a new, equal response, and the time and resource consumption associated with that job. ### Shopware HTTP Cache -Shopware 5, like previous versions, includes its own HTTP cache implementation, in PHP. You can read more details about it in the [Shopware 5 performance guide for developers](/developers-guide/shopware-5-performance-for-devs). While this is not the most performing HTTP cache implementation, it requires no additional system configuration, and should work properly even in entry level hosting solutions, where resources and configuration access are limited. +Shopware 5, like previous versions, includes its own HTTP cache implementation, in PHP. +You can read more details about it in the [Shopware 5 performance guide for developers](/developers-guide/shopware-5-performance-for-devs). +While this is not the most performing HTTP cache implementation, it requires no additional system configuration, +and should work properly even in entry level hosting solutions, where resources and configuration access are limited. ### Varnish -Varnish is also an HTTP cache implementation, but offers much better performance and customization than Shopware's PHP HTTP cache. It's a very scalable HTTP cache, meaning it can be used for small as well as enterprise grade shops. Shopware officially supports [Varnish configuration](/sysadmins-guide/varnish-setup/) for customers with Enterprise licenses. +Varnish is also an HTTP cache implementation, but offers much better performance and customization than Shopware's PHP HTTP cache. +It's a very scalable HTTP cache, meaning it can be used for small as well as enterprise grade shops. +Shopware officially supports [Varnish configuration](/sysadmins-guide/varnish-setup/) for customers with Enterprise licenses. diff --git a/source/sysadmins-guide/shopware-cluster-setup/index.md b/source/sysadmins-guide/shopware-cluster-setup/index.md index ab1b8e9bf7..dfd26fbefc 100644 --- a/source/sysadmins-guide/shopware-cluster-setup/index.md +++ b/source/sysadmins-guide/shopware-cluster-setup/index.md @@ -5,53 +5,52 @@ github_link: sysadmins-guide/shopware-cluster-setup/index.md indexed: true group: System Guides menu_title: Cluster setup -menu_order: 80 +menu_order: 90 --- -Installing and running Shopware on a single server LAMP stack is easy to accomplish and a common solution for small and -mid size customers. When it comes to high performance and high reliability, however, having a clustered, redundant -setup is inevitable. +Installing and running Shopware on a single server LAMP stack is easy to accomplish and a common solution for small and mid-size customers. +When it comes to high performance and high reliability, however, having a clustered, redundant setup is inevitable. -The following document will describe ways to cluster Shopware as well as considerations related to running Shopware -in a clustered setup. +The following document will describe ways to cluster Shopware as well as considerations related to running Shopware in a clustered setup.
    -Clustering is always highly individual and depends on the customer's requirements as well as the functional scope of the shop. For this reason every project will need to adjust the suggestions of this document for the actual hosting circumstances and customer / deploment workflows. +Clustering is always highly individual and depends on the customer's requirements as well as the functional scope of the shop. +For this reason every project will need to adjust the suggestions of this document for the actual hosting circumstances and customer / deployment workflows.
    ## What is clustering? -Generally speaking, clustering is a way to link multiple computers for a certain purpose. Usually this purpose is to increase -availability and / or performance of the setup and has several benefits: +Generally speaking, clustering is a way to link multiple computers for a certain purpose. +Usually this purpose is to increase availability and / or performance of the setup and has several benefits: -* you can introduce redundancy for any single component (e.g. cache, appserver or database). Even if a component -fails, the shop will still work, as there is no "single point of failure" -* the load (i.e. the users) can be distributed across the cluster. So there is not a single appserver that will -need to handle all users - but all users are distributed across all available appservers. -* scaling the setup becomes easier: As any component is layed out in a redundant manner, you can easily add another -varnish server or appserver on the fly once your shop is confronted with more traffic (e.g. after an TV advertisement). +* you can introduce redundancy for any single component (e.g. cache, appserver or database). +Even if a component fails, the shop will still work, as there is no "single point of failure" +* the load (i.e. the users) can be distributed across the cluster. +So there is not a single appserver that will need to handle all users - but all users are distributed across all available app servers. +* scaling the setup becomes easier: As any component is laid out in a redundant manner, +you can easily add another varnish server or appserver on the fly once your shop is confronted with more traffic (e.g. after an TV advertisement). ## Shopware cluster setup The following schema shows a simplified cluster setup. The components will be discussed in detail below. ![server setup overview](/sysadmins-guide/shopware-cluster-setup/img/setup.svg) ### Load Balancer (LB) -The load balancer is the foremost instance in every cluster setup. It will handle all customer requests and dispatch -them to one of the varnish cache instances. +The load balancer is the foremost instance in every cluster setup. +It will handle all customer requests and dispatch them to one of the varnish cache instances. Responsibility: * SSL offloading -* equal distribution of the traffic across all caches / appservers +* equal distribution of the traffic across all caches / app servers Software to run: * Nginx (TLS offloading as well as load balancing) Scaling: -* second load balancer as failover -* possibly floating IP and health checks for automated failover +* second load balancer as fail-over +* possibly floating IP and health checks for automated fail-over ### Varnish servers -If stand alone caches are required, Shopware recommends Varnish, as there is a [varnish configuration](/sysadmins-guide/varnish-setup/) +If standalone caches are required, Shopware recommends Varnish, as there is a [varnish configuration](/sysadmins-guide/varnish-setup/) available. Responsibility: @@ -64,8 +63,7 @@ Scaling: * scales horizontally (numerous cache server possible) ### Appserver -An appserver runs the actual Shopware application and handles all requests which could not be handled by the cache -before: +An appserver runs the actual Shopware application and handles all requests which could not be handled by the cache before: Responsibility: * handle user requests @@ -82,9 +80,9 @@ Scaling: * scales horizontally (numerous appserver possible) ### Admin server -The admin server is an appserver dedicated to the Shopware back office. It is also the leading appserver - all code changes -(e.g. deployment) happen on the admin server and are synced to the appservers. Furthermore all periodic tasks should be -run here. +The admin server is an appserver dedicated to the Shopware back office. +It is also the leading appserver - all code changes (e.g. deployment) happen on the admin server and are synced to the app servers. +Furthermore, all periodic tasks should be run here. Responsibility: * serving of /backend (the back office) @@ -113,8 +111,8 @@ Scaling: * cluster like percona / galera ### Memcache -For high performance setups, we recommend to store the sessions in memcache instead of the database (which is Shopware's -default behaviour). See [setup description here](/sysadmins-guide/memcached-as-session-handler/). +For high performance setups, we recommend to store the sessions in memcache instead of the database (which is Shopware's default behaviour). +See [setup description here](/sysadmins-guide/memcached-as-session-handler/). Responsibility: * Store sessions @@ -124,12 +122,12 @@ Scaling: * solutions like repcache available ### Elasticsearch -Elasticsearch is a so called "no sql" storage, a non relational database engine, which is very efficient in searching -and filtering big catalogues. For that reason it can optionally be used, if you have many articles in your shop -or if there are special requirements for filtering and searching. Shopware generally recommends using Elasticsearch -if more then ~140000 articles are in place. Even with less articles your overall system performance might profit from -using Elasticsearch, as filtering and search queries will usually generate quite some load on the database and -have a low cache hit rate. +Elasticsearch is a so-called "no sql" storage, a non-relational database engine, +which is very efficient in searching and filtering big catalogues. +For that reason it can optionally be used, if you have many articles in your shop or if there are special requirements for filtering and searching. +Shopware generally recommends using Elasticsearch if more than ~140000 articles are in place. +Even with fewer articles your overall system performance might profit from using Elasticsearch, +as filtering and search queries will usually generate quite some load on the database and have a low cache hit rate. Additional information regarding [Elasticsearch are available here](/sysadmins-guide/elasticsearch-setup/). Responsibility: @@ -143,80 +141,75 @@ Scaling: * scales horizontally (numerous ES server possible) ### Images -Images are usually uploaded on the admin server - but need to be available on all appservers as well. Usually syncing -(duplicating) the images is not to be recommended for larger setups, so there are alternatives in place: +Images are usually uploaded on the admin server - but need to be available on all app servers as well. +Usually syncing (duplicating) the images is not to be recommended for larger setups, so there are alternatives in place: -* Shopware media service: The [shopware media services](/developers-guide/shopware-5-media-service/) enables you, to use -external storages like Amazon S3. A proof of concept implementation for S3 is -[available on github](https://github.com/ShopwareLabs/SwagMediaS3). Other backend are possible through the filesystem - abstraction layer "[flysystem](http://flysystem.thephpleague.com/)", e.g. the [SFTP adapter](https://github.com/shopwareLabs/SwagMediaSftp). -* Network storages: Network storages like NFS are still very common to share images and other files across multiple -appservers. Please be aware, that NFS can have a massive performance impact, so Shopware does not recommend to use NFS -other than for images. +* Shopware media service: The [shopware media services](/developers-guide/shopware-5-media-service/) enables you, +to use external storages like Amazon S3. A proof of concept implementation for S3 is [available on github](https://github.com/shopware5/SwagMediaS3). +Other backend are possible through the filesystem abstraction layer "[flysystem](http://flysystem.thephpleague.com/)", e.g. the [SFTP adapter](https://github.com/shopware5/SwagMediaSftp). +* Network storages: Network storages like NFS are still very common to share images and other files across multiple app servers. +Please be aware, that NFS can have a massive performance impact, so Shopware does not recommend to use NFS other than for images. ## Variations -There is not "the one and only" way to build a cluster. The layout of the cluster should be adjusted to the needs and -budget of the customer. For that reason there are a lot of variations in the setup that should be considered: +There is not "the one and only" way to build a cluster. +The layout of the cluster should be adjusted to the needs and budget of the customer. +For that reason there are a lot of variations in the setup that should be considered: ### Caching on the appserver -In some cases you might want to consider moving the cache layer to the appservers themselves. In that case the -appserver would include varnish, apache and the Shopware application. This will keep the overal infrastructure -smaller, but will force you to optimize the appserver for varnish *and* the webserver / application. +In some cases you might want to consider moving the cache layer to the app servers themselves. +In that case the appserver would include varnish, apache and the Shopware application. +This will keep the overall infrastructure smaller, but will force you to optimize the appserver for varnish *and* the webserver / application. -As an alternative you could also remove the varnish cache entirely and only rely on Shopware's built in HTTP cache. -If you are using many uncached ESI tags or uncached pages in general, this might even be beneficial, as Shopware -has some optimizations regarding the handling of ESI tags from within the built in HTTP cache. Please notice, that -this will force you to optimize for appserver *and* built in cache. +As an alternative you could also remove the varnish cache entirely and only rely on Shopware's built-in HTTP cache. +If you are using many uncached ESI tags or uncached pages in general, this might even be beneficial, +as Shopware has some optimizations regarding the handling of ESI tags from within the built-in HTTP cache. +Please notice, that this will force you to optimize for appserver *and* built in cache. ### Sessions -Shopware handles sessions in the database by default. As this is not recommended for frontend sessions in a -high performance setup, using memcached instead was discussed before. In some cases, the memcached instance can -run alongside with other services like the load balancer, varnish or elastic search, if used. -If memcached is not an option in your case, using redis or even dedicated databases are possible by providing -custom session backends. +Shopware handles sessions in the database by default. +As this is not recommended for frontend sessions in a high performance setup, using memcached instead was discussed before. +In some cases, the memcached instance can run alongside with other services like the load balancer, varnish or elastic search, if used. +If memcached is not an option in your case, using redis or even dedicated databases are possible by providing custom session backends. ## Additional topics to be aware of -Clustering usually has quite some implications regarding the "single source of truth" in the setup. This especially applies -for local caches (e.g. APCu, proxy caches, generated attribute models), locally generated files (e.g. user upload) -and the sourcecode itself. +Clustering usually has quite some implications regarding the "single source of truth" in the setup. +This especially applies for local caches (e.g. APCu, proxy caches, generated attribute models), +locally generated files (e.g. user upload) and the sourcecode itself. ### Invalidating caches -Since Shopware 5.2.0 you are able to configure multiple HTTP reverse proxies (e.g. varnish). So whenever the HTTP cache -is cleared or certain pages needs to be invalidated, Shopware will distribute this BAN and PURGE requests to all configured -caches. +Since Shopware 5.2.0 you are able to configure multiple HTTP reverse proxies (e.g. varnish). +So whenever the HTTP cache is cleared or certain pages need to be invalidated, +Shopware will distribute this BAN and PURGE requests to all configured caches. -This does not yet apply for clearing caches like attribute cache, proxy caches, object caches etc. You can clear those caches -using the [cache endpoint of the Shopware REST API](/developers-guide/rest-api/api-resource-cache/). +This does not yet apply for clearing caches like attribute cache, proxy caches, object caches etc. +You can clear those caches using the [cache endpoint of the Shopware REST API](/developers-guide/rest-api/api-resource-cache/). ### Uploads -Usually content is generated in and distributed by the admin server. The configured file syncs and media shares will -ensure, that all appservers to have access to this information. If you are using plugins that allow customers -to upload files in the shop frontend, the uploaded files will not be available for all other appservers automatically. -In those cases, you need to make sure, that the uploaded files are shared using CDN, shared storages or other syncing -mechanisms. +Usually content is generated in and distributed by the admin server. +The configured file syncs and media shares will ensure, that all app servers to have access to this information. +If you are using plugins that allow customers to upload files in the shop frontend, +the uploaded files will not be available for all other app servers automatically. +In those cases, you need to make sure, that the uploaded files are shared using CDN, +shared storages or other syncing mechanisms. ### Adminserver -Usually the shopware backoffice is available at `http://my-shop.com/backend`. Many customers reason, that introducing -a rule on the load balancer, which redirects all `backend/*` traffic to the admin server will be sufficient. -This is not always the case: The shopware backoffice will dynamically load files from other locations such as -`vendor/*` or `themes/*`. If your regular appservers are down for maintanance or other reasons, these requests will -fail if your load balancer rule is too naive. +Usually the shopware backoffice is available at `http://my-shop.com/backend`. +Many customers reason, that introducing a rule on the load balancer, which redirects all `backend/*` traffic to the admin server will be sufficient. +This is not always the case: The shopware backoffice will dynamically load files from other locations such as `vendor/*` or `themes/*`. +If your regular app servers are down for maintanance or other reasons, these requests will fail if your load balancer rule is too naive. For that reason we recommend, having a separate virtual host for your backoffice, such as `http://admin.my-shop.com`. -This way the backoffice will fetch all required files from that specific virtual host - and you can easily configure - your load balancer correspondingly. +This way the backoffice will fetch all required files from that specific virtual host - and you can easily configure your load balancer correspondingly. ### Syncing -After deploying Shopware from VCS, installing plugins or generating themes from the admin panel the file base of the -admin server should be synced to all the appservers. `rsync` is a commonly used tool for this kind of task - -but you could also consider using [lsyncd](https://github.com/axkibe/lsyncd), which is an extension to `rsync` and -watches and syncs directories automatically. -As most shop setups are quite individual and will include custom plugins, there is no finite list of directories that needs -to be synced. Generally all files / directories of the Shopware setup should be synced across the appservers. +After deploying Shopware from VCS, installing plugins or generating themes from the admin panel the file base of the admin server should be synced to all the app servers. +`rsync` is a commonly used tool for this kind of task - but you could also consider using [lsyncd](https://github.com/axkibe/lsyncd), +which is an extension to `rsync` and watches and syncs directories automatically. +As most shop setups are quite individual and will include custom plugins, there is no finite list of directories that need to be synced. +Generally all files / directories of the Shopware setup should be synced across the app servers. The following directories, however, need special treatment: **No syncing**: * `/var/cache`: Handled individually on every appserver, no syncing -* `/web`: Handled individually on every appserver, no syncing needed as of Shopware 5.2 **Larger directories**: * `/files`: Synced to each appserver or shared storage. Depends on installed plugins and used Shopware featured such as ESD etc. @@ -227,10 +220,11 @@ The following directories, however, need special treatment: * `/themes/Frontend`: Changed when new themes are created from the admin panel * `/media`: Changed when new images / videos / media are uploaded in the admin panel * `/files`: Changed when ESD items are uploaded or order documents are generated +* `/web`: Sitemap generation ## Additional resources * [Shopware system requirements](/sysadmins-guide/system-requirements/) -* [Performance tipps for sysadmins](/sysadmins-guide/shopware-5-performance-for-sysadmins/) +* [Performance tips for sysadmins](/sysadmins-guide/shopware-5-performance-for-sysadmins/) * [Elasticsearch configuration](/sysadmins-guide/elasticsearch-setup/) * [Varnish configuration](/sysadmins-guide/varnish-setup/) * [Memcached configuration](/sysadmins-guide/memcached-as-session-handler/) diff --git a/source/sysadmins-guide/system-requirements/index.md b/source/sysadmins-guide/system-requirements/index.md index 6dc3a5472e..4f3a088aa3 100644 --- a/source/sysadmins-guide/system-requirements/index.md +++ b/source/sysadmins-guide/system-requirements/index.md @@ -12,13 +12,11 @@ menu_order: 10 ### Webserver - Linux-based operating system with Apache 2.2 or 2.4 web server with enabled `mod_rewrite` module and ability to override options in `.htaccess` files -- PHP 5.6.4 or higher* -- MySQL 5.5 or higher +- PHP 7.2.0 or higher +- MySQL 5.7 or higher - Possibility to set up cron jobs - Minimum 4 GB available hard disk space - (\*) PHP 5.6.0 - 5.6.3 are not compatible caused by a [session bug](https://bugs.php.net/bug.php?id=68331) - ### Required PHP extensions: - ctype @@ -47,24 +45,48 @@ It's strongly recommended that you verify the APCu -- IonCube Loader version 5.0 or higher only needed for encrypted third-party plugins - When using Shopware ESD functionalities, it's highly recommended to use Apache `mod_xsendfile` +- For enabling the HSTS header, Apache's `header` module is necessary. +Simply add the line `Header always set Strict-Transport-Security "max-age=31536000"` to your .htaccess ### Other requirements -The requirements specified above reflect only the minimum requirements of Shopware. Specific hardware requirements vary depending on the size and expected traffic of your shop. Additional server configuration may be required. Plugins installed on your shop may increase Shopware's resource needs or add additional system dependencies. Please refer to each plugin's documentation for more information. +The requirements specified above reflect only the minimum requirements of Shopware. +Specific hardware requirements vary depending on the size and expected traffic of your shop. +Additional server configuration may be required. +Plugins installed on your shop may increase Shopware's resource needs or add additional system dependencies. +Please refer to each plugin's documentation for more information. ### Alternative server setups -The above requirements reflect the officially supported and recommended system setup to run Shopware. However, you might be able to run Shopware on equivalent setups (Mac OS, nginx, MariaDB, etc). Please keep in mind that we are unable to provide official support on those setups. +The above requirements reflect the officially supported and recommended system setup to run Shopware. +However, you might be able to run Shopware on equivalent setups (Mac OS, nginx, MariaDB, etc.). +Please keep in mind that we are unable to provide official support on those setups. + +## Elasticsearch +Shopware supports [Elasticsearch](https://www.elastic.co/products/elasticsearch) servers in versions 2.x, 5.x and 6.x out of the box. +For more details see the Elasticsearch setup. ## Shopware 5 System Requirements - Administration client -The administration of your shop can be done completely online via the web browser. The following requirements should be met by any client system that uses the administration backend. These requirement differ from the frontend user system requirements. +The administration of your shop can be done completely online via the web browser. +The following requirements should be met by any client system that uses the administration backend. +These requirements differ from the frontend user system requirements. ### Requirements: @@ -72,5 +94,4 @@ The administration of your shop can be done completely online via the web browse - JavaScript and Cookies enabled - 4 GB RAM - Dual-core CPU -- Minimum backend resolution: 1366 x 768 pixels - +- Minimum backend resolution: 1366 x 768 pixels diff --git a/source/sysadmins-guide/varnish-setup/index.md b/source/sysadmins-guide/varnish-setup/index.md index 583b5ba267..0595f64799 100644 --- a/source/sysadmins-guide/varnish-setup/index.md +++ b/source/sysadmins-guide/varnish-setup/index.md @@ -9,7 +9,7 @@ tags: indexed: true group: System Guides menu_title: Varnish setup -menu_order: 60 +menu_order: 70 ---
    @@ -26,7 +26,7 @@ This configuration requires at least version 4.0 of Varnish and at least version The PHP based reverse proxy has to be disabled, which can be done by adding the following section to your `config.php`: ``` -'httpCache' => array( +'httpcache' => array( 'enabled' => false, ), ``` @@ -150,6 +150,13 @@ sub vcl_recv { # Mitigate httpoxy application vulnerability, see: https://httpoxy.org/ unset req.http.Proxy; + # Strip query strings only needed by browser javascript. Customize to used tags. + if (req.url ~ "(\?|&)(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=") { + # see rfc3986#section-2.3 "Unreserved Characters" for regex + set req.url = regsuball(req.url, "(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=[A-Za-z0-9\-\_\.\~]+&?", ""); + } + set req.url = regsub(req.url, "(\?|\?&|&)$", ""); + # Normalize query arguments set req.url = std.querysort(req.url); @@ -235,10 +242,14 @@ sub vcl_recv { return (pass); } - # Do not cache these paths. - if (req.url ~ "^/backend" || - req.url ~ "^/backend/.*$") { - + # Always pass these paths directly to php without caching + # Note: virtual URLs might bypass this rule (e.g. /en/checkout) + if (req.url ~ "^/(checkout|account|backend)(/.*)?$") { + return (pass); + } + + # Workaround for Basket Widget Caching and Compare. Will be fixed with https://issues.shopware.com/issues/SW-23673 + if (req.url ~ "^/\?module=widgets&controller=checkout&action=info$" || req.url ~ "^/\?module=widgets&controller=compare$") { return (pass); } @@ -266,7 +277,11 @@ sub vcl_hash { sub vcl_hit { if (obj.http.X-Shopware-Allow-Nocache && req.http.cookie ~ "nocache=") { - set req.http.X-Cookie-Nocache = regsub(req.http.Cookie, "^.*?nocache=([^;]*);*.*$", "\1"); + if (obj.http.X-Shopware-Allow-Nocache && req.http.cookie ~ "slt=") { + set req.http.X-Cookie-Nocache = regsub(req.http.Cookie, "^.*?nocache=([^;]*);*.*$", "\1, slt"); + } else { + set req.http.X-Cookie-Nocache = regsub(req.http.Cookie, "^.*?nocache=([^;]*);*.*$", "\1"); + } if (std.strstr(req.http.X-Cookie-Nocache, obj.http.X-Shopware-Allow-Nocache)) { return (pass); } @@ -327,6 +342,11 @@ sub vcl_deliver { ## unset the headers, thus remove them from the response the client sees unset resp.http.X-Shopware-Allow-Nocache; unset resp.http.X-Shopware-Cache-Id; + + # remove link header, if session is already started to save client resources + if (req.http.cookie ~ "session-") { + unset resp.http.Link; + } # Set a cache header to allow us to inspect the response headers during testing if (obj.hits > 0) { @@ -347,4 +367,8 @@ The proxy is not recognized as a "[trusted proxy](https://developers.shopware.co ### Error message "Reverse proxy returned invalid status code" This message appears when automatic cache invalidation fails. A proxy (mostly the SSL Proxy) didn't forward the BAN or PURGE request to the cache. Storing the cache proxy's IP (e.g. http://127.0.01/) should solve the problem. [Backend configuration](/developers-guide/http-cache/#backend) -If the problem still persists, investigate on the actual status code the proxy returns. Code 405 indicates, that the appserver is not permitted to purge the cache, code 404 indicates, that the proxy's IP is wrong or not accessible by the appserver. +If the problem still persists, investigate on the actual status code the proxy returns. Code 405 indicates, that the appserver is not permitted to purge the cache, code 404 indicates, that the proxy's IP is wrong or not accessible by the appserver. + +### Varnish has no hits when using HTTP Authenticate or Authorization + +If you are using any kind of HTTP authentication or authorisation please be aware, that by default our Varnish configuration ignores these request and does not cache them! If you want to use Varnish combined with HTTP authentication, you can use a webserver which handles the authentication beforehand and unsets the corresponding headers ("Authenticate" and "Authorization"). diff --git a/source/theme-guide/index.html b/source/theme-guide/index.html index df9da19b67..91521eccfe 100644 --- a/source/theme-guide/index.html +++ b/source/theme-guide/index.html @@ -15,7 +15,7 @@
  • Getting started with Shopware Templating
  • Getting started with Smarty
  • Getting started with LESS
  • -
  • Using CSS and JavaScript files in themes
  • +
  • Using CSS and JavaScript in themes
  • Using the Responsive theme default components
  • Getting started with jQuery plugins and the StateManager
  • Using the Theme.php for custom theme configuration
  • diff --git a/watch.sh b/watch.sh index ae6cf81e6c..8a3b4caa5a 100755 --- a/watch.sh +++ b/watch.sh @@ -3,6 +3,8 @@ set -o nounset set -o errexit set -o pipefail +rm -rf ./output_* + sculpinBin="./vendor/bin/sculpin" PORT=${1:-8000}