403Webshell
Server IP : 46.101.140.223  /  Your IP : 216.73.216.134
Web Server : nginx/1.22.1
System : Linux debian-s-1vcpu-1gb-fra1-01 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
User : deploy-others ( 1001)
PHP Version : 8.2.32
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /home/deploy/projects/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/deploy/projects/mysql.sql
-- Adminer 4.8.1 MySQL 5.5.5-10.6.4-MariaDB-1:10.6.4+maria~focal dump

SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';

SET NAMES utf8mb4;

CREATE DATABASE `folkprog` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `folkprog`;

DROP TABLE IF EXISTS `abstract_post`;
CREATE TABLE `abstract_post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category_id` int(11) DEFAULT NULL,
  `type` int(11) NOT NULL,
  `views_count` int(11) DEFAULT NULL,
  `likes_count` int(11) DEFAULT NULL,
  `dis_likes_count` int(11) DEFAULT NULL,
  `title` varchar(1000) COLLATE utf8mb3_unicode_ci NOT NULL,
  `slug` varchar(1000) COLLATE utf8mb3_unicode_ci NOT NULL,
  `text` longtext COLLATE utf8mb3_unicode_ci NOT NULL,
  `published` tinyint(1) NOT NULL,
  `published_at` datetime DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  `meta_description` varchar(255) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `meta_keywords` varchar(1000) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `show_in_main_menu` tinyint(1) DEFAULT NULL,
  `_order` int(11) DEFAULT NULL,
  `attributes` longtext COLLATE utf8mb3_unicode_ci DEFAULT NULL COMMENT '(DC2Type:array)',
  PRIMARY KEY (`id`),
  KEY `IDX_A96E1F8C12469DE2` (`category_id`),
  CONSTRAINT `FK_A96E1F8C12469DE2` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

