<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Engineer to Entrepreneur]]></title><description><![CDATA[Crafting Code, Shipping Projects – Dive into SaaS and Software with Me]]></description><link>https://www.ptrdev.com/</link><image><url>https://www.ptrdev.com/favicon.png</url><title>Engineer to Entrepreneur</title><link>https://www.ptrdev.com/</link></image><generator>Ghost 5.79</generator><lastBuildDate>Wed, 21 Feb 2024 23:03:04 GMT</lastBuildDate><atom:link href="https://www.ptrdev.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[I switched to Golang.]]></title><description><![CDATA[I switched to Golang from PHP. Learning Golang. Building project with Golang.]]></description><link>https://www.ptrdev.com/i-switched-to-golang/</link><guid isPermaLink="false">65b9e4427abd6100017a66ad</guid><category><![CDATA[golang]]></category><category><![CDATA[backend]]></category><category><![CDATA[entrepreneur]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Wed, 31 Jan 2024 06:24:37 GMT</pubDate><media:content url="https://www.ptrdev.com/content/images/2024/01/30-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://www.ptrdev.com/content/images/2024/01/30-1.png" alt="I switched to Golang."><p>As you can see, I don&apos;t write that much about PHP anymore because I lost motivation. I used PHP and Symfony, in particular, for a long time. </p><p>With Go, I found it so refreshing and motivated me. I need to learn new things because otherwise, everything gets boring.</p><p>I&apos;ve built first website using Nuxt.js and Golang as backend and wrote crawler/scraper in Go.<br>Why Go?</p><ol><li>Firstly, I want to mention that I learned Golang back in 2016 but never used it back then in a real project, and it was kinda immature and not so popular then. So I dropped it.</li><li>Last year, I saw that Job market is getting bigger with Golang, and the language is much better in 2023/24, so I decided to give it a GO.</li><li>Learning new Go is pretty straightforward and easy, especially for someone who knows multiple languages already.</li><li>You don&apos;t really need complicated frameworks to build things. Go Standard Library is excellent.</li><li>Deploying applications is much easier than PHP</li><li>Statically typed</li><li>Compiled high-level programmed language</li></ol><p>I learned it pretty quickly and then jumped right into a building project. My main advice is to build something if you want to learn something. It can be anything: side project, SaaS, API, or parser. <br>You will learn so much.<br><br>As you can see, I don&apos;t write that much about PHP anymore because I lost motivation.</p><p>In my blog, I will write more about Golang and how I build SaaS and Side projects here. Stay tuned. And, of course, technical-related stuff.</p>]]></content:encoded></item><item><title><![CDATA[Here's why you should always read the F... manual.]]></title><description><![CDATA[<p>Well recently I needed to set CORS headers for my side project, at first I used Nginx to send additional headers for static files, but then I needed something more configurable. <br>And I decided to use NelmioCorsBundle, but to my surprise, I couldn&apos;t run it. This is something</p>]]></description><link>https://www.ptrdev.com/rtfm/</link><guid isPermaLink="false">65006f4bac9a6100011aebac</guid><category><![CDATA[programming]]></category><category><![CDATA[software engineer]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Mon, 13 Feb 2023 14:03:11 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1600712242868-18d4e92fb599?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDUyfHxtYW51YWx8ZW58MHx8fHwxNjc2Mjk2OTE0&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1600712242868-18d4e92fb599?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDUyfHxtYW51YWx8ZW58MHx8fHwxNjc2Mjk2OTE0&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Here&apos;s why you should always read the F... manual."><p>Well recently I needed to set CORS headers for my side project, at first I used Nginx to send additional headers for static files, but then I needed something more configurable. <br>And I decided to use NelmioCorsBundle, but to my surprise, I couldn&apos;t run it. This is something I hate about Symfony in general, and I guess that&apos;s why so many people choose Laravel over Symfony for its simplicity. But well I spent some time digging into configuration.<br>Actually, it was pretty simple to change, but again as I said, Symfony bundle configuration is <code>evil</code> ^_- &#xA0;sometimes.</p><p>So I ran the command</p><p><code>composer req cors</code> </p><p>Symfony configured everything for me with default values:</p><pre><code class="language-yaml">nelmio_cors:
    defaults:
        origin_regex: true
        allow_origin: [&apos;*&apos;]
        allow_methods: [&apos;GET&apos;, &apos;OPTIONS&apos;, &apos;POST&apos;, &apos;PUT&apos;, &apos;PATCH&apos;, &apos;DELETE&apos;]
        allow_headers: [&apos;Content-Type&apos;, &apos;Authorization&apos;]
        expose_headers: [&apos;Link&apos;]
        max_age: 3600
    paths:
        &apos;^/&apos;: null

#    %env(CORS_ALLOW_ORIGIN)%
</code></pre><p>I added <code>/api</code> endpoint to test everything </p><pre><code class="language-php">        &apos;^/api&apos;:
            allow_origin: [&apos;myhost.com&apos;]
            allow_headers: [&apos;*&apos;]
            allow_methods: [&apos;GET&apos;, &apos;OPTIONS&apos;, &apos;POST&apos;, &apos;PUT&apos;, &apos;PATCH&apos;, &apos;DELETE&apos;]
            max_age: 3600
</code></pre><p>And I tried to run some endpoints from the allowed origin, but it did not work. Then I changed this line: <code>allow_origin to <code>[&quot;*&quot;]</code></code> and it still does not work.</p><p>Well, what do you think? it was <code>origin_regex: true</code>(If you don&apos;t use regex of course). I removed it and everything went well, so remember kids to RTFM :).</p><blockquote>If <code>origin_regex</code> is set, <code>allow_origin</code> must be a list of regular expressions matching allowed origins. Remember to use <code>^</code> and <code>$</code> to clearly define the boundaries of the regex.</blockquote><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2023/02/4669289.webp" class="kg-image" alt="Here&apos;s why you should always read the F... manual." loading="lazy" width="319" height="400"><figcaption>RTFM</figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Symfony 6 tutorial. Frontend setup and configuration.]]></title><description><![CDATA[Symfony advanced tutorial. Frontend setup and configuration. Vue.js Vuex Vue Router, JWT authentication bundle configuration. Webpack encore bundle configuration.]]></description><link>https://www.ptrdev.com/symfony-advanced-tutorial-frontend-setup-and-configuration-part3/</link><guid isPermaLink="false">65006f4bac9a6100011aeba2</guid><category><![CDATA[symfony]]></category><category><![CDATA[vue]]></category><category><![CDATA[vuex]]></category><category><![CDATA[frontend]]></category><category><![CDATA[backend]]></category><category><![CDATA[sideproject]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Mon, 08 Nov 2021 21:01:04 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1509619159938-4efea8aa33c3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDUzfHx2aWV3fGVufDB8fHx8MTYzNjQwMDI0NQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1509619159938-4efea8aa33c3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDUzfHx2aWV3fGVufDB8fHx8MTYzNjQwMDI0NQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Symfony 6 tutorial. Frontend setup and configuration."><p>In this part, I will set up Vue 3 and Vuex store and a JWT authentication bundle.<br>First of all, I need to install the Webpack encore bundle(plus MakerBundle):</p>
<pre><code class="language-bash">composer require symfony/webpack-encore-bundle
composer require --dev symfony/maker-bundle
npm install
yarn install</code></pre>
<p>Suppose you have it, cool. Now let&apos;s add Vue, Vuex store, and Vue Router.</p>
<pre><code class="language-bash">npm install vue@next
npm install vue-router@next --save
npm install -D @vue/compiler-sfc
npm install -g @vue/cli
vue upgrade --next
npm install vuex@next --save
yarn add vue-loader@^16.1.0 --dev</code></pre>
<p>Also, you can install chrome dev tools for <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg?ref=ptrdev.com">VUE</a>(it helps with debugging).</p>
<p>Now I need one more bundle for <a href="https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.md?ref=ptrdev.com#getting-started">JWT authentication</a> </p>
<pre><code class="language-bash">composer require &quot;lexik/jwt-authentication-bundle&quot;</code></pre>
<p>After installation, I need to generate SSL keys:</p>
<pre><code class="language-bash">bin/console lexik:jwt:generate-keypair</code></pre>
<p>And update security.YAML file with the following:</p>
<pre><code class="language-yaml">        login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: /api/login_check # or api_login_check as defined in config/routes.yaml
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
        api:
            pattern: ^/api
            stateless: true</code></pre>
<p>I don&apos;t have a User Entity yet. Well, I can use <a href="https://github.com/FriendsOfSymfony/FOSUserBundle?ref=ptrdev.com">FOSUserBundle</a>, but it&apos;s too much for this kind of project. So I am going to create a simple user entity:</p>
<pre><code class="language-bash">./bin/console make:user</code></pre>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2021/11/Screen_1_make_user.png" class="kg-image" alt="Symfony 6 tutorial. Frontend setup and configuration." loading="lazy" width="1019" height="630" srcset="https://www.ptrdev.com/content/images/size/w600/2021/11/Screen_1_make_user.png 600w, https://www.ptrdev.com/content/images/size/w1000/2021/11/Screen_1_make_user.png 1000w, https://www.ptrdev.com/content/images/2021/11/Screen_1_make_user.png 1019w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">User Entity</span></figcaption></figure>
<p>Next run migration:</p>
<pre><code class="language-bash">php bin/console make:migration
$ php bin/console doctrine:migrations:migrate</code></pre>
<p>At first, I wanted to use foundation, but when I saw that it uses jquery, I changed my mind and decided to go with the tailwind(strange? - maybe, but it is what it is).<br>Install Tailwind CSS</p>
<pre><code class="language-bash">npm i tailwindcss postcss-loader autoprefixer</code></pre>
<p>create <strong><em>postcss.config.js </em></strong>in the main folder:</p>
<pre><code class="language-js">let tailwindcss = require(&apos;tailwindcss&apos;);

module.exports = {
        plugins: [
            tailwindcss(&apos;./tailwind.config.js&apos;),
            require(&apos;autoprefixer&apos;),
            require(&apos;postcss-import&apos;)
        ]
}</code></pre>
<p>Create tailwind.css file in <code>/assets/styles/tailwind.css</code></p>
<pre><code class="language-CSS">@tailwind base;

@tailwind components;

@tailwind utilities;</code></pre>
<p>update Webpack configuration with new tailwind CSS, add these lines:</p>
<pre><code class="language-PHP">    Encore
    //.......
    .enableVueLoader()
    .addStyleEntry(&apos;tailwind&apos;, &apos;./assets/styles/tailwind.css&apos;)
    // enable post css loader
    .enablePostCssLoader((options) =&gt; {
        options.postcssOptions = {
            config: &apos;./postcss.config.js&apos;
        };
    })</code></pre>
<p>Don&apos;t forget to update your <code>base.html.twig</code> file, add this:</p>
<pre><code class="language-twig">        {% block stylesheets %}
            {{ encore_entry_link_tags(&apos;tailwind&apos;) }}
        {% endblock %}</code></pre>