INSERT INTO `abstract_post` (`id`, `category_id`, `type`, `views_count`, `likes_count`, `dis_likes_count`, `title`, `slug`, `text`, `published`, `published_at`, `created_at`, `updated_at`, `meta_description`, `meta_keywords`, `show_in_main_menu`, `_order`, `attributes`) VALUES
(82,	40,	0,	10353,	4,	0,	'Installation of previous/old versions of packages in Debian',	'installing-previous-versions-of-packages-in-debian',	'<ol>\n<li><p>Go to <a href=\"http://snapshot.debian.org/\">http://snapshot.debian.org/</a> and enter the desired <strong>package</strong> in the search (for example, php5).</p>\n</li>\n<li><p>In the list, select the desired version (for example, 5.5.12+dfsg-2), go to it, select the <strong>repository</strong> where it was last seen (in my case, we see &quot;Seen in debian on 2014-05-12 23:24:48 in <a href=\"http://snapshot.debian.org/archive/debian/20140512T232448Z/pool/main/p/php5/\">/pool/main/p/php5</a>.&quot;, click on the link. Modify the address from <a href=\"http://snapshot.debian.org/archive/debian/20140512T232448Z/pool/main/p/php5/\">http://snapshot.debian.org/archive/debian/20140512T232448Z/pool/main/p/php5/</a> to <a href=\"http://snapshot.debian.org/archive/debian/20140512T232448Z/\">http://snapshot.debian.org/archive/debian/20140512T232448Z/</a> (convert to the <strong>repository</strong> address), add to sources.list:</p>\n</li>\n</ol>\n<pre><code>nano /etc/apt/sources.list\n</code></pre>\n<pre><code>deb http://snapshot.debian.org/archive/debian/20140512T232448Z/ testing main non-free contrib\ndeb http://snapshot.debian.org/archive/debian/20131028T215931Z/ testing main non-free contrib\n</code></pre>\n<pre><code>deb http://snapshot.debian.org/archive/debian/20140512T232448Z/ main testing\n</code></pre>\n<p>(The last line is added)</p>\n<ol start=\"3\">\n<li>Then update the indexes:</li>\n</ol>\n<pre><code>aptitude -o Acquire::Check-Valid-Until=false update\n</code></pre>\n<p>Now let&#39;s see the available versions of php (for example):</p>\n<pre><code>aptitude show -v php5\n</code></pre>\n<ol start=\"5\">\n<li>Then install the specific version:</li>\n</ol>\n<pre><code>aptitude install php5=version\n</code></pre>\n<p>For example:</p>\n<pre><code>aptitude install php5=5.5.12+dfsg-2\n</code></pre>\n<p>(Also, most likely, you will need to downgrade php5-cli). Then follow the hints of aptitude, choose a reasonable option with the downgrade of php5 - packages, not the removal of over 9000 packages. 6. It may be useful to freeze the package from further update:</p>\n<pre><code>aptitude hold package_name\n</code></pre>\n<p>To undo:</p>\n<pre><code>aptitude unhold package_name\n</code></pre>\n',	1,	'2014-09-30 00:00:00',	'2015-03-25 00:28:19',	'2024-09-15 18:08:11',	'To achieve the goal, we use the snapshot of the Debian repository',	'Debian package previous version repository',	NULL,	NULL,	'a:1:{s:11:\"original_id\";i:576;}'),
(83,	12,	0,	4486,	2,	0,	'Creating backups of server data in Dropbox.',	'site-backup-on-dropbox',	'<p>So, the task is as follows: with a certain frequency, databases and the contents of certain directories need to be backed up. Backups should be kept for a specific period of time to save space.</p>\n<p>Also, I want to not only back up to the server and send it somewhere to the cloud, but also receive it locally on my hard drive with minimum effort; data synchronization is needed.</p>\n<p>I decided to use <strong>Dropbox</strong> for this. (For reference, the server is running <strong>Debian</strong>)</p>\n<p>But first, some preparations.</p>\n<p>Basic idea: Create a user (I named it backupper), install <strong>Dropbox</strong> for that user, and create a directory for synchronization.</p>\n<h2>1. Installing Dropbox on the server</h2>\n<p>I hope <strong>Dropbox</strong> is already installed and configured on your machine; we won&#39;t cover that process here. Let&#39;s move on to installing on the server.</p>\n<p>To start, let&#39;s create a new user:</p>\n<pre><code class=\"language-bash\">useradd backupper\n</code></pre>\n<p>Log in as the new user.</p>\n<p>Next, follow the instructions from <a href=\"https://www.dropbox.com/install?os=lnx\">https://www.dropbox.com/install?os=lnx</a>:</p>\n<pre><code class=\"language-bash\">cd ~ &amp;&amp; wget -O - &quot;https://www.dropbox.com/download?plat=lnx.x86&quot; | tar xzf -\n</code></pre>\n<pre><code>\n~/.dropbox-dist/dropboxd\n</code></pre>\n<p>But I&#39;ll tell you right away that there&#39;s an easier way: download the cli script, as recommended (you&#39;ll need to download it anyway to manage the <strong>Dropbox</strong> daemon):</p>\n<pre><code class=\"language-bash\">wget https://www.dropbox.com/download?dl=packages/dropbox.py -O dropbox.py\n</code></pre>\n<p>Next, move the script to /usr/local/bin/dropbox.py, although you can put it in the home directory of <strong>dropbox</strong> and add the necessary directory to the path if desired, like this:</p>\n<pre><code class=\"language-bash\">mv  dropbox.py ~/local/bin/\nchmod u+x ~/bin/dropbox.py\n</code></pre>\n<p>Add it to the PATH:</p>\n<pre><code class=\"language-bash\">nano ~/.bashrc\n</code></pre>\n<pre><code class=\"language-bash\">PATH=$PATH:$HOME/bin\n</code></pre>\n<p>Now</p>\n<pre><code class=\"language-bash\">dropbox start -i\n</code></pre>\n<p>will install <strong>Dropbox</strong>.</p>\n<p>Next, simply</p>\n<pre><code class=\"language-bash\">dropbox start\n</code></pre>\n<p>You can add it to autostart:</p>\n<pre><code class=\"language-bash\">dropbox.py autostart y\n</code></pre>\n<h2>2: Back up databases (MySQL).</h2>\n<p>I used <strong>mysqldump</strong> for this.</p>\n<pre><code class=\"language-bash\">#!/bin/bash\n\n# Config\ndb_user=root\ndb_pass=pass\ntarget_dir=&quot;/home/backupper/backup/&quot;\ndbs=(&quot;folkprog&quot; &quot;site1&quot; &quot;site2&quot; &quot;site3&quot;)\ndate_string=`date +%Y-%m-%d-%H-%M`\n\n# DBs backup\nfor db in &quot;${dbs[@]}&quot;; do\n    mysqldump -u&quot;$db_user&quot; -p&quot;$db_pass&quot; &quot;$db&quot; | gzip -c &gt; &quot;${target_dir}${db}_${date_string}.sql.gz&quot;\ndone\n</code></pre>\n<h2>3. Back up site(s) data and configs**</h2>\n<p>I used <strong>tar</strong> for this.</p>\n<pre><code class=\"language-bash\">#!/bin/bash\n\n# Config\ntarget_dir=&quot;/home/backupper/backup/&quot;\nfolkprog_assets_base_dir=&quot;/home/deploy/projects/folkprog.net/web/assets/&quot;\ndeploy_others_project_dir=&quot;/home/deploy-others/projects/&quot;\nnginx_config_dir=&quot;/etc/nginx/&quot;\n\n# Folkprog files backup\ntar -czf &quot;${target_dir}folkprog-files.tar.gz&quot; &quot;${folkprog_assets_base_dir}&quot;\n\n# 3rd-party sites files backup\ntar -czf &quot;${target_dir}deploy-others.tar.gz&quot; &quot;$deploy_others_project_dir&quot;\n\n# Nginx config backup\ntar -czf &quot;${target_dir}nginx.gz&quot; &quot;$nginx_config_dir&quot;\n</code></pre>\n<h2>4. Clean up old files</h2>\n<pre><code class=\"language-bash\">#!/bin/bash\n\n# Config\ntarget_dir=&quot;/home/backupper/backup/&quot;\nkeep_files_days=60\n\n# Clean old backup files\nfind &quot;$target_dir&quot; -type f -mtime +&quot;$keep_files_days&quot; -exec rm -f {} \\;\n</code></pre>\n<h2>5. Packing and sending files to the sync directory</h2>\n<pre><code class=\"language-bash\">#!/bin/bash\n\n# Packing files to Dropbox sync dir\ntar -czf &quot;${target_sync_dir}backup_${date_string}.tar.gz&quot; &quot;$target_dir&quot;\n</code></pre>\n<p>The final version of the script <strong>/usr/local/bin/backup</strong>, which will be executed as root:</p>\n<pre><code class=\"language-bash\">#!/bin/bash\n\n# Config\ndb_user=root\ndb_pass=pass\ntarget_dir=&quot;/home/backupper/backup/&quot;\ntarget_sync_dir=&quot;/home/backupper/Dropbox/&quot;\nfolkprog_assets_base_dir=&quot;/home/deploy/projects/folkprog.net/web/assets/&quot;\ndeploy_others_project_dir=&quot;/home/deploy-others/projects/&quot;\nnginx_config_dir=&quot;/etc/nginx/&quot;\ndbs=(&quot;folkprog&quot; &quot;site1&quot; &quot;site2&quot; &quot;site3&quot;)\nkeep_files_days=60\n# End Config\n\ndate_string=`date +%Y-%m-%d-%H-%M`\n\n# DBs backup\nfor db in &quot;${dbs[@]}&quot;; do\n    mysqldump -u&quot;$db_user&quot; -p&quot;$db_pass&quot; &quot;$db&quot; | gzip -c &gt; &quot;${target_dir}${db}_${date_string}.sql.gz&quot;\ndone\n\n# Folkprog files backup\ntar -czf &quot;${target_dir}folkprog-files.tar.gz&quot; &quot;${folkprog_assets_base_dir}&quot;\n\n# 3rd-party sites files backup\ntar -czf &quot;${target_dir}deploy-others.tar.gz&quot; &quot;$deploy_others_project_dir&quot;\n\n# Nginx config backup\ntar -czf &quot;${target_dir}nginx.gz&quot; &quot;$nginx_config_dir&quot;\n\n# Clean old backup files\nfind &quot;$target_dir&quot; -type f -mtime +&quot;$keep_files_days&quot; -exec rm -f {} \\;\n\n# Packing files to Dropbox sync dir\ntar -czf &quot;${target_sync_dir}backup_${date_string}.tar.gz&quot; &quot;$target_dir&quot;\n</code></pre>\n<h2>6. Add a cron task</h2>\n<pre><code class=\"language-bash\">crontab -u root -e\n</code></pre>\n<pre><code class=\"language-bash\">0 4 * * 4     /usr/local/bin/backup\n</code></pre>\n<p>The script <strong>/usr/local/bin/backup</strong> will be executed every Thursday at 4 AM.</p>\n<p>That&#39;s basically it. Backups will come to you automatically.</p>\n<p>For further searching: the task can also be solved using <strong>duplicity</strong>.</p>\n',	1,	'2018-03-04 00:00:00',	'2015-03-25 00:27:21',	'2024-09-15 16:06:12',	'With a certain periodicity, it is necessary to backup databases and all contents of certain directories. It is necessary to save backups for a certain amount of time in order to save space. Dropbox was used to solve this task.',	'',	NULL,	NULL,	'a:1:{s:11:\"original_id\";i:569;}'),
(92,	NULL,	1,	NULL,	NULL,	NULL,	'Web developer blog',	'index',	'',	1,	'2012-05-19 00:00:00',	'2013-12-23 00:50:45',	'2020-10-13 19:26:56',	'',	'',	0,	NULL,	NULL),
(102,	38,	0,	6760,	0,	0,	'Iteration over a time period in PHP',	'time-period-iteration-in-php',	'<p>I suggest several options for solving this problem, both the most obvious way and a more elegant way (as it seemed to me).</p>\n<p>Of course, you can change the date in a loop, constantly adding one day and checking the exit condition from the loop, for example like this:</p>\n<pre><code class=\"language-php\">&lt;?php\n\n$startDate = new DateTime(&#39;-1 week&#39;);\n$endDate = new DateTime();\n\n$date = clone $startDate;\n\nwhile ($date &lt;= $endDate) {\n    echo &quot;{$date-&gt;format(&#39;d-m-y&#39;)}\\n&quot;;\n    $date-&gt;modify(&#39;+1 day&#39;);\n}\n</code></pre>\n<p>(Just an example, don&#39;t take it seriously ;))</p>\n<p>The line <em>$date = clone $dateStart;</em> is needed to preserve the initial value of the <em>$dateEnd</em> variable, as the <em>modify</em> method changes the value in the object.</p>\n<p>But there is a more elegant solution, as it seemed to me, using <strong>DatePeriod</strong> and <strong>DateInterval:</strong></p>\n<pre><code class=\"language-php\">&lt;?php\n\n$startDate = new DateTime(&#39;-1 week&#39;);\n$endDate = new DateTime();\n$period = new DatePeriod($startDate, new \\DateInterval(&#39;P1D&#39;), $endDate-&gt;modify(&#39;+1 day&#39;));\n\nforeach ($period as $date) {\n    echo &quot;{$date-&gt;format(&#39;d-m-y&#39;)}\\n&quot;;\n}\n</code></pre>\n<p>In the example, we create a <strong>DatePeriod</strong> object that can be iterated through using <em>foreach</em> in the usual way.</p>\n<p>Please note that when iterating through such an object, it does not include the last element of the period (it works this way if you used strictly less than in the first example), so we add one day:</p>\n<pre><code class=\"language-php\">$endDate-&gt;modify(&#39;+1 day&#39;)\n</code></pre>\n<p>(Possibly clone is also required here.)</p>\n<p>Thus, you can iterate through any period with the desired interval (in our example &#39;P1D&#39; - 1 day).</p>\n',	1,	'2015-12-22 00:00:00',	'2015-12-21 17:50:04',	'2024-09-15 17:25:15',	'I have repeatedly had to work in php with a list of dates (by day, for example) if there is a start and end of the period ($dateStart, $dateEnd). For example, in order to synchronize working/weekend days with the backend for the DatePicker from jQueryUI.',	'',	NULL,	NULL,	'a:0:{}'),
(103,	35,	0,	3552,	2,	0,	'Symfony Blog Bundle',	'symfony-blog-bundle',	'<h2>Requirements</h2>\n<p>Version ^4.0 - Symfony ^4.0, twig/twig:^2.0\nVersion ^3.0 - Symfony ^3.0\nVersion ^1.0|^2.0 - Symfony ^2.7</p>\n<h2>Installation</h2>\n<h3>1. With Symfony Flex</h3>\n<p>TODO</p>\n<h3>2. Without Symfony Flex</h3>\n<ol>\n<li>Install bundle</li>\n</ol>\n<pre><code class=\"language-bash\">$ composer.phar require harentius/blog-bundle\n</code></pre>\n<ol start=\"2\">\n<li>Add bundles (in config/bundles.php or Kernel):</li>\n</ol>\n<pre><code class=\"language-php\">&lt;?php\n// config/bundles.php\n\nreturn [\n//...\n    Harentius\\BlogBundle\\HarentiusBlogBundle::class =&gt; [&#39;all&#39; =&gt; true],\n//...\n    Stof\\DoctrineExtensionsBundle\\StofDoctrineExtensionsBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Knp\\Bundle\\PaginatorBundle\\KnpPaginatorBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Knp\\Bundle\\MenuBundle\\KnpMenuBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    WhiteOctober\\BreadcrumbsBundle\\WhiteOctoberBreadcrumbsBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Sonata\\CoreBundle\\SonataCoreBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Sonata\\BlockBundle\\SonataBlockBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Sonata\\DoctrineORMAdminBundle\\SonataDoctrineORMAdminBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Sonata\\AdminBundle\\SonataAdminBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Sonata\\TranslationBundle\\SonataTranslationBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Sonata\\Doctrine\\Bridge\\Symfony\\Bundle\\SonataDoctrineBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    FOS\\JsRoutingBundle\\FOSJsRoutingBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Presta\\SitemapBundle\\PrestaSitemapBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Liip\\ImagineBundle\\LiipImagineBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Hautelook\\AliceBundle\\HautelookAliceBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Fidry\\AliceDataFixtures\\Bridge\\Symfony\\FidryAliceDataFixturesBundle::class =&gt; [&#39;all&#39; =&gt; true],\n    Nelmio\\Alice\\Bridge\\Symfony\\NelmioAliceBundle::class =&gt; [&#39;all&#39; =&gt; true],\n];\n</code></pre>\n<p><em>Please note that HarentiusBlogBundle can provide default configuration for other bundles.\nTo make this work, HarentiusBlogBundle should be registered before other bundles</em></p>\n<p><em>HarentiusBlogBundle also has default security config. So please make sure it does not conflict with your app config or skip using it</em></p>\n<ol start=\"3\">\n<li>Include routes:</li>\n</ol>\n<pre><code class=\"language-yml\"># config/routes.yaml\nblog:\n    resource: &quot;@HarentiusBlogBundle/Resources/config/routing.yml&quot;\n    prefix: /\n</code></pre>\n<ol start=\"4\">\n<li>Include default configs:\n<em>Note: this step can be skipped if you decide to configure bundles (i.e. DoctrineBundle, SecurityBundle, etc) yourself</em></li>\n</ol>\n<pre><code class=\"language-yml\"># config/packages/harentius_blog.yaml\nimports:\n    - { resource: &#39;@HarentiusBlogBundle/Resources/config/config.yml&#39; }\n</code></pre>\n<h3>Configuration</h3>\n<p>Config reference:</p>\n<pre><code class=\"language-yml\">harentius_blog:\n    # Localizations to be used by blog\n    locales: [&#39;en&#39;, &#39;uk&#39;]\n    primary_locale: &#39;en&#39;\n    articles:\n        # Path where previews stored\n        image_previews_base_uri: /assets/images/preview/\n    sidebar:\n        # ~ - no cache, 0 - unlimited cache\n        cache_lifetime: 3600\n        # Max tags number (ordered by max popularity)\n        tags_limit: 10\n        # Percent tags size, unlimited variants number (valid values: [50, 100], [25, 50, 75, 100], etc)\n        tag_sizes: [65, 80, 100]\n    homepage:\n        # ~ - no page, feed only or page slug\n        page_slug: index\n        # ~ - no feed\n        feed:\n            # ~ - all\n            category: ~\n            # Last articles number\n            number: 5\n    list:\n        posts_per_page: 20\n    # For avoiding internal apc cache conflicts if run multiple sites on one server.\n    cache:\n        apc_global_prefix: blog\n</code></pre>\n<h3>Post Installation/Configuration actions</h3>\n<ol>\n<li>Create/Update your DB according to chosen installation type</li>\n</ol>\n<pre><code class=\"language-bash\">bin/console doctrine:database:create # If fresh installation\nbin/console doctrine:schema:create\n</code></pre>\n<ol start=\"2\">\n<li>Install assets</li>\n</ol>\n<pre><code class=\"language-bash\">bin/console assets:install --symlink\n</code></pre>\n<ol start=\"3\">\n<li>Load fixtures (optional)</li>\n</ol>\n<pre><code class=\"language-bash\">bin/console hautelook:fixtures:load\n</code></pre>\n<h2>Additional Resources</h2>\n<p><a href=\"https://folkprog.net\">https://folkprog.net</a> - example of blog based on this bundle.</p>\n<p><a href=\"https://github.com/harentius/folkprog\">https://github.com/harentius/folkprog</a> - git repository with sources of this blog.</p>\n',	1,	'2016-02-22 00:00:00',	'2016-02-14 19:13:34',	'2024-09-15 17:15:17',	'HarentiusBlogBundle - a bundle for a blog/simple portal.',	'Symfony Blog Bundle blog',	NULL,	NULL,	'a:0:{}'),
(116,	36,	0,	4466,	1,	0,	'Asset versioning using gulp',	'version-assets-when-using-gulp',	'<p>You can solve it in several ways.</p>\n<p>Let&#39;s say, for example, 7e50961489 is the new version of the assets. Actually, it can be any version, the main thing is that it should be different from the previous ones. You can simply number them - 1, 2, 3, etc. But it&#39;s not very convenient. Later it will be clear why.</p>\n<p><em>1) You can always change the names of files when updating the application:</em></p>\n<pre><code class=\"language-html\">&lt;script src=&quot;/js/lib-7e50961489.js&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;\n</code></pre>\n<p><em>2) You can add the version of the assets, for example:</em></p>\n<pre><code class=\"language-html\">&lt;script src=&quot;/js/lib.js?v=7e50961489&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;\n</code></pre>\n<p><em>3) You can change the file path:</em></p>\n<pre><code class=\"language-html\">&lt;script src=&quot;/js/7e50961489/lib.js&quot; type=&quot;text/javascript&quot;&gt;&lt;/script&gt;\n</code></pre>\n<p>The last option is good because you don&#39;t need to change the file name, and the path fragment can be handled through the Nginx configuration. (This way, you don&#39;t need to change the file structure).</p>\n<p>In this example, let&#39;s consider the first option.</p>\n<p>In one of the previous notes, we discussed an example of a gulp file configuration for a frontend project. If you haven&#39;t worked with Gulp before, be sure to familiarize yourself with it before reading further.</p>\n<p>In order to solve the task set at the beginning, we will need three more plugins for Gulp: gulp-rev, gulp-rev-replace, and gulp-rename:</p>\n<pre><code class=\"language-bash\">npm install gulp-rev gulp-rev-replace gulp-rename --save\n</code></pre>\n<p>The main work is done by gulp-rev. Its task is to rename asset files according to the version.</p>\n<p>As promised, let me explain why the version is not simply 1, 2, 3, etc. The version contains information about the checksum of the files. Therefore, unchanged files are not rebuilt.</p>\n<p>So, let&#39;s expand the CoffeeScript file build task. First, let&#39;s use gulp-rev:</p>\n<pre><code class=\"language-javascript\">gulp.task(&#39;coffee-lib&#39;, function () {\n    gulp.src([&#39;./js/**/*.coffee&#39;, &#39;!./js/app/web.coffee&#39;])\n        .pipe(coffee())\n        .pipe(concat(&#39;lib.js&#39;))\n        // add version to the file name\n        .pipe(rev())\n        .pipe(gulp.dest(&#39;./public/js/&#39;))\n        // save the version to a file - will be needed later\n        .pipe(rev.manifest(&#39;rev-manifest-lib.json&#39;))\n        .pipe(gulp.dest(&#39;./build&#39;))\n    ;\n});\n</code></pre>\n<p>We have dealt with the asset files themselves. Next, we need the new files to be used in the inclusion places. For this, I created a &quot;template file&quot; index.html.dist, which will be built into index.html with each build, and the latter should not be under version control.</p>\n<pre><code class=\"language-html\"> &lt;!--index.html.dist--&gt;\n....\n&lt;script src=&quot;js/lib.js&quot;&gt;&lt;/script&gt;\n....\n</code></pre>\n<p>We use rev-replace to replace paths to the included files and rename to rename (or rather, copy) index.html.dist to index.html:</p>\n<pre><code class=\"language-javascript\">gulp.task(&#39;template&#39;, [&#39;coffee-lib&#39;], function () {\n    var manifest = gulp.src([\n        &#39;./build/rev-manifest-lib.json&#39;,\n    ]);\n\n    return gulp.src(&#39;./public/index.html.dist&#39;)\n        .pipe(revReplace({\n            manifest: manifest,\n            replaceInExtensions: [&#39;.dist&#39;]\n        }))\n        // rename the file\n        .pipe(rename(&quot;index.html&quot;))\n        .pipe(gulp.dest(&#39;./public&#39;))\n    ;\n});\n</code></pre>\n<p>Pay attention to the line in the config replaceInExtensions: [&#39;.dist&#39;]. By default, rev-replace does not process .dist files.</p>\n<p>Now, we just need to add the new (&#39;template&#39;) task to the &#39;default&#39; task (the &#39;coffee-lib&#39; task should have been added earlier):</p>\n<pre><code class=\"language-javascript\">gulp.task(&#39;default&#39;, [..., &#39;template&#39;]);\n</code></pre>\n',	1,	'2016-06-21 00:00:00',	'2016-06-20 12:48:33',	'2024-09-15 17:00:25',	'Asset versioning using gulp with gulp-rev, gulp-rev-replace, and gulp-rename.',	'gulp assets version rename gulp-rev gulp-rev-replace gulp-rename',	NULL,	NULL,	'a:0:{}'),
(122,	39,	0,	10420,	9,	0,	'Uploading files in Symfony',	'file-upload-in-symfony',	'<p>Uploading files in Symfony** is conceptually no different from other PHP platforms, but still has its own features due to the presence of additional tools provided by the framework.</p>\n<p>First of all, it is worth noting that there are ready-made solutions that solve the task at hand.\nI strongly recommend familiarizing yourself with them, and only after that, if you decide that they do not suit you, implement your own solution.</p>\n<p>In this note we will try to show possible ways to solve the task, using both ready-made solutions (<strong>VichUploaderBundle</strong>, <strong>IphpFileStoreBundle</strong>),\nand using our own implementation (in <strong>Symfony</strong> controllers and admin classes from the <strong>SonataAdminBundle</strong>).</p>\n<h2>1. <a href=\"https://github.com/dustin10/VichUploaderBundle\">VichUploaderBundle</a></h2>\n<p>The usage is extremely simple.</p>\n<p><em>Installation:</em></p>\n<pre><code class=\"language-bash\">composer require vich/uploader-bundle\n</code></pre>\n<p>Add to <strong>AppKernel</strong>:</p>\n<pre><code class=\"language-php\">// app/AppKernel.php\nclass AppKernel extends Kernel\n{\n    public function registerBundles()\n    {\n        $bundles = [\n            // ...\n            new Vich\\UploaderBundle\\VichUploaderBundle(),\n            // ...\n        ];\n    }\n}\n</code></pre>\n<p>Basic configuration:</p>\n<pre><code class=\"language-php\"># app/config/config.yml\nvich_uploader:\n    db_driver: orm # or mongodb or propel or phpcr\n</code></pre>\n<p>Next, you need to configure the types of uploaded files. For example, let&#39;s configure the upload of user avatars:</p>\n<pre><code class=\"language-php\"># app/config/config.yml\nvich_uploader:\n    db_driver: orm\n\n    mappings:\n        user_avatar:\n            uri_prefix:         /images/avatars\n            upload_destination: &#39;%kernel.root_dir%/../web/images/avatars&#39;\n</code></pre>\n<p>After that, add the necessary fields to the entity:</p>\n<pre><code class=\"language-php\">&lt;?php\n\nnamespace Acme\\DemoBundle\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Component\\HttpFoundation\\File\\File;\nuse Vich\\UploaderBundle\\Mapping\\Annotation as Vich;\nuse File|\\Symfony\\Component\\HttpFoundation\\File\\UploadedFile\n\n/**\n * @ORM\\Entity\n * @Vich\\Uploadable\n */\nclass User\n{\n    // ..... entity fields\n\n    /**\n     * NOTE: This is not a mapped field of entity metadata, just a simple property.\n     *\n     * @Vich\\UploadableField(mapping=&quot;user_avatar&quot;, fileNameProperty=&quot;avatarFileName&quot;)\n     *\n     * @var File\n     */\n    private $avatarFile;\n\n    /**\n     * @ORM\\Column(type=&quot;string&quot;, length=255)\n     *\n     * @var string\n     */\n    private $avatarFileName;\n\n    /**\n     * @param File|UploadedFile $avatarFile\n     *\n     * @return User\n     */\n    public function setAvatarFile(File $avatarFile = null)\n    {\n        $this-&gt;avatarFile = $avatarFile;\n\n        return $this;\n    }\n\n    /**\n     * @return File|null\n     */\n    public function getAvatarFile()\n    {\n        return $this-&gt;avatarFile;\n    }\n\n    /**\n     * @param string $avatarFileName\n     *\n     * @return User\n     */\n    public function setAvatarFileName($avatarFileName)\n    {\n        $this-&gt;avatarFileName = $avatarFileName;\n\n        return $this;\n    }\n\n    /**\n     * @return string|null\n     */\n    public function getAvatarFileName()\n    {\n        return $this-&gt;avatarFileName;\n    }\n}\n</code></pre>\n<p>Next, create the form and use one of the form types available in <a href=\"https://github.com/dustin10/VichUploaderBundle/tree/master/Form/Type\">https://github.com/dustin10/VichUploaderBundle/tree/master/Form/Type</a></p>\n<p>For example:</p>\n<pre><code class=\"language-php\">&lt;?php\n\nnamespace Acme\\DemoBundle\\Form\\Type;\n\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\nuse Vich\\UploaderBundle\\Form\\Type\\VichImageType;\n\nclass UserType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options = [])\n    {\n        $builder\n           // ... other fields\n            -&gt;add(&#39;avatarFile&#39;, VichImageType::class, [\n                &#39;required&#39; =&gt; true,\n            ])\n        ;\n    }\n}\n</code></pre>\n<h2>2. <a href=\"http://symfony.com/doc/current/controller/upload_file.html\">IphpFileStoreBundle</a></h2>\n<p>Fork of the previous bundle. Key differences - improved integration with SonataAdminBundle, as well as extended saved file information (the field stores an array of metadata, not the file path).</p>\n<p>Usage is very similar to <strong>VichUploaderBundle</strong>, for more details see the <a href=\"https://github.com/vitiko/IphpFileStoreBundle\">documentation</a>.</p>\n<h2>3. Symfony</h2>\n<p>If for some reason the previous bundles did not suit you, you can implement the upload yourself.</p>\n<p>We&#39;ll show an example using the same User as before:</p>\n<ol>\n<li>Entity:</li>\n</ol>\n<pre><code class=\"language-php\">&lt;?php\n// src/AppBundle/Entity/User.php\nnamespace AppBundle\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\nclass User\n{\n    // ...\n\n    /**\n     * @ORM\\Column(type=&quot;string&quot;)\n     *\n     * @Assert\\NotBlank()\n     * @Assert\\Image()\n     */\n    private $avatar;\n\n    public function getAvatar()\n    {\n        return $this-&gt;avatar;\n    }\n\n    public function setAvatar($avatar)\n    {\n        $this-&gt;avatar = $avatar;\n\n        return $this;\n    }\n}\n</code></pre>\n<ol start=\"2\">\n<li>Form:</li>\n</ol>\n<pre><code class=\"language-php\">&lt;?php\n// src/AppBundle/Form/Type/UserType.php\nnamespace AppBundle\\Form\\Type;\n\nuse AppBundle\\Entity\\User;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType;\n\nclass UserType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options)\n    {\n        $builder\n            // ...\n            -&gt;add(&#39;avatar&#39;, FileType::class)\n            // ...\n        ;\n    }\n\n    public function configureOptions(OptionsResolver $resolver)\n    {\n        $resolver-&gt;setDefaults(array(\n            &#39;data_class&#39; =&gt; User::class,\n        ));\n    }\n}\n</code></pre>\n<p>Next, render the form. The most interesting part is the data handling. For simplicity, we&#39;ll show how to do it in the controller (example partially borrowed from the <a href=\"http://symfony.com/doc/current/controller/upload_file.html\">Symfony documentation</a>). Ideally, the <strong>file upload processing logic</strong> should be moved to a <strong>separate service</strong>. You can also <a href=\"http://symfony.com/doc/current/controller/upload_file.html#using-a-doctrine-listener\">move the logic into Doctrine events</a>.</p>\n<pre><code class=\"language-php\">&lt;?php\n// src/AppBundle/Controller/UserController.php\nnamespace AppBundle\\Controller;\n\nuse Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Route;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse AppBundle\\Entity\\User;\nuse AppBundle\\Form\\Type\\UserType;\n\nclass UserController extends Controller\n{\n    /**\n     * @Route(&quot;/user/register&quot;, name=&quot;app_user_new&quot;)\n     */\n    public function newAction(Request $request)\n    {\n        $form = $this-&gt;createForm(UserType::class);\n        $form-&gt;handleRequest($request);\n\n        if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) {\n            $user = $form-&gt;getData();\n            // $file stores the uploaded file\n            /** @var Symfony\\Component\\HttpFoundation\\File\\UploadedFile $file */\n            $file = $user-&gt;getAvatar();\n\n            // Generate a unique name for the file before saving it\n            $fileName = md5(uniqid()).&#39;.&#39;.$file-&gt;guessExtension();\n\n            // Move the file to the directory where user avatars are stored\n            $file-&gt;move(\n                // This parameter should be configured\n                $this-&gt;getParameter(&#39;avatars_directory&#39;),\n                $fileName\n            );\n\n            // Update the &#39;avatar&#39; property to store file name\n            // instead of its contents\n            $user-&gt;setAvatar($fileName);\n\n            // ... persist the $user variable or any other work and redirect\n\n            return $this-&gt;redirect($this-&gt;generateUrl(&#39;homepage&#39;));\n        }\n\n        return $this-&gt;render(&#39;user/new.html.twig&#39;, [\n            &#39;form&#39; =&gt; $form-&gt;createView(),\n        ]);\n    }\n}\n</code></pre>\n<h2>4. SonataAdminBundle</h2>\n<p>Finally, I would like to show an example of <strong>file upload in SonataAdminBundle</strong>.</p>\n<p>The simplest option is to move the logic from the previous example to the <strong>prePersist</strong>/<strong>preUpdate</strong> hooks - it suggests itself. (If you still decide to do this option and didn&#39;t ignore the last recommendation to move the upload processing logic to a separate service - it will greatly simplify the task). (Actually, I&#39;m cheating a little, it can be even simpler: if you move the upload logic to Doctrine events, you don&#39;t need to add any additional processing in <strong>Sonata</strong> hooks at all.)</p>\n<p>Let&#39;s show a slightly more &quot;advanced&quot; option for uploading images using ckeditor and an image library. (For example, for inserting into notes, not just as an entity field).</p>\n<p>For these purposes, let&#39;s create a Controller for <strong>Sonata</strong>:</p>\n<pre><code class=\"language-php\">&lt;?php\n\nnamespace Harentius\\BlogBundle\\Controller;\n\nuse Sonata\\AdminBundle\\Controller\\CRUDController as BaseCRUDController;\nuse Symfony\\Component\\Finder\\Finder;\nuse Symfony\\Component\\Finder\\SplFileInfo;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass CRUDController extends BaseCRUDController\n{\n    /**\n     * @param Request $request\n     * @return Response\n     */\n    public function uploadAction(Request $request)\n    {\n        $manager = $this-&gt;get(&#39;harentius_blog.assets.manager&#39;);\n        $assetFile = $manager-&gt;handleUpload($request-&gt;files-&gt;get(&#39;upload&#39;));\n\n        return $this-&gt;render(&#39;HarentiusBlogBundle:Admin:ck_upload.html.twig&#39;, [\n            &#39;func_num&#39; =&gt; $request-&gt;query-&gt;get(&#39;CKEditorFuncNum&#39;),\n            &#39;uri&#39; =&gt; $assetFile-&gt;getUri(),\n        ]);\n    }\n\n    /**\n     * @param Request $request\n     * @param string $type\n     * @return Response\n     */\n    public function browseAction(Request $request, $type = &#39;image&#39;)\n    {\n        if (!in_array($type, [&#39;image&#39;, &#39;audio&#39;])) {\n            throw new \\InvalidArgumentException(sprintf(&quot;Unknown files type &#39;%s&quot;, $type));\n        }\n\n        $resolver = $this-&gt;get(&#39;harentius_blog.assets.resolver&#39;);\n        $directory = $resolver-&gt;assetPath($type);\n        $files = [];\n        $finder = new Finder();\n        $finder\n            -&gt;files()\n            -&gt;in($directory)\n            -&gt;ignoreDotFiles(true)\n        ;\n\n        /** @var SplFileInfo $file */\n        foreach ($finder as $file) {\n            $uri = $resolver-&gt;pathToUri($file-&gt;getPathname());\n\n            if ($uri) {\n                $files[$uri] = pathinfo($uri, PATHINFO_BASENAME);\n            }\n        }\n\n        return $this-&gt;render(sprintf(&quot;HarentiusBlogBundle:Admin:ck_browse_%ss.html.twig&quot;, $type), [\n            &#39;func_num&#39; =&gt; $request-&gt;query-&gt;get(&#39;CKEditorFuncNum&#39;),\n            &#39;files&#39; =&gt; $files,\n        ]);\n    }\n}\n</code></pre>\n<p>The first method uses a service for file upload (<a href=\"https://github.com/harentius/blog-bundle/blob/master/src/Assets/Manager.php\">see the implementation</a>), and the second one generates a list of files for display in <strong>ckeditor</strong>.</p>\n<p>Templates used by this controller:</p>\n<pre><code class=\"language-html\">{# src/Resources/views/Admin/ck_upload.html.twig #}\n&lt;script type=&quot;text/javascript&quot;&gt;window.parent.CKEDITOR.tools.callFunction(&quot;{{ func_num }}&quot;, &quot;{{ uri }}&quot;, &quot;{{ message|default(&#39;&#39;) }}&quot;);&lt;/script&gt;\n</code></pre>\n<pre><code class=\"language-html\">{# src/Resources/views/Admin/ck_browse_images.html.twig #}\n\n{% for uri, name in files %}\n    &lt;div class=&quot;clickable pull-left&quot; onclick=&quot;ckEditorChoose(&#39;{{ func_num }}&#39;, &#39;{{ uri }}&#39;)&quot;&gt;\n        &lt;img width=&quot;300px&quot; src=&quot;{{ uri }}&quot; /&gt;\n    &lt;/div&gt;\n{% endfor %}\n\n&lt;script&gt;\n    function ckEditorChoose(funcNum, uri) {\n        window.opener.CKEDITOR.tools.callFunction(funcNum, uri);\n        window.close();\n    }\n&lt;/script&gt;\n</code></pre>\n<p>Initialization of <strong>ckeditor</strong>:</p>\n<pre><code class=\"language-html\">{% extends &#39;SonataAdminBundle:CRUD:edit.html.twig&#39; %}\n\n{% block javascripts %}\n    {{ parent() }}\n    &lt;script src=&quot;{{ asset(&#39;assets/vendor/ckeditor/ckeditor.js&#39;) }}&quot;&gt;&lt;/script&gt;\n    &lt;script src=&quot;{{ asset(&#39;assets/vendor/ckeditor/adapters/jquery.js&#39;) }}&quot;&gt;&lt;/script&gt;\n\n    &lt;script&gt;\n        &#39;use strict&#39;;\n\n        jQuery(function ($) {\n\n{# extra plugins, probably, you don&#39;t need all of them #}\n            CKEDITOR.plugins.addExternal(&quot;youtube&quot;, &quot;{{ asset(&#39;assets/vendor/ckeditor-youtube-plugin/youtube/plugin.js&#39;) }}&quot;);\n            CKEDITOR.plugins.addExternal(&quot;wpmore&quot;, &quot;{{ asset(&#39;assets/vendor/ckeditor-more-plugin/wpmore/plugin.js&#39;) }}&quot;);\n            CKEDITOR.plugins.addExternal(&quot;audio&quot;, &quot;{{ asset(&#39;assets/vendor/ckeditor-audio-plugin/audio/plugin.js&#39;) }}&quot;);\n            CKEDITOR.plugins.addExternal(&quot;wordcount&quot;, &quot;{{ asset(&#39;assets/vendor/ckeditor-wordcount-plugin/wordcount/plugin.js&#39;) }}&quot;);\n\n            CKEDITOR.config.allowedContent = true;\n            $(&#39;.blog-page-edit&#39;).ckeditor({\n                height: 700,\n                extraPlugins: &quot;youtube,justify,div,wpmore,codesnippet,audio,image2,wordcount&quot;,\n                removePlugins: &#39;image,forms&#39;,\n                toolbarGroups: [\n                    { name: &quot;basicstyles&quot;, groups: [ &quot;basicstyles&quot;, &quot;cleanup&quot; ] },\n                    { name: &quot;editing&quot;,     groups: [ &quot;find&quot;, &quot;selection&quot; ] },\n                    { name: &quot;align&quot; },\n                    { name: &quot;paragraph&quot;,   groups: [ &quot;list&quot;, &quot;indent&quot;, &quot;blocks&quot; ] },\n                    { name: &quot;links&quot; },\n                    { name: &quot;insert&quot; },\n                    { name: &quot;styles&quot; },\n                    { name: &quot;colors&quot; },\n                    { name: &quot;forms&quot; },\n                    { name: &quot;clipboard&quot;,   groups: [ &quot;clipboard&quot;, &quot;undo&quot; ] },\n                    { name: &quot;document&quot;,    groups: [ &quot;mode&quot;, &quot;document&quot;, &quot;doctools&quot; ] },\n                    { name: &quot;tools&quot; },\n                    { name: &quot;others&quot; }\n                ],\n                wordcount: {\n                    showParagraphs: false\n                },\n{# Your point of interest: #}\n                filebrowserUploadUrl: &quot;{{ admin.generateUrl(&#39;upload&#39;) }}&quot;,\n                filebrowserBrowseUrl: &quot;{{ admin.generateUrl(&#39;browse&#39;, {&#39;type&#39;: &#39;image&#39;}) }}&quot;,\n                filebrowserAudioBrowseUrl: &quot;{{ admin.generateUrl(&#39;browse&#39;, {&#39;type&#39;: &#39;audio&#39;}) }}&quot;\n            });\n        });\n    &lt;/script&gt;\n{% endblock %}\n</code></pre>\n<p>Most likely, you don&#39;t need the entire configuration, but I provided a complete example for a better understanding.</p>\n<p>The template can be overridden when registering the admin service:</p>\n<pre><code class=\"language-php\">services:\n...\n    harentius_blog.admin.article:\n        class: Harentius\\BlogBundle\\Admin\\ArticleAdmin\n        tags:\n            - { name: sonata.admin, manager_type: orm, group: Content, label: Articles, show_in_dashboard: true }\n        arguments:\n            - null\n            - Harentius\\BlogBundle\\Entity\\Article\n            - HarentiusBlogBundle:CRUD\n        calls:\n            - [ setTranslationDomain, [ &#39;HarentiusBlogBundle&#39; ] ]\n            - [ setTemplate, [ &#39;edit&#39;, &#39;HarentiusBlogBundle:Admin/CRUD:edit_post.html.twig&#39;] ]\n            - [ setEntityManager, [ &#39;@doctrine.orm.entity_manager&#39; ] ]\n....\n</code></pre>\n<h2>Resources</h2>\n<ol>\n<li><a href=\"http://symfony.com/doc/current/controller/upload_file.html\">http://symfony.com/doc/current/controller/upload_file.html</a></li>\n<li><a href=\"https://github.com/dustin10/VichUploaderBundle\">https://github.com/dustin10/VichUploaderBundle</a></li>\n<li><a href=\"https://github.com/vitiko/IphpFileStoreBundle\">https://github.com/vitiko/IphpFileStoreBundle</a></li>\n</ol>\n',	1,	'2017-01-30 00:00:00',	'2017-01-13 21:33:01',	'2024-09-15 16:29:28',	'Uploading files in Symfony is conceptually no different from other PHP platforms, but still has its own peculiarities due to the presence of additional tools provided by the framework.',	'Symfony file upload with SonataAdminBundle',	NULL,	NULL,	'a:0:{}'),
(123,	39,	0,	7678,	3,	1,	'Refreshing Nested Set Tree Indexes in Symfony',	'refreshing-nested-set-tree-indexes-in-symfony',	'<p>Once I used <a href=\"https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/tree.md\">Nestedset behavior</a> for <strong>Doctrine</strong>, <strong>Symfony</strong> in a project, and faced issue with broken indexes.\nYou can read how <strong>Nested Set</strong> works <a href=\"https://en.wikipedia.org/wiki/Nested_set_model\">here</a>.\nThis happened because low-level MySQL query executed during migration, and because of this <strong>Doctrine</strong> events responsible for refreshing indexes was not executed.\nTo fix that a Symfony Command was written, which refreshes indexes.\nThis is just example. Better way is to move this code to the service and add one more migration which will refresh indexes.\nHere is just simple example how this issue can be solved.</p>\n<p>Argument of the command is an entity to refresh.</p>\n<pre><code class=\"language-php\">&lt;?php\n\nnamespace Vendor\\Bundle\\SomeBundle\\Command;\n\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Symfony\\Bundle\\FrameworkBundle\\Command\\ContainerAwareCommand;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass NestedSetRefreshCommand extends ContainerAwareCommand\n{\n    /**\n     * {@inheritdoc}\n     */\n    protected function configure()\n    {\n        $this\n            -&gt;setName(&#39;vendor:nested-set:refresh&#39;)\n            -&gt;addArgument(&#39;entity&#39;, InputArgument::REQUIRED)\n        ;\n    }\n\n    /**\n     * @param InputInterface $input\n     * @param OutputInterface $output\n     * @throws \\Exception\n     * @return int\n     */\n    protected function execute(InputInterface $input, OutputInterface $output)\n    {\n        $container = $this-&gt;getContainer();\n        /** @var EntityManagerInterface $em */\n        $em = $container-&gt;get(&#39;doctrine.orm.entity_manager&#39;);\n        $repository = $em-&gt;getRepository($input-&gt;getArgument(&#39;entity&#39;));\n\n        $refreshLeftAndRight = function($root, $left) use ($repository, &amp;$refreshLeftAndRight) {\n            $right = $left + 1;\n            $children = $repository-&gt;findBy([\n                &#39;parent&#39; =&gt; $root,\n            ]);\n\n            foreach ($children as $entity) {\n                // recursive execution of this function for each\n                // child of this node\n                // $right is the current right value, which is\n                // incremented by the refreshLeftAndRight function\n                $right = $refreshLeftAndRight($entity, $right);\n            }\n\n            // we&#39;ve got the left value, and now that we&#39;ve processed\n            // the children of this node we also know the right value\n            $this-&gt;updateValue($left, $root, &#39;left&#39;);\n            $this-&gt;updateValue($right, $root, &#39;right&#39;);\n            return $right + 1;\n        };\n\n        foreach ($repository-&gt;findBy([&#39;parent&#39; =&gt; null]) as $rootEntry) {\n            $refreshLeftAndRight($rootEntry, 1);\n        }\n\n        $em-&gt;flush();\n\n        foreach ($repository-&gt;findAll() as $entity) {\n            $level = 0;\n            $parent = $entity;\n\n            /** @noinspection PhpUndefinedMethodInspection */\n            while ($parent = $parent-&gt;getParent()) {\n                $level++;\n            }\n\n            $this-&gt;updateValue($level, $entity, &#39;level&#39;);\n        }\n\n        $em-&gt;flush();\n    }\n\n    /**\n     * @param mixed $value\n     * @param object $entity\n     * @param string $property\n     */\n    private function updateValue($value, $entity, $property)\n    {\n        $reflection = new \\ReflectionProperty(get_class($entity), $property);\n        $reflection-&gt;setAccessible(true);\n        $reflection-&gt;setValue($entity, $value);\n    }\n}\n</code></pre>\n<p><a href=\"https://gist.github.com/harentius/d70af4a470cee0cec1bb36db966b0d9a\">https://gist.github.com/harentius/d70af4a470cee0cec1bb36db966b0d9a</a></p>\n',	1,	'2017-04-19 00:00:00',	'2017-04-10 05:59:48',	'2024-09-15 18:24:50',	'Symfony command for recalculating indexes of a Nested Set tree.',	'Symfony nested set index recalculation',	NULL,	NULL,	'a:0:{}'),
(124,	38,	0,	2808,	5,	0,	'How to find the line on which execution was interrupted in PHP',	'how-to-find-out-the-string-on-which-execution-was-stopped',	'<p>But when the task is to find the place where code execution is interrupted, you can use the following snippet:  </p>\n<pre><code class=\"language-php\">&lt;?php\n\nfunction shutdown_find_exit()\n{\n    file_put_contents(\n        __DIR__ . &#39;/tmp/&#39; . uniqid(&#39;&#39;, true) . &#39;.dump&#39;,\n        var_export($GLOBALS[&#39;dbg_stack&#39;], true)\n    );\n}\n\nregister_shutdown_function(&#39;shutdown_find_exit&#39;);\n\nfunction write_dbg_stack()\n{\n    $GLOBALS[&#39;dbg_stack&#39;] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);\n}\n\nregister_tick_function(&#39;write_dbg_stack&#39;);\ndeclare(ticks=1);\n</code></pre>\n<p>This code should be placed at the beginning of the front-controller (or script). On each tick, it writes the backtrace, and in case of <strong>execution interruption</strong>, it writes everything to the file tmp/uniq_name (if necessary, change the name), which can be analyzed later.</p>\n',	1,	'2017-06-08 00:00:00',	'2017-06-04 16:56:19',	'2024-09-15 16:08:49',	'Example of code to determine the line where the script execution was interrupted',	'',	NULL,	NULL,	'a:0:{}'),
(128,	37,	0,	2142,	2,	0,	'Continuous integration using Travis CI and Behat',	'continuous-integration-with-travis-ci-and-behat',	'<p>This note begins a series of notes on application development and deployment processes. To start with, I will show how you can relatively easily implement a code quality and functionality checking process using the <strong>Travis CI</strong> service.</p>\n<p>The practice of continuously checking the build for defects is called <strong>Continuous Integration</strong>.</p>\n<p>The question of writing tests will not be addressed in this article - it is the subject of another article. It is assumed that you have already set up <strong>Behat</strong>, and the tests can already be run locally using <strong>Selenium</strong>.</p>\n<p>To begin with, let&#39;s check if there are any rule violations in the code. This will allow you to familiarize yourself with and start using <strong>Travis CI</strong>. If you don&#39;t have any tests, I strongly recommend at least checking the code style.<br>The justification for the importance of code consistency and style can be found in many places, for example, Steve McConnell&#39;s &quot;Code Complete&quot;, part 7, section 31.</p>\n<p>To do this, we use <a href=\"https://github.com/FriendsOfPHP/PHP-CS-Fixer\">PHP-CS-Fixer</a>.<br> You can put the settings in the <em>.php_cs</em> file in the root of the project. Here is my config:</p>\n<pre><code class=\"language-php\">&lt;?php\n\nreturn PhpCsFixer\\Config::create()\n    -&gt;setRiskyAllowed(true)\n    -&gt;setCacheFile(__DIR__ . &#39;/.php_cs.cache&#39;)\n    -&gt;setRules([\n        &#39;@Symfony&#39; =&gt; true,\n        &#39;@Symfony:risky&#39; =&gt; true,\n        &#39;@PHP56Migration&#39; =&gt; true,\n        &#39;combine_consecutive_unsets&#39; =&gt; true,\n        &#39;array_syntax&#39; =&gt; [&#39;syntax&#39; =&gt; &#39;short&#39;],\n        &#39;no_useless_else&#39; =&gt; true,\n        &#39;no_useless_return&#39; =&gt; true,\n        &#39;ordered_class_elements&#39; =&gt; true,\n        &#39;ordered_imports&#39; =&gt; true,\n        &#39;concat_space&#39; =&gt; [&#39;spacing&#39; =&gt; &#39;one&#39;],\n        &#39;psr4&#39; =&gt; true,\n        &#39;strict_comparison&#39; =&gt; true,\n        &#39;strict_param&#39; =&gt; true,\n        &#39;phpdoc_align&#39; =&gt; false,\n        &#39;phpdoc_order&#39; =&gt; true,\n        &#39;phpdoc_separation&#39; =&gt; false,\n        &#39;phpdoc_to_comment&#39; =&gt; false,\n        &#39;no_empty_phpdoc&#39; =&gt; false,\n        &#39;phpdoc_add_missing_param_annotation&#39; =&gt; [&#39;only_untyped&#39; =&gt; false],\n        &#39;modernize_types_casting&#39; =&gt; true,\n        &#39;no_php4_constructor&#39; =&gt; true,\n        &#39;php_unit_construct&#39; =&gt; true,\n        &#39;php_unit_strict&#39; =&gt; true,\n        &#39;semicolon_after_instruction&#39; =&gt; true,\n        &#39;doctrine_annotation_indentation&#39; =&gt; true,\n        &#39;doctrine_annotation_spaces&#39; =&gt; [&#39;around_array_assignments&#39; =&gt; false],\n        &#39;pre_increment&#39; =&gt; false,\n        &#39;yoda_style&#39; =&gt; false,\n    ])\n;\n</code></pre>\n<p>Next, add to <em><strong>.travis.yml</strong></em></p>\n<pre><code class=\"language-php\">language: php\nphp: &#39;7.2&#39;\n\n\njobs:\n  include:\n    - stage: codestyle-check\n      before_script:\n        - composer global require friendsofphp/php-cs-fixer\n      script:\n        - $HOME/.composer/vendor/bin/php-cs-fixer --diff --dry-run --verbose fix ./src\n</code></pre>\n<p>I immediately highlighted the <em>codestyle-check</em> stage, as we will add a stage later for running tests as well.</p>\n<p>The <em>php-cs-fixer</em> package is installed separately because it is not necessary to check the vendor style - this speeds up the check.</p>\n<p>Add another stage for running behavioral tests. For this, you need a fully functional application.</p>\n<p>We will use <strong>docker</strong> and <strong>docker-compose</strong> to simplify managing the interaction between containers.</p>\n<p>To begin with, you need to create a <strong>Dockerfile</strong> for the application. Because of the specific nature of PHP (because PHP itself is not a web server), I created an image that contains PHP and Nginx. I immediately made it into a separate, base image, so that it can be reused for production later on. Supervisor was used to manage running processes. You can familiarize yourself with the image here: <a href=\"https://github.com/harentius/docker-blog-bundle-base\">https://github.com/harentius/docker-blog-bundle-base</a></p>\n<p>Based on this image, you can easily create a <em>docker-compose.yml</em> file to run behavioral tests:</p>\n<pre><code class=\"language-php\">version: &#39;3.4&#39;\n\nservices:\n    blog_app:\n        image: harentius/blog-bundle-base\n        container_name: blogbundle_app\n        volumes:\n            - ./:/app\n        links:\n            - mysql\n\n    mysql:\n        image: mysql\n        container_name: blogbundle_mysql\n\n    selenium:\n        image: selenium/standalone-chrome\n        links:\n            - blog_app\n</code></pre>\n<p>Since the sources are already cloned in Travis, the directory with the sources (and vendors) is simply mounted (not copied) into the container</p>\n<p>Add a stage for behavioral tests to the <em><strong>.travis.yml</strong></em> file:</p>\n<pre><code class=\"language-php\">language: php\nphp: &#39;7.2&#39;\n\ncache:\n  directories:\n    - $HOME/.cache/composer\n\njobs:\n  include:\n    - stage: codestyle-check\n      before_script:\n        - composer global require friendsofphp/php-cs-fixer\n      script:\n        - $HOME/.composer/vendor/bin/php-cs-fixer --diff --dry-run --verbose fix ./src\n\n    - stage: behavioral-tests\n      sudo: required\n      services:\n        - docker\n      before_script:\n        - mkdir ./tests/app/var/db-dumps\n        - composer install --no-interaction\n        - docker-compose -f support/ci/docker-compose.yml --project-directory=. up --build -d\n        # Even after mysql fully started (it can be pinged by &#39;mysqladmin ping&#39;),\n        # there is still some time during which mysql for some reason is inaccessible by PDO.\n        # Following workaround fixes that\n        - |\n          while ! docker exec -it blogbundle_app php -r &quot;new PDO(&#39;mysql:host=mysql&#39;, &#39;root&#39;, &#39;1111&#39;);&quot; ; do\n            sleep 1;\n          done\n        - docker exec -it blogbundle_app /app/tests/app/bin/install\n      script:\n        - docker exec -it blogbundle_app /app/vendor/bin/behat -c /app/behat.yml.dist\n</code></pre>\n<p>Note a few tricks:</p>\n<ol>\n<li>To speed up subsequent launches, the Composer cache directory is cached</li>\n<li>/bin directory is non-standard because it is not testing a project, but a standalone bundle, for which an application is made to start</li>\n<li>A mechanism for checking mysql availability from the php container was used. The problem is that mysql becomes available for PDO much later than it starts pinging with <strong>mysqladmin ping</strong>. If someone figures this out and writes the reason in the comments, I will be sincerely grateful. =)</li>\n</ol>\n<h2>Resources</h2>\n<ol>\n<li><a href=\"https://github.com/FriendsOfPHP/PHP-CS-Fixer\">https://github.com/FriendsOfPHP/PHP-CS-Fixer</a></li>\n<li><a href=\"https://github.com/Behat/Behat\">https://github.com/Behat/Behat</a></li>\n<li><a href=\"https://travis-ci.org/\">https://travis-ci.org/</a></li>\n<li><a href=\"https://github.com/harentius/docker-blog-bundle-base\">https://github.com/harentius/docker-blog-bundle-base</a></li>\n<li><a href=\"https://github.com/harentius/blog-bundle\">https://github.com/harentius/blog-bundle</a></li>\n</ol>\n',	1,	'2018-03-04 00:00:00',	'2018-03-03 19:23:34',	'2024-09-15 15:56:39',	'Continuous integration using Travis CI and Behat. Building a docker container for running on Travis CI.',	'',	NULL,	NULL,	'a:0:{}'),
(129,	37,	0,	5098,	4,	1,	'Continuous Integration with Circle CI v2 with Node.js and PHP examples',	'continuous-integration-for-node-js-with-circle-ci-v2',	'<p>Second version of <strong>Circle CI</strong> configuration changed significantly.</p>\n<p>It introduced workflows, and they are used in pipes meaning.</p>\n<p>Every workflow can include multiple steps.</p>\n<p><strong>Node.js</strong> example:  </p>\n<pre><code># .circleci/config.yml\nversion: 2\njobs:\n  build:\n    working_directory: ~/attraction-wars-server\n    docker:\n      - image: circleci/node:10.0.0\n    steps:\n      - checkout\n      - run:\n          name: install-npm\n          command: &#39;sudo npm install -g npm@latest&#39;\n      - restore_cache: # special step to restore the dependency cache\n          key: dependency-cache-{{ checksum &quot;package.json&quot; }}\n      - run:\n          name: install-dependencies\n          command: npm install\n      - save_cache: # special step to save the dependency cache\n          key: dependency-cache-{{ checksum &quot;package.json&quot; }}\n          paths:\n            - ./node_modules\n      - run:\n          name: lint\n          command: npm run lint\n</code></pre>\n<p>Cache is used to increase speed of future runs.</p>\n<p>Working command described in <strong>package.json</strong> (<strong>eslint/tslint</strong>, or any other)</p>\n<p>Example of run for <strong>php</strong>:</p>\n<pre><code>version: 2\n\njobs:\n  build:\n    docker:\n      - image: circleci/php:7.2-cli-node-browsers\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n          - composer-{{ checksum &quot;composer.lock&quot; }}\n      - run:\n          name: install\n          command: composer install\n      - save_cache:\n          key: composer-{{ checksum &quot;composer.lock&quot; }}\n          paths:\n            - ./vendor\n      - run:\n          name: lint\n          command: ./vendor/bin/php-cs-fixer --diff --dry-run --verbose fix ./src\n</code></pre>\n<p>Vendor sources are also cached there.</p>\n<h2>Resources</h2>\n<ol>\n<li><a href=\"https://circleci.com/docs/2.0/\">https://circleci.com/docs/2.0/</a></li>\n<li><a href=\"https://circleci.com/docs/2.0/language-javascript/\">https://circleci.com/docs/2.0/language-javascript/</a></li>\n<li><a href=\"https://circleci.com/docs/2.0/language-php/\">https://circleci.com/docs/2.0/language-php/</a></li>\n<li><a href=\"https://github.com/harentius/attraction-wars-server/blob/master/.circleci/config.yml\">https://github.com/harentius/attraction-wars-server/blob/master/.circleci/config.yml</a></li>\n</ol>\n',	1,	'2018-06-20 00:00:00',	'2018-05-10 05:42:21',	'2024-09-15 15:13:48',	'Continuous integration with Circle CI v2 with examples of code style check in Node.js and PHP.',	'',	NULL,	NULL,	'a:0:{}'),
(130,	35,	0,	1444,	10,	0,	'How games (or any pet projects) should never be developed',	'attraction-wars-game',	'<p><strong>Attraction Wars</strong> is an online browser game I developed in my free from job time.</p>\n<p>I am surrounded with &quot;success stories&quot;. But very few new projects (or startups if you want) succeed. According to the different sources, it is less than 10%, I more believe sources which say that it is even less than 1%.</p>\n<p>Of course, it dramatically depends on which stage startup was accounted for this statistics... But this story is not about it. I just feel that I should write &quot;story of NOT success&quot;, just to dilute all that success stories with other fairy tale.</p>\n<p>Honestly I think that history of <strong>Attraction Wars</strong> development is an example of how games (or any pet projects) should never be developed.</p>\n<p>However it teached me a lot, including technologies and outlook, so I hope this experience will be useful for someone. Or only for me as reminder.</p>\n<p>But if you are interested of this deep dive to cave, then please go ahead.</p>\n<h2>1. How idea was born. Gamedesign.</h2>\n<p>First was an idea. And the idea was to model <a href=\"https://en.wikipedia.org/wiki/Gravity_assist\">Gravity Assist</a>. Why? Just because it seemed to be cool. (Omg;)) At this stage it was not so wrong though.</p>\n<p>Then the idea quickly evolved into some &quot;enhanced&quot; <a href=\"https://agar.io/\">agar.io</a> clone.</p>\n<p>Players quickly became planets, and &quot;food&quot; - asteroids. Basically player wanders around, collects asteroids and eventually fights with other players. All this increases players mass and score.</p>\n<p><p><a href=\"/assets/attraction-wars-game/c85f726474c3c18a8ff0f334d91482f3.gif\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/c85f726474c3c18a8ff0f334d91482f3.gif?width=815\"/></a></p></p>\n<p>After some thinking how gravity assist should work I eventually came to the idea to create 3 zones around player with different radiuses.</p>\n<p>Those zones work when a smaller player is in influence area of the other player.</p>\n<p>Closest to the player zone - is a zone of simple attraction. There smaller objects are simply attracted to bigger, force is increasing with reducing of the distance.</p>\n<p>Next zone is a zone of gravity assist itself. If player made his way to this zone, he receives bonus speed and score.</p>\n<p><p><a href=\"/assets/attraction-wars-game/599430af6350d0e85e5cb8af4128a507.gif\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/599430af6350d0e85e5cb8af4128a507.gif?width=815\"/></a></p></p>\n<p>And the last zone is an orbiting zone - there smaller objects are rotating around bigger ones.</p>\n<h2>2. Idea is worth nothing without implementation. Technical side.</h2>\n<p>That all sounds just casual. There was no mind-blowing idea which could quickly attract some folk. But that is not so important and, to be honest, it is just too rare case when a completely new idea arrives to this world. Basic approach is either usage of different ideas combination which were not combined yet, or usage of some established idea with new elements. So I didn&#39;t throw this idea at early stage.</p>\n<p>I decided to use <strong>javascript</strong> ecosystem, just because I was a bit familiar with it and wanted to master my skills.</p>\n<p>I chose <a href=\"https://phaser.io\"><strong>Phaser</strong></a> as client game engine because I knew it a bit, and honestly there are not that many alternatives for js.</p>\n<p>For the backend I decided to use <strong>Node.js</strong> with <strong><a href=\"https://socket.io\">socket.io</a>.</strong></p>\n<p>So, basic tools were selected and I started implementation. And very <strong>first mistake</strong> was already made at this stage. I created two repositories, for client and backend parts. Scale of the project was too small at that point, and I could not early feel problems with chosen solution.</p>\n<p>Basic mechanics with absorption, border conditions, visualizations and client-server interaction went relatively fast. Few evenings and I had agar.io parody (yeah, parody, I meant that). Then was statistics, leaderboard, UI, etc. Everything is simple, without real graphics:</p>\n<p><p><a href=\"/assets/attraction-wars-game/screen1.png\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/screen1.png?width=815\"/></a></p></p>\n<p>So far so good.</p>\n<p>Then I started to implement interactions in zones. Attraction and orbiting zones were also implemented like a charm. But implementation of gravity assist zone was long, painful and buggy. I spent too much time polishing it and making it decent. Spoiler: It still works bad, at least worse then everything else in the game.</p>\n<p>And that was my <strong>second mistake</strong>. <em><strong>This feature&#39;s impact on gameplay was dramatically less than efforts to implement it.</strong></em></p>\n<p>Eventually I made it just work and went further.</p>\n<p>There was time for the first tests with real users. It was relatively fine in local network and a bit later in the Internet, but I saw some tiny lags, and that was just in time.</p>\n<p>The size of packages exchanged between server and client was too big. I made it hundred times smaller by optimizing amount of data (by sending only user related data) and by using binary format (lib <a href=\"https://github.com/darrachequesne/socket.io-msgpack-parser\">https://github.com/darrachequesne/socket.io-msgpack-parser</a> was pretty handy).</p>\n<p>Now size of rapidly (every 30ms) exchanging packages is around 500 bytes. Can be even smaller, but efforts were not worth it. I learned the lesson :).</p>\n<p>Game was crystalizing, and I started to care about better UI, deploying it somewhere for public access, or at least for testing by small group of people, updating process and all other not so interesting things.</p>\n<p>Parallelly my girlfriend performed few commits with dramatical graphics improvement:</p>\n<p><p><a href=\"/assets/attraction-wars-game/screen2.png\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/screen2.png?width=815\"/></a></p></p>\n<p>And then soon few commits brought final appearance:</p>\n<p><p><a href=\"/assets/attraction-wars-game/3333.png\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/3333.png?width=815\"/></a></p></p>\n<p>I noticed that client rendering was not smooth enough. This issue was fixed very quickly by optimizing assets (images). I didn&#39;t think that size of images could affect anything except page load time, but I was wrong. Finally I achieved 60 fps on almost all devices except of few extremely low-end.</p>\n<p>After some experiments I figured out that performance can be slightly improved on such kind of devices by using <strong>canvas instead of WebGL</strong>. But that was not always true, and I didn&#39;t find a reliable pattern when WebGL should be disabled. So I quickly added a checkbox so user could disable WebGL just in case.</p>\n<p>I was preparing to first release and suddenly understood that online game without initial users just will not take off.</p>\n<p>Solution was so unpredictable) :</p>\n<p><p><a href=\"/assets/attraction-wars-game/terminator1_1.jpg\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/terminator1_1.jpg?width=815\"/></a></p></p>\n<p>I created a new repository for bots (forth one, third was for deploy system, bend your fingers ;) ). For bots I wanted to make same rules as for real players. So they are in the same sandbox, connected to server in same way and so on.</p>\n<p>Now I understood that I already implemented low-level client which synchronised backend data and stored it in local objects. So, what? Right, I extracted it to the <strong>fifth repository.</strong> So, I had physical boundaries, no way to mess dependencies and blah blah blah. Also I needed to spread out every change in dependent library among all dependers, and only then perform integration testing. And that was not fun at all. So now I think you got an idea why &quot;<strong>first mistake</strong>&quot; was really a mistake.</p>\n<p>And it is time for next bucket of my mistakes)</p>\n<p>I tried 3 different algorithms for implementing bots. I wrote off my notebook with formulas like long time ago when I studied in university) However results didn&#39;t satisfy me.</p>\n<p><p><a href=\"/assets/attraction-wars-game/IMG_20190919_233848.jpg\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/IMG_20190919_233848.jpg?width=815\"/></a></p> \n<em>As you can see, I carefully blurred the name of the company which kindly gave me that notebook. This took a month for me. I wrote tests to verify my math algorithms, I even wrote integration tests. But all together it just didn&#39;t work correctly in runtime.</em></p>\n<p>I don&#39;t want to make you tired with explanation of first algorithms (I would do if they were right, I swear ;)). Will only say that I tried to find intersections on the possible movement directions, then sort this directions by maximizing reward (object to absorb) and minimizing risk to be absorbed by bigger object.</p>\n<p>After a month of unsuccessful tryings, I remembered about agar.io and googled something like &quot;agario bots&quot; and went to the <s>porn</s>github to find inspiration. And I found it! I read few repos for maybe half an hour and found an awesome and simple idea.</p>\n<p>Bot should just select closest target, then move to it. Once it sees danger, it changes direction to 90 degrees, travels some time and then repeats the loop. Simple as that and it worked glorious! I added some small random dispersion to 90 degrees, just to avoid rare &quot;looping&quot; and make it look more real. And for it I spent <strong>one evening</strong> after wasting almost a month!</p>\n<p>And that definitely was my <strong>third mistake</strong>. I just underestimated complexity of the task and arrogantly thought that it wasn&#39;t worth to google that.</p>\n<p>Later I used <a href=\"https://github.com/Marak/Faker.js\">faker</a> to generate names.</p>\n<p>Later I spent some time fighting memory leaks in server and bots services by using nodejs inspect mode (run &quot;<strong>node --inspect</strong>&quot;) and by attaching <strong>Chrome dev tools</strong>.</p>\n<p>With bots I quickly estimated how many users online server was capable to handle reliably. So I added connections limit (<strong>40 users</strong>) to backend and self balancing system for bots. They randomly populate the world, but once real user logins, some of bots disconnect accordingly to free up place for humans. (So far they obey me ;) )</p>\n<p>And... That was final release of the game)</p>\n<p>Virtual world populated with artificial creatures which try to survive and beat other creatures.</p>\n<p><p><a href=\"/assets/attraction-wars-game/screen3.png\" target=\"_blank\"><img src=\"/assets/attraction-wars-game/screen3.png?width=815\"/></a></p></p>\n<h2>3. Let&#39;s summarize</h2>\n<p>So what I did wrong? And what I would change if I made this game again?</p>\n<p><em><strong>1. I&#39;d use one repository for it.</strong></em> If I did that, it would be much easier to sync changes between components or share data structures.</p>\n<p><em><strong>2. Realize efforts to implement an idea and don&#39;t set too ambitious goals</strong></em>. This was only my second game, but problem was not even that. Problem was that implementation and decent support of such online game was not possible for pet project with 10 development hours per week.</p>\n<p>To understand what was planned, just check trello board of the project: <a href=\"https://trello.com/b/udbODP6r/attraction-wars\">https://trello.com/b/udbODP6r/attraction-wars</a>. I somehow finished MVP after 6 months of dev time, but implementing the rest of features requires at least one year. And that doesn&#39;t include development of scalability to handle more than 40 users (except simplest variant of random user distribution by nginx on the batch of unconnected nodes). To make it 100% production ready, one more sophisticated feature is required - network latency toleration. And that is also difficult to achieve within that time... And Android version with its own control system... I think you got an idea.</p>\n<p><em><strong>3. Think more about game idea and mechanics. Implementation has to be after planning and sketches.</strong></em> That will not only save tons of time, but also will just make game better. <strong>Do not waste time on features which are almost invisible for users or are not important.</strong></p>\n<p><em><strong>4. Think twice if you want to implement online game.</strong></em> It is harder than you think, even if you are familiar with web development.</p>\n<p><em><strong>5. Learn more powerful game engines like Unity (and I did that) or Unreal Engine.</strong></em> Time for studying will save a lot of your dev time, and resulting game will be much better.</p>\n<h2>4. Related resources</h2>\n<ol>\n<li>Repositories:</li>\n</ol>\n<p><a href=\"https://github.com/harentius/attraction-wars-server\">https://github.com/harentius/attraction-wars-server</a></p>\n<p><a href=\"https://github.com/harentius/attraction-wars-client\">https://github.com/harentius/attraction-wars-client</a></p>\n<p><a href=\"https://github.com/harentius/attraction-wars-client-storage\">https://github.com/harentius/attraction-wars-client-storage</a></p>\n<p><a href=\"https://github.com/harentius/attraction-wars-bots\">https://github.com/harentius/attraction-wars-bots</a></p>\n<ol start=\"2\">\n<li><p>Trello board: <a href=\"https://trello.com/b/udbODP6r/attraction-wars\">https://trello.com/b/udbODP6r/attraction-wars</a></p>\n</li>\n<li><p>Game itself: <a href=\"https://aw.folkprog.net\">https://aw.folkprog.net</a></p>\n</li>\n</ol>\n<h1>5. Bonus resources</h1>\n<p>I seriously interested in game dev, so I&#39;d like to share resources I checked the last few months and which I found very useful ;)</p>\n<ol>\n<li><p><strong>Free Unity Course</strong> <a href=\"https://learn.unity.com/course/create-with-code\">https://learn.unity.com/course/create-with-code</a>. After finishing it, you will be able to build your games with Unity. Not too sophisticated, but I think it is very decent for indie game dev. Example of what I did just to check that I mastered the course: \n<iframe class=\"video\" frameborder=\"0\" src=\"https://www.youtube.com/embed/sXL2hECK8KQ?wmode=transparent\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope;\" allowfullscreen></iframe></p>\n</li>\n<li><p><strong>Jesse Schell &quot;The Art of Game Design: A Book of Lenses&quot;</strong> - awesome book to sort your thinking</p>\n</li>\n<li><p><strong>Scott Rogers &quot;Level Up! The Guide to Great Video Game Design&quot;</strong></p>\n</li>\n</ol>\n',	1,	'2020-03-12 00:00:00',	'2020-02-26 11:49:58',	'2024-09-14 17:42:52',	'My real life story about indie game development in free time and how I failed that',	'gamedev indie study gamedesign',	NULL,	NULL,	'a:0:{}'),
(131,	36,	0,	2953,	2,	0,	'Cypress tests organizing (or any e2e/ui tests)',	'cypress-or-any-e2e-ui-tests-project-structure',	'<blockquote>\n<p><em>&quot;Cypress is a next generation front end testing tool built for the modern web.&quot;</em></p>\n</blockquote>\n<p><a href=\"https://docs.cypress.io/guides/overview/why-cypress.html#In-a-nutshell\">https://docs.cypress.io/guides/overview/why-cypress.html#In-a-nutshel</a></p>\n<p>I had some experience with <strong>Behat</strong> and other types of tests like unit/integarational, but today I will tell you about e2e.</p>\n<p>When I wrote <strong>Behat</strong> tests I didn&#39;t think a lot about how to structure my test cases in a maintainable way. This led me to the antipattern called &quot;<strong>Single-Layer Architecture</strong>&quot;<a href=\"https://medium.com/better-programming/what-not-to-do-when-writing-e2e-tests-ef7b9d09cc81\">[1]</a>.</p>\n<p>Extending such tests quickly becomes a mess and tends to become an ineffective work:</p>\n<ol>\n<li>You can&#39;t focus on business logic, you always need to remember low-level details to introduce a new step definition</li>\n<li>Your low-level solutions are often duplicated, and you even don&#39;t notice that</li>\n<li>Changing testing framework means rewriting or updating all step definitions!</li>\n</ol>\n<p>Fortunately there is an approach which is very easy to follow and which mitigates mentioned issues.</p>\n<p>First time I saw it in the Martin&#39;s Fowler article <a href=\"https://martinfowler.com/bliki/PageObject.html\"><strong>Page Objects</strong> [2]</a>.</p>\n<p>I wonder why a lot of testing frameworks documentations including <a href=\"https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html\"><strong>cypress</strong> documentation[4]</a> don&#39;t scream about this from the first pages.</p>\n<p>Anyway, let&#39;s consider a folder structure of an <a href=\"http://github.com/harentius/cypress-example\">example project[5]</a> with <strong>cypress</strong> tests.</p>\n<p>Scenarios lay here: <a href=\"https://github.com/harentius/cypress-example/tree/main/tests/e2e/features\">https://github.com/harentius/cypress-example/tree/main/tests/e2e/features</a></p>\n<p>Let&#39;s look at one of them:</p>\n<pre><code class=\"language-javascript\"> // features/Login.js\nimport Login from &#39;../support/business/Login&#39;;\n\ndescribe(&#39;Login&#39;, () =&gt; {\n  it(&quot;Login with wrong credentials shows error message&quot;, () =&gt; {\n    Login.open();\n\n    Login.fillCredentials(&#39;wrong_name&#39;, &#39;wrong_password&#39;);\n    Login.submitForm();\n\n    Login.assertNotification(&#39;Incorrect username or password.&#39;);\n  })\n})\n</code></pre>\n<p>As you can see, it is well structured and semantic. If you prefer <strong>gherkin</strong> syntax, migration will be a rather technical and straightforward task: steps decomposition is already done.</p>\n<p>It doesn&#39;t know at all which testing framework is used or how actions and assertions perform, or about the &quot;driver&quot; which is used to work with the framework. It uses high-level commands to perform actions and asserting.</p>\n<p>We have 2 implementation layers for scenario(s):</p>\n<p>First is <a href=\"https://github.com/harentius/cypress-example/tree/main/tests/e2e/support/business\">https://github.com/harentius/cypress-example/tree/main/tests/e2e/support/business</a></p>\n<p>Here lays dedicated class which backends login process scenario:</p>\n<pre><code class=\"language-javascript\">// support/business/Login.js\nimport Navigation from &#39;../driver/Navigation&#39;;\nimport Form from &#39;../driver/Form&#39;;\nimport Page from &#39;../driver/Page&#39;;\n\nclass Login\n{\n  static open()\n  {\n    Navigation.visit(&#39;/login&#39;)\n  }\n\n  static fillCredentials(username, password)\n  {\n    Form.fill(&#39;#login_field&#39;, username);\n    Form.fill(&#39;#password&#39;, password);\n  }\n\n  static submitForm()\n  {\n    Form.submit(&#39;form&#39;);\n  }\n\n  static assertNotification(notification)\n  {\n    Page.assertElementContains(&#39;.flash&#39;, notification)\n  }\n}\n\nexport default Login\n</code></pre>\n<p>It does know about the driver layer and uses it as a backend to achieve testing goals, but doesn&#39;t know about testing framework (<strong>cypress</strong>).</p>\n<p>It uses only the limited API provided by the second <strong>&quot;driver&quot; layer</strong>.</p>\n<p><a href=\"https://github.com/harentius/cypress-example/tree/main/tests/e2e/support/driver\">https://github.com/harentius/cypress-example/tree/main/tests/e2e/support/driver</a></p>\n<p>This layer makes a convenient API for your specific project.</p>\n<pre><code class=\"language-javascript\">// driver/Page.js\nclass Page {\n  static click(text) {\n    cy.contains(text).click();\n  }\n\n  static clickElement(selector, text) {\n    let e = cy.get(selector);\n\n    if (text !== null) {\n      e = e.contains(text);\n    }\n\n    e.click();\n  }\n\n  static assertContains(text) {\n    return cy.contains(text);\n  }\n\n  static assertElementContains(selector, text) {\n    return cy.get(selector).contains(text);\n  }\n}\n\nexport default Page;\n</code></pre>\n<pre><code class=\"language-javascript\">// driver/Navigation.js\nclass Navigation {\n  static visit(url) {\n    cy.visit(url);\n  }\n\n  static assertPageUrlIs(url) {\n    cy.location().its(&#39;pathname&#39;).should(&#39;eq&#39;, url);\n  }\n}\n\nexport default Navigation;\n</code></pre>\n<pre><code class=\"language-javascript\">// driver/Form.js\nclass Form\n{\n  static fill(selector, text) {\n    cy.get(selector).type(text);\n  }\n\n  static submit(selector) {\n    cy.get(selector).get(&#39;input[type=&quot;submit&quot;]&#39;).click();\n  }\n}\n\nexport default Form\n</code></pre>\n<p>When you play for a while with this approach you will realise that:</p>\n<ol>\n<li>&quot;<strong>Driver</strong>&quot; layer is surprisingly thin. Basically you need a very limited amount of commands to create e2e tests, like &quot;open page&quot;, &quot;assert that content exists&quot;, &quot;find by some locator&quot;</li>\n<li>Nevertheless it is thin, it prevents duplication of the knowledge about the system. Remember, bad is not the code copy-paste itself but the copy-paste of <em><strong>knowledge about system</strong></em>. And that is exactly what happens when you spam with &quot;<em>cy.get(...)</em>&quot; in every scenario</li>\n<li>Even if you don&#39;t plan to change testing framework, this approach makes your tests cleaner</li>\n</ol>\n<p>You can check/run this example project <a href=\"https://github.com/harentius/cypress-example\">here</a>.</p>\n<h2>References</h2>\n<ol>\n<li>What Not to Do When Writing E2E Tests <a href=\"https://medium.com/better-programming/what-not-to-do-when-writing-e2e-tests-ef7b9d09cc81\">https://medium.com/better-programming/what-not-to-do-when-writing-e2e-tests-ef7b9d09cc81</a></li>\n<li>PageObject <a href=\"https://martinfowler.com/bliki/PageObject.html\">https://martinfowler.com/bliki/PageObject.html</a></li>\n<li>The Practical Test Pyramid <a href=\"https://martinfowler.com/articles/practical-test-pyramid.html\">https://martinfowler.com/articles/practical-test-pyramid.html</a></li>\n<li>Introduction to Cypress <a href=\"https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html\">https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html</a></li>\n<li><a href=\"https://github.com/harentius/cypress-example\">https://github.com/harentius/cypress-example</a></li>\n</ol>\n',	1,	'2020-10-20 00:00:00',	'2020-09-09 18:12:10',	'2024-09-13 16:01:28',	'Cypress tests folder structure, organizing and best practices',	'cypress architecture tests e2e folder structure',	NULL,	NULL,	'a:0:{}'),
(133,	35,	0,	1182,	7,	2,	'A Night Before the Deadline Press Kit',	'a-night-before-the-deadline-press-kit',	'<ul>\n<li><strong>Developer</strong>: Harentius</li>\n<li><strong>Genre</strong>: Indie, Adventrue</li>\n<li><strong>Release Date:</strong> June 1st, 2021</li>\n<li><strong>Platforms:</strong> Windows, Linux</li>\n<li><strong>Price:</strong> $3.99</li>\n<li><strong>Availability:</strong> Digital Download</li>\n<li><strong>Languages:</strong> English, French, German, Turkish, Ukrainian</li>\n<li><strong>ESRB</strong>: E for everyone</li>\n<li><a href=\"https://store.steampowered.com/app/1578540/\"><strong>Steam page</strong></a></li>\n<li><a href=\"https://folkprog.net/a-night-before-the-deadline-press-kit/\"><strong>Website</strong></a></li>\n<li><strong>Request keys here</strong>: <a href=\"https://www.keymailer.co/g/games/120591\">Keymailer</a></li>\n</ul>\n<h2>Description</h2>\n<p>A Night Before the Deadline is an adventure story-driven game with a nice low poly aesthetic and ambient soundtrack.<br> You play as a developer Joe who stays at the office till late night because his boss asked him to finish the work. All you need to do is to collect code fragments written by other developers, merge them and deploy the project. Nothing special, right?<br> Everything seems to go as planned but at some point strange things start to happen. You see visions you can’t explain and receive phone calls from an unknown number. That person obviously just wants to distract you from doing your work.<br> Or is he warning you about something? As you progress the work further you begin to realize that something is wrong…</p>\n<p><strong>Key features:</strong></p>\n<ul>\n<li>Explore an empty night office</li>\n<li>Solve minigame puzzles to progress the story</li>\n<li>Figure out the cause of your visions and unravel a mystery of an unknown caller</li>\n</ul>\n<h2>Video Trailer</h2>\n<p><iframe class=\"video\" frameborder=\"0\" src=\"https://www.youtube.com/embed/Cl3Cwl5jZ6w?wmode=transparent\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope;\" allowfullscreen></iframe></p>\n<h2>Game logos</h2>\n<p><p><a href=\"/assets/a-night-before-the-deadline-press-kit/capsule_main_en.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/capsule_main_en.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/library_6x9_template_en.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/library_6x9_template_en.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/header_en.jpg\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/header_en.jpg?width=815\"/></a></p> \n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/capsule_hero_en.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/capsule_hero_en.png?width=815\"/></a></p></p>\n<p><em>All logos in single file</em>:\n<a href=\"https://drive.google.com/file/d/1uL8Ys0kD9IH68OWYVUlqaNn-1EzwOFPY/view?usp=sharing\">Google drive</a></p>\n<h2>Screenshots</h2>\n<p><p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_1-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_1-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_2-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_2-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_4-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_4-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_5-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_5-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_9-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_9-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_13-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_13-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_15-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_15-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_18-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_18-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_20-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_20-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_21-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_21-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_22-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_22-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_23-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_23-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_24-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_24-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_24-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_24-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/20210405214334_1-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/20210405214334_1-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_10-min.png\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/Screenshot_10-min.png?width=815\"/></a></p>\n<p><a href=\"/assets/a-night-before-the-deadline-press-kit/DeployMinigameAfter-min.jpg\" target=\"_blank\"><img src=\"/assets/a-night-before-the-deadline-press-kit/DeployMinigameAfter-min.jpg?width=815\"/></a></p></p>\n<p><em>All screens in a single file:</em>\n<a href=\"https://drive.google.com/file/d/1uL8Ys0kD9IH68OWYVUlqaNn-1EzwOFPY/view?usp=sharing\">Google Drive</a></p>\n<h2>Project history</h2>\n<p>This is a tiny project developed in spare time by full-time web developer. A lot of artwork was done by my wife, a lot was taken from Unity assets store (thank you, <strong>Synty Studios</strong>)</p>\n<p>I had idea for this game for the few years and even tried to implement the first version in the pixel-art style.</p>\n<p>First efforts are here:</p>\n<p><iframe class=\"video\" frameborder=\"0\" src=\"https://www.youtube.com/embed/QBqBvjHr2ZI?wmode=transparent\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope;\" allowfullscreen></iframe></p>\n',	1,	'2021-04-05 00:00:00',	'2021-04-04 20:36:31',	'2024-09-15 18:31:50',	'A Night Before the deadline is an adventure story-driven game with a nice low poly aesthetic and ambient soundtrack.',	'indie game press kit',	NULL,	NULL,	'a:0:{}');