<p>Lets build our first component.</p>
<p>Now let&apos;s create a basic Vue project structure.</p>
<p>Please create the following folders and files in your <code>assets/js</code> folder:</p>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2021/11/assets.png" class="kg-image" alt="Symfony 6 tutorial. Frontend setup and configuration." loading="lazy" width="501" height="458"><figcaption><span style="white-space: pre-wrap;">Vue.js project structure</span></figcaption></figure>
<p>main.js - our entry js file.</p>
<pre><code class="language-js">import &apos;../styles/app.css&apos;;
import {createApp} from &apos;vue&apos;;
import App from &apos;./App&apos;;
import router from &apos;./router/router&apos;;
import store from &apos;./store/index&apos;;


createApp(App)
    .use(router)
    .use(store)
    .mount(&apos;#app&apos;)</code></pre>
<p>App.vue - the main component, for now, it&apos;s a pretty simple file:</p>
<pre><code class="language-js">&lt;template&gt;
  &lt;div&gt;
    &lt;router-view&gt;
      &lt;home /&gt;
    &lt;/router-view&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Home from &quot;./components/Home.vue&quot;;
export default {
  name: &quot;App&quot;,
  components: {
    Home
  }
};
&lt;/script&gt;

</code></pre>
<p><code>/router/router.js</code> - contains our routes;  I created only one route but will add more later.</p>
<pre><code class="language-js">import Home from &apos;../components/Home.vue&apos;
import {createRouter, createWebHistory} from &apos;vue-router&apos;

export default createRouter({
    history: createWebHistory(),
    routes: [
        {
            name: &apos;home&apos;,
            path: &apos;/&apos;,
            component: Home
        }
    ]
})
</code></pre>
<p><code>store/index.js</code> - Vuex store. Nothing special, I will show what we are going to do with this in the next chapter.</p>
<pre><code class="language-js">import {createStore} from &apos;vuex&apos;

export default createStore({
    state() {
        return {
            data: 1
        }
    }
})</code></pre>
<p>And of course our first component - Home.vue</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;div&gt;
    Home controller
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &quot;Home&quot;
};
&lt;/script&gt;
</code></pre>
<p>One more thing, update your <code>FrontendController</code> <code>Index</code> action:</p>
<pre><code class="language-php">    #[Route(&apos;/&apos;, name: &apos;frontend&apos;)]
    #[Route(&apos;/{route}&apos;, name: &apos;vue&apos;, requirements: [&apos;route&apos; =&gt; &apos;^.+&apos;])]
    public function index(): Response</code></pre>
<pre><code class="language-bash">yarn encode dev --watch</code></pre>
<p>Voila, and we are done!</p>
<p>In the next chapter, I will build a login/sign-up form with basic authentication.</p>
<p><a href="https://www.ptrdev.com/symfony-advanced-tutorial-project-configuration/">Back to Part 2</a></p>]]></content:encoded></item><item><title><![CDATA[Symfony 6 tutorial. Symfony project setup and configuration.]]></title><description><![CDATA[Symfony advanced tutorial. Project set up and configuration. Symfony docker configuration.]]></description><link>https://www.ptrdev.com/symfony-advanced-tutorial-project-configuration/</link><guid isPermaLink="false">65006f4bac9a6100011aeba1</guid><category><![CDATA[symfony]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Sun, 26 Sep 2021 18:35:42 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1605745341075-1b7460b99df8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDJ8fGRvY2tlcnxlbnwwfHx8fDE2MzI2ODE0Nzc&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1605745341075-1b7460b99df8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDJ8fGRvY2tlcnxlbnwwfHx8fDE2MzI2ODE0Nzc&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Symfony 6 tutorial. Symfony project setup and configuration."><p>Hi, this is a series of tutorials. On how to build a Symfony 6 application from scratch. Symfony 6 project setup and configuration.</p>
<h2 id="docker-configuration">Docker configuration</h2>
<p>Let&apos;s start with the docker image. Let&apos;s create a Dockerfile</p>
<pre><code class="language-bash">FROM php:8.0-fpm

# Copy composer.lock and composer.json
COPY composer.json /var/www/

# Set working directory
WORKDIR /var/www

# Install dependencies
RUN apt-get update &amp;&amp; apt-get install -y \
    build-essential \
    libssl-dev \
    acl \
    locales \
    zip \
    vim \
    git \
    curl \
    nodejs \
    npm

RUN apt-get install -y libicu-dev \
    &amp;&amp; docker-php-ext-configure intl

RUN npm install -g yarn

# Install extensions
RUN docker-php-ext-install pdo_mysql exif pcntl intl

# Install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Add user
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www

# Copy existing application directory contents
COPY . /var/www

# Copy existing application directory permissions
COPY --chown=www:www . /var/www

# Change current user to www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD [&quot;php-fpm&quot;]
</code></pre>
<p>Now let&apos;s make a docker-compose.yml file in the root directory. It should look like this. </p>
<pre><code class="language-yaml">version: &apos;3&apos;
services:

  #PHP Service
  application:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: application
    tty: true
    environment:
      SERVICE_NAME: application
      SERVICE_TAGS: dev
    working_dir: /var/www
    volumes:
      - ./:/var/www
      - ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
    networks:
      - app-network

  #Nginx Service
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      - ./:/var/www/
      - ./docker/nginx/conf.d/:/etc/nginx/conf.d
      - ./docker/nginx/ssl/:/etc/ssl
    networks:
      - app-network

  #MySQL Service
  database:
    image: mysql:5.7.22
    container_name: database
    restart: unless-stopped
    tty: true
    ports:
      - &quot;3306:3306&quot;
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - dbdata:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/my.cnf
      - ./docker/tmp/database:/tmp/database
    command: mysqld --init-file=&quot;/tmp/database/init.sql&quot;
    networks:
      - app-network

#Docker Networks
networks:
  app-network:
    driver: bridge
#Volumes
volumes:
  dbdata:
    driver: local
</code></pre>
<p>With docker-compose, I can build multiple containers and configure a database and a web server along with PHP. Also, you need to create additional folders in your root directory. The tree should look like this:</p>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2021/09/Screenshot-2021-09-26-at-00.05.24.png" class="kg-image" alt="Symfony 6 tutorial. Symfony project setup and configuration." loading="lazy" width="1136" height="1564" srcset="https://www.ptrdev.com/content/images/size/w600/2021/09/Screenshot-2021-09-26-at-00.05.24.png 600w, https://www.ptrdev.com/content/images/size/w1000/2021/09/Screenshot-2021-09-26-at-00.05.24.png 1000w, https://www.ptrdev.com/content/images/2021/09/Screenshot-2021-09-26-at-00.05.24.png 1136w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Symfony project docker.</span></figcaption></figure>
<p>Now, I need to configure each container. </p>
<h3 id="mysql-configuration">MySQL configuration</h3>
<p>Create a docker folder in a root directory and MySQL directory, and <code>my.cnf</code> file.<br>Inside <code>my.cnf</code> you can add additional MySQL configuration; in my case, it&apos;s just an empty file. Also, create <code>tmp/database/init.sql</code>. This is just a SQL file with predefined commands to create a user and database. Here is what it looks like:</p>
<pre><code class="language-SQL">CREATE DATABASE mydatabase;
CREATE USER &apos;user&apos;@&apos;%&apos; IDENTIFIED BY &apos;password&apos;;
GRANT ALL PRIVILEGES ON *.* TO &apos;user&apos;@&apos;%&apos;;
FLUSH PRIVILEGES;
</code></pre>
<p>If you want, you can change your password and username. Also, you can grant privileges to a single database. But for the dev environment, it&apos;s okay.</p>
<h3 id="php">PHP</h3>
<p>Next, php/local.ini add whatever PHP configuration you need, for example</p>
<pre><code class="language-php">upload_max_filesize=20M
post_max_size=20M
</code></pre>
<h3 id="webserver">Webserver</h3>
<p>Now we need to configure our web server, Nginx. I use NGINX because it&apos;s <a href="https://serverguy.com/comparison/apache-vs-nginx/?ref=ptrdev.com#:~:text=At%20serving%20static%20content%2C%20Nginx%20is%20the%20king!&amp;text=It%20performs%202.5%20times%20faster,requests%20with%20that%20costly%20overhead.">faster than apache</a>, and it serves static files and data without PHP. Why would you use something else? So, create the main Nginx folder and two child folders - <code>conf.d</code> and <code>certs</code>.</p>
<p>Inside <code>conf.d</code> create <code>app.conf</code> and copy-paste this code:</p>
<pre><code class="language-bash">server {
    listen 80;
    listen 443 ssl;
    ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
    ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
    index index.php index.html;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /var/www/public;
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass application:9000;
        client_body_timeout 3000;
        fastcgi_read_timeout 3000;
        client_max_body_size 32m;
        fastcgi_buffers 8 128k;
        fastcgi_buffer_size 128k;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME 	   $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }
}
</code></pre>
<p>I want to access my website via HTTPS. Thus I need to add certificates. You can simply generate them. I am not going to explain in detail how to generate them. But there is a great blog post on Digitalocean - how to <a href="https://www.digitalocean.com/community/tutorials/how-to-create-a-self-signed-ssl-certificate-for-nginx-in-ubuntu-16-04?ref=ptrdev.com">create self-signed certificates.</a> Take a look. And generate those certificates in the Nginx/SSL folder.</p>
<h3 id="run-containers">Run containers</h3>
<p>Ok, now we are ready to start our containers. Run this command from the terminal.</p>
<p><code>docker-compose up -d</code></p>
<p>You should see something like this:</p>
<figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2021/09/Screenshot-2021-09-26-at-00.23.04.png" class="kg-image" alt="Symfony 6 tutorial. Symfony project setup and configuration." loading="lazy" width="2000" height="568" srcset="https://www.ptrdev.com/content/images/size/w600/2021/09/Screenshot-2021-09-26-at-00.23.04.png 600w, https://www.ptrdev.com/content/images/size/w1000/2021/09/Screenshot-2021-09-26-at-00.23.04.png 1000w, https://www.ptrdev.com/content/images/size/w1600/2021/09/Screenshot-2021-09-26-at-00.23.04.png 1600w, https://www.ptrdev.com/content/images/2021/09/Screenshot-2021-09-26-at-00.23.04.png 2084w" sizes="(min-width: 720px) 720px"></figure>
<h3 id="symfony-set-up">Symfony set up</h3>
<p>Symfony is not installed yet. Now it&apos;s time to install the project. Let&apos;s connect to our application container via ssh.</p>
<pre><code class="language-bash">docker exec -it application bash</code></pre>
<p>Run this command.</p>
<p><code>composer create-project symfony/website-skeleton ./project &amp;&amp; cp -r ./project/* ./ &amp;&amp; rm -rf ./project/</code></p>
<p>What it does is create a website skeleton. Then it copies the contents to our current folder and removes the project folder. This magic is needed because you only can install the project into an empty folder.</p>
<p><br>Let&apos;s init git repository here:</p>
<pre><code class="language-bash">git init</code></pre>
<p>Don&apos;t forget to change <code>/etc/hosts</code> and add your hostname. In my case, it&apos;s a <code>127.0.0.1</code> <code>tracker.local.com</code></p>
<p>restart nginx - <code>docker exec nginx nginx -s reload</code></p>
<p>Now, update the database URL in <code>.env</code> file in the root directory. </p>
<pre><code class="language-yaml">DATABASE_URL=mysql://user:password@database:3306/mydatabase?serverVersion=5.7.22</code></pre>
<p>Since I will write frontend in the same project, I need to manage CSS/js files. For this, we need to install the <a href="https://symfony.com/doc/current/frontend/encore/installation.html?ref=ptrdev.com">Symfony Encore bundle</a>. It will help manage all the Symfony assets.</p>
<p><br>Run these commands:</p>
<p><code>composer require symfony/webpack-encore-bundle</code></p>
<p>A few more things.</p>
<p>Run <code>yarn install</code> command. And create a first controller. You should run this command from the application container <code>docker exec -it application bash</code> .</p>
<p><code>./bin/console make:controller FrontendController</code></p>
<p>Open up your browser, and you will see this page:</p>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2021/09/Screenshot-2021-09-26-at-00.39.52.png" class="kg-image" alt="Symfony 6 tutorial. Symfony project setup and configuration." loading="lazy" width="1498" height="606" srcset="https://www.ptrdev.com/content/images/size/w600/2021/09/Screenshot-2021-09-26-at-00.39.52.png 600w, https://www.ptrdev.com/content/images/size/w1000/2021/09/Screenshot-2021-09-26-at-00.39.52.png 1000w, https://www.ptrdev.com/content/images/2021/09/Screenshot-2021-09-26-at-00.39.52.png 1498w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">frontend controller</span></figcaption></figure>
<p></p>
<p>Congratulations, your Symfony project is now set up. We can move on.<br><br>Let&apos;s move to the third part of the tutorial &#x2014; front-end development. In the next chapter, I will build a simple vue.js application in a Symfony project. I think I will break it into several parts. The first part - login/signup with JWT authentication. I will configure the JWT bundle for this case. The next part would be a CRUD for tracking actions.</p>
<p><br>Read next - <a href="https://www.ptrdev.com/symfony-advanced-tutorial-frontend-setup-and-configuration-part3/">Frontend setup and configuration.</a></p>
<p>Back to <a href="https://www.ptrdev.com/symfony-advanced-tutorial-building-progress-tracker-website-part-one/">Part 1</a></p>]]></content:encoded></item><item><title><![CDATA[Symfony 6 tutorial. Building progress tracker website with Symfony 6 and Vue.js 3. Part One.]]></title><description><![CDATA[Symfony advanced tutorial. Building a website from scratch using Symfony and Vue.js.]]></description><link>https://www.ptrdev.com/symfony-advanced-tutorial-building-progress-tracker-website-part-one/</link><guid isPermaLink="false">65006f4bac9a6100011aeba0</guid><category><![CDATA[symfony]]></category><category><![CDATA[startup]]></category><category><![CDATA[sideproject]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Mon, 20 Sep 2021 20:29:46 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1432888498266-38ffec3eaf0a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIwfHxwcm9qZWN0fGVufDB8fHx8MTYzMjE2OTYxMA&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1432888498266-38ffec3eaf0a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIwfHxwcm9qZWN0fGVufDB8fHx8MTYzMjE2OTYxMA&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Symfony 6 tutorial. Building progress tracker website with Symfony 6 and Vue.js 3. Part One."><p><code>Last updated: 14/09/2023</code></p>
<p>Hello, my dear guest. In this tutorial, I will explain how to build a website with Symfony 6 and some JavaScript. I assume that you have basic Symfony knowledge and you know how to install your project. If not, you can check my tutorial on setting <a href="https://www.ptrdev.com/symfony-tutorial-for-beginners/">up the Symfony project from scratch</a>.</p>
<h2 id="motivation">Motivation</h2>
<p>Why did I start writing this tutorial? The motivation behind this is to show how to build a single project from scratch using Symfony and Vue.js. There are a lot of tutorials on how to write to-do apps or something like that. Yet, it does not show how to build something more significant. I want to create something from a simple mockup to the deployed project. This project will be published online, and I will post the source code to GitHub.</p>
<h2 id="table-of-contents">Table of contents</h2>
<p><br>I will build this project in parts:</p>
<ol><li>In this part, you will see a big-picture, project overview.</li><li>The second part will be dedicated to project setup and configuration.</li><li>Essential frontend part built with Vue.js.</li><li>Backend API configuration and implementation(Symfony 6 and PHP 8).</li><li>Deployment to Digitalocean/AWS</li></ol>
<h2 id="what-da-hell-is-this">What da hell is this?</h2>
<p>I want to build an app to track all my progress in any area of my life(well, almost). So let&apos;s assume that I need to track my progress in the gym. So I need to add my exercises on any day and add weights/time between workouts and comments. So I can compare how I am doing relative to previous days. One more example would be my progress in losing weight. I want to write down calories(just a number). And distribution of fats, proteins, and carbs, my weight(also additional metrics - e.g., waist circumference).</p>
<h2 id="how-it-should-look-like">How it should look like</h2>
<p> <br>I want to build as simple an interface as possible. I am a big fan of drawing sketches with pen and paper. I strongly suggest envisioning your project on paper before you start coding. Let&apos;s see what we got here.</p>
<ul><li>This is a login and sign-up page. I will use a simple email and password login on a signup page for now. (Possibly add google sign up option)</li></ul>
<img src="https://www.ptrdev.com/content/images/2021/09/image-2.png" class="kg-image" alt="Symfony 6 tutorial. Building progress tracker website with Symfony 6 and Vue.js 3. Part One." loading="lazy" width="200" srcset="https://www.ptrdev.com/content/images/size/w600/2021/09/image-2.png 600w, https://www.ptrdev.com/content/images/2021/09/image-2.png 731w" sizes="(min-width: 720px) 720px">
<ul><li>Dashboard page - on this page, you can choose the type of your action.</li></ul>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2021/09/image-3.png" class="kg-image" alt="Symfony 6 tutorial. Building progress tracker website with Symfony 6 and Vue.js 3. Part One." loading="lazy" width="1080" height="581" srcset="https://www.ptrdev.com/content/images/size/w600/2021/09/image-3.png 600w, https://www.ptrdev.com/content/images/size/w1000/2021/09/image-3.png 1000w, https://www.ptrdev.com/content/images/2021/09/image-3.png 1080w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Dashboard</span></figcaption></figure>
<ul><li>If you click on Gym activity, you will see the list of exercises.</li></ul>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2021/09/image-4.png" class="kg-image" alt="Symfony 6 tutorial. Building progress tracker website with Symfony 6 and Vue.js 3. Part One." loading="lazy" width="1101" height="580" srcset="https://www.ptrdev.com/content/images/size/w600/2021/09/image-4.png 600w, https://www.ptrdev.com/content/images/size/w1000/2021/09/image-4.png 1000w, https://www.ptrdev.com/content/images/2021/09/image-4.png 1101w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">List</span></figcaption></figure>
<ul><li>When you click on Add new activity, you will be redirected to the details page.</li></ul>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2021/09/image-5.png" class="kg-image" alt="Symfony 6 tutorial. Building progress tracker website with Symfony 6 and Vue.js 3. Part One." loading="lazy" width="954" height="449" srcset="https://www.ptrdev.com/content/images/size/w600/2021/09/image-5.png 600w, https://www.ptrdev.com/content/images/2021/09/image-5.png 954w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Details</span></figcaption></figure>
<ul><li> You can add actions and options(these would be different for each type of action). Also, you will see a list of all activities.</li><li>Later on, I am going to show statistics, but for now, that&apos;s it.</li></ul>
<p>Indeed not the best drawing, but think of it as a short sketch.</p>
<h2 id="technologies">Technologies</h2>
<p><br>In this project, I will write the backend with Symfony 6 and PHP 8. I will do the frontend part with Vue.js. Also, I will use docker and MYSQL database. And I am going to deploy my code to the Digitalocean server.</p>
<p>Read next - <a href="https://www.ptrdev.com/symfony-advanced-tutorial-project-configuration/">Symfony project configuration.</a><br></p>
<p></p>]]></content:encoded></item><item><title><![CDATA[Set up php-cs-fixer in PHPStorm]]></title><description><![CDATA[Set up php-cs-fixer in PHPStorm. Install php-cs-fixer, configure php-cs-fixer in PHPStorm]]></description><link>https://www.ptrdev.com/php-cs-fixer-in-phpstorm/</link><guid isPermaLink="false">65006f4bac9a6100011aeb90</guid><category><![CDATA[php]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Thu, 24 Dec 2020 22:03:09 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1517852058149-07c7a2e65cc6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MXwxMTc3M3wwfDF8c2VhcmNofDIwfHxwaHB8ZW58MHx8fA&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1517852058149-07c7a2e65cc6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MXwxMTc3M3wwfDF8c2VhcmNofDIwfHxwaHB8ZW58MHx8fA&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Set up php-cs-fixer in PHPStorm"><p>Updated: 13.07.2023</p>
<p>If you want to automatically fix the code style in <code>Phpstorm</code> using <code>php-cs-fixer</code>, create a configuration file inside your project folder, name it - <code>.php-cs-fixer.dist.php</code></p>
<p>Then, copy and paste this code:</p>
<figure class="kg-card kg-code-card"><pre><code class="language-php">&lt;?php
$finder = (new PhpCsFixer\Finder())
    -&gt;in(__DIR__)
    -&gt;exclude(&apos;var&apos;)
;

return (new PhpCsFixer\Config())
    -&gt;setRules([
        &apos;@PER&apos; =&gt; true,
        &apos;@Symfony&apos; =&gt; true,
// other rules
    ])
    -&gt;setFinder($finder)
;</code></pre><figcaption><p><span style="white-space: pre-wrap;">I use this configuration daily.</span></p></figcaption></figure>
<p>As you can see, there is a list of rules: <code>@PER, @Symfony</code>, etc. <a href="https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/2.17/doc/rules/index.rst?ref=ptrdev.com"> </a>You can add more rules, <a href="https://cs.symfony.com/doc/rules/index.html?ref=ptrdev.com">here you can find a list of rules</a></p>
<p>Next, in PhpStorm go to: <code>File -&gt; Settings -&gt; Tools -&gt; External Tools</code></p>
<p>click - <code>Add new</code></p>
<figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2020/12/Screenshot-2020-12-24-at-23.46.30.png" class="kg-image" alt="Set up php-cs-fixer in PHPStorm" loading="lazy" width="2000" height="903" srcset="https://www.ptrdev.com/content/images/size/w600/2020/12/Screenshot-2020-12-24-at-23.46.30.png 600w, https://www.ptrdev.com/content/images/size/w1000/2020/12/Screenshot-2020-12-24-at-23.46.30.png 1000w, https://www.ptrdev.com/content/images/size/w1600/2020/12/Screenshot-2020-12-24-at-23.46.30.png 1600w, https://www.ptrdev.com/content/images/size/w2400/2020/12/Screenshot-2020-12-24-at-23.46.30.png 2400w" sizes="(min-width: 720px) 720px"></figure>
<p>And fill these lines:</p>
<ul><li>Program:  <code>/path-to-php-cs-fixer/bin/php-cs-fixer</code></li><li>Arguments: <code>--verbose .php-cs-fixer.dist.php fix &quot;$FileDir$/$FileName$&quot;</code> </li><li>Working directory: <code>$ProjectFileDir$</code></li></ul>
<p>One more thing, you need to add a key binding for a new command. Go to <code>File -&gt; Settings -&gt; Keymap</code>: (Search by the name of your external tool). In my case, it&apos;s a <code>csfixer</code>.</p>
<p>If you are using PHP 8.2+ and you see an error like this:</p>
<p><code>PHP needs to be a minimum version of PHP 7.4.0 and maximum version of PHP 8.1.*.</code></p>
<p>Do this:<br>create a new file in <code>./bin</code> directory of your project name it <code>cs-fixer</code> and copy and paste this code:</p>
<pre><code class="language-php">#!/usr/bin/env php
&lt;?php

putenv(&apos;PHP_CS_FIXER_IGNORE_ENV=1&apos;);
include __DIR__ . &apos;/../vendor/bin/php-cs-fixer&apos;;</code></pre>
<p>And go to <code>File -&gt; Settings -&gt; Tools -&gt; External Tools</code> , select your external tool, and replace <code>Program</code> to <code>your-project-path/bin/cs-fixer</code></p>
<p>That&apos;s it; now you can use it.</p>
<p>Open any PHP file and use your key binding - it will fix your code according to your configuration.</p>]]></content:encoded></item><item><title><![CDATA[How to make an HTTP Request with SSL  certificate in Kotlin using a PEM file]]></title><description><![CDATA[How to make an HTTP Request with SSL  certificate in Kotlin using a PEM file]]></description><link>https://www.ptrdev.com/how-to-make-an-http-request-with-ssl-certificate-in-kotlin-using-a-pem-file/</link><guid isPermaLink="false">65006f4bac9a6100011aeb7c</guid><category><![CDATA[kotlin]]></category><category><![CDATA[programming]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Thu, 14 May 2020 19:58:23 GMT</pubDate><media:content url="https://www.ptrdev.com/content/images/2020/05/kotlin.png" medium="image"/><content:encoded><![CDATA[<img src="https://www.ptrdev.com/content/images/2020/05/kotlin.png" alt="How to make an HTTP Request with SSL  certificate in Kotlin using a PEM file"><p>With CURL you can send a request using the PEM file, but in Java/Kotlin, it&apos;s not that easy. You need some magic.</p><p>To send a request using CURL you just need to use this option:</p><p><em><code>CURLOPT_SSLCERT =&gt; &quot;path/to/pem/file&quot;</code></em></p><p>Well, if you want to send a CURL request from Java/Kotlin you can do something like <a href="https://www.baeldung.com/java-curl?ref=ptrdev.com">this</a>.</p><p>But I want to use HTTP Client to send request. So I need something else.</p><p>To send an HTTP request with SSL Certificate in Kotlin, we need to use a <a href="https://www.digitalocean.com/community/tutorials/java-keytool-essentials-working-with-java-keystores?ref=ptrdev.com">Keystore</a> and we need certificates.</p><p>To export certificates from your PEM file, you have to run these commands:</p><pre><code># Extract key
openssl pkey -in foo.pem -out foo-key.pem

# Extract all the certs
openssl crl2pkcs7 -nocrl -certfile file.pem |
  openssl pkcs7 -print_certs -out file-certs.pem</code></pre><p>Then follow <a href="https://docs.oracle.com/cd/E19509-01/820-3503/6nf1il6er/index.html?ref=ptrdev.com">this</a> tutorial to generate a <code>keystore.ks</code> file.</p><p>When you have <code>keystore.ks</code> file(and password for this file), you can use it to create an SSL Context and use it in the request.</p><p>Here you can find a function that creates an HTTP Client. You can use this Client to send an HTTP request with SSL Certificate.</p><pre><code class="language-kotlin">    fun buildClient(): HttpClient {
        //locate keystore file with password to that file
        val keystore = &quot;path/to/keystore.ks&quot;
        val keystorePass = &quot;password&quot;
        
        val ks = KeyStore.getInstance(&quot;pkcs12&quot;)
        val kmf = KeyManagerFactory.getInstance(&quot;SunX509&quot;)
        //create new ssl context
        val sslContext = SSLContext.getInstance(&quot;SSL&quot;)
        //load your file to keystore
        ks.load(FileInputStream(keystore), keystorePass.toCharArray())
        kmf.init(ks, keystorePass.toCharArray())
		//set global system properties
        System.setProperty(&quot;java.protocol.handler.pkgs&quot;, &quot;com.sun.net.ssl.internal.www.protocol&quot;)
        System.setProperty(&quot;javax.net.ssl.keyStoreType&quot;, &quot;pkcs12&quot;)
        System.setProperty(&quot;javax.net.ssl.keyStore&quot;, keystore)
        System.setProperty(&quot;javax.net.ssl.keyStorePassword&quot;, keystorePass)

		//initialize sslContext
        sslContext.init(kmf.keyManagers, getTrustAllCert(), null)

        val client = java.net.http.HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .sslContext(sslContext)

                .build()
		//you can send request with your ssl certificate
        return client
    }
    
       private fun getTrustAllCert(): Array&lt;TrustManager&gt; {

        return arrayOf(object : X509TrustManager {
            override fun getAcceptedIssuers(): Array&lt;X509Certificate&gt;? = null
            override fun checkClientTrusted(certs: Array&lt;X509Certificate&gt;, authType: String) {}
            override fun checkServerTrusted(certs: Array&lt;X509Certificate&gt;, authType: String) {}
        })
    }</code></pre><p>However, if you don&apos;t want to use all these tricks, &#xA0;you can parse the PEM file directly in your code.</p><p>See the example below:</p><pre><code class="language-kotlin">
fun buildClient(): HttpClient {
       val trustAllCerts = arrayOf&lt;TrustManager&gt;(object : X509TrustManager {
        override fun getAcceptedIssuers(): Array&lt;X509Certificate&gt;? = null
        override fun checkClientTrusted(certs: Array&lt;X509Certificate&gt;, authType: String) {}
        override fun checkServerTrusted(certs: Array&lt;X509Certificate&gt;, authType: String) {}
    })


    val context = SSLContext.getInstance(&quot;TLS&quot;)
    val certAndKey = Files.readAllBytes(Paths.get(&quot;/path/to/file.pem&quot;))
    
    //get cert and key
    val certBytes = parseDERFromPEM(certAndKey, &quot;-----BEGIN CERTIFICATE-----&quot;, &quot;-----END CERTIFICATE-----&quot;)
    val keyBytes = parseDERFromPEM(certAndKey, &quot;-----BEGIN PRIVATE KEY-----&quot;, &quot;-----END PRIVATE KEY-----&quot;)
    val cert = generateCertificateFromDER(certBytes)
    val key = generatePrivateKeyFromDER(keyBytes)
    val keystore = KeyStore.getInstance(&quot;JKS&quot;)
    
    keystore.load(null)
    keystore.setCertificateEntry(&quot;alias&quot;, cert)
    keystore.setKeyEntry(&quot;alias&quot;, key, &quot;password&quot;.toCharArray(), arrayOf&lt;Certificate&gt;(cert))
    //save certificate to keyManager
    val kmf = KeyManagerFactory.getInstance(&quot;SunX509&quot;)
    kmf.init(keystore, &quot;password&quot;.toCharArray())
    
    context.init(kmf.keyManagers, trustAllCerts, null)
    
    val client = java.net.http.HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .sslContext(context)

            .build()

    return client
   
}


private fun parseDERFromPEM(pem: ByteArray, beginDelimiter: String, endDelimiter: String): ByteArray {
    val data = String(pem)
    var tokens = data.split(beginDelimiter).toTypedArray()
    tokens = tokens[1].split(endDelimiter).toTypedArray()
    
    return DatatypeConverter.parseBase64Binary(tokens[0])
}

@Throws(InvalidKeySpecException::class, NoSuchAlgorithmException::class)
private fun generatePrivateKeyFromDER(keyBytes: ByteArray): RSAPrivateKey {
    val spec = PKCS8EncodedKeySpec(keyBytes)
    val factory = KeyFactory.getInstance(&quot;RSA&quot;)
    
    return factory.generatePrivate(spec) as RSAPrivateKey
}

@Throws(CertificateException::class)
private fun generateCertificateFromDER(certBytes: ByteArray): X509Certificate {
    val factory = CertificateFactory.getInstance(&quot;X.509&quot;)

    return factory.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate
}</code></pre><p>I was a bit shocked that I need to write so many lines of code to achieve this in Kotlin/Java. Although using CURL it is so easy to do.</p>]]></content:encoded></item><item><title><![CDATA[Symfony tutorial for beginners]]></title><description><![CDATA[Symfony tutorial for beginners. Learn how to build simple CRUD application using Symfony framework. ]]></description><link>https://www.ptrdev.com/symfony-tutorial-for-beginners/</link><guid isPermaLink="false">65006f4bac9a6100011aeb74</guid><category><![CDATA[symfony]]></category><category><![CDATA[php]]></category><category><![CDATA[tutorial]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Thu, 23 Apr 2020 14:16:00 GMT</pubDate><media:content url="https://www.ptrdev.com/content/images/2020/04/dayne-topkin-cB10K2ugb-4-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.ptrdev.com/content/images/2020/04/dayne-topkin-cB10K2ugb-4-unsplash.jpg" alt="Symfony tutorial for beginners"><p><em><code>Last updated: 20/08/2022</code></em></p><p>Hey there. </p><p>In this tutorial, you will learn how to create a basic Symfony application. When you finish this tutorial, you will know how to create a simple CRUD application. And you will get familiar with controllers, templates, entities, routes, and so on. </p><p>The source code is available on <a href="https://github.com/neok/symfony-tutorial-for-beginners?ref=ptrdev.com">GitHub</a>.</p><h3 id="requirements">Requirements</h3><p><br>I assume that you have basic knowledge of these things:</p><ol><li>Basic PHP/MySQL knowledge</li><li>HTML/CSS knowledge</li><li>You should know how to use the terminal(console)</li></ol><p>And you have installed:</p><ol><li><a href="https://www.php.net/downloads.php?ref=ptrdev.com">PHP</a> 7+</li><li><a href="https://dev.mysql.com/downloads/installer/?ref=ptrdev.com">MySQL</a> 5.6+ or <a href="https://mariadb.com/kb/en/getting-installing-and-upgrading-mariadb/?ref=ptrdev.com">MariaDB</a></li><li>You are using macOS or Linux(Ubuntu, Mint .. whatever)</li><li>If you are on Windows, you will need a <a href="https://www.apachefriends.org/index.html?ref=ptrdev.com">XAMMP</a> or better install <a href="https://docs.microsoft.com/en-us/windows/wsl/install?ref=ptrdev.com">Linux on Windows with WSL 2.</a></li><li>Text editor - <a href="https://www.jetbrains.com/phpstorm/?ref=ptrdev.com">PhpStorm</a> or <a href="https://code.visualstudio.com/?ref=ptrdev.com">Visual Studio Code</a> or any other you like.</li><li>Also, I recommend installing Symfony plugin for <a href="https://plugins.jetbrains.com/plugin/7219-symfony-support?ref=ptrdev.com">PhpStorm</a> or <a href="https://marketplace.visualstudio.com/items?itemName=TheNouillet.symfony-vscode&amp;ref=ptrdev.com">VSCode</a></li></ol><p>Let&apos;s start.</p><h3 id="installation"><br>Installation</h3><p>Download the Symfony installer. Go to <a href="https://symfony.com/download?ref=ptrdev.com">https://symfony.com/download</a> and download it.</p><p>In macOS you can install it with this line:</p><pre><code class="language-bash">curl -sS https://get.symfony.com/cli/installer | bash</code></pre><p>After installation you should be able to write commands like this one:</p><pre><code>symfony help</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2020/04/image.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"><figcaption>Symfony help screenshot.</figcaption></figure><p><a href="https://symfony.com/doc/current/components/console.html?ref=ptrdev.com">Symfony CLI</a> application has a lot of built-in features. We will use them a lot.</p><p>I am going to build a simple blog, so let&apos;s create a new Symfony project. </p><pre><code>symfony new --full blog</code></pre><p>This command will create a skeleton of the app. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2020/04/image-3.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"><figcaption>The project is ready.</figcaption></figure><p>To run our website, we need a web server. There are two most popular web servers for PHP: <a href="https://httpd.apache.org/?ref=ptrdev.com">Apache2</a> and <a href="https://www.nginx.com/?ref=ptrdev.com">Nginx</a>. But in this tutorial, I will use a simple built-in PHP <a href="https://www.php.net/manual/en/features.commandline.webserver.php?ref=ptrdev.com">webserver</a>.</p><p>To run a web server, open up a new terminal window and write the next line in your project directory.</p><pre><code>symfony server:start</code></pre><p>Or you can use this one:</p><pre><code>php -S 127.0.0.1:8000</code></pre><p>Now you have a web server up and running.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2020/04/image-4.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"><figcaption>The web server is running</figcaption></figure><p>Open that <a href="http://127.0.0.1:8000/?ref=ptrdev.com">link</a> in the browser and you should see a welcome page.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2020/04/image-5.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"><figcaption>Symfony 5 start page.</figcaption></figure><h3 id="first-page">First page</h3><p>To create a page you need to create a <a href="https://symfony.com/doc/current/routing.html?ref=ptrdev.com">route</a> and a <a href="https://symfony.com/doc/current/controller.html?ref=ptrdev.com">controller</a>.<br>Let&apos;s start with the controller.<br>First, I&apos;ll make a home page for my blog to show a list of blog posts.<br>Create an &#xA0;<code>MainController.php</code> under &#xA0;<code>src/Controller</code> folder:</p><pre><code class="language-php">&lt;?php

namespace App\Controller;

class MainController
{

    public function index()
    {
        return &apos;test&apos;;
    }
}</code></pre><p>Now you need to create a route(actual path) for this action. We will use <a href="https://symfony.com/doc/current/routing.html?ref=ptrdev.com#creating-routes-as-annotations">Symfony Annotations</a> to add routes.</p><pre><code class="language-php">&lt;?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;

class MainController
{
    /**
     * @Route(&quot;/&quot;, name=&quot;index&quot;)
     */
    public function index()
    {
        return new Response(&apos;test&apos;);
    }
}</code></pre><p>As you can see we added a new annotation directive <code>@Route</code> . It&apos;s a special annotation that tells the framework to use <code>index</code> method to process our main route.</p><p>Reload your page, you should see the word <code>test</code> in your browser. </p><p>Congratulations, you have created your first action.</p><h3 id="database-schema-entity">Database Schema, Entity</h3><p>Before we move forward, let`s create a database schema for our blog<em>(you can run native SQL queries if you want, but it&apos;s not recommended)</em>. I will go with something simple. Our blog will have next fields:</p><ol><li>title</li><li>short_description</li><li>body</li><li>image</li></ol><p>I hope that you have the MySQL client installed on your local machine. If not, you can install <a href="https://www.heidisql.com/?ref=ptrdev.com">HeidiSQL</a> or <a href="https://www.mysql.com/products/workbench/?ref=ptrdev.com">MySQL Workbench</a> or use the native <a href="https://dev.mysql.com/doc/refman/8.0/en/mysql.html?ref=ptrdev.com">mysql command-line client</a>.</p><p>The Symfony framework uses <a href="https://www.doctrine-project.org/projects/orm.html?ref=ptrdev.com">Doctrine ORM</a> to interact with the database. It&#x2019;s an abstraction layer between the application and the database. I will not go into details here, because it&apos;s a big topic, you can read about it <a href="https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/tutorials/getting-started.html?ref=ptrdev.com">here</a>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2022/08/image.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy" width="690" height="230" srcset="https://www.ptrdev.com/content/images/size/w600/2022/08/image.png 600w, https://www.ptrdev.com/content/images/2022/08/image.png 690w"><figcaption>Doctrine ORM</figcaption></figure><p><br>If I want to save something to the database, I need to create a specific class called - Entity(PHP object).</p><p>So let&apos;s create our first entity. To do so, write the next command in your terminal:</p><p><code>php bin/console make:entity</code> </p><p>You will see an interactive window. Follow the steps I described below.</p><figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2020/04/image-7.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"></figure><!--kg-card-begin: markdown--><ol>
<li>Name it <code>Blog</code>.</li>
<li>Now you need to add properties:<br>
2.1 <code>title</code>, type <code>string</code>, length <code>40</code>, nullable - <code>no</code><br>
2.2 <code>short_description</code>, type <code>string</code>, length <code>40</code>, nullable - <code>no</code><br>
2.3 <code>body</code>, type <code>text</code>, nullable - <code>no</code><br>
2.4 <code>image</code>, type <code>string</code>, length <code>100</code>, nullable <code>yes</code></li>
</ol>
<!--kg-card-end: markdown--><p>Now you should see two files that were created:</p><ol><li><code>src/Entity/Blog.php</code></li><li><code>src/Repository/BlogRepository.php</code></li></ol><p><br>If you messed up with something, don&apos;t worry. You can delete those files and do it again. If you still struggle, check <a href="https://github.com/neok/symfony-tutorial-for-beginners?ref=ptrdev.com">this link</a>. It&apos;s a full project, so you can find those files.</p><p>Ok great, we have our entity in place, but before we move on we need to create an actual database schema.</p><p>Create a new MySQL database:</p><figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2020/04/image-8.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"></figure><pre><code class="language-bash">mysql -u {USER} -p{PASSWORD}

create database blog;

exit;</code></pre><p>Replace <code>{USER}</code> with your MySQL user name, and <code>{PASSWORD}</code> with your user password.</p><p>Change your <code>.env</code> file(located in the root directory of the project).</p><p><code>DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7</code></p><p>Change <code>db_user</code> to your <code>mysql user</code>and <code>db_password</code> to your <code>database password</code>, <code>db_name</code> to <code>blog</code> </p><p>After that, populate the schema by running the next command:</p><p><code>php bin/console doctrine:schema:create</code></p><p>Ok, we have created schema and entity. Now it&apos;s time to populate the database with fake data(fake blog information).</p><p>For this purpose, there is a Symfony library called - <a href="https://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html?ref=ptrdev.com">FixtureBundle</a></p><p>Open up a terminal window and run the next command:</p><p><code>composer require --dev orm-fixtures</code></p><p>This command will install the extra library and will create a file <code>App\DataFixtures\AppFixtures</code></p><p>Open that file - <code>src/DataFixtures/AppFixtures.php</code> and let&apos;s create a few blog posts.</p><pre><code class="language-php">&lt;?php

namespace App\DataFixtures;

use App\Entity\Blog;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i &lt; 4; ++$i) {
            $blog = new Blog();
            $blog-&gt;setTitle(&apos;Lorem ipsum&apos;);
            $blog-&gt;setBody(&apos;Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
              Proin sodales, arcu non commodo vulputate, neque lectus luctus metus, 
              ac hendrerit mi erat eu ante. Nullam blandit arcu erat,
              vitae pretium neque suscipit vitae. 
              Pellentesque sit amet lacus in metus placerat posuere. Aliquam hendrerit risus elit, non commodo nulla cursus id. 
              Vivamus tristique felis leo, vitae laoreet sapien eleifend vitae. Etiam varius sollicitudin tincidunt&apos;);
            $blog-&gt;setShortDescription(&apos;Lorem ipsum description&apos;);
        }

        $manager-&gt;flush();
    }
}
</code></pre><p></p><p>As you can see, we created four blog posts. <code>ObjectManager</code> is a main public interface.</p><p>In the example above we used <code>persist</code> and <code>flush</code> methods. Persist tells <code>ObjectManager</code> that we want to save our object to a database table. <br>And <code>flush</code> saves changes into a database(MySQL in our case).</p><p>To generate fixtures, simply run this command from the console:</p><p><code>php bin/console doctrine:fixtures:load</code></p><p>Cool, now let&apos;s create a base view with a list of our blogs.</p><h3 id="index-action-extended">Index action extended</h3><p>Symfony uses a template engine called <a href="https://twig.symfony.com/?ref=ptrdev.com">Twig.</a> </p><p>I strongly recommend reading more about twig in the documentation.</p><p>To display an HTML page from a controller you need to create a template.</p><p>There is a <code>base.html.twig</code> file that you can use as a base template.</p><p>Go to the root directory of the project and find a <code>template</code> folder. Create a new file called <code>list.html.twig</code> and copy/paste the next code.</p><pre><code class="language-html">{% extends &apos;base.html.twig&apos; %}

{% block body %}
&lt;h1&gt;Blogs&lt;/h1&gt;
{% endblock %}</code></pre><p>This line means that we extend a base twig template and I overwrote a body block with a <code>Blogs</code> heading.</p><p>So let&apos;s change our controller&apos;s index action. Go to your <code>MainController</code> and extend it:</p><pre><code class="language-php">use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;

class MainController extends AbstractController</code></pre><p>We need to extend <code>AbstractController</code> because we want to use additional methods from that controller.</p><p>Now, modify your index function:</p><p><code>return $this-&gt;render<em>(</em>&apos;list.html.twig&apos;<em>)</em>;</code></p><p>Reload your page:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2020/04/image-9.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"><figcaption>You should see this text on your screen.</figcaption></figure><p>Before moving on to the next section and actually displaying all blogs. Let&apos;s add <a href="https://getbootstrap.com/?ref=ptrdev.com">bootstrap</a> CSS to our code. Add this code to the <code>&lt;head&gt;</code> section of <code>base.html.twig</code> file.</p><pre><code class="language-html">    &lt;link rel=&quot;stylesheet&quot; href=&quot;https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css&quot; integrity=&quot;sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T&quot; crossorigin=&quot;anonymous&quot;&gt;</code></pre><h3 id="blog-list">Blog List</h3><p>It&apos;s time to show the list of blogs on your index page.</p><p>To do so we need to fetch these blogs from the database. So, we need to <a href="https://symfony.com/doc/current/service_container/injection_types.html?ref=ptrdev.com">inject</a> a <code>BlogRepository</code> into our method.</p><pre><code class="language-php">    /**
     * @Route(&quot;/&quot;)
     *
     * @param BlogRepository $blogRepository
     *
     * @return Response
     */
    public function index(BlogRepository $blogRepository)
    {
        return $this-&gt;render(&apos;list.html.twig&apos;);
    }</code></pre><p>What is BlogRepository? &#xA0;This is a special Doctrine ORM class. If you want to write database queries, this is where you should do it.</p><p>In order to fetch all the blogs that we have, use a <code>fetchAll</code> method. Let&apos;s modify our index action.</p><pre><code class="language-php">    public function index(BlogRepository $blogRepository)
    {
        return $this-&gt;render(&apos;list.html.twig&apos;, [&apos;blogs&apos; =&gt; $blogRepository-&gt;findAll()]);
    }</code></pre><p></p><p>And change <code>list.html.twig</code> file, add this line under <code>h1</code> tag:</p><p><code><em>{{ </em>dump<em>(</em>blogs<em>) }}</em></code></p><p>This command is useful when you want to dump data into a readable format to debug it. If you run it, you will see something like this:</p><figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2020/04/image-10.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"></figure><p>Let&apos;s bring it to a normal view. I will create a table to display a blog list.</p><pre><code class="language-html">{% extends &apos;base.html.twig&apos; %}
{% block stylesheets %}
    &lt;style&gt;
        .align-center {
            margin: 0 auto;
        }
    &lt;/style&gt;
{% endblock %}
{% block body %}
    &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col-lg-8 align-center&quot;&gt;
            &lt;h1&gt;Blogs&lt;/h1&gt;
            &lt;table class=&quot;table table-striped &quot;&gt;
                &lt;thead class=&quot;thead-dark&quot;&gt;

                &lt;tr&gt;
                    &lt;th&gt;Title&lt;/th&gt;
                    &lt;th&gt;Short description&lt;/th&gt;
                    &lt;th&gt;Actions&lt;/th&gt;
                &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody&gt;
                {% for blog in blogs %}
                    &lt;tr&gt;
                        &lt;td&gt;{{ blog.title }}&lt;/td&gt;
                        &lt;td&gt;{{ blog.shortDescription }}&lt;/td&gt;
                        &lt;td&gt;
                            &lt;button class=&quot;btn btn-primary&quot;&gt;Edit&lt;/button&gt;
                        &lt;/td&gt;
                    &lt;/tr&gt;
                {% endfor %}
                &lt;/tbody&gt;
            &lt;/table&gt;

        &lt;/div&gt;
    &lt;/div&gt;


{% endblock %}
</code></pre><p>As you can see I used a FOR loop and displayed titles and short descriptions.</p><p>You can reload your page and you should see the list of blogs with edit action shown in the image below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.ptrdev.com/content/images/2020/04/image-11.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"><figcaption>List</figcaption></figure><p>Try to add an extra button, <code>DELETE</code> action for each blog post.</p><p>Ok, we have a list of blogs in place, now we need a way to create a single blog post.</p><h3 id="create-action">Create action</h3><p>To create a new blog, we need to create a new action in our controller. Let&apos;s name it <code>createBlog</code>.</p><pre><code class="language-PHP"> use Symfony\Component\HttpFoundation\Request;
 
 ........
 
    /**
     * @param Request $request
     *
     * @return Response
     */
    public function createBlog(Request $request)
    {
        
        return $this-&gt;render(&apos;create.html.twig&apos;);
    }</code></pre><p>Create a new template <code>create.html.twig</code></p><pre><code class="language-html">{% extends &apos;base.html.twig&apos; %}

{% block body %}
&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-lg-8 align-center&quot;&gt;
        &lt;h1&gt;Create Blog&lt;/h1&gt;

    &lt;/div&gt;
&lt;/div&gt;
{% block %}
</code></pre><p>To create a form in Symfony you need to create a <a href="https://symfony.com/doc/current/reference/forms/types.html?ref=ptrdev.com">FormType</a>. Symfony has powerful Form features. You don&apos;t have to worry about things like validation, form field rendering, and more. I recommend reading official <a href="https://symfony.com/doc/current/forms.html?ref=ptrdev.com">documentation</a> and getting familiar with forms.</p><p>So let&apos;s create our form type. Create a file called <code>BlogFormType.php</code> inside <code>./src/Form/Type</code> folder:</p><pre><code class="language-php">&lt;?php

namespace App\Form\Type;

use App\Entity\Blog;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BlogFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder-&gt;add(&apos;title&apos;, TextType::class, [&apos;attr&apos; =&gt; [&apos;class&apos; =&gt; &apos;form-control&apos;]]);
        $builder-&gt;add(&apos;shortDescription&apos;, TextType::class, [&apos;attr&apos; =&gt; [&apos;class&apos; =&gt; &apos;form-control&apos;]]);
        $builder-&gt;add(&apos;body&apos;, TextType::class, [&apos;attr&apos; =&gt; [&apos;class&apos; =&gt; &apos;form-control&apos;]]);
        $builder-&gt;add(&apos;imageFile&apos;, FileType::class, [
            &apos;attr&apos;     =&gt; [&apos;class&apos; =&gt; &apos;form-control&apos;,],
            &apos;mapped&apos; =&gt; false,
        ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver-&gt;setDefaults(
            [
                &apos;data_class&apos; =&gt; Blog::class,
            ]
        );
    }
}
</code></pre><p><br><code>buildForm</code> - method is used to describe form fields, and form types. We added one unmapped field, which means that we do not save this <code>imageFile</code> in the database, instead, we gonna save <code>imageName</code> . </p><p>We set a <code>Blog</code> class as a data class of our form. Thus, Symfony will know that it works with a Blog entity.</p><p>Now, let&apos;s render it, update the code in <code>create.html.twig file</code>:</p><pre><code class="language-html">    &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col-lg-8 align-center&quot;&gt;
            &lt;h1&gt;Create Blog&lt;/h1&gt;
            {{ form_start(form) }}

            {{ form_rest(form) }}
            &lt;input type=&quot;submit&quot; class=&quot;btn btn-primary&quot;/&gt;
        
            {{ form_end(form) }}
        &lt;/div&gt;
    &lt;/div&gt;</code></pre><p>and <code>createBlog</code> action inside <code>MainController.php</code>:</p><pre><code class="language-php">    /**
     * @Route(&quot;/create&quot;)
     *
     * @param Request $request
     *
     * @return Response
     */
    public function createBlog(Request $request)
    {
        $form = $this-&gt;createForm(BlogFormType::class, new Blog());

        return $this-&gt;render(&apos;create.html.twig&apos;, [
            &apos;form&apos; =&gt; $form-&gt;createView()
        ]);
    }</code></pre><p>As you can see, we created a form. We passed a new Blog entity object to this form, created a form view, and passed it to our twig template.</p><p>Reload your page, you should see your form:</p><figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2020/04/image-14.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"></figure><p>Let&apos;s update our form style. If you want to know how to customize forms and how to render them consider reading this <a href="https://symfony.com/doc/current/form/form_customization.html?ref=ptrdev.com">documentation</a>.</p><p>Update <code>create.html.twig</code> file.</p><pre><code class="language-html">        &lt;div class=&quot;col-lg-8 align-center&quot;&gt;
            &lt;h1&gt;Create new blog entry.&lt;/h1&gt;

            {{ form_start(form) }}

            &lt;div class=&quot;form-group&quot;&gt;
                {{ form_widget(form.title) }}
            &lt;/div&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                {{ form_widget(form.shortDescription) }}
            &lt;/div&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                {{ form_widget(form.imageFile) }}
            &lt;/div&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                {{ form_widget(form.body) }}
            &lt;/div&gt;

            &lt;input type=&quot;submit&quot; class=&quot;btn btn-primary&quot;/&gt;

            {{ form_end(form) }}
        &lt;/div&gt;</code></pre><figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2020/04/image-15.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"></figure><p>Now it looks much prettier. Let&apos;s update our <code>create</code> action method to process our form submission:</p><pre><code class="language-php">
        $form-&gt;handleRequest($request);
        if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) {
            $blog = $form-&gt;getData();
            $entityManager-&gt;persist($blog);
            $entityManager-&gt;flush();
            $this-&gt;addFlash(&apos;success&apos;, &apos;Blog was created!&apos;);
        }</code></pre><p>And add this code to <code>create.html.twig</code> under <code>&lt;h1&gt;</code> tag:</p><pre><code class="language-html">            {% for label, messages in app.flashes %}
                {% for message in messages %}
                    &lt;div class=&quot;flash-{{ label }}&quot;&gt;
                        {{ message }}
                    &lt;/div&gt;
                {% endfor %}
            {% endfor %}</code></pre><p>So what have we done? In our controller, we added form validation and persisted our Blog post into a database.<br>In our template, we displayed a success message. </p><p>At this point, you can try to create a blog. </p><p>But what about the image? Yes, right we need to save the image somewhere on our computer. Let&apos;s update <code>createBlog</code> action.</p><pre><code class="language-php">        if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) {
            $blog = $form-&gt;getData();
            $imageFile = $form-&gt;get(&apos;image&apos;)-&gt;getData();
            if ($imageFile) {
                $originalFilename = pathinfo($imageFile-&gt;getClientOriginalName(), PATHINFO_FILENAME);
                $safeFilename = $slugger-&gt;slug($originalFilename);
                $newFilename = $safeFilename.&apos;-&apos;.uniqid().&apos;.&apos;.$imageFile-&gt;guessExtension();

                try {
                    $imageFile-&gt;move(
                        $this-&gt;getParameter(&apos;image_directory&apos;),
                        $newFilename
                    );
                } catch (FileException $e) {
                    $this-&gt;addFlash(&apos;error&apos;, &apos;Image cannot be saved.&apos;);
                }
                $blog-&gt;setImage($newFilename);
            }

            $entityManager-&gt;persist($blog);
            $entityManager-&gt;flush();
            $this-&gt;addFlash(&apos;success&apos;, &apos;Blog was created!&apos;);
        }</code></pre><p>We have added image processing. We check if the image exists in the request. If so, we change its name and move it to the image_directory and then save the image filename.</p><p>I forgot to mention that you have to add <code>image_directory</code> parameter in </p><p><code>/config/services.yaml</code> under <code>parameters</code> section:</p><pre><code>parameters:
    image_directory: &apos;%kernel.project_dir%/public&apos;</code></pre><p>Try it out, you should be able to upload the image. You should see a newly created image under the <code>public</code> directory.</p><h3 id="validation">Validation</h3><p>Before we move on to edit action, we need to add validation to our form. </p><p>Why? </p><p>Ask yourself these questions:</p><ul><li>What if a user will upload a 100 megabytes pdf file? </li><li>What if I inject HTML code into a title? </li></ul><p>We have to handle such cases. For this purpose, Symfony has in-built <a href="https://symfony.com/doc/current/reference/constraints.html?ref=ptrdev.com">Validation Constraints</a>. I will show you how to use them.</p><p>We can add validation constraints with annotations. Let&apos;s edit our <code>/Entity/Blog.php</code> class.</p><pre><code class="language-php">use Symfony\Component\Validator\Constraints as Assert;

.......

    /**
     * @ORM\Column(type=&quot;string&quot;, length=40)
     * @Assert\NotBlank(message=&quot;Title cannot be empty.&quot;)
     * @Assert\Length(max=40)
     */
    private $title;

    /**
     * @ORM\Column(type=&quot;string&quot;, length=40)
     * @Assert\NotBlank()
     * @Assert\Length(max=40)
     */
    private $short_description;

    /**
     * @ORM\Column(type=&quot;text&quot;)
     * @Assert\NotBlank()
     */
    private $body;

    /**
     * @ORM\Column(type=&quot;string&quot;, length=100, nullable=true)
     * @Assert\Image()
     */
    private $image;</code></pre><p>We added assertations here. Our fields cannot be blank, and we asserted that the image should be an actual image. There are many more options for Image validation, but this is out of the scope of this tutorial. You can read about the options and try them <a href="https://symfony.com/doc/current/reference/constraints/Image.html?ref=ptrdev.com">here</a>.</p><p>Now you can try to create a blog. And try to upload some files (not images), you should see the error.</p><h3 id="improvements">Improvements</h3><p>So far, we created a list and <code>createBlog</code> actions. We need to add one more thing. We need a <code>create</code> button on this page. Find a <code>list.html.twig</code> file and copy-paste this code(at the bottom of the body block):</p><pre><code class="language-html">
    &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col-lg-8  align-center&quot;&gt;
            &lt;a href=&quot;{{ path(&apos;app_main_createblog&apos;) }}&quot; class=&quot;btn btn-primary&quot;&gt;Create blog&lt;/a&gt;
        &lt;/div&gt;
    &lt;/div&gt;
</code></pre><p><a href="https://symfony.com/doc/current/reference/twig_reference.html?ref=ptrdev.com#path">Path</a> - is a special function, that you can use in a twig to generate routes. To find a route, use this command:</p><p><code>php /bin/console debug:route</code></p><figure class="kg-card kg-image-card"><img src="https://www.ptrdev.com/content/images/2020/04/image-16.png" class="kg-image" alt="Symfony tutorial for beginners" loading="lazy"></figure><p>It will show every route you have in your project and its name. I recommend installing Symfony plugin in your IDE of choice. I use PHPStorm, but if you are using VisualStudioCode or something else, search for the Symfony plugin. With a plugin, you don&#x2019;t need to debug the route. Route autocomplete will be available for you.</p><p>You can add a name to your route explicitly, like this(in this case you don&apos;t have to debug routes - <strong>recommended</strong>):</p><p><em><code><strong>@Route</strong>(&quot;/&quot;, name=&quot;app_index&quot;)</code></em></p><p>Lastly, I want to redirect the user to the list page after creating the blog. Change this line in <code>createBlog</code> action.</p><pre><code class="language-php">            $this-&gt;addFlash(&apos;success&apos;, &apos;Blog was created!&apos;);

            return $this-&gt;redirectToRoute(&apos;app_main_index&apos;);</code></pre><p>And update <code>list.html.twig</code>, insert this code under <code>&lt;h1&gt;</code> tag:</p><pre><code class="language-html">            {% for label, messages in app.flashes %}
                {% for message in messages %}
                    &lt;div class=&quot;flash-{{ label }}&quot;&gt;
                        {{ message }}
                    &lt;/div&gt;
                {% endfor %}
            {% endfor %}</code></pre><p>Ok, we&apos;re good. </p><h3 id="edit-action">Edit action</h3><p>Edit blog is pretty similar to <code>create</code> action. We need to fetch the blog by <code>id</code> and populate the form with existing data from the database.</p><p>Create a new method in the controller called <code>editBlog</code></p><pre><code class="language-php">    /**
     * @Route(&quot;/edit/{id}&quot;)
     *
     * @ParamConverter(&quot;blog&quot;, class=&quot;App:Blog&quot;)
     *
     * @return Response
     */
    public function editBlog(Blog $blog, Request $request, EntityManagerInterface $entityManager, SluggerInterface $slugger)
    {
        $blog-&gt;setImage(new File(sprintf(&apos;%s/%s&apos;, $this-&gt;getParameter(&apos;image_directory&apos;), $blog-&gt;getImage())));
        $form = $this-&gt;createForm(BlogFormType::class, $blog);

        $form-&gt;handleRequest($request);
        if ($form-&gt;isSubmitted() &amp;&amp; $form-&gt;isValid()) {
            $blog      = $form-&gt;getData();
            $imageFile = $form-&gt;get(&apos;imageFile&apos;)-&gt;getData();
            if ($imageFile) {
                $originalFilename = pathinfo($imageFile-&gt;getClientOriginalName(), PATHINFO_FILENAME);

                $safeFilename = $slugger-&gt;slug($originalFilename);
                $newFilename  = $safeFilename.&apos;-&apos;.uniqid().&apos;.&apos;.$imageFile-&gt;guessExtension();

                try {
                    $imageFile-&gt;move(
                            $this-&gt;getParameter(&apos;image_directory&apos;),
                            $newFilename
                        );
                } catch (FileException $e) {
                    $this-&gt;addFlash(&apos;error&apos;, &apos;Image cannot be saved.&apos;);
                }
                $blog-&gt;setImage($newFilename);
            }

            $entityManager-&gt;persist($blog);
            $entityManager-&gt;flush();
            $this-&gt;addFlash(&apos;success&apos;, &apos;Blog was edited!&apos;);
        }

        return $this-&gt;render(&apos;create.html.twig&apos;, [
            &apos;form&apos; =&gt; $form-&gt;createView(),
        ]);
    }</code></pre><p>As you can see this method is almost the same as <code>createAction</code>.</p><pre><code class="language-php">     * @Route(&quot;/edit/{id}&quot;)
     *
     * @ParamConverter(&quot;blog&quot;, class=&quot;App:Blog&quot;)</code></pre><p><code>{id}</code> is a wildcard, it&apos;s a blog id, <code>@ParamConverter</code> annotation converts id into an Object, in our case, it&apos;s a Blog. Basically you can do it on your own using the repository method - <code>find</code>.</p><p>Additionally, we need to update our <code>BlogFormType</code> to avoid file processing(in case there is no file present). Add a <code>required =&gt; false</code> here:</p><pre><code class="language-php">        $builder-&gt;add(&apos;imageFile&apos;, FileType::class, [
            &apos;attr&apos;     =&gt; [&apos;class&apos; =&gt; &apos;form-control&apos;,],
            &apos;mapped&apos; =&gt; false,
            &apos;required&apos; =&gt; false
        ]);</code></pre><p>And one finishing touch, change <code>edit</code> button URL to existing edit action. You have to update <code>list.html.twig</code> file:</p><pre><code class="language-html">&lt;td&gt;
    &lt;a href=&quot;{{ path(&apos;app_main_editblog&apos;, {id: blog.id}) }}&quot; class=&quot;btn btn-primary&quot;&gt;Edit&lt;/a&gt;
&lt;/td&gt;</code></pre><p>Try it out, you should be able to click on the edit button and it will redirect you to the edit form. </p><p>Let&apos;s move on and build a <code>delete</code> action.</p><h3 id="delete-blog">Delete blog</h3><p>We need to add one more action to our <code>MainController</code>:</p><pre><code class="language-php">    /**
     * @Route(&quot;/delete/{id}&quot;, name=&quot;app_blog_delete&quot;)
     *
     * @param Blog                   $blog
     * @param EntityManagerInterface $em
     *
     * @return RedirectResponse
     */
    public function deleteBlog(Blog $blog, EntityManagerInterface $em): RedirectResponse
    {
        $em-&gt;remove($blog);
        $em-&gt;flush();
        $this-&gt;addFlash(&apos;success&apos;, &apos;Blog was edited!&apos;);

        return $this-&gt;redirectToRoute(&apos;app_main_index&apos;);
    }</code></pre><p>It&apos;s pretty simple, we remove the blog with <code>entityManager</code> and then we add a success message and redirect the user to the main page.</p><p><br>If you have not added the delete button yet, let&apos;s do it now. Go to <code>list.html.twig</code> and add the <em>delete button</em>, under the<em> edit button</em>.</p><pre><code class="language-html">&lt;a href=&quot;{{ path(&apos;app_blog_delete&apos;, {id: blog.id}) }}&quot; onclick=&quot;return confirm(&apos;Are you sure?&apos;);&quot; class=&quot;btn btn-danger&quot;&gt;DELETE&lt;/a&gt;</code></pre><p>That&apos;s it.</p><h3 id="conclusion">Conclusion</h3><p>In this tutorial, you have learned how to create a simple <a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete?ref=ptrdev.com">CRUD</a> application. From this point, I suggest learning more about doctrine, and Symfony forms.</p><p>If you want to see the full project, you can find it here on <a href="https://github.com/neok/symfony-tutorial-for-beginners?ref=ptrdev.com">GitHub</a>.</p><p>Thank you.</p><h3 id="update">Update</h3><p>If you are interested, I wrote a new Symfony advanced tutorial, <a href="https://www.ptrdev.com/symfony-advanced-tutorial-building-progress-tracker-website-part-one/">check it out</a>.</p>]]></content:encoded></item><item><title><![CDATA[20 side project ideas for a programmer.]]></title><description><![CDATA[Side project - is a great way to start a business. A unique list of ideas for programmers. Create a business with your next side project.]]></description><link>https://www.ptrdev.com/20-side-project-ideas-for-a-programmer/</link><guid isPermaLink="false">65006f4bac9a6100011aeb62</guid><category><![CDATA[sideproject]]></category><category><![CDATA[programming]]></category><category><![CDATA[ideas, startups, entrepreneur]]></category><dc:creator><![CDATA[Peter]]></dc:creator><pubDate>Wed, 24 Jul 2019 11:45:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1542627250-da40d2d18228?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1542627250-da40d2d18228?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="20 side project ideas for a programmer."><p>Bored with your main project or a job? Sick of people telling you what to do? Feel empty or stuck in your career? Great, these are the reasons why you should work on a side project. </p><p><strong>Side project</strong> - is a great way to start a business. If you always wanted to start something new, this is a great time to do it now. In the era of the internet, we have tremendous opportunities no matter where you live. Software developers have an advantage, you can code. You could make something that people want, and as a reward, you will get money and the freedom to work on your own terms. Although, you can start a side project even if you don&apos;t want to build a business. Do it to learn something new and gain experience.</p><p>Ask yourself these questions:</p><ul><li>Do I want to learn something new?</li><li>Does it help me in my career or future opportunities?</li><li>Can I make money on it?</li></ul><p>When you decide what you want to get out of this project, you can move on and think about what can you do.</p><h2 id="side-project-ideas-"><br>Side project ideas:</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://images.unsplash.com/photo-1489533119213-66a5cd877091?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" class="kg-image" alt="20 side project ideas for a programmer." loading="lazy"><figcaption>Photo by <a href="https://unsplash.com/@dsmacinnes?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit">Danielle MacInnes</a> / <a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit">Unsplash</a></figcaption></figure><!--kg-card-begin: html--><!-- Begin Mailchimp Signup Form -->
<link href="//cdn-images.mailchimp.com/embedcode/horizontal-slim-10_7.css" rel="stylesheet" type="text/css">
<style type="text/css">
	#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; width:100%;}
	/* Add your own Mailchimp form style overrides in your site stylesheet or in this style block.
	   We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
</style>
<div id="mc_embed_signup">
<form action="https://gmail.us3.list-manage.com/subscribe/post?u=13999e91c3e870af135e21b03&amp;id=49599b087c" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
    <div id="mc_embed_signup_scroll">
	<label for="mce-EMAIL">Subscribe if you want to receive a list of blog posts weekly.</label>
	<input type="email" value name="EMAIL" class="email" id="mce-EMAIL" placeholder="email address" required>
    <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
    <div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_13999e91c3e870af135e21b03_49599b087c" tabindex="-1" value></div>
    <div class="clear"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
    </div>
</form>
</div>

<!--End mc_embed_signup--><!--kg-card-end: html--><h3 id="1-online-course-"><br>1. Online course.</h3><p><br>If you are an experienced developer, you already have a lot of knowledge on a subject so that you can make a small online course about your favorite framework or language. Although it can be something new, interesting for you. You can either sell a course on platforms like Udemy or your website(blog). Using platforms has its advantage - &#xA0;you don&apos;t have to have an audience, they do it for you(bring audience). However, if you already have a blog and an audience, it would be better to sell it on your website.</p><h3 id="2-time-tracker-"><br>2. Time tracker.</h3><p><br>Time trackers are being used more and more, because more and more people are starting to work remotely, and the trends of digital nomads are growing. Working on such a project, you will learn how to track and record processes on a computer, how to track the time spent on specific applications. Create an application for Mac, PC or Linux. This project can be distributed as freemium, so you can add some extra features to get paid.</p><h3 id="3-web-scraper-"><br>3. Web scraper.</h3><p><br>This type of project is my favorite. By scraping, you can get a lot of potentially interesting data. For example: jump over e-commerce competitors to compare prices and be ahead of them. There are a lot of frameworks and libraries to scrape a website. There are two things you can do here. </p><p>&#x2003;1. Build your own scraper if you want to learn something new. </p><p>&#x2003;2. Take some existing framework and learn how to scrape data for your needs. For example, I like to check projects on a <a href="https://www.producthunt.com/?ref=ptrdev.com">Producthunt</a>, so I can build a scraper to send me the weekly email update of the top 10 products. And then parse scraped data to see what product categories are most popular.</p><h3 id="4-chatbot-"><br>4. Chatbot.</h3><p><br>Chatbots are so popular nowadays; there are plenty of platforms on top of each you can build a chatbot. You can create a Telegram bot that will ask general user questions and answer with predefined phrases. Or create something more sophisticated with <a href="https://towardsdatascience.com/5-heroic-tools-for-natural-language-processing-7f3c1f8fc9f0?ref=ptrdev.com">NLP libraries</a>.</p><h3 id="5-personal-finance-calculator-"><br>5. Personal finance calculator.</h3><p><br>Pick a new programming language or a framework you always wanted to learn and build a simple finance calculator to track your expenses. </p><h3 id="6-fitness-tracker-app-"><br>6. Fitness tracker app.</h3><p><br>If you are a gym addict as I am, you may know that keeping track of &#xA0;your weight is difficult, especially if you keep all your information in your head. Instead, you can create a fitness tracker app using a modern frameworks such as <a href="https://flutter.dev/?ref=ptrdev.com">Flutter</a> or <a href="https://facebook.github.io/react-native/?ref=ptrdev.com">React Native</a>, or others at your discretion.</p><h3 id="7-calorie-tracker-app-"><br>7. Calorie tracker app.</h3><p><br>Like a fitness tracker, this app will allow you to count calories. You can even combine the Fitness tracker app and calorie tracker into one great app. I hate counting calories. If you have ever tried to count calories by yourself, you know how difficult it is.</p><h3 id="8-domain-name-generator-and-domain-name-checker-"><br>8. Domain name generator and domain name checker.</h3><p><br>It&#x2019;s hard to come up with a great domain name, why not help people create unusual names for their companies. To do this, you will need something more than a simple word generator. You need to think about the different aspects of word generation. In addition to this name generator, you can add another function - domain name-checking.</p><h3 id="9-instagram-facebook-schedule-application-"><br>9. Instagram/Facebook schedule application.</h3><p><br>Managing Instagram accounts with a huge number of subscribers is difficult; especially if you need to post regularly. But what if you need to post in the middle of the night? Well, you can wake up and do it. However, you can do this with your application. There are plenty of such applications on the internet. But most of them are complex and has a lot of features. So instead, create something simple to solve one problem. A scheduled message.</p><h3 id="10-a-simple-game-for-kids-ios-android-web-"><br>10. A simple game for kids(iOs/Android/Web).</h3><p><br>Kids love simple games like puzzles, racing, and others. You can build a pretty simple game using Unity. If you don&apos;t want to develop an app, you can create a web version, using javascript.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://images.unsplash.com/photo-1552308995-2baac1ad5490?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" class="kg-image" alt="20 side project ideas for a programmer." loading="lazy"><figcaption>Photo by <a href="https://unsplash.com/@oskaryil?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit">Oskar Yildiz</a> / <a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit">Unsplash</a></figcaption></figure><h3 id="11-freelance-monitor-tool-"><br>11. Freelance monitor tool.</h3><p><br>I did a lot of freelancing. And honestly, it is difficult to track all the freelance marketplaces. Especially if you need to track some specific jobs. You can create monitoring tools to track new jobs by phrases or tags and send the results by email to the client. You can monetize such service, there are millions of freelancers out there, and the number of digital nomads is increasing every year.</p><h3 id="12-image-auto-tagging-platform-"><br>12. Image auto-tagging platform.</h3><p><br>Machine learning and deep learning are now in trends, so I suggest building something in this area if you are already familiar with the principles of ML/DL. Image auto-tagging is a platform where I, as a user, can upload an image and get image tags as output. You can build an API for such a service or create a simple Instagram tag generation application.</p><h3 id="13-brand-tracker-"><br>13. Brand tracker.</h3><p><br>Brands want to be able to respond to specific messages as quickly as possible and know what customers are saying about their product. I want to be able to track what people are saying about me(or my brand) on the internet and want to see it in some structured way. This project is both exciting and technically challenging to implement. You need to track many sources, scrape google and other places to find specific words or keywords. </p><h3 id="14-simple-kanban-board-"><br>14. Simple Kanban board.</h3><p><br>Pick a new framework or language and create a Kanban board similar to Trello. I suggest making such tools if you are learning a new programming language or framework.</p><h3 id="15-utility-for-macos-windows-linux-"><br>15. Utility for MacOs/Windows/Linux.</h3><p><br>Create a simple widget or application for your favorite operating system. </p><blockquote>When I search for something in macOS Finder, it searches around the entire disk. I want to perform a search in my current folder, so I would like to create a widget to search the folder in macOS Finder.</blockquote><p>Think about something bothering you, some pain, or something you did not like and fix it. No need to build something massive, start with simple things.</p><h3 id="16-wordpress-plugin-"><br>16. Wordpress plugin.</h3><p><br>Wordpress - the most popular content management system on the web; there are thousands of plugins created by various developers. How to find what to build? Check Wordpress marketplace and find plugins with a large number of downloads, but with bad ratings. Read the comments and find pain points. Then fix it with your new modern library.</p><h3 id="17-shopify-plugin-"><br>17. Shopify plugin.</h3><p><br>On the other hand, you can build a Shopify plugin. Shopify, it&apos;s an e-commerce platform. To create a plugin, you need to know some basic node.js and next.js. Although you can learn it by completing the <a href="https://help.shopify.com/en/api/tutorials/build-a-shopify-app-with-node-and-express?ref=ptrdev.com">tutorial</a>(I strongly recommend it). To find inspiration and ideas, you can visit the Shopify marketplace.</p><h3 id="18-slack-app-"><br>18. Slack app.</h3><p><br>Slack is a collaboration platform for teams. There are a huge number of apps available on slack, it is something you can monetize because many companies use slack. Here are some ideas for you:</p><ul><li>SCRUM plan poker game</li><li>Daily log from Trello/Jira (get a list of the things you did yesterday, and publish it as a summary in the morning).</li></ul><h3 id="19-site-blocker-application-"><br>19. Site blocker application.</h3><p><br>A simple tool that allows you to block any website for a certain amount of time. This will help you to stay more productive throughout the day, and do more work.</p><h3 id="20-sdk-library-"><br>20. SDK library.</h3><blockquote><br>I have been working with the Telegram API for a long time. But due to the lack of functionality in this library, I decided to create my own with much more functions and support of my favorite framework.</blockquote><p>Open source is a great way to help other people and share your knowledge. You don&apos;t have to start writing your own library. Instead, you can contribute to existing projects.</p><p>If you like this article, please share it on twitter or facebook.<br>Thanks.</p><p></p>]]></content:encoded></item></channel></rss>