DROP TABLE IF EXISTS `api_key`;
CREATE TABLE `api_key` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `api_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `secret` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `api_key` (`id`, `api_key`, `secret`, `created_at`, `updated_at`) VALUES
(1,	'1b40b4314aa417f0ecb0efadb8145488',	'$2y$10$OQrLDmkG8VsnhAe.b9oP3OB9a8D0DtCDFPx3jmQI7x14a9pnIvDXC',	'2024-09-07 16:42:31',	'2024-09-07 16:42:31');

DROP TABLE IF EXISTS `article_tag`;
CREATE TABLE `article_tag` (
  `article_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY (`article_id`,`tag_id`),
  KEY `IDX_919694F97294869C` (`article_id`),
  KEY `IDX_919694F9BAD26311` (`tag_id`),
  CONSTRAINT `FK_919694F97294869C` FOREIGN KEY (`article_id`) REFERENCES `abstract_post` (`id`) ON DELETE CASCADE,
  CONSTRAINT `FK_919694F9BAD26311` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

INSERT INTO `article_tag` (`article_id`, `tag_id`) VALUES
(82,	26),
(82,	27),
(82,	40),
(83,	38),
(83,	39),
(102,	18),
(102,	19),
(103,	15),
(103,	16),
(103,	17),
(116,	14),
(116,	19),
(122,	16),
(122,	19),
(123,	16),
(123,	19),
(124,	18),
(124,	19),
(128,	18),
(128,	19),
(128,	31),
(128,	48),
(129,	14),
(129,	18),
(129,	19),
(129,	48),
(131,	14),
(131,	19),
(131,	48);

DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent_id` int(11) DEFAULT NULL,
  `name` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
  `slug` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
  `root` int(11) DEFAULT NULL,
  `lvl` int(11) NOT NULL,
  `lft` int(11) NOT NULL,
  `rgt` int(11) NOT NULL,
  `meta_description` varchar(255) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `meta_keywords` varchar(1000) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `IDX_64C19C1727ACA70` (`parent_id`),
  CONSTRAINT `FK_64C19C1727ACA70` FOREIGN KEY (`parent_id`) REFERENCES `category` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

INSERT INTO `category` (`id`, `parent_id`, `name`, `slug`, `root`, `lvl`, `lft`, `rgt`, `meta_description`, `meta_keywords`) VALUES
(3,	NULL,	'Process Modeling',	'process-modelling',	3,	0,	1,	2,	'',	''),
(5,	NULL,	'Robotics',	'robotics',	5,	0,	1,	2,	'',	''),
(6,	NULL,	'Microcontrollers',	'microcontrollers',	6,	0,	1,	2,	'',	''),
(7,	NULL,	'Developers humour',	'developers-humour',	7,	0,	1,	2,	'',	''),
(8,	NULL,	'News',	'news',	8,	0,	1,	2,	'',	''),
(9,	NULL,	'Web development',	'web-development',	9,	0,	1,	28,	'',	''),
(11,	NULL,	'Administration',	'administration',	11,	0,	1,	2,	'',	''),
(12,	NULL,	'Development',	'development',	12,	0,	1,	20,	'',	''),
(13,	NULL,	'SEO',	'seo',	13,	0,	1,	2,	'',	''),
(14,	NULL,	'Other',	'other',	14,	0,	1,	6,	'',	''),
(15,	9,	'PHP',	'php',	9,	1,	4,	5,	'',	''),
(16,	9,	'Philosophy',	'philosophy',	9,	1,	6,	7,	'',	''),
(17,	9,	'Yii framework',	'yii-framework',	9,	1,	8,	9,	'',	''),
(18,	9,	'Symfony',	'symfony',	9,	1,	10,	11,	'',	''),
(19,	9,	'Ruby',	'ruby',	9,	1,	12,	13,	'',	''),
(20,	9,	'HTML/CSS',	'html-css',	9,	1,	14,	15,	'',	''),
(21,	9,	'Wordpress',	'wordpress',	9,	1,	16,	17,	'',	''),
(22,	9,	'JavaScript',	'javascript',	9,	1,	18,	19,	'',	''),
(23,	12,	'QT',	'qt',	12,	1,	2,	3,	'',	''),
(24,	12,	'OpenGL + Qt',	'opengl-qt',	12,	1,	4,	5,	'',	''),
(25,	14,	'What to read?',	'what-to-read',	14,	1,	2,	3,	'',	''),
(27,	9,	'Projects',	'projects',	9,	1,	2,	3,	'',	''),
(28,	9,	'Tools',	'tools',	9,	1,	20,	21,	'',	''),
(29,	9,	'nodejs',	'nodejs',	9,	1,	22,	23,	'',	''),
(31,	12,	'Go (Golang)',	'go-golang',	12,	1,	6,	7,	'',	''),
(32,	14,	'Other',	'other',	14,	1,	4,	5,	'',	''),
(33,	9,	'Development Practices and Processes',	'development-practices',	9,	1,	24,	25,	'',	''),
(34,	9,	'Software Architecture',	'software-architecture',	9,	1,	26,	27,	'',	''),
(35,	NULL,	'Projects',	'projects-1',	35,	0,	1,	2,	NULL,	NULL),
(36,	12,	'JavaScript',	'javascript-1',	12,	1,	8,	9,	NULL,	NULL),
(37,	12,	'CI',	'ci',	12,	1,	10,	11,	NULL,	NULL),
(38,	12,	'PHP',	'php-1',	12,	1,	12,	13,	NULL,	NULL),
(39,	12,	'Symfony',	'symfony-1',	12,	1,	14,	15,	NULL,	NULL),
(40,	12,	'Linux',	'linux',	12,	1,	16,	17,	NULL,	NULL),
(41,	12,	'OpenGL',	'opengl',	12,	1,	18,	19,	NULL,	NULL);

DROP TABLE IF EXISTS `doctrine_migration_versions`;
CREATE TABLE `doctrine_migration_versions` (
  `version` varchar(191) COLLATE utf8mb3_unicode_ci NOT NULL,
  `executed_at` datetime DEFAULT NULL,
  `execution_time` int(11) DEFAULT NULL,
  PRIMARY KEY (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

INSERT INTO `doctrine_migration_versions` (`version`, `executed_at`, `execution_time`) VALUES
('Application\\Migrations\\Version20240902203410',	'2024-09-07 16:42:04',	16),
('Application\\Migrations\\Version20240904181151',	'2024-09-07 16:42:04',	2);

DROP TABLE IF EXISTS `setting`;
CREATE TABLE `setting` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `_key` longtext COLLATE utf8mb3_unicode_ci NOT NULL,
  `name` longtext COLLATE utf8mb3_unicode_ci NOT NULL,
  `value` longtext COLLATE utf8mb3_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

INSERT INTO `setting` (`id`, `_key`, `name`, `value`) VALUES
(1,	'project_name',	'Project Name',	'Folkprog'),
(2,	'homepage_meta_description',	'Homepage Meta Description',	'Folkprog is a web developer\'s blog, who started with Symfony framework and then used other technologies'),
(3,	'homepage_meta_keywords',	'Homepage Meta Keywords',	'web developer blog symfony qt javascript php');

DROP TABLE IF EXISTS `tag`;
CREATE TABLE `tag` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
  `slug` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

INSERT INTO `tag` (`id`, `name`, `slug`) VALUES
(1,	'OpenGL',	'opengl'),
(2,	'Qt',	'qt'),
(3,	'Development',	'development'),
(4,	'humor',	'humor'),
(5,	'handmade',	'handmade'),
(6,	'Microcontrollers',	'microcontrollers'),
(7,	'Development Methodology',	'development-methodology'),
(8,	'Ruby',	'ruby'),
(9,	'wordpress',	'wordpress'),
(10,	'SEO',	'seo'),
(11,	'translation',	'translation'),
(12,	'html5',	'html5'),
(13,	'canvas',	'canvas'),
(14,	'javascript',	'javascript'),
(15,	'bundle',	'bundle'),
(16,	'symfony',	'symfony'),
(17,	'My project',	'my-project'),
(18,	'php',	'php'),
(19,	'code snippet',	'code-snippet'),
(21,	'localization',	'localization'),
(22,	'frontend',	'frontend'),
(23,	'html',	'html'),
(24,	'customization',	'customization'),
(25,	'twitter bootstrap',	'twitter-bootstrap'),
(26,	'Administration',	'administration'),
(27,	'linux',	'linux'),
(28,	'server',	'server'),
(29,	'Setup',	'setup'),
(30,	'book',	'book'),
(31,	'tools',	'tools'),
(32,	'prototyping',	'prototyping'),
(33,	'review',	'review'),
(34,	'patterns',	'patterns'),
(35,	'code',	'code'),
(36,	'Methodology',	'methodology'),
(37,	'best practice',	'best-practice'),
(38,	'backup',	'backup'),
(39,	'dropbox',	'dropbox'),
(40,	'debian',	'debian'),
(41,	'backend',	'backend'),
(42,	'tutorial',	'tutorial'),
(43,	'css',	'css'),
(45,	'Go',	'go'),
(46,	'Symfony forms',	'symfony-forms'),
(47,	'for begginers',	'for-begginers'),
(48,	'CI',	'ci');

-- 2024-09-15 18:33:14

Youez - 2016 - github.com/yon3zu
LinuXploit