[{"content":"I prefer working with #Docker, but sometimes it’s required to have #PHP locally, and even more - multiple versions of it! There’s a simple way of achieving this: ASDF 😎.\nFirst of all, you need to install asdf using method of your choice, after that you need to add this to your ~/.zshrc:\nexport PATH=\u0026#34;${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH\u0026#34; ASDF’s shim files have to be discoverable before any other binaries on your machine, to handle calls to the tools and resolve desired version (no worry, it’s possible to fall back to system versions too).\nThe next step is enabling PHP plugin:\nasdf plugin add php Installing PHP versions is really straightforward, all you need to do is execute asdf install php 8.4.10, and then in any directory where you want to use this version: asdf set php 8.4.10. The idea behind ASDF is that it keeps .tool-versions file with a list of versions that must be used under this directory and its subdirectories. It looks like this:\nphp 8.4.10 nodejs 24.4.0 If you\u0026rsquo;re using p10k, then you even can see what version of PHP is configured for specific directory you\u0026rsquo;re currently in:\nIn terms of PHP, before installing first version you most probably need to install some OS libraries, so ASDF can compile PHP from sources. In my case, for newly configured MacBook, I had to run:\nbrew install autoconf bison gd icu4c@77 libiconv libsodium libzip pkgconf re2c You probably want to add export HOMEBREW_NO_AUTO_UPDATE=1 to your ~/.zshrc to avoid unwanted updates of packages like icu4c during brew upgrade \u0026lt;something\u0026gt; (which may cause breakage of you ASDF\u0026rsquo;s PHP installations with \u0026ldquo;dynamic library can\u0026rsquo;t be loaded\u0026rdquo;). Also, to get rid of SSL-related build failures, some configure options have to be passed when installing PHP:\nPHP_CONFIGURE_OPTIONS=\u0026#34;--with-openssl=$(brew --prefix openssl) --with-iconv=$(brew --prefix libiconv)\u0026#34; asdf install php 8.4.10 Using PHP_CONFIGURE_OPTIONS you can modify how PHP\u0026rsquo;s build process will look like, it\u0026rsquo;s really powerful and flexible, but most importantly can solve your issues when your PHP installation requires a lot of additional extensions (like imagick, grpc or other).\nUsing PHP_CONFIGURE_OPTIONS will override default OS-related flags that ASDF prepares under the hood. Much better approach is suggested in this PR, but since it’s still not merged, you can achieve it on your machine by modifying ~/.asdf/plugins/php/bin/install file and putting this:\nif [ \u0026#34;$PHP_ADDITIONAL_CONFIG_OPTIONS\u0026#34; != \u0026#34;\u0026#34; ]; then configure_options=\u0026#34;$configure_options $PHP_ADDITIONAL_CONFIG_OPTIONS\u0026#34; fi right after configure_options initialisation.\nWhen you have PHP version compiled and working, you may want to install another extensions using PECL or PIE. During the installation, it may be required to provide paths to libs used by that extensions - just use brew --prefix \u0026lt;lib\u0026gt; in separate terminal, so you can copy \u0026amp; paste correct path. It can be sometimes tricky, because Homebrew packages\u0026rsquo; names not necessarily match extensions\u0026rsquo; names. Here are example mappings:\nfor imagick extension you need to use brew --prefix imagemagick for amqp extension you need to use brew --prefix rabbitmq-c for rdkafka extension you need to use brew --prefix librdkafka Summary ASDF makes it possible to fine-tune versions of tools on per-directory basis. ASDF resolves tools\u0026rsquo; versions in real time when you execute CLI commands (looks for .tool-versions file in current directory and then goes up the tree). ASDF supports many plugins (languages), it\u0026rsquo;s not only for PHP. ASDF is not a Docker replacement, but can come handy in some scenarios. ","permalink":"https://blog.codito.dev/2025/07/asdf-multiple-php-versions-on-localhost/","summary":"\u003cp\u003eI prefer working with \u003ca href=\"/tags/docker\" target=\"_blank\"\u003e#Docker\u003c/a\u003e, but sometimes it’s required to have \u003ca href=\"/tags/php\" target=\"_blank\"\u003e#PHP\u003c/a\u003e locally, and even more - multiple versions of it! There’s a simple way of achieving this: \u003ca href=\"https://asdf-vm.com\"  target=\"_blank\"\u003eASDF\u003c/a\u003e 😎.\u003c/p\u003e","title":"ASDF: Multiple PHP versions on localhost"},{"content":"I am excited to announce that I joined uSoftware as a Senior Software Engineer and will provide services for Printify in a Developer Enablement team 🤩!\nBackground For those who don\u0026rsquo;t know, at the end of May I was laid off from GetResponse along many others, and then I spent whole June on several recruitment processes. It was really tough time, because me and my family still struggle with renovations after moving out to the new place, so we already had a lot on our shoulders. Being laid off at such a crucial moment is not nice, it causes stress and anxiety, but it is unfortunately part of adult life and professional careers. It wasn\u0026rsquo;t the first time when it happened for me, but every time I was forced to look for new opportunities in the past, it ended up pretty nice - this time wasn\u0026rsquo;t different!\nRecruitment process(es) After hearing the decision from GetResponse I immediately started to look for new opportunities. Heck, I even spontanously (day later!) went to PHPers Day conference in Poznań to do some networking. I messaged several people that I knew they were interested in my services in the past. I also prepared this post to summarise my profile and expectations I have for the new role, to make HRs\u0026rsquo; life easier 😉, and shared it on all my social accounts (you can find them in the site\u0026rsquo;s header). I also updated my LinkedIn profile, even though I don\u0026rsquo;t use it that much on daily basis. Basically, I did everything to help myself with finding next job as soon, as possible. It worked, as I quickly was invited for several meetings and started recruitment processes.\nI had to make choices where I want to engage, and where not. It\u0026rsquo;s not an easy task, but entering too many processes at the same time can affect their outcome - it\u0026rsquo;s better to focus on fewer ones, but to be able to give 100% for each of them. In my case I had 4 main processes I was focused on, and kept few alternatives in early phase to continue later if needed. These 4 recruitment processes consisted of multiple meetings, home tasks, quizes and other steps, so it was a lot of work anyway.\nThe Choice I did my best in each of the processes, at least I thought so 😅. It turned out later that I did not cover all the cases in one of the home tasks (overlook), and also in the other one I made wrong decision to implement one crucial thing in a simplified way, with explanation in the readme file, which in the end costed me a rejection. Basically, I have completed 3 of 4 recruitment processes, with 2 of them ending with an offer: Shipmonk and Printify 🥳.\nTo be honest: it wasn\u0026rsquo;t an easy choice. Both companies looked very nice through the whole recruitment process, either from technical and team\u0026rsquo;s point of view. Both companies did their best to convince me to join them, for which I am extremely grateful ❤️. In the end the decisive factor was a profile of the role: I chose Printify because they offered me a possibility to continue working in the Developer Enablement area, which I find really interesting and fulfilling.\nFriendly advices To make this post something more than an informative note, I would like to share some advices that maybe someone finds useful when looking for new opportunities.\nBe quick Don\u0026rsquo;t waste time. Job offer that is available now may be unavailable tomorrow. You got a message/email about the recruitment process? Answer as soon as possible, even if it\u0026rsquo;s a short info that you are busy now and will dig deeper later. You have one week for your home task? Fine, but don\u0026rsquo;t leave it until the very end because then most probably you won\u0026rsquo;t be able to do everything as good as it should be - starting early can help you with estimation and it\u0026rsquo;s always better to have time margin.\nBe publicly active Remember: all your public activity can be an (dis)advantage from companies\u0026rsquo; point of view. It includes speaking at conferences, publishing blog, creating content on social media and Open Source work. Having something that you can proudly share during the process is crucial, as it shows your real-world skills.\nUse your network Don\u0026rsquo;t hesitate to reach people from your network (both indirectly and directly). It can open doors that you may not even knew before. In my case both of the job offers I got were the outcome of my tweet or direct message sent to friend. Similar for other processes, one of them started because Jagoda saw my \u0026ldquo;open to work\u0026rdquo; announcement and referred me to NordSecurity. That\u0026rsquo;s the power of networking and being recognisable. Invest in yourself.\nBe ambitious I know from my own experience (I always underestimate myself) that sometimes job offers seem unreachable. You may think: \u0026ldquo;I am too weak for that\u0026rdquo;. The truth is: job offers are often overpacked with requirements and you don\u0026rsquo;t need to meet them all. Even if you get rejected, this can be a valuable lesson for future recruitment processes and for your self-development. Always aim higher.\nBe honest Talk openly with your potential employees, so they know whether you\u0026rsquo;re going through other recruitment processes, whether you already have some offers. It doesn\u0026rsquo;t mean you have to tell everything, but act in a good will and don\u0026rsquo;t use companies as a lever to get better offer in other companies (I mean: don\u0026rsquo;t do it on purpose, there\u0026rsquo;s nothing inherently wrong in telling one company that you have better offer from other one). During interviews don\u0026rsquo;t be afraid to tell that you don\u0026rsquo;t know something. Software development is not about knowing everything, but about ability to find solutions. I don\u0026rsquo;t know many things, even those considered as basic stuff, and I am fine with that because I know what I know and what I can.\nLearn English It may sound obvious, but from my experience not everyone invest in learning English, even though code is almost always written in this language. Knowing English will make your work easier each day, but also can open opportunities. I\u0026rsquo;ve been using English for years mostly for reading and writing, but at GetResponse I took advantage of English lessons with native speaker to improve my speaking skills and foundamentals in general (grammar, phrasal verbs etc). I believe it greatly impacted my recent recruitment processes as I felt much more comfortable during the interviews. If you have such a perk in your company - use it! If not, use DuoLingo or any other learning platform. Even your local companies may have English as an requirement, so don\u0026rsquo;t limit your options because of not knowing it.\nDo your best Respect each others\u0026rsquo; time and do your best at every step of every recruitment process you\u0026rsquo;re in. Prepare for the interviews (re-learn things, remind yourself the basics, list your successes and failures etc), when doing home tasks always implement more than required (QA tooling, Docker runtime, tests). Show your strengths, don\u0026rsquo;t hesitate to point your weaknesses. Provide context and additional information (readme, comments). Use Git in a way to make code review easier, just as you would do at work. Don\u0026rsquo;t skip crucial parts of the task and don\u0026rsquo;t make shortcuts there (I failed one process because I implemented money operations with floats and provided info in the readme that in the real-world app it should be done differently - even though I knew what\u0026rsquo;s the correct implementation, I decided to make it simpler, and that was a mistake).\nFollow your heart, not wallet Even though money is really important, it can\u0026rsquo;t be a main driver for your career (well, unless you really want as much money as possible, which is not the case for me). I believe sometimes it\u0026rsquo;s better to choose an offer with less money but with better-fitting role. If the role is closer to your heart and the salary is higher there - it\u0026rsquo;s even better 😁. Listen to your heart and choose wisely.\nSpecial Thanks I would like to send huge THANKS to people who have been important for me in this transition period:\nmy wonderful wife who supported me at this difficult time and always believes in me and my skills even more than I am ❤️. Damian Dziaduch for referral, and whole Printify/uSoftware team involved in the process. Jan Nedbal for surprising referral, Ondřej Klamt for all the efforts, and all the people at Shipmonk. Jagoda Lasek for surprising referral, I am glad that my public activity paid off 😅. Marko Kunic for referral, sorry for letting you down 😅. Adrian Słowik for all of your efforts, you were first who wanted to help me ❤️. Arek Berliński, my great friend and person who hired me for my first professional job at IT. Thank you for reaching me and trying to help me ❤️. Michael Telgmann for referral. ","permalink":"https://blog.codito.dev/2025/07/printify-here-we-go/","summary":"\u003cp\u003eI am excited to announce that I joined \u003ca href=\"https://www.usoftware.co/\"  target=\"_blank\"\u003euSoftware\u003c/a\u003e as a Senior Software Engineer and will provide services for \u003ca href=\"https://printify.com/\"  target=\"_blank\"\u003ePrintify\u003c/a\u003e in a Developer Enablement team 🤩!\u003c/p\u003e","title":"Printify: Here We Go!"},{"content":"After four years at GetResponse it\u0026rsquo;s time for new opportunities! I am looking for a role related to #PHP, ideally in Developer Enablement context, but I am also open for alternatives. Read further for more details.\nAbout me I am software engineer with 15 years of professional experience in IT. For all these years I\u0026rsquo;ve been working mostly with #PHP, but I am not limited to that language, as I know - more or less - other ones. I am a conference speaker and #OSS contributor (maintainer of #PHP-CS-Fixer).\nMy strengths are:\nattention to details proactivity open mind continuous self-development focus on quality ability to work both as \u0026ldquo;free electron\u0026rdquo; and as team player wide technical knowledge around languages, frameworks, libraries and tools My weaknesses are:\nexcessive pedantry inability to use keyboard with all fingers 😅 My past responsibilities In the past, I\u0026rsquo;ve been working as a developer, tech leader, team leader (with technical responsibilities) and software architect. How the role is called does not matter that much for me, what I care is delivering value. I really enjoy being able to work in Developer Enablement context, which means:\nDockerization of projects, maintaining Dockerfile and Docker Compose stack creating and maintaining QA and CI/CD processes introducing, maintaining and upgrading tools and processes related to SDLC designing and implementing common approaches for tackling architectural/development challenges other things that can unlock faster and/or more friendly development for every developer, for which teams often don\u0026rsquo;t have time because of business deadlines What this means in practice? Take a look here to see what I did in my first year at GetResponse. I loved working there and I can say with full conviction that it was very fruitful time of my career. I\u0026rsquo;ve learned new things, met great people, did a lot (around 560 merged MRs in company\u0026rsquo;s Gitlab, hundreds of code reviews). Unfortunately, everything comes to an end.\nLooking for new opportunities Most importantly, I am looking for a remote position. It\u0026rsquo;s crucial for me and my family. I don\u0026rsquo;t have a problem with being at the office from time to time, but I\u0026rsquo;ve spent so much time travelling to/from work in the past, that at this point I don\u0026rsquo;t want to waste time for this anymore. Especially that I am well self-organised and don\u0026rsquo;t have problems with motivation to work at home.\nTechnically speaking, I would love to continue work in exact same or really similar role to what I described above (Developer Enablement). It can be around PHP, but I am open for other opportunities, if company is willing to let me learn along the way ☺️.\nSummary I am available starting from July 1st, earlier if really needed Looking for Developer Enablement, Software Engineer/Architect or Tech Lead remote positions (open to hear about alternatives) You can get dedicated, highly qualified employee that cares ❤️ Thanks for sharing!\n","permalink":"https://blog.codito.dev/2025/05/open-to-work/","summary":"\u003cp\u003eAfter \u003cem\u003efour years at GetResponse\u003c/em\u003e it\u0026rsquo;s time for new opportunities! I am looking for a role related to \u003ca href=\"/tags/php\" target=\"_blank\"\u003e#PHP\u003c/a\u003e, ideally in Developer Enablement context, but I am also open for alternatives. Read further for more details.\u003c/p\u003e","title":"Open to work"},{"content":"Pie, new extension installer for #PHP was released! Let\u0026rsquo;s have a look at it and see what it brings to the table.\nBackground Pie is a successor of PECL and was designed and developed under the PHP Foundation.\nYou can read about its goals, roadmap and scope here, you can also watch its development and even contribute here.\nUsage in Docker builds I\u0026rsquo;ve created gist that shows how to install and use Pie in Docker builds, both Debian and Alpine 😊. Both were tricky, as Pie requires stuff not mentioned in the official docs (like ZIP extension, but also several build-related packages on Alpine).\nWith this setup, when you run docker compose build php-debian \u0026amp;\u0026amp; docker compose run -it --rm php-debian php -v (use -alpine suffix for Alpine build), you should see the output like:\nPHP 8.3.13 (cli) (built: Nov 12 2024 05:52:30) (NTS) Copyright (c) The PHP Group Zend Engine v4.3.13, Copyright (c) Zend Technologies with Xdebug v3.4.0beta1, Copyright (c) 2002-2024, by Derick Rethans which means Pie successfully installed XDebug 😎. Please keep in mind you should also clean up the dev packages at the end of your build, example shown above is as minimal as possible to show Pie\u0026rsquo;s usage, it\u0026rsquo;s not focused on Docker optimisation.\nCredits If you found it useful, consider sharing my posts:\nTwitter BlueSky Mastodon And of course: follow me for more 😁!\n","permalink":"https://blog.codito.dev/2024/11/pie-new-extension-installer-for-php/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/php/pie\"  target=\"_blank\"\u003ePie\u003c/a\u003e, new extension installer for \u003ca href=\"/tags/php\" target=\"_blank\"\u003e#PHP\u003c/a\u003e was released! Let\u0026rsquo;s have a look at it and see what it brings to the table.\u003c/p\u003e","title":"Pie: new extension installer for PHP"},{"content":"Four months ago my goal was clear: to feel good with myself again when I hit my 40th birthday. It was true challenge, because last year I barely did any activities, I gained weight and I did not look and feel well. Here\u0026rsquo;s my story I am truly proud of 😎!\nHow it started I was always rather thin and physically active. But when I started my #OSS journey with #PHP-CS-Fixer, I went all-in which basically led to me sitting in front of the computer most of the time. I was sitting at work, and I was still sitting in my free time, really often through the night. I did not balance it, I was so fascinated about the outcome provided to all the users, that I did not see I am getting bigger and bigger. Of course, this is not OSS\u0026rsquo; fault, it was me who did not care about physical activity and proper diet - watching football games or movies with pack of chips and a beer also was not good for my condition.\nAt the beginning of 2024 I reached the point where I really was not feeling good with myself. Not only my body started to be uncomfortable on daily basis (problems with knees, inability to bend myself properly, overall tiredness), but also I just looked bad and seeing myself in the mirror has been making me really sad. Going to the public swimming pool or to the beach was hard for me, as I was ashamed about my belly.\nI\u0026rsquo;ve been trying to do something with this for quite some time, but did not have enough motivation, and I thought I don\u0026rsquo;t have time needed to improve my situation. And then tiny little thing happened, that was the missing puzzle for me - we met my wife\u0026rsquo;s cousin on the car park, and he joked \u0026ldquo;huh, the belly has grown a bit, right?\u0026rdquo; 😅. I know him, and I am sure he did not want to laugh at me, it was rather a friendly mockery, and you know what? It worked. In the following days I bought a gym pass, prepared my skateboard and started working hard.\nHow it was done May 13th vs August 29th: https://twitter.com/_Codito_/status/1829065547876569446\nI started my physical renovation in May, and it really became my new habit pretty quick. I\u0026rsquo;ve set myself a goal: be satisfied with my body again in the end of August. To be able to reach that goal, several things had to be changed in my life.\nPhysical activity It was right about time for me to start working out and regain proper physical condition.\nAs it turned out, my foremost workout type was\u0026hellip; skateboarding 🛹! I\u0026rsquo;ve been skating since I was ~15, but with variable frequency, especially in recent years. I am not significantly good at it, but I have so much fun from skating, so I decided to make it my main cardio and of course a way for losing fat. Not only I skated more during these four months than in few years back together, I also learned new trick I always wanted to do: crooked grind 😍!\nCrooked grind ❤️🛹 #skateboarding\nSlowly I get it consistent. Started with figuring out the technique, foot positioning, now I\u0026#39;m adding speed. I need to work on going off the ledge though. I love this trick, I wasted so many years by not practicing it 😭. pic.twitter.com/oF5Uowzvby\n\u0026mdash; Greg Korba 🛠️🛹 Codito (@_Codito_) August 24, 2024 Besides skating a lot, I\u0026rsquo;ve been working hard in the gym. I did not have any personal trainer or strict routine, I just did everything that looked interesting for me and involved different muscles. Since my goal was to lose fat, especially from the belly, I did many exercises around it (AB crunch, rower, elliptical), but also did many other weight-oriented stuff to build muscles too. My second goal was to strengthen my knees, so I did a lot of squatting and leg curling too. When my legs were tired, I just did exercises with upper body. When my arms were dead from lifting weights, I trained my abs. Since I did not have much time for exercises, I always squeezed it to the maximum and barely had any rest time during workout.\nI was told that one of the best ways to burn fat is\u0026hellip; walking. So I included long dog walks to my daily routine, and walked few kilometers almost every day. It was sometimes frustrating, as my dog wants to stop every few steps to sniff around or to pee, but we somehow managed to find a way to satisfy both of us. During these walks I almost always did DuoLingo lessons, so it was a double gain 😃.\nIn the end, I recorded 140 hours of activities, but I am pretty sure there was even more, I just only sometimes forgot to track it. Anyway, pretty nice achievement, considering the fact I always thought \u0026ldquo;I don\u0026rsquo;t have time\u0026rdquo; when it comes to such activities.\nActivities tracked with Mi Band\nWhen it comes to \u0026ldquo;freestyle\u0026rdquo; workouts, these were exercises I can do at home: Ring Fit, half balance ball, resistance bands etc. Instead of eating snacks in front of TV, I watched football games and TV shows while exercising. Actually, I tried to do even small exercises several times during a day - whenever it was possible.\nI can tell you, this is addictive 😆.\nActivities tracked with Mi Band, by month\nTo keep an eye of required workout load, I\u0026rsquo;ve set daily goals on my Mi Band - started with 700 kcal, 7000 steps and 10 hours with standing position. Later I bumped it to 1000 kcal, 10000 steps and 12 hours of standing. These numbers may not be huge, but I believe it\u0026rsquo;s not that important - the key is consistency and daily goals that are challenging, so you need to do significant amount of work to reach them. August was perfect in that regard, as I was able to hit daily goals every single day:\nSometimes it required a lot of effort, but there\u0026rsquo;s a saying: no pain, no gain 😎.\nDiet I am not a food expert, this is only a summary of my personal decisions around diet. Don\u0026rsquo;t consider it as a professional advices! As my main objective was to lose fat, it was pretty obvious to me that I need to eat less. Less in general, but also less fatty food. I did not go to any diet advisor, I just did few things:\ngot rid of sweets got rid of snacks, especially chips got rid of beer started to eat protein puddings, muesli with milk and other nutritious food started to eat less for a single meal, but more meals during a day avoided eating in the late evening avoided fried food All of this not dogmatically, but rather as a guideline to follow. If I felt a need to eat something sweet or there was an occasion like birthday party, I just did it. If I did a superb workout and I was proud of myself, I allowed myself to drink good beer once in a while. I believe it\u0026rsquo;s important to not torment yourself with strict rules, because sooner or later you\u0026rsquo;ll start to hate the process. If you work hard, you deserve good things 😃!\nOf course, it wouldn\u0026rsquo;t work without exercising a lot, but I believe what I did around my diet was also key in dropping around 10 kilograms in four months:\nFrom 78kg to 68kg in four months\nSleeping habit I always considered myself as a night owl - doing stuff when there\u0026rsquo;s silence is really great. My kids also don\u0026rsquo;t go to bed early, so this was probably the hardest part for me - find a way to be able to do workouts during a day, especially when it comes to skateboarding (as I prefer sunlight).\nIt was tough, but I cut off OSS almost completely. I went to bed almost instantly after my kids (and a dog walk). This made it possible for me to wake up really early, so multiple times I skated around 7am or even earlier (the weather was great and sun was shining pretty hard already). If I couldn\u0026rsquo;t get up, I just tried to exercise later, with gym as the last resort around 10pm (they close at 11pm).\nFortunately I don\u0026rsquo;t have problems with sleeping - I fell asleep quick and I rather don\u0026rsquo;t wake up in the middle of the night. So the only thing I needed to keep an eye of, was the length of sleeping (7h+), and going to bed as early as possible. It\u0026rsquo;s not easy to change sleeping habits, but step by step it\u0026rsquo;s possible to improve this routine.\nHow it\u0026rsquo;s going Reaching my initial goal was great, but it would be a pity to waste such an effort and stop working hard. That\u0026rsquo;s why I continue my fitness journey, even though it\u0026rsquo;s a little bit harder after kids went back to school and there are additional activities I have to fit in my daily routine (driving kids here and there).\nActivities registered in September 2024\nActivities tracked with Mi Band as of October 3rd, 2024\nTwo weeks after initial deadline I looked even better, and could make a 3-month comparison with a one of not-so-many pictures of me taken during my fitness challenge:\n3 month difference\nOne month after my 40th birthday I had an opportunity to analyse my body composition, and it turned out I\u0026rsquo;ve burned too much fat, as it was only 5.7% 😅! The most impressive part is that my metabolic age is 25 😲!\nBody composition analysis result\nIf numbers don\u0026rsquo;t speak for you, here\u0026rsquo;s an actual result:\n✅ Burning fat\n✅ Losing weight\n⏳ Body building#fitness #gym #skateboarding #health pic.twitter.com/BL4SkNCOiE\n\u0026mdash; Greg Korba 🛠️🛹 Codito (@_Codito_) September 28, 2024 At this point I don\u0026rsquo;t need to focus on burning fat anymore, I just can work on improving my shape and building more muscles. I don\u0026rsquo;t aim for being true body builder, I just want to look good 😊.\nSpecial thanks My metamorphosis was possible thanks to my wife, who supported me along the way. If it wasn\u0026rsquo;t for her, I couldn\u0026rsquo;t have exercised as much as I did. I believe it wasn\u0026rsquo;t easy for her, even though I really tried to minimize the impact of my hard work, and really often skated/exercised early in the morning or late in the evening. Anyway, she made it possible for me to reach my goal, and I am really grateful for that 😘!\nAlso, I would like to thank Muhammed Sarı for an inspiration, he did such a wonderful job around his body that pushed me towards my own journey.\nSummary There\u0026rsquo;s nothing like \u0026ldquo;I don\u0026rsquo;t have time\u0026rdquo;, it\u0026rsquo;s only about priorities Diet is important, but don\u0026rsquo;t make yourself a prisoner of your own food rules Finding kind of workout that gives you fun is crucial Good sleep is essential for your well-being Conscientiousness, conscientiousness and once again conscientiousness I really hope this article can motivate others to reach their own goals. Let me know!\n","permalink":"https://blog.codito.dev/2024/08/40-is-a-new-20/","summary":"\u003cp\u003eFour months ago my goal was clear: to feel good with myself again when I hit my 40th birthday. It was true challenge, because last year I barely did any activities, I gained weight and I did not look and feel well. Here\u0026rsquo;s my story I am truly proud of 😎!\u003c/p\u003e","title":"40 is a new 20"},{"content":"It\u0026rsquo;s hard to believe, but the --all option for the messenger:consume command in Symfony was unavailable until version 7.1 — on lower versions list of transports must be passed explicitly to the executed command. But if you need this, it\u0026rsquo;s really easy to port this feature to 6.4!\nBackground In GetResponse we\u0026rsquo;re currently preparing new approach for AMQP workers, because the one we have is not suitable anymore. After doing some research, we decided to use Symfony Messenger with KEDA auto-scaling. In regular Symfony application, with control over AMQP instance it\u0026rsquo;s really easy to set everything up, but the hard part in our case is that we need to fit into existing infrastructure and our application does not own AMQP server (only is one of the consumers). That means we can\u0026rsquo;t use auto-setup, dead letter queues or delayed messages out of the box, and we need to introduce transports with full backward compatibility for message payloads. A lot of customisation had to be done, but we\u0026rsquo;re almost there!\nAnyway, since our AMQP server uses multiple exchanges, Symfony Messenger has to be configured with multiple transports. That leads us to one little inconvenience: we need either specify list of the transports when executing messenger:consume command, or leave it empty and interactively choose transport from the list that is printed in CLI.\nBut we want to consume all the transports, at least locally or in test instances (on production we will group transports and queues, so they can be scaled). How to achieve this?\nSchrödinger\u0026rsquo;s --all option Fortunately, few months ago pull request was merged, which added --all option to the messenger:consume command. But there are 2 problems with this: because of feature-freeze period in 7.0, it had to be moved to 7.1 milestone (which is not released at the time of writing), and even if it landed little earlier, our app still uses Symfony 6.4 🤷‍♂️. This is something we need to work on at some point, but is not going to happen now.\nSo in the end it seems like this is a Schrödinger\u0026rsquo;s option for us — it is, but at the same time it\u0026rsquo;s not available.\nPorting --all to Symfony 6.4 I did some proof of concept today, and it looks like it\u0026rsquo;s possible to backport --all option to Symfony 6.4 pretty easily. To introduce this option in messenger:consume command we need to extend Symfony\u0026rsquo;s built-in command:\n\u0026lt;?php declare(strict_types=1); namespace Codito\\App\\Cli; use Symfony\\Component\\Console\\Attribute\\AsCommand; use Symfony\\Component\\Console\\Input\\InputInterface; use Symfony\\Component\\Console\\Input\\InputOption; use Symfony\\Component\\Console\\Output\\OutputInterface; use Symfony\\Component\\Messenger\\Command\\ConsumeMessagesCommand as SymfonyConsumeMessagesCommand; /** * This is only a proxy to original `messenger:consume` command, with `--all` option ported from Symfony 7.1, * command\u0026#39;s FQCN is overridden dynamically in compiler pass ({@see OverrideMessengerConsumeCommandCompilerPass}). * * @see https://github.com/symfony/symfony/pull/52411 @TODO Remove this workaround when Symfony 7.1 is used */ #[AsCommand(name: \u0026#39;messenger:consume\u0026#39;, description: \u0026#39;Wrapper for Symfony\\\u0026#39;s command that adds `--all` option\u0026#39;)] class ConsumeMessagesCommand extends SymfonyConsumeMessagesCommand { /** @var list\u0026lt;string\u0026gt; */ private array $transportNames = []; /** * @param list\u0026lt;string\u0026gt; $transportNames */ public function setTransportNames(array $transportNames): void { $this-\u0026gt;transportNames = $transportNames; } protected function configure(): void { parent::configure(); $this-\u0026gt;addOption(\u0026#39;all\u0026#39;, \u0026#39;a\u0026#39;, InputOption::VALUE_NONE, \u0026#39;Consume messages from all receivers\u0026#39;); } protected function execute(InputInterface $input, OutputInterface $output): int { if ($input-\u0026gt;getOption(\u0026#39;all\u0026#39;)) { if ([] === $this-\u0026gt;transportNames) { throw new \\RuntimeException(\u0026#39;There are no transports configured\u0026#39;); } $input-\u0026gt;setArgument(\u0026#39;receivers\u0026#39;, $this-\u0026gt;transportNames); } return parent::execute($input, $output); } protected function interact(InputInterface $input, OutputInterface $output): void { if ($input-\u0026gt;getOption(\u0026#39;all\u0026#39;)) { return; } parent::interact($input, $output); } } OK, but this command is already registered in Symfony application, because framework bundle provides it out-of-the-box when Messenger component is installed. How to tell Symfony to use our customised version of the command? Compiler pass to the rescue!\n\u0026lt;?php declare(strict_types=1); namespace Codito\\App\\DependencyInjection\\Compiler; use Codito\\App\\Cli\\ConsumeMessagesCommand; use Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface; use Symfony\\Component\\DependencyInjection\\ContainerBuilder; /** * This compiler pass is responsible for registering enhanced version of `messenger:consume` command. * * @see https://github.com/symfony/symfony/pull/52411 @TODO Remove this workaround when Symfony 7.1 is used */ class OverrideMessengerConsumeCommandCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $symfonyConsumeCommandDefinition = $container-\u0026gt;getDefinition(\u0026#39;console.command.messenger_consume_messages\u0026#39;); $symfonyConsumeCommandDefinition-\u0026gt;setClass(ConsumeMessagesCommand::class); /** @uses ConsumeMessagesCommand::setTransportNames() */ $symfonyConsumeCommandDefinition-\u0026gt;addMethodCall( \u0026#39;setTransportNames\u0026#39;, [$symfonyConsumeCommandDefinition-\u0026gt;getArgument(4)] ); } } This compiler pass does two things:\nOverrides class parameter in console.command.messenger_consume_messages service definition, under which messenger:consume command is registered in DI container. That means, when command in executed and related service is fetched from the container, Symfony will initialise it using extended class. Registers method call on our custom Codito\\App\\Cli\\ConsumeMessagesCommand service, that will inject the list of configured transports, so we can automatically use it when --all option is present. The value that will be passed to the command is taken from original command\u0026rsquo;s definition (and is calculated in yet another compiler pass). To make it work, we of course need to register compiler pass in our kernel:\n\u0026lt;?php declare(strict_types=1); namespace Codito\\App; use Codito\\App\\DependencyInjection\\Compiler\\OverrideMessengerConsumeCommandCompilerPass; use Symfony\\Bundle\\FrameworkBundle\\Kernel\\MicroKernelTrait; use Symfony\\Component\\Config\\Loader\\LoaderInterface; use Symfony\\Component\\DependencyInjection\\ContainerBuilder; use Symfony\\Component\\HttpKernel\\Kernel as SymfonyKernel; class Kernel extends SymfonyKernel { use MicroKernelTrait; /** * Used in {@see MicroKernelTrait::registerContainerConfiguration()} */ protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void { $loader-\u0026gt;load($this-\u0026gt;getConfigDir() . \u0026#39;/services.yml\u0026#39;); $container-\u0026gt;addCompilerPass(new OverrideMessengerConsumeCommandCompilerPass()); } } After that, when running bin/console messenger:consume --all, we automatically provide complete list of transports and instantly consume messages from all of them:\n[OK] Consuming messages from transports \u0026#34;poc1, poc2, poc3\u0026#34;. // The worker will automatically exit once it has received a stop signal via the messenger:stop-workers command. // Quit the worker with CONTROL-C. // Re-run the command with a -vv option to see logs about consumed messages This implementation assumes there are only AMQP transports. Original feature implements additional logic that filters out synchronous transports. If you need that, you need to modify compiler pass and inject narrowed list of transports to the command. Summary Symfony\u0026rsquo;s dependency injection container is incredibly flexible. The fact you work with definitions, not with concrete instances of the services, allows you to dynamically hook into compilation process and do whatever you want — the only limit is your imagination!\nPlease keep in mind that DI container\u0026rsquo;s compilation process has multiple steps, so be careful how you register your compiler passes. Ordering matters, both in terms of steps and priorities within each step.\n","permalink":"https://blog.codito.dev/2024/04/messenger-consume-all-transports-on-symfony-6.4/","summary":"\u003cp\u003eIt\u0026rsquo;s hard to believe, but the \u003ccode\u003e--all\u003c/code\u003e option for the \u003ccode\u003emessenger:consume\u003c/code\u003e command in Symfony was unavailable until version 7.1 — on lower versions list of transports must be passed explicitly to the executed command. But if you need this, it\u0026rsquo;s really easy to port this feature to 6.4!\u003c/p\u003e","title":"Messenger: consume all transports on Symfony 6.4"},{"content":"While working with Git version control system you have quite freedom about how you work with commits and branches. But there are universal practices you should know to keep your repository clean. How rebase can help you with this?\nThis is a Twitter thread turned into a blog post. Introduction Rebase is a Git strategy for synchronising work with base branch (main or any other branch) by moving commits from one place to another. It may look scary, but actually it\u0026rsquo;s pretty straightforward 😎. Let\u0026rsquo;s take a look at this in practice!\nFrom my experience, most of the developers use merge strategy to synchronise work between branches. It is of course valid usage of Git, but it can quickly get really messy:\ndefault merge commit messages are not helpful you end up with spaghetti history 😵‍💫 In my opinion, rebase is much better solution. It preserves linear history, which allows you to browse commits more effectively. The exact same code (as with merge strategy shown before) in both A and B branches could be achieved by rebasing both of them. History would look like this:\nBut how it should be done? Well, as always it depends 🙂. In the CLI you can rebase branch with simple git rebase \u0026lt;target_branch\u0026gt; command, in our example it would be like this:\n$ git checkout A $ git rebase main In PHPStorm you can do it by right-clicking on target branch and choosing \u0026ldquo;Rebase X onto Y\u0026rdquo; option. This will execute the same CLI command under the hood 😉.\nEffectively you would get branch A on top of main branch. Commits\u0026rsquo; content would be the same, but their hashes would be different!\nIt\u0026rsquo;s main difference between merge and rebase: the latter rewrites history (references to old commits\u0026rsquo; hashes are no longer valid). Rebasing stacked branches Things get a little bit more complicated when it comes to rebasing branch B. Since it was based on the branch A before A was rebased, now it contains its own commits (B1-B2) and commits from old branch A (A1-A2). But we need only B, how? --onto to the rescue 😁! First of all, you need SHA of last commit that is not part of your changes. This commit is a base of your work, now you need new base - that\u0026rsquo;s exactly why this is called rebase 😁. You can get SHA from git log, and when you have it, you just need to execute command:\n$ git checkout B $ git rebase --onto A \u0026lt;previous_base_hash\u0026gt; In PHPStorm you can get base commit\u0026rsquo;s SHA by right-clicking it and selecting \u0026ldquo;Copy Revision Number\u0026rdquo; option (or using keyboard shortcut). Then you can open Git → Rebase from the menu, and you\u0026rsquo;ll get rebase modal. Under \u0026ldquo;Modify options\u0026rdquo; you\u0026rsquo;ll find --onto option, and when you select it, you\u0026rsquo;ll see form field for providing SHA of the old base. The last thing you must provide is a target branch (in our example: A).\nAs a side note: you can rebase branch in #Gitlab\u0026rsquo;s UI by using /rebase command in the comment 🙂. Worth to mention that such rebase:\n✅ does not remove approvals (because Gitlab knows that rebased code is exactly the same as before) ❌ does not sign commits with your GPG key Conflicts during rebase There\u0026rsquo;s one thing: when you do git rebase you may encounter code conflicts. But if your branch is conflicted with target branch, you would get exact same conflict during merge, so in fact it does not change anything 🤷‍♂. The only difference is that sometimes during rebase you need to resolve the same conflicts multiple times. This can be really irritating 😩. But fortunately when you rebase often and keep your branch up-to-date, it does not happen much.\nYou can also enable rerere option in Git and try reusing recorded resolution 🙂.\n$ git config --global rerere.enabled true Other advantages There are several other advanced Git rebase options that I won\u0026rsquo;t cover here in details, but here\u0026rsquo;s a glance what you can do during rebase:\n--interactive mode allows you to change, reorder, reword, squash or remove commits during rebase. This means you can clean up your branch from time to time when you\u0026rsquo;re advancing with your work. This makes rebase even more important in terms of readability of Git history. In PHPStorm you can find this option in the Rebase dialog and then you can visually choose what to do with each commit. It\u0026rsquo;s really useful and comfortable! you can preserve merge commits if needed, which means you can rebase even more complex branches, keeping their history intact. Summary So let\u0026rsquo;s sum it up! Git rebase is intended for keeping linear commit history and is an alternative to merge strategy. Rebase rewrites history because rebased commits have different SHAs, so it should be used mostly on development stage.\nI really suggest you to read Git rebase documentation and give rebase a try 😁. I swear it\u0026rsquo;s not hard, it just requires some kind of mental switch 😉.\nWhen you start using it, suddenly it becomes your best friend in Git.\n","permalink":"https://blog.codito.dev/2024/02/git-rebase-why-you-should-use-it/","summary":"\u003cp\u003eWhile working with Git version control system you have quite freedom about how you work with commits and branches. But there are universal practices you should know to keep your repository clean. How \u003cem\u003erebase\u003c/em\u003e can help you with this?\u003c/p\u003e","title":"Git Rebase: why you should use it"},{"content":"Recently I\u0026rsquo;ve created pull request to #PHP-CS-Fixer that aimed to prevent installing stevebauman/unfinalize along with friendsofphp/php-cs-fixer, effectively blocking Fixer to be installed as a dev dependency for that tool. That escalated quickly 😅!\nSome time ago in a galaravelaxy not so far away In #PHP world, discussion about final is everlasting. Every time it pops out somewhere, it engages a lot of people (including me), but it\u0026rsquo;s mostly wasting everyone\u0026rsquo;s time, since both sides rather have well-formed opinion about it and are not open to change it. One of these takes sparked initial conflict of interests:\nPlease don\u0026#39;t compare us, tool for improving codebase, with a tool that degrades code quality by removing its important parts 😬. Fixer would rather add final and readonly, than remove them. We even have auto-review tests to ensure our classes are final 🤷‍♂️. Final is good! 😎\n\u0026mdash; PHP-CS-Fixer (@PHPCSFixer) May 6, 2023 \u0026ldquo;Next week\u0026rdquo; was actually few months later, and unfinalize was released. What happened later is one hell of a PHP story!\nHalf-joke, half-intention Shortly after unfinalize was released I created pull request to #PHP-CS-Fixer and sent a message on our core members\u0026rsquo; Messenger chat:\nTranslation: „Easter egg at the lowest cost‟\nBecause unfinalize was some kind of trolling (but with all the seriousness under the hood, when it comes to getting rid of final keyword and giving \u0026ldquo;freedom\u0026rdquo; to developers), I thought we can continue this way. Of course, making fun was only part of that, because I was also dead serious in the PR\u0026rsquo;s description. Maybe the fact I described it with that particular tone was decisive when it came to general reception by the community? Probably I should keep that pastiche trend and write some kind of Robin Under-the-Hood speech 😉.\nAnyway, it probably wouldn\u0026rsquo;t escalate so quick if I hadn\u0026rsquo;t commented this PR and hadn\u0026rsquo;t got involved in the discussion, that drove Ghostwriter mad 😅. He was so disgusted by my \u0026ldquo;act of sabotage\u0026rdquo;, that he changed implementation of unfinalize to be Fixer-agnostic (which I found totally valid and I think it should be merged anyway). This PR has put the change made into Fixer straight under unfinalize\u0026rsquo;s author nose\u0026hellip;\nThe Drama This is hilarious! They actually did it! 🎉 They blocked Unfinalize inside of PHP CS Fixer itself! 😂\nI consider this a badge of honour friends 👏\nTime to fork 🫡https://t.co/Erkk2Qklvc\n\u0026mdash; Steve Bauman (@heystevebauman) October 3, 2023 Yeah, it clicked 😆. From this point the amount of dislikes on my PR got rising curve, people started to calling me names (examples: 1, 2), and discussion became 🌶️ hot.\nI did my best to not get provoked by any of the comments, to provide arguments and reasoning, but it was mostly tilting at windmills. Some people came there only to call me \u0026ldquo;petty\u0026rdquo;, without even the slightest will to understand what happened. Not like it surprised me in any way, but still 😅.\nIn the end my pull request was reverted and Keradus took the blame on himself, which I really respect, but honestly: it was my initiative, I\u0026rsquo;ve created the pull request and it was me who advocated for it on private chat.\nReasons behind the pull request Let\u0026rsquo;s ignore the joke part of that PR for now, and focus on all the serious reasons behind it. If you have the will and time, you can read original PR\u0026rsquo;s and revert PR\u0026rsquo;s comments, along with several tweets/threads linked here and there. Even though I think reverting this change was the best we could do, I still stand behind my reasoning.\nModifying vendor code is NOT a coding standard #PHP-CS-Fixer is a tool which aims for raising standards in PHP codebases. It was created to let developers apply fixes to their code in an automated way. As unfinalize has showed, it\u0026rsquo;s also technically possible to use Fixer as an engine for applying changes to vendor code. The fact you could do it does not mean you should. Modifying vendor code is not a part of any coding standard and that\u0026rsquo;s why we don\u0026rsquo;t like the fact it is used for that.\nAdditionally, unfinalize is a CLI tool built with Laravel Zero, that bundles its dependencies into executable PHAR file, hiding them from end users. I believe that even though Fixer is free to use and modify under its licence, bundling it without any really visible credit (not counting GitHub\u0026rsquo;s readme and composer.json) is not fair.\nEnabling disallowed inheritance may be harmful Software design is hard. Open source maintenance is hard. Combine these two and you get ticking bomb.\nI truly believe that \u0026ldquo;final by default\u0026rdquo; is a great approach (more here and here). It does not mean developers should put final everywhere blindly, it just means that it\u0026rsquo;s more safe to keep your classes final, and open them only when needed. Developers should design their code with users in their mind, so proper extension points should be provided, which would make extending not required (because Open-Closed principle is not about inheritance).\nHaving that in mind, I think that unfinalize may be harmful for its users. Removing final from vendor code breaks the contract between the libraries\u0026rsquo; authors and the users, who do it. You have to remember, that from authors\u0026rsquo; point of view those classes are still final, and they maintain and develop them with totally different mindset than classes that were explicitly opened for inheritance. If you unfinalize vendor code and extend classes that supposed to be non-extendable, you may encounter breaking changes on any Composer update. Moreover: you can\u0026rsquo;t expect the support from the authors, because you\u0026rsquo;re using their code not in a way they designed it.\nExample: you wanted to have some method, but it wasn\u0026rsquo;t there, so you removed final, extended the class and added that method, but now it\u0026rsquo;s added in the upstream, it has different signature and PHP throws fatal error\u0026hellip; It\u0026rsquo;s of course recoverable, but do you really want to waste your time on adapting to upstream\u0026rsquo;s changes instead of implementing proper solution in the first place? You should get familiar with what vendor code offers, try to customise its behaviour through existing, public contract, and if your use case is not covered, first thing you should do is raising an issue in the project\u0026rsquo;s repository. Applying modifications to vendor code should be last thing to do, and even if you really need it, you can do it using several approaches that were already available before unfinalize was created.\nI can\u0026rsquo;t even imagine the level of potential harmfulness when this idea gets implemented\u0026hellip; 🙄\nUnfinalize is a serious joke Last and least important: I really dislike the way how unfinalize is marketed. Release notes are hilarious and make you consider the whole thing as a joke, but as I already said, seriousness is there. Steve truly believes that using final in OSS code is wrong, and maintainers are servants. All that \u0026ldquo;bring back the freedom\u0026rdquo; blabbering really irritates me, because final does not take freedom from the users, it only keeps control on maintainers\u0026rsquo; side (as it should be, since they\u0026rsquo;re responsible for the package). Actual freedom for users can be provided in many other ways, not only through inheritance.\nThere were many people stating that my pull request was based on personal preferences. Steve even suggested it\u0026rsquo;s a vendetta 😂. All of these people have focused on the wrong thing. PR\u0026rsquo;s description was clear: it was about Fixer being used as an engine for unfinalize. Talking about \u0026ldquo;personal preference\u0026rdquo; is simply laughable.\nFrequently raised counter-arguments Composer patches has been around for years Yes, and I really think it\u0026rsquo;s a great tool. The fact it already existed makes unfinalize redundant and all the drama could be avoided just by\u0026hellip; not implementing it 🤷‍♂️.\nBut, but\u0026hellip; dg/bypass-finals does the same Yes, and no.\nThere is a huge difference between bypassing `final` in your own classes for testing purposes (dg/bypass-finals), and trimming `final` from vendor code to hijack it (stevebauman/unfinalize). Also a huge difference in the authors\u0026#39; approach (\u0026quot;marketing\u0026quot;).\n\u0026mdash; Greg Korba 🛠️🛹 Codito (@_Codito_) September 30, 2023 dg/bypass-finals aims for making devs\u0026rsquo; lives easier when it comes to testing. As we all know, when class is final, PHPUnit\u0026rsquo;s mocking engine doesn\u0026rsquo;t work, that\u0026rsquo;s why if you want to mark class as final, but at the same time you have the need to mock it in your tests, with simple DG\\BypassFinals::enable(); in your tests\u0026rsquo; bootstrap you can make these classes mockable again.\nOf course, nothing prevents you from adding it to app\u0026rsquo;s main bootstrap and bypass all the finals in the production code. Just like nobody prevents you from putting the nail into electrical outlet or drink gasoline\u0026hellip;\nThe actual impact of my pull request Now let\u0026rsquo;s go back to the fun part. It was pretty interesting to observe people\u0026rsquo;s reactions, considering how much they did not think about an actual impact of introducing that conflict entry. Many comments suggested that I \u0026ldquo;killed the project\u0026rdquo;, but here are some facts:\nconflict introduced by my PR effectively started to work in Fixer v3.32.0, while for unfinalize it wasn\u0026rsquo;t needed to bump to that version. Project could still use older versions for building PHAR and everything would work without any change on their side (because Composer would resolve dependencies automatically and just wouldn\u0026rsquo;t install newer, \u0026ldquo;conflicting\u0026rdquo; version). Since unfinalize is provided as pre-built CLI tool that can be downloaded directly from release pages, it\u0026rsquo;s not even required to install it through Composer, so conflict entry does not matter from end users\u0026rsquo; perspective. Large part of unfinalize\u0026rsquo;s potential users are from Laravel community, that does not use Fixer, but Pint (well, effectively they actually use Fixer, but they don\u0026rsquo;t know about it, they waited for Pint their whole lives 😂). Since Pint also bundles Fixer in executable file, there\u0026rsquo;s no conflict with unfinalize. The conflict entry itself was naive approach that could be easily worked around in several ways. I\u0026rsquo;ll put it straight: the impact was almost non-existent 😆. The only scenario where this conflict would seriously impact users: Fixer installed as project\u0026rsquo;s direct dependency with the need to install unfinalize. I believe it would be marginal subset of projects, which means the whole drama and the witch hunt, that part of the PHP community carried out, was totally unnecessary.\nLessons learned The most important lesson from this story is that open source is not a playground or circus arena. There are real users, with real projects, that expect upstream packages to be viable, stable and trustworthy. Even though the actual impact of my pull request was really negligible, it could affect some developers and waste their time on trying to work around the conflict, which in fact is not their problem. After all the discussions and based on my own reflections, I would like to say sorry for the potential trouble my PR could make. It does not mean I\u0026rsquo;ve changed my mind about unfinalize - no, I stand firm behind all I wrote and said about the tool itself, I just only think it did not make much sense to create that easter egg and to fight joke with a joke, using widely used, mature tool like a PHP-CS-Fixer as a platform.\nAnother lesson is much more mundane: Larafolks have no chill 😂. It\u0026rsquo;s not the first situation where they show double standards, and at this point it does not even surprise me. #GoodVibesOnly looks great, but in practice some of these people are just toxic.\nThe last lesson is simple: don\u0026rsquo;t waste your time on trying to convince someone that doesn\u0026rsquo;t want to listen. I\u0026rsquo;m always open for discussion, but arguing with close-minded people is only throwing peas against the wall.\n","permalink":"https://blog.codito.dev/2023/10/final-word-about-unfinalize/","summary":"\u003cp\u003eRecently I\u0026rsquo;ve created \u003ca href=\"https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7343\"  target=\"_blank\"\u003epull request\u003c/a\u003e to \u003ca href=\"/tags/php-cs-fixer\" target=\"_blank\"\u003e#PHP-CS-Fixer\u003c/a\u003e that aimed to prevent installing \u003ccode\u003estevebauman/unfinalize\u003c/code\u003e along with \u003ccode\u003efriendsofphp/php-cs-fixer\u003c/code\u003e, effectively blocking Fixer to be installed as a dev dependency for that tool. That escalated quickly 😅!\u003c/p\u003e","title":"Final word about `unfinalize`"},{"content":"A Dockerfile is to a project what sheet music is to a song - it ensures that the same operations are always performed, in the same order, with the same result.\nMake sure you know basic terminology before reading further. What is a Dockerfile Imagine a README.md file in a project or a page on Confluence, where all the steps required for setting up an application\u0026rsquo;s runtime are included - Dockerfile is just such an instruction, only automated 😁. This is a kind of simplification, but the fact is that the Dockerfile provides us with a consistent runtime environment, which thanks to this file will be the same for everyone working on the project.\nSo what can such a file define? For example:\noperating system: it is the base for further operations and the foundation of the environment. installation of system packages: using the package manager built into the operating system, we can install everything we need for the proper functioning of the application. environment for the programming language: everything needed for our code to run properly. Typically, programming languages provide official images that have a ready-made environment and tools to extend it (e.g. PHP images contain pecl, which is a library of extensions that can be added on demand). Using them, we can easily extend the default environment with additional extensions and, if necessary, provide custom configurations. listened ports: this is a kind of contract with the outside world. The container launched from the built image will be able to communicate through these ports. For example, the MySQL database image exposes port 3306, and you can connect to it later. In addition to the above-mentioned fundamental things, the Dockerfile can define many other operations, but it\u0026rsquo;s impossible to list them all because every application is different and has different requirements. Need to create a default directory structure for later use? Need to include a set of files in the image that the application will use later? No problem 🙂. You can do basically any operations - the only limitation is the Dockerfile syntax and the operating system used as the basis for our image.\nDockerfile syntax Full syntax documentation is available on the official website and I strongly encourage you to read it. In this article, however, we will focus on the basic instructions offered by Dockerfile, which will allow us to prepare for more advanced operations, which will be discussed in series\u0026rsquo; subsequent posts. The others will be discussed briefly or even omitted and, if necessary, will appear later in this series.\nBasically, a Dockerfile is a set of instructions, arranged in the order in which they are executed. The way we arrange them determines how such an image will be built, as well as what its final content will be. Optimizing these instructions is a topic for a separate entry, at this point we will only focus on what each of them do.\nFROM The very basic instruction is FROM, and it starts every build definition. We can assign an alias to this instruction, thus defining the so-called build target, and this is done by adding as \u0026lt;alias\u0026gt;. There can be many such targets in the Dockerfile file. When executing docker build, we can indicate such a target by passing the --target \u0026lt;name\u0026gt; flag, and if we do not specify it explicitly, the last target defined in Dockerfile will be built.\nThe FROM instruction can take several values as an argument:\nFROM scratch: as you can easily guess, we start with a \u0026ldquo;blank page\u0026rdquo;, our image contains nothing. FROM php:8.2: the basis for further instructions will be the PHP image in version 8.2. After the colon, we specify the expected version, and the list of tags for a given image can be found in the registry we use, in this case in Docker Hub. If the version is not specified explicitly, then the latest tag indicating the latest version is used by default (or at least that\u0026rsquo;s the convention, because the existence of the latest tag must be guaranteed by the person/team responsible for the build and publication). FROM other-target: in this case we indicate that the base for further operations is another target defined in Dockerfile. Such constructions are automatically resolved by the Docker engine, so when building one of the targets, we do not need to build its dependencies (other-target) beforehand - it will be built automatically, and then it will be used as the base for the next target. In case we refer to a target that doesn\u0026rsquo;t exist, the build will fail, and we\u0026rsquo;ll get an error message. In summary, the entry FROM php:8.2-cli-alpine as php-base defines the php-base target, which will be built on top of the official php image in version 8.2-cli-alpine, containing the PHP runtime for CLI in the Alpine Linux operating system.\nRUN The RUN instruction is probably the most commonly used statement in the Dockerfile, although this of course depends on its specifics. In any case, its purpose is to execute the indicated command within the environment available during the image build process. Therefore, if we use the previously mentioned php:8.2-cli-alpine as a base, then our environment is Alpine Linux and each RUN can run commands available within this system or those that we install ourselves using the apk package manager.\nIt\u0026rsquo;s useful to think of RUN as a CLI command, whether on your own machine or a remote one via SSH. We simply have a specific environment (usually operating systems such as Debian or the aforementioned Alpine Linux) and within it, we execute all kinds of commands as if we were preparing a local environment or server to ensure the correct runtime for our application.\nThus, RUN apk add git (Alpine) or RUN apt-get install git (Debian) will install the git package for us, which we can later use in the further part of the build process or in the target container that was created using the built image.\nCOPY The COPY statement is the link between the local file system (the one from which build is executed) and the image being built. Thanks to it, we can add to the built image any files that are needed for later operation. In the case of applications, the entire application folder is usually copied, but it is worth noting right away that we can limit this context by using the .dockerignore file, in which we define which paths are to be omitted during copying (so we can skip folders such as vendor or node_modules). The notation COPY . . tells that all files (except ignored ones) will be copied into the image, into its working directory. Of course, it is possible to specify more specific paths, such as COPY ./bin/example /usr/bin/example, we have full flexibility in this matter.\nIt is worth noting that the COPY instruction can use not only the local file system as a source, but also already built images or other build targets. For this purpose, use the entry COPY --from=..., specifying the source from which the files are to be copied.\nThis makes installing Composer in PHP images trivial, especially when using binary-only images, which I am originator 😎. Another very helpful flag is --chown, thanks to which we can define their owner already at the moment of copying files. Very often our application must be fully available to the user who runs the web server, so it is common practice to execute COPY --chown=www-data:www-data. Thanks to this flag, we don\u0026rsquo;t have to perform two operations: COPY and RUN chmod 🙂.\nADD The ADD statement basically does the same as COPY, but it has additional functionality - it can copy files from remote locations and handle local TAR files (source).\nIn the case of copying files from remote locations, it is possible to verify the checksum of the file by using the --checksum=\u0026lt;checksum\u0026gt; option.\nENV The ENV instruction is used to define environment variables that are available during the next steps of building the image, and finally also in the container launched from such image. Therefore, be careful when defining these types of environment variables, as they may affect the behavior of the tools contained in the image.\nARG ARG is similar to ENV, but differs from it in that the life cycle of ARG is limited to image building process. ARG can take default values which can be overridden (or simply supplied) later using --build-arg \u0026lt;name\u0026gt;=\u0026lt;value\u0026gt; option.\nVariables passed in this way can affect the build process and the resulting image. For example, by doing ARG CLI_VERBOSITY='' we could declare the default verbosity of the messages returned by CLI comments, and then build the image using docker build --build-arg CLI_VERBOSITY=-vvv. Then all that\u0026rsquo;s left is to use this variable in commands, for example, RUN bin/console cache:clear $CLI_VERBOSITY. This may not be a particularly real life example, but it shows the principle of operation 😉.\nARG should not be used to pass sensitive data (passwords, tokens) - that\u0026rsquo;s what secrets are for, but it\u0026rsquo;s a topic for another post. The reason for this is the values of these arguments are permanently stored in the image and can be accessed using the docker history command. WORKDIR This simple statement defines the path in which any RUN, COPY, ADD, CMD and ENTRYPOINT statements that are defined after WORKDIR will be executed. In practice, this means that if we do WORKDIR /app and then RUN bin/console, we assume that there is an executable in the path /app/bin/console - if it doesn\u0026rsquo;t, we will of course see an error, and the build process will be aborted.\nCMD CMD defines the default run command for the container. For example, for php:8.2.3-fpm it is CMD [\u0026quot;php-fpm\u0026quot;], which means that PHP-FPM will be started when the container starts.\nThere can only be one CMD statement in the image, so for each build target the last one is used. Usually when building images for our applications we don\u0026rsquo;t need to define CMD, because when using base images like PHP we already have it defined. However, nothing stands in the way of adapting the manual to your needs.\nENTRYPOINT The ENTRYPOINT topic is quite complicated, and you could write an entire dedicated article about it and CMD, but in short this statement defines the starting point for a container. To simplify: we make the container to be considered as a command (executable script), so during its launch we can pass additional flags/arguments that will be passed to the entrypoint. For example, docker run \u0026lt;image\u0026gt; -d will pass -d to the entrypoint.\nUSER As the name suggests, USER is used to define a user (and optionally a group) that will be used to perform all operations during image building, as well as to run ENTRYPOINT and CMD.\nEXPOSE If the container is to provide an interface to communicate with the services it contains, we should use the EXPOSE statement. It defines the ports that are listened to inside the container (TCP and UDP protocols are supported, the former is the default).\nFor example, in images containing a web server, you can find EXPOSE 80/tcp, which means that the container is listening on port 80 in the TCP protocol. These ports can then be accessed from the outside by doing docker run -p 80:80/tcp \u0026lt;image\u0026gt;.\nVOLUME If we need a filesystem interface between the container and the system the container is running on, we can use the VOLUME statement. It defines the so-called mount point and causes the files contained in such a volume to be synchronized to the operating system. More on volumes can be found in this article\nHEALTHCHECK It\u0026rsquo;s one thing to run a container, but to monitor that it\u0026rsquo;s running properly all the time is quite different. HEALTHCHECK can help us with this, it defines the way how the container can be checked if it\u0026rsquo;s operational (and if not, it can be automatically restarted).\nDockerfile example FROM php:8.2-cli-alpine WORKDIR /app # See: https://twitter.com/_Codito_/status/1587052303869267968 COPY --from=composer/composer:2-bin /composer /usr/bin/composer # Install some PHP extensions, then clean up things a bit. # It\u0026#39;s important to do it in the same `RUN`, so there are no leftovers in the image. RUN apk add --no-cache icu \\ \u0026amp;\u0026amp; apk add --no-cache --update \\ --virtual .build-deps \\ $PHPIZE_DEPS \\ icu-dev \\ linux-headers \\ \u0026amp;\u0026amp; pecl install xdebug-3.2.0 \\ \u0026amp;\u0026amp; docker-php-ext-install intl \\ \u0026amp;\u0026amp; docker-php-ext-enable xdebug \\ \u0026amp;\u0026amp; apk del -f .build-deps # This will copy all local files (from where `Dockerfile` is used) \\ # to `/app` (which was set as WORKDIR above). COPY . . # Prepare app\u0026#39;s runtime by installing Composer dependencies. RUN composer install --no-dev --no-scripts Validating Dockerfile A good IDE should help us write a Dockerfile in terms of allowed statements and their syntax. However, there are tools like Hadolint that also allow you to keep an eye on good practices when creating a Dockerfile. Information on how to use hadolint can be found on the project\u0026rsquo;s website 🙂.\nHadolint is the project where I had my first (and so far only) contact with Haskell programming language - I added support to the tool for verifying COPY statements that use external images. It started with a discussions because the rule DL3022 was giving me trouble 😅, and ended with my Merge Request. Summary All of the above information can be overwhelming at first, especially if you don\u0026rsquo;t have experience with Docker. The truth is, however, that you do not need to know all of this to start your adventure with containerisation 🙂. You may never need some instructions available in the Dockerfile (e.g. I don\u0026rsquo;t remember using VOLUME personally), some may appear later on as the project and its CI/CD processes grow, which will require more advanced implementations.\nTL:DR; Dockerfile is a set of instructions that define how the runtime in the container should look like At the beginning you can focus on FROM, RUN and COPY It\u0026rsquo;s always worth using the official documentation There are tools to help you write good Dockerfiles, such as hadolint In the next post in this series, we\u0026rsquo;ll look at the second file that is extremely important from Docker\u0026rsquo;s perspective - the Compose file (compose.yaml), which defines the stack in which the application is run. See ya!\n","permalink":"https://blog.codito.dev/2023/02/dockerfile-runtime-definition/","summary":"\u003cp\u003eA \u003ccode\u003eDockerfile\u003c/code\u003e is to a project what sheet music is to a song - it ensures that the same operations are always performed, in the same order, with the same result.\u003c/p\u003e","title":"Dockerfile: runtime definition"},{"content":"Docker has been in the developers\u0026rsquo; world for years, but for many people it is still something remote and enigmatic. In a series of posts, I would like to introduce both theory and practice - you will find out how Docker can help you in your daily work, how to prepare an environment based on Docker and how to use this environment.\nWhy you should use Docker In short: to provide a uniform, automated runtime environment for our applications that anyone who starts working on the project can run. Only and so much 😁.\nLet\u0026rsquo;s go back a few years, when I worked in a team of several developers. The team had multiple internal applications in its portfolio, their stable and trouble-free operation was not the highest priority - if something did not work, users informed us about it, then we fixed it. Each of the developers worked on his own computer, installed all the tools necessary for the application to work on his own. This led to a situation where one developer had PHP 7.0, another 7.1, one had MySQL 5.6, another MySQL 5.7 🤷‍♂️. Someone working on functionalities installed a PHP extension, other developers did not have it. As if that wasn\u0026rsquo;t enough, sometimes not even everyone was aware of what versions were running in the production instance and whether the runtime had all the required extensions and libraries. So you could write code that worked in development but didn\u0026rsquo;t work in production. \u0026ldquo;It works on my workstation\u0026hellip;\u0026rdquo;\nIn retrospect, I am shocked that it worked at all, because differences in the language and tool versions can easily lead to serious problems. And sometimes they did, generating a lot of work both in terms of analysis and repair.\nDocker eliminates all these problems. Thanks to Docker, we can easily compose the stack so that it contains everything needed to run the application, and that everyone working on the project has exactly the same environment. Is there a need to add a low-level system library or a PHP extension? No problem, one person adds such steps in the build definition, the other people synchronise the repository and have the same runtime.\nThe unquestionable advantage of working with Docker is that on the first day of work, when you get a new, company computer, you can install just two things and start working on the project right away. You just install Docker and Git, clone the repository, and run the Docker stack - done! Of course, we are talking about a simplified scenario here, because there are more complex applications that will require a bit more to run 😉. However, this does not change the fact that Docker allows us to unify runtime environments, moreover: it allows us to prepare a standardised runtime tailored for development, as well as a production runtime, optimised in terms of security and performance.\nGlossary of terms Before we move on to discussing the actual topics, we need to familiarise ourselves with a few concepts that we will be using. This will make it easier for us to understand the individual elements of the process of building and running containers.\nDocker Docker is a runtime engine for containerised applications. It is installed as a system package that provides CLI tools, GUI interface (Docker Desktop), and also installs and configures processes running in the background of our operating system. Think of Docker as a system framework for running applications.\nIn this series, I will not describe the Docker installation, because I think it is best to use the official documentation. Docker Compose Docker Compose is an extension of the basic tool and is used to define and run complete sets of services that make up our system (the stack). For example, it can be our application, database, queuing system and others. Essentially, it\u0026rsquo;s about us being able to simulate a production environment and create a fully operational system for development. I don\u0026rsquo;t say that Compose can\u0026rsquo;t be used outside of your local machine, but that\u0026rsquo;s another story 😉.\nCurrently, Docker Compose is an extension built into the docker tool and is accessed through the docker compose command. Previously, there was a standalone tool docker-compose which is now discontinued. Dockerfiles The Dockerfile is the runtime definition for our application. It contains step-by-step instructions on what needs to be done to ensure a functional runtime. On its basis, the process of building images takes place.\ncompose.yml A file in YAML format that defines the application stack, i.e. all services, volumes, networks and dependencies between them. It contains information about what services are called, what images they are launched from or how they are built, how they communicate with each other and how they are seen from the outside (from the perspective of the local computer and operating system).\nFormerly, such a definition was stored in the docker-compose.yml file. Image The image is a product of the Dockerfile execution process: a build. Such an image is created as a result of invoking the docker build command or running the Compose stack (docker compose up -d), in which services define build context.\nThe image usually consists of the operating system, the required libraries, the runtime for the required programming language, extensions for it, and of course the code of the application to be launched.\nRegistry The Docker Image Registry is a place where built images are uploaded and from which they can be downloaded by anyone who needs them. Of course, there is an official and freely available Docker Hub, but in fact images can be stored in many other places: Gitlab has a built-in registry for each project, there are also dedicated apps like Harbor.\nThe registry may require authentication to upload/download images. Container A container is an instance of an image. From a programmer\u0026rsquo;s perspective, we can think of it as a class (image) → object (container) relationship. The container has a root process (pid 0), which is the basis of its operation.\nVolume Volumes are used to map files from the local file system to the running container. Thanks to this operation, we can overwrite a fragment of the file tree inside the container (e.g. files that were created as a result of build and are part of the image from which the container was launched).\nThanks to this, we can map our local project to the place from which the application is launched inside the container for local development and verify the changes made in real time.\nVolumes are also used for persistency of data created and/or modified inside running container, e.g. MySQL\u0026rsquo;s schema and data. With volumes, we retain this type of data even when we stop a running stack - when we resume it, it will be in exactly the same state.\nNetwork Networking in Docker is very important because it allows separation of services. We can run different services side by side, which will not know about each other and will not have physical access to each other.\nCompose by default provides a network where all the services within the stack are. However, nothing stands in the way of opening communication between services in separate stacks.\nSummary You just learned the basics of Docker 😎! If the theory does not appeal to you, I invite you to the next entries in the series - in the next one we will discuss Dockerfile based on real examples.\n","permalink":"https://blog.codito.dev/2023/01/docker-for-local-development/","summary":"\u003cp\u003eDocker has been in the developers\u0026rsquo; world for years, but for many people it is still something remote and enigmatic. In a series of posts, I would like to introduce both theory and practice - you will find out how Docker can help you in your daily work, how to prepare an environment based on Docker and how to use this environment.\u003c/p\u003e","title":"Docker for local development"},{"content":"In everyday work with code, it is important not to waste time on operational matters. However, how not to get lost in your and other people\u0026rsquo;s reports and contributions? How do you know what to look at? How not to miss anything? Both GitHub and Gitlab have their own activity monitoring mechanisms - let\u0026rsquo;s take a look at them!\nContext First, let\u0026rsquo;s outline the perspective from which I will present both of these systems.\nI have been dealing with Gitlab on a daily basis for many years, because the companies I have worked with and currently work with, chose this platform for code storage, code review and automation of CI/CD processes (an important argument for such a decision is the fact that Gitlab can be run as an On Premise instance in your own infrastructure, without releasing the code outside the company network).\nGitHub, in turn, is a kind of standard in the Open Source world, so it can be safely said that using it is inevitable in a sense - whether in the form of issue submissions in used projects or in the form of contributions. I also keep my own OSS projects on GitHub, mainly to learn about its capabilities from the maintainer side.\nThese circumstances mean that I use Gitlab much more than GitHub, but at the same time it does not mean that I would clearly indicate one of these platforms as better - they are simply different, although they have many similarities.\nStay informed What I would like to focus on in this article are the techniques of observing and reacting to activity in the system, mainly in the context of merge/pull requests (but not only). We can distinguish 2 basic needs:\naccess to the list of all tasks or merge/pull requests that we are watching (whether as an author, reviewer or just following what\u0026rsquo;s happening) access to activities within these (other people\u0026rsquo;s comments, code changes, state changes) We need the list to be able to find the task or merge request we are interested in at any time, deciding what we are looking for at the moment. We may need to add some comment that sheds new light on the matter, or want to share the link with others. At such a time, we want to be able to search for things we are interested in using various criteria.\nIn turn, wanting to be up-to-date with what is happening in the notifications or MRs we observe, we would like to have access to the chronological stream of events in the areas we observe.\nWhile the first need is fulfilled by both platforms, the second one was addressed only by GitHub with its notification system. Personally, I really like this view, and it makes it much easier for me to keep track of what\u0026rsquo;s going on. In the case of Gitlab, I have to use the meager equivalent of email notifications\u0026hellip;\nWorking with GitHub Watched areas GitHub gives us many predefined views to track the areas we are watching, two of which are available in the main top menu:\nPull Requests created by us Issues created by us These two views offer both the ability to navigate to other predefined views and to change the default search criteria (by default, open PRs/issues in all organisations are displayed).\nMy Pull Requests Let\u0026rsquo;s look at Pull Requests\u0026rsquo; list and what it offers:\nContext switcher: opened and closed PRs Predefined views of PRs: created by me (domyślny); assigned to me; mentioning me; with review request Advanced search offering a lot of filters Helpful filters and view settings Labels added to PR Result of last Actions Checklist progress within PR (state of work) Related issues Comments\u0026rsquo; count My Issues Very similar functionalities and a set of presented information can be found in the issues view:\nAll these elements are helpful in the context of searching for the content we are interested in, and it is hard to find a scenario where finding specific things would be impossible. There are also helpful hints displayed below the list, which contain random information about the available filters and how to use them.\nSearch engine Nevertheless, a spoonful of tar went into this barrel of honey. The GitHub search engine has one, but a big drawback: does not offer suggestions. The result is that we have to learn filters by heart, or use documentation.\nFortunately, there are unofficial solutions to this problem, namely user scripts like this. They make it much easier to use the search engine, suggesting which filters can be used and what values these filters take (in the case of dictionary values).\nIt is possible to use exclusion filters, for this purpose use the syntax with the - prefix, e.g. -author:Wirone. Notification View As I mentioned earlier, GitHub offers a notification view that I really like. Let\u0026rsquo;s take a look at it:\nContexts: Inbox: there we can find all new notifications. I also use it to keep a list of things I want to keep up with Saved: items saved for later (see point 7) Done: items marked as done Quick filters to limit the displayed notifications by type of activity Assigned to me I participate in any way (following, commenting, etc.) I\u0026rsquo;ve been mentioned A team I\u0026rsquo;m a member of has been mentioned I was asked for a code review List of repositories from which notifications come, thanks to this we can quickly limit the results to single repos Filter to restrict notifications by criteria. Interestingly, in this view you can use autocomplete 🤷 ♂️ The type of item to which the notification relates. Thanks to this, you can clearly see whether the notification concerns an issue, Pull Request, discussion or a new version released in the watched project. More: You can even see whether a ticket or PR is open or closed. Subscription method: shows us whether we are the authors or, for example, commented in the past Contextual controls: after moving the mouse cursor over the selected element, action buttons are shown: mark as done unsubscribe save for later Last activity time As you can see, we have a lot of valuable information here and many ways to search for what we need. I use this view on a regular basis, reacting to new notifications and taking appropriate actions. When I can\u0026rsquo;t do something right away, I leave it in my Inbox and come back to it when I have free time. Thanks to this, the notification view serves me at the same time as To Do list.\nGitHub in practice I\u0026rsquo;ve started using GitHub a bit more recently as I\u0026rsquo;ve contributed to various Open Source projects. It wasn\u0026rsquo;t a large-scale use of course - there are plenty of people who maintain/support many more repositories and have to track a huge amount of activity, so my perspective may not be entirely objective and authoritative. However, it can still be useful 😉\nIn the case of GitHub, I treat e-mail notifications only as an additional channel that in some situations can speed up my reaction - after all, I have my phone with me almost all the time, so there is a much better chance that I will notice this type of notification faster than in the GitHub UI (especially in the afternoon and evening). In this case, when I take action after receiving an e-mail, I immediately mark such an item as completed in the notification view.\nI aim for \u0026ldquo;0 Inbox\u0026rdquo;, which is an empty inbox in the notification view. Of course, this is not a rigid rule, because sometimes I consciously leave there elements with which I have already become acquainted (and even reacted) in order to be able to easily come back to them later. On the other hand, if I would like to be able to find elements that I consider important, but which have already been completed (e.g. merged PR), I mark them as saved.\nI believe that the key to good organisation in the context of activity tracking is dispatching as soon as possible - you can\u0026rsquo;t afford too many notifications, because then it becomes more and more difficult to work with them, which can lead to \u0026ldquo;callousness\u0026rdquo; and complete cessation of using this view. If it comes to a situation where there are more notifications than we can handle, you should consider changing the notification settings in individual projects - perhaps some are not so important, and they don\u0026rsquo;t need to flood your inbox.\nOf course, you can also create some internal arrangements in organisations/projects and divide the work between many people. Of course, there is also the possibility of automation using bots, but that\u0026rsquo;s a completely different story 😉\nWorking with Gitlab Watched areas In Gitlab, the top menu provides us three buttons that direct us to dedicated views:\nTasks to which we are assigned Merge Requests (drop-down menu): assigned to us in which we were asked for a code review To-Do list A trained eye may have noticed that the links to the task views and Merge Requests were missing above. This is not a coincidence - unfortunately, Gitlab generates contextual links that contain the login of the currently logged in user. Therefore, there are no universal URLs (as in GitHub) where we can find our own tasks/MRs. I reported this need almost a year ago, so if you agree that it\u0026rsquo;s something useful - leave a thumbs up 😉 In the following, I will present the view of tasks and Merge Requests in contexts other than those linked in the top menu. Firstly, because I can\u0026rsquo;t use screenshots from company Gitlab instances here, and secondly, because in publicly available Gitlab, I have blanks in these views because I\u0026rsquo;m not assigned anywhere 😉. However, the information displayed on these views will be exactly the same, the only difference being the selected filter.\nMy issues Let\u0026rsquo;s take a look at the issue list in Gitlab:\nContexts: open, closed, all Various types of minor controls: RSS feed, calendar subscription, import/export, create a new issue Search engine (filters) Choose how to sort the results Last modification date Reaction and comment counters Labels Milestone to which the task is assigned The view is neat, but it gets heavy with lots of labels (just take a look at Gitlab\u0026rsquo;s issue tracker).\nMy merge requests The view of merge requests is very similar, it differs in details:\nThere are slightly different contexts, slightly fewer controls, and in each item on the list we also have:\nMR\u0026rsquo;s status (visible only in the context of All) Status of the last pipeline Assignees and reviewers If Approval Rules is used, we also have information about whether the MR has been approved Review Requests The review requests view is basically no different from other views with the Merge Requests list, it is simply that the search criteria is set so that we can see where our code review (and possibly approval) is required. This view has one major flaw - it\u0026rsquo;s absolutely useless.\nThe reason is simple: in Gitlab there is no concept of \u0026ldquo;expected changes\u0026rdquo;, i.e. when we finish the code review, and we do not approve, but left comments, we would like such MR to be removed from our list until changes are made and there\u0026rsquo;s review request again. Currently, when we are reviewers, we see this MR all the time in the list, which makes it very difficult to control where our action is required if there are many MRs.\nSome time ago Gitlab was working on the concept of \u0026ldquo;Merge Requests requiring my attention\u0026rdquo; which looked promising for tracking activity in MRs. Unfortunately, the idea was abandoned and work focused on the concept of \u0026ldquo;review rounds\u0026rdquo;. I tried to actively participate in both topics, presenting the user\u0026rsquo;s perspective (here, here, here, here, here and various other places).\nUnfortunately, in this context, GitHub has better support for tracking places that require our action. I hope that three-state reviews will be introduced in Gitlab (accepted, expected changes, rejected), which will enable more convenient work with the tool.\nTo-Do list The To-Do view groups the actions we should take. The data presented in it is very readable:\nContexts: to do, done Filters to narrow down the criteria Sorting type I must admit that I rarely visit the To-Do view. This is because the information in it is not helpful to me, or the other views just provide what I need.\nItems appear in the list at different times: when we are added as reviewers, when someone @mentions us in a comment, etc. However, it seems to happen in a chaotic way - for example, when I add myself as a reviewer in MR, the item appears in the To-Do list, but in the context of Done 🙄. And even when elements appear in the correct To Do context, they disappear from it automatically after performing an action (e.g. commenting on MR, which does not mean that the review is finished).\nAs you can see in the picture above, however, I found some use for this view - I add there elements that I observe and to which I want to have quick access. To do this, I click \u0026ldquo;Add a to do\u0026rdquo; in the right-side menu in the issue/MR view.\nI\u0026rsquo;ve been using Gitlab for many years, and yet to this day I haven\u0026rsquo;t been able to squeeze more out of this view. Maybe I should give it some attention, and maybe it can help me more? I would love to hear your opinions and advice 🙂\nSearch engine The search engine in Gitlab is pleasant to use because it suggests possible filters, operators (types of comparisons) and values. Visually it looks good and is helpful when using search, but unfortunately there are a few things that I can complain about:\nfilters are few, especially when compared to the multitude of filters supported by GitHub if we choose the wrong value from the list, deleting it is inconvenient: using backspace, we can delete it (but not in one go, only as edited text), while the ❌ button deletes the value along with the operator and filter It is not possible to create a universal filter based on the currently logged-in user, which makes it impossible to easily share links to views with pre-filled search criteria. I have submitted such a request, but it is unlikely to be implemented in the near future\u0026hellip; Notifications\u0026hellip; by email Gitlab does not have a built-in notification view, so unfortunately tracking activity in watched tasks and merge requests is very difficult. The previously described views don\u0026rsquo;t make it any easier. Although sorting by last modified date may help a little in finding things to review, nevertheless you have to rely heavily on your memory or routinely check everything every day to not have to wonder what has already been checked and what hasn\u0026rsquo;t.\nPersonally, I have developed a process based on e-mail notifications. It is very similar to how I work with the notification view in GitHub, only I do it in the email client. And this is the basic disadvantage of this solution - it requires jumps between tools. My process looks like this:\nall notifications from Gitlab are enabled and I have email rule, so all notifications go to a dedicated folder unread mail = notification to get familiar with or to come back to it later mail read = activity verified, but still waiting for action on the other side (e.g. corrections after my review) e-mail deleted = notification does not require my reaction The goal is to keep an empty \u0026ldquo;Gitlab\u0026rdquo; folder in the mailbox, so emails are deleted on a regular basis - either when they do not require my reaction, or when I perform such a reaction. This also fits well with a smooth code review process, as fast processing of notifications helps with quick code integration. The sooner I take the action resulting from the notification, the sooner I will unblock the person \u0026ldquo;on the other side of the process\u0026rdquo; 🙂.\nI only leave e-mails in the inbox that I want to come back to (e.g. when I see that MR has received new commits and another code review iteration needs to be performed). If something is important and I want to come back to it sooner, I leave a message marked as unread. This approach, combined with the views of MRs (where I am an author or reviewer) allows me to stay up-to-date with what I am responsible for.\nHowever, this does not mean that the process is convenient and optimal\u0026hellip; It is rather a workaround, not a solution 🤷‍.\nGitlab in practice As you can see above, my way of working with Gitlab is a mix of using predefined views with the so-called email-driven approach. This may not be the optimal use of Gitlab\u0026rsquo;s potential, but I have developed such a system over the years of working with this system, and it seems to me that it works quite well. Which, of course, does not mean that there is nothing in Gitlab that could be improved.\nWhat really hinders the work from the reviewer\u0026rsquo;s perspective is the lack of the aforementioned three-state reviews. It even got to the point that I created a user script that adds an item to the top menu - it directs me to the list of MRs in which I am a reviewer, but which I have not yet approved. Since we follow the rule that every push clears approval, I know that after accepting MR, it will only return to this list if there are new changes to it. This slightly improves the comfort of work, but still does not solve the scenario when, as a reviewer, I expect changes (I finished the code review, but I did not accept the MR); in such a situation, I have to track e-mails or tediously check the \u0026ldquo;Review requests for you\u0026rdquo; view and verify the dates of the last modification.\nI also tried to provide some menu changes to make it a little friendlier, but I hit the moment when work on the aforementioned \u0026ldquo;requiring attention\u0026rdquo; and my changes were not accepted. Unfortunately, the not-so-useful menu works to this day, although it\u0026rsquo;s been almost a year\u0026hellip; In the case of Gitlab, I also aim for \u0026ldquo;0 inbox\u0026rdquo;, but more in the context of unread messages than messages in the inbox in general (I try not to have any messages that I should react to). I would also like to aim for \u0026ldquo;0 To-Do\u0026rdquo;, but due to the lack of a meaningful notification view, I use this view as a \u0026ldquo;notepad\u0026rdquo;, so I constantly see a non-zero counter in the menu 🤷‍♂️.\nGitHub vs Gitlab: pros and cons Overall, I have to admit that I prefer GitHub when it comes to tracking activity, which is a bit sad considering that I spend more time on Gitlab 😉.\nThe notification view makes it much easier for me to stay up to date with new activities and return to the topics I follow. And even if I think that Gitlab in the top menu has links that interest me more (they direct to views where my action is needed, while in GitHub they direct to views with my activities), this advantage quickly turns out to be of little importance when it turns out that in these views it is difficult to find what I need. The more so that in GitHub it is enough to change the context with one click and instantly go to, for example, the Pull Requests view, where my review is really needed.\nShare your experience and opinion! I am very curious how others see GitHub and Gitlab in the context of activity tracking and how they technically model their workflow with these tools. I will therefore be grateful for any opinion, suggestion or question opening the discussion. I encourage you to leave comments or interact on Twitter 🙂\n","permalink":"https://blog.codito.dev/2022/12/organising-work-in-github-and-gitlab/","summary":"\u003cp\u003eIn everyday work with code, it is important not to waste time on operational matters. However, how not to get lost in your and other people\u0026rsquo;s reports and contributions? How do you know what to look at? How not to miss anything? Both GitHub and Gitlab have their own activity monitoring mechanisms - let\u0026rsquo;s take a look at them!\u003c/p\u003e","title":"Organising work in GitHub and Gitlab"},{"content":"Today, I came across Saeghe — new package manager for PHP. Official site describes it as \u0026ldquo;modern PHP package manager that boosts your code\u0026rdquo;, let\u0026rsquo;s check it out!\nPreparing environment Saeghe is installed globally in the system (Linux and MacOS are supported), so it requires PHP to be installed too, obviously. For people working with the code long enough, it may be something natural, but today, in virtualisation-oriented times it seems like something obsolete. But OK, let\u0026rsquo;s do this old school 😉.\nDocumentation says about two possible installation methods: manual and with installer script. I used the latter, so I run:\nbash -c \u0026#34;$(curl -fsSL https://raw.github.com/saeghe/installation/master/install.sh)\u0026#34; Up to this point everything went correctly:\nIndeed, saeghe binary file was available in my CLI (after terminal restart), so installer properly added tool\u0026rsquo;s directory to my $PATH. Going further with \u0026ldquo;Getting started\u0026rdquo; documentation, I configured GitHub token. If you ever encountered problems with API limits while installing Composer dependencies, you know it\u0026rsquo;s required in large projects. This step also went without any problems: saeghe credential github.com $GITHUB_TOKEN properly saved my token in Saeghe\u0026rsquo;s configuration file (~/.saeghe/saeghe/credentials.json).\nFor more comfortable usage, mostly because of tool\u0026rsquo;s hard name, it\u0026rsquo;s good to set an alias. I added it to my ~/.zshrc:\nalias s=saeghe and now I can execute commands much faster (e.g. s install).\nSaeghe in action Migrating from Composer I took my plugin for #Rector, which is small and is perfect for such experiments. Saeghe comes with migration command, so I run saeghe migrate and I got an error:\nsaeghe migrate Warning: opendir(\u0026lt;project_path\u0026gt;/rector-money/vendor/roave/security-advisories): Failed to open directory: No such file or directory in ~/.saeghe/saeghe/Source/Commands/Migrate.php on line 131 Well, it looks like Saeghe does not support meta-packages, which don\u0026rsquo;t contain any actual files, like roave/security-advisories (which you should use 😉). Ok, so for the sake of the experiment, let\u0026rsquo;s remove it\u0026hellip; After that, migration command worked properly, and in my project\u0026rsquo;s directory Packages directory appeared along with 2 files:\nsaeghe.config.json: { \u0026#34;map\u0026#34;: { \u0026#34;Codito\\\\Rector\\\\Money\u0026#34;: \u0026#34;src\u0026#34; }, \u0026#34;entry-points\u0026#34;: [], \u0026#34;excludes\u0026#34;: [ \u0026#34;vendor\u0026#34; ], \u0026#34;executables\u0026#34;: [], \u0026#34;packages-directory\u0026#34;: \u0026#34;Packages\u0026#34;, \u0026#34;packages\u0026#34;: { \u0026#34;https:\\/\\/github.com\\/phpstan\\/phpstan.git\u0026#34;: \u0026#34;1.9.0\u0026#34;, \u0026#34;https:\\/\\/github.com\\/rectorphp\\/rector.git\u0026#34;: \u0026#34;0.14.6\u0026#34;, \u0026#34;https:\\/\\/github.com\\/moneyphp\\/money.git\u0026#34;: \u0026#34;v4.0.5\u0026#34;, \u0026#34;https:\\/\\/github.com\\/phparkitect\\/arkitect.git\u0026#34;: \u0026#34;0.2.32\u0026#34;, \u0026#34;https:\\/\\/github.com\\/phpstan\\/extension-installer.git\u0026#34;: \u0026#34;1.2.0\u0026#34;, \u0026#34;https:\\/\\/github.com\\/phpstan\\/phpstan-strict-rules.git\u0026#34;: \u0026#34;1.4.4\u0026#34;, \u0026#34;https:\\/\\/github.com\\/sebastianbergmann\\/phpunit.git\u0026#34;: \u0026#34;9.5.26\u0026#34;, \u0026#34;https:\\/\\/github.com\\/symfony\\/dependency-injection.git\u0026#34;: \u0026#34;v6.1.5\u0026#34;, \u0026#34;https:\\/\\/github.com\\/symplify\\/easy-coding-standard.git\u0026#34;: \u0026#34;11.1.16\u0026#34;, \u0026#34;https:\\/\\/github.com\\/webmozarts\\/assert.git\u0026#34;: \u0026#34;1.11.0\u0026#34; } } saeghe.config-lock.json (fragment): { \u0026#34;packages\u0026#34;: { \u0026#34;https:\\/\\/github.com\\/phpstan\\/phpstan.git\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;1.9.0\u0026#34;, \u0026#34;hash\u0026#34;: \u0026#34;e08de53a5eec983de78a787a88e72518cf8fe43a\u0026#34;, \u0026#34;owner\u0026#34;: \u0026#34;phpstan\u0026#34;, \u0026#34;repo\u0026#34;: \u0026#34;phpstan\u0026#34; }, ... } } I think those files\u0026rsquo; naming could be better, personally I would go with saeghe.json and saeghe.lock.json (or just saeghe.lock). Power in simplicity, also it would be more consistent with what we already know from Composer. This is small detail only, there are other issues too, but will come back to them later\u0026hellip;\nInstalling dependencies After migration I wanted to see how saeghe install behaves, but unfortunately it did not work and I got errors:\nWarning: rename(\u0026lt;project_path\u0026gt;/rector-money/Packages/phpstan/phpstan-phpstan-ed473a6,\u0026lt;project_path\u0026gt;/rector-money/Packages/phpstan/phpstan): Directory not empty in ~/.saeghe/saeghe/Source/Git/GitHub.php on line 149) I removed Packages directory, run command again and this time it finished correctly, but it took 52 seconds.\nUpdating dependencies Command used for updating packages is similar to Composer\u0026rsquo;s, but it works differently because it requires packages\u0026rsquo; repo URL, not the name. Also, it\u0026rsquo;s not possible to update all packages at once. We use saghe update like this:\nsaeghe update https://github.com/{owner}/{repo}.git --version={version-tag} Unfortunately I wasn\u0026rsquo;t able to check how it works because I got errors all the time:\nsaeghe update https://github.com/phpstan/phpstan.git Warning: file_get_contents(\u0026lt;project_dir\u0026gt;/rector-money/Packages/phpstan/phpstan/saeghe.config.json): Failed to open stream: No such file or directory in ~/.saeghe/saeghe/Source/FileManager/FileType/Json.php on line 7 It looks like Saeghe supports only repositories that\u0026hellip; already use Saeghe 🤷‍♂️.\nBuilding application When I tried saeghe build, I got errors too, but at some point I managed to get it running. Build took 43 seconds, even 59 seconds in subsequent run\u0026hellip; I think it\u0026rsquo;s too long considering so small project, and a really good hardware it was executed on (MacBook Pro M1).\nNevertheless, builds/development was created with file structure that was actually my project\u0026rsquo;s files. Difference between vendor and build/development/Packages directories was only 0.04MB. So what I gain? 🤔\nBuilding in real time (watcher) In principle, this command is to react in real time to changes in source files, and generate output files - a mechanism known from many tools, such as Hugo used on my blog. Cool theory, but not necessarily practice.\nRunning saeghe watch resulted in a wall of the same alerts as with update, so I was not able to get familiar with this functionality.\nDeveloper Experience Of course, a short adventure with Saeghe does not allow me to make a final opinion about this manager, but even after such a short time I was able to notice many shortcomings and/or problems:\nNo support for GITHUB_TOKEN Saeghe needs a token for GitHub, but I believe that instead of storing it in yet another configuration file, it could just support setting token using the GITHUB_TOKEN environment variable (which is already used for e.g. GitHub CLI configuration).\nNo support for short commands There is a mechanism in Composer that makes commands available under the shortest possible, unique aliases. Since update is the only command that begins with u, it is possible to use it as composer u (or even c u when using an alias for composer). It is a very convenient functionality that significantly minimises the need to tap the keys.\nSaeghe does not have this option, so you have to type complete commands every time, e.g. saeghe update (or s update if you are using the alias mentioned at the beginning).\nInconvenient update mechanism As mentioned earlier, to update packages we need the URL to the repository from which the package comes. We don\u0026rsquo;t expect to know these addresses by heart, so there is a need to do a copy/paste. Unfortunately, Saeghe stores these URLs in a way that prevents it, for example: https:\\/\\/github.com\\/rectorphp\\/rector.git. Sure, there\u0026rsquo;s a chance we\u0026rsquo;ll have a command like this in our shell\u0026rsquo;s history, but personally, I find this interface just awkward.\nIn Composer, the vendor/package format and going through one abstraction layer (Packagist) is not a whim, but a well-thought-out mechanism by which:\npackages are independent of their physical location: the author can migrate the code, and it is nearly unnoticeable for end users the risk of name collisions is practically eliminated: each vendor can name their packages freely within their space (so there can be e.g. foo/collections and bar/collections) dependency operations (adding, updating, deleting etc.) are based on the package name, which is much easier to remember and use in commands In this context, Saeghe seems to swim against the tide, but may drown as a result 😉.\nManaging package versions In saeghe.config.json, we define packages, which are the packages to be installed. Unfortunately, unlike in Composer, you cannot use ranges here, each package requires the exact version (tags with the v prefix or without it, depending on the projects\u0026rsquo; convention). I think this is a big step backwards - the idea behind the constraints in Composer is to specify the minimum required version once and then only update it. I can\u0026rsquo;t imagine manually changing the package version every time, executing a URL-based command, etc. Poor.\nNo cache for packages After emptying Packages, it took 52 seconds to install my dependencies. Every time you delete Packages, all packages are downloaded again, and it takes about the same time. For comparison, Composer stores downloaded versions of packages in a cache, so even after removing the vendor directory and running composer install, the installation is instant, because packages are taken from the cache. It works globally - we download the same package only once in a given version, and it is installed with the cache in each project.\nSemi-automatic migration I mentioned that the migration went smoothly, however there are small details that required manual fixes too. The saeghe migrate command does not add both Packages and builds to .gitignore, so after such a migration, Git informs us of hundreds or even thousands of new untracked files.\nSimplified owner-repo convention As you can see above, in saeghe.config-lock.json there is a list of currently installed packages and their versions. However, the greatly simplified approach to the origin of the package is puzzling: each has the owner and repo fields. This is based on the assumption that the repository comes from a location that has two levels of nesting. You can see that the author of Saeghe did not deal with Gitlab, in which project groups can be nested multiple times, which would result in addresses like https://gitlab.com/foo/bar/baz/package - so, what is owner, what is repo here 🤔?\nLooking at the functional implementation of communication with Github and the rigid implementation of repository I wonder if the author foresaw sources other than GitHub, but that\u0026rsquo;s a completely different story\u0026hellip;\nPHP version requirement I have not noticed anywhere in Saeghe the possibility of defining the required PHP version, which means that we are not able to force the required version of the runtime.\nDependency conflict resolution Does not exist. The exact versions of packages that are defined in saeghe.config.json are installed, and whether they work together is a completely different story\u0026hellip;\nPoor CLI interface Saeghe has a CLI interface, but it is very clunky. As mentioned above, error handling in the commands leaves much to be desired, and the commands themselves are not convenient to use due to the lack of verbose mode or even the lack of help (calls with --help do not offer any additional information on how the command works).\nCompared to the commands in CLI applications based on symfony/console, this interface is just plain and unfriendly.\nComposition and saeghe build Reading Saeghe\u0026rsquo;s documentation, I wonder what the support for composition in the code looks like\u0026hellip; Since build generates code consisting only of files that are used, is there a risk that the concrete implementations will be cut during building?\nImagine that there are interface Foo {}, class Bar { public method __construct (private Foo $foo); } and class Baz implements Foo {}. Instance of the Baz class is injected into the constructor of the Bar class by the Dependency Injection container, and in the whole Bar class we only operate on the Foo interface, not even knowing what its implementation is - what will Saeghe do with it? Especially when the container configuration would be in YAML, and not in a PHP file? 🤔\nMaybe the answer to this question will come after publication 😉\nSummary Saeghe seems to be an unstable and not fully thought-out experiment so far. I was not able to fully test it, and maybe I do not fully understand it, but the number of problems and bugs I have encountered in this short time make me question the sense of using Saeghe in real projects. Interestingly, the latest version is 1.6, but I personally think it should be rather 0.1.6\u0026hellip; For comparison, #PHPStan 0.12 (not supported, the current version is 1.x), or #Rector 0.14 are powerful tools incomparably more advanced than Saeghe at this point.\nPersonally, I think Saeghe should still be at 0.x, develop slowly, shape the public API, and listen to the feedback from the PHP community. It is definitely not a stable tool that is ready for commercial use.\nHowever, I wish the author perseverance and good luck 🙂\n","permalink":"https://blog.codito.dev/2022/11/saeghe-new-package-manager-for-php/","summary":"\u003cp\u003eToday, I came across \u003ca href=\"https://saeghe.com/\"  target=\"_blank\"\u003eSaeghe\u003c/a\u003e — new package manager for PHP. Official site describes it as \u0026ldquo;modern PHP package manager that boosts your code\u0026rdquo;, let\u0026rsquo;s check it out!\u003c/p\u003e","title":"Saeghe: new package manager for PHP"},{"content":"Few months ago I came with an idea to build Composer\u0026rsquo;s Docker images, but containing only binary file. Yesterday my pull request was merged, and now it\u0026rsquo;s possible to use such images in your own Docker builds as the simplest way to get Composer binary in your own image! 😁\n💥 I\u0026#39;m happy to announce that my PR to #Composer was merged, so soon it\u0026#39;ll be possible to improve #Docker build and copy Composer\u0026#39;s binary from low-size (~2.5MB), binary-only images 🥳\nℹ️ Short `composer` alias is not supported.https://t.co/wIxFefWaVP\nHappy #PHP building! 😁 pic.twitter.com/0SX1tg5e3R\n\u0026mdash; Greg Korba 🛠️🛹 Codito (@_Codito_) October 31, 2022 Explanation If you want to include Composer in your Docker builds, you have several options for that. But previously if you wanted to use COPY --from=composer approach, you had to download ~190MB image of Composer just to pull out one ~2.5MB file from it. Obviously, it wasn\u0026rsquo;t optimal, even when we consider Docker build cache.\nAfter my changes, every Composer\u0026rsquo;s Docker image will have binary-only equivalent. There are 3 differences though:\ncomposer/composer image must be used (details below) -bin suffix has to be added Composer binary is located in the root directory (/composer), unlike in the full image (/usr/bin/composer) For example, if you want to install the latest release from v2 branch, you need:\nFROM php:8-alpine COPY --from=composer/composer:2-bin /composer /usr/bin/composer There is composer image on Docker Hub, which is an alias for composer/composer. The latter is an official image delivered by Composer team, while the alias is semi-automated and requires additional work when new releases are available. Due to this process, and the specificity of docker-library/official-images repo, it was impossible to provide binary-only versions of the alias (details here). When to use it (or not) Binary-only images are useful only when you build your own PHP-based images, and you want to install Composer there too. Instead of installing it programmatically, you can use Docker image and extract ready-to-use binary file from it.\nHaving said that, it seems obvious that binary-only images are not suitable to run anything using them. Running docker run -it --rm composer/composer:2-bin \u0026lt;anything\u0026gt; won\u0026rsquo;t work because those images do not contain anything more than Composer\u0026rsquo;s binary - there is no PHP runtime or shell.\nImplementation details In mentioned Pull Request there are 2 kind of changes:\nbuild targets were defined in Dockerfiles in order to be able to build both type of images GitHub Actions were modified, so every pipeline contains Docker build both for regular and binary-only images, which are tagged respectively The first change is more interesting, so let\u0026rsquo;s look at the changes for 2.4 branch:\ndiff --git a/2.4/Dockerfile b/2.4/Dockerfile index c0b4ca7..866be1c 100644 --- a/2.4/Dockerfile +++ b/2.4/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8-alpine +FROM php:8-alpine AS binary-with-runtime RUN set -eux ; \\ apk add --no-cache --virtual .composer-rundeps \\ @@ -89,3 +89,10 @@ WORKDIR /app ENTRYPOINT [\u0026#34;/docker-entrypoint.sh\u0026#34;] CMD [\u0026#34;composer\u0026#34;] + +FROM scratch AS standalone-binary + +COPY --from=binary-with-runtime /usr/bin/composer /composer + +# This is defined as last target to be backward compatible with build without explicit --target option +FROM binary-with-runtime AS default First of all, binary-with-runtime name was added to existing build, which is then used in two ways: for building full image (docker build --target binary-with-runtime), and as a base for binary-only image (COPY --from=binary-with-runtime /usr/bin/composer /composer). The latter is called multi-stage build, and it\u0026rsquo;s a way to split build into many independent stages, from which some files can be copied to other layers. It\u0026rsquo;s helpful especially for optimising final image size, because e.g. any temporary files can be easily omitted.\nThe last line is interesting though - it ensures backward compatibility by aliasing binary-with-runtime with default stage. Since this is last stage in the file, it will be used when --target is not defined in build command.\nSummary It is small, but really important and helpful change, that can optimise many existing, real-world pipelines. Try it out in your build and let me know what you think 🙂\n","permalink":"https://blog.codito.dev/2022/11/composer-binary-only-docker-images/","summary":"\u003cp\u003eFew months ago \u003ca href=\"https://twitter.com/_Codito_/status/1544967275513630721\"  target=\"_blank\"\u003eI came with an idea\u003c/a\u003e to build Composer\u0026rsquo;s Docker images, but containing only binary file. Yesterday \u003ca href=\"https://github.com/composer/docker/pull/250\"  target=\"_blank\"\u003emy pull request\u003c/a\u003e was merged, and now it\u0026rsquo;s possible to use such images in your own Docker builds as the simplest way to get Composer binary in your own image! 😁\u003c/p\u003e","title":"Composer: binary-only Docker images"},{"content":"There were many discussions if #Laravel\u0026rsquo;s facades implement Gang of Four\u0026rsquo;s Facade Pattern, but I think it does not matter at this point - the Laravel team won\u0026rsquo;t change naming convention anyway. Naming is not a problem, I see other issues with facades - let\u0026rsquo;s look at it!\nThis topic was initially covered in Twitter thread, but I decided to convert it to blog post in order to enhance it with examples and maybe more content. It\u0026rsquo;s also easier to read, share and search. This is not critique First of all, before stating anything about Laravel Facades, I would like to emphasise that I am not against both of them: framework and the pattern. The whole point of this article is to shed some light on the facades, provide different perspective. I hope this will spark some discussion, make developers think about it.\nI am not saying you have to drop facades entirely and use only Dependency Injection - just choose wisely 😉 Do you want to be a PHP programmer or Laravel Developer? Because if you follow general programming principles, you won\u0026rsquo;t have problems after switching to another framework, but if you do \u0026ldquo;the Laravel Way\u0026rdquo;, you may encounter many problems outside the framework.\nIt\u0026rsquo;s a kind of magic Some may see it as advantage, but I consider it as drawback. You don\u0026rsquo;t execute exact code you\u0026rsquo;re calling, but your call is proxied to some underlying service. It strictly couples your code with the framework, which handles it. Facades\u0026rsquo; API has to be added as comments in PHPDoc (with @method annotation) which is error-prone because it\u0026rsquo;s easy to forget to update facade\u0026rsquo;s phpDoc when underlying service (accessor) is changed. But even if autocompletion in IDE works, you just can\u0026rsquo;t simply navigate to method\u0026rsquo;s code.\nThere are of course tools like barryvdh/laravel-ide-helper which can solve some (or even all) of those problems, but for me, it\u0026rsquo;s not a great developer experience that you need external tool installed just to work with the code in reasonable way.\nAs a side note: the same problem is with Laravel\u0026rsquo;s helper functions. Of course, they simplify code (well, it\u0026rsquo;s subjective), but at the expense of very strict coupling with the framework. The main problem I see with it, is that people learn how to use Laravel, not how to write in PHP.\nThey break Single Responsibility Principle Laravel\u0026rsquo;s facades act as static proxy to service locator, but at the same time\u0026hellip; they provide testing API. In theory, you end up on production with mockable code.\nSince facades are used statically, they can\u0026rsquo;t be replaced with other implementation during object initialisation. That\u0026rsquo;s why base Facade class uses Mockery and provides a bunch of methods that allows testing. That overflows API: your IDE suggests methods that you don\u0026rsquo;t need for regular development since they\u0026rsquo;re only for testing purposes.\nYou may think: \u0026ldquo;But it works! I can test my code\u0026rdquo; and you\u0026rsquo;re somehow right. But since Facade uses Mockery, while Laravel has it only in require-dev, you may end up with \u0026ldquo;malfunctioning\u0026rdquo; facades when you install with --no-dev option (e.g. on production). For me, it\u0026rsquo;s a code smell.\nOn the other hand, when you build/deploy improperly (composer install without --no-dev), you may end up with mockable facades on production - testing code should not reach this stage. Add to it \u0026ldquo;debug on production\u0026rdquo; approach and here you go, looking for troubles 😅\nReal-time facades Before preparing thread on Twitter I wasn\u0026rsquo;t even aware of such concept in Laravel, but I did my homework before posting and read documentation. This one particular section struck me:\nyou may treat any class in your application as if it was a facade\nI\u0026rsquo;m just amazed that someone had this idea and Laravel officially suggests wrapping any service with magic facade 🤯 It\u0026rsquo;s not even magic, it\u0026rsquo;s sorcery! 😅\nBut honestly, I don\u0026rsquo;t like this idea at all. If you want to use framework\u0026rsquo;s built-in facades - OK. If you want to provide your own facade - OK. But magically wrap external classes with non-existing namespace just to use class\u0026rsquo; API statically? Nope. Again, some may say \u0026ldquo;It\u0026rsquo;s awesome! It simplifies a lot!\u0026rdquo; and again they\u0026rsquo;ll be somehow right. But also again: it breaks SOLID principles, covers explicit dependencies with magic, strictly couples code with Laravel, teaches wrong (IMHO) practices.\nCustom autoloading It was not covered in the Twitter thread, but there is one small detail related to facades - you can omit importing facade\u0026rsquo;s FQCN and treat them as they were in global namespace, and they still will work 😩\nAs you can see in the image above, the IDE suggests that class does not exist, but executing that code works.\nIt\u0026rsquo;s because of another magic approach - configurable aliasing (with defaults). Basically, you configure which aliases (consider them as non-existing classes from PHP\u0026rsquo;s global namespace) should be treated as facade and resolved as actual class.\nThis is magic behind another magic. Static proxy aliased through configuration. For me it\u0026rsquo;s too much 😅\nAs I said, I am not saying you should stop using facades, but you really should stop using aliases. Import actual classes where you need to use them and let IDE and static analysis tools recognise them properly, without additional tooling and plugins. The alternative Instead of using facades, you could use Dependency Injection approach and use facades\u0026rsquo; underlying services directly. They\u0026rsquo;re listed on Laravel docs, most of them have Service Container Binding, that can be used for injecting.\nI assure you, it will help you when you will need to work with other frameworks. Switching from Laravel to different framework is, in my opinion, harder than the other way around, so if there\u0026rsquo;s a chance that you will work with other frameworks, this approach will prepare you better for it. Sticking to Laravel\u0026rsquo;s conventions may lead to habits that are not applicable elsewhere.\nBut of course, it\u0026rsquo;s always your (or your team\u0026rsquo;s) choice 🙂\n","permalink":"https://blog.codito.dev/2022/08/drawbacks-behind-laravel-facades/","summary":"\u003cp\u003eThere were many discussions if \u003ca href=\"/tags/laravel\" target=\"_blank\"\u003e#Laravel\u003c/a\u003e\u0026rsquo;s facades implement Gang of Four\u0026rsquo;s \u003ca href=\"https://en.wikipedia.org/wiki/Facade_pattern\"  target=\"_blank\"\u003eFacade Pattern\u003c/a\u003e, but I think it does not matter at this point - the Laravel team won\u0026rsquo;t change naming convention anyway. Naming is not a problem, I see other issues with facades - let\u0026rsquo;s look at it!\u003c/p\u003e","title":"Drawbacks behind Laravel Facades"},{"content":"What makes Code Review process good? What should you do, and what you should not? Let me share with you my experience on this 🙂\nThis article is highly based on internal content created by me for GetResponse during migration from Crucible to Gitlab, which was one of my initiatives during my first year at the company. I use \u0026ldquo;merge request\u0026rdquo; (MR) convention because I mostly work with Gitlab, but everything applies to pull requests too and code review in general, even if it\u0026rsquo;s done with Git patches sent via email 😉 The Purpose of Code Review Code review, regardless to where it\u0026rsquo;s processed, has very important goal: deliver efficient, well written (readable, optimal, company-compliant), working code that satisfies business needs. From client\u0026rsquo;s or user\u0026rsquo;s point of view it\u0026rsquo;s not important what is the naming convention, coding standards and so on - it just has to work as expected.\nIt does not mean that Code Review does not have other goals, others are just less important 😉. Code Review allows us to:\nshare the knowledge within team or cross-team projects learn developers with less experience with the language and/or the application find scenarios that code\u0026rsquo;s author did not think of before ensure that the general idea behind the changes is correct General rules Between author of the Merge Request and Reviewers/Approvers exists contract, which can be different within teams and projects, but some of general rules can be always applied. I am not talking about technical rules, but things that can make the process more efficient and friendly. Let\u0026rsquo;s look at it!\nBe proactive Keep track of the merge requests you\u0026rsquo;re participating. No matter if you\u0026rsquo;re author or reviewer, you really should keep an eye on opened MRs you\u0026rsquo;re participating in. Feel free (or even obligated) to ping author/reviewers for reaction if merge request is stalled, because the sooner it\u0026rsquo;s finished, the better. For example, #Gitlab\u0026rsquo;s top-right menu has helpful items to simply go to the list of MRs and \u0026ldquo;todos\u0026rdquo; - you should observe these, and react if you have non-zero counters on them. In Github, there\u0026rsquo;s notifications view that contains everything what\u0026rsquo;s going on in the issues and merge requests in which you participate - keep it as empty as possible.\nBe responsive It\u0026rsquo;s really important to respond to merge request\u0026rsquo;s activity - do requested code review, answer comments, resolve discussions, push fixes etc. as soon as possible. Don\u0026rsquo;t ignore email notifications, if you feel that there\u0026rsquo;s too much of them consider changing notifications settings or create filtering rules. If you work in a distributed team, don\u0026rsquo;t mute communicator if there is no real need for it, so your team members can reach you if they need.\nBe clear in communication It\u0026rsquo;s really important to communicate well in order to not waste others\u0026rsquo; time.\nAs an author of merge request, describe what your code changes do, how it\u0026rsquo;s working and how it should be verified. Write good commits (that could work as description), put comments in the code if something is complex or not intuitive.\nAs a reviewer write good comments that would clearly inform author about what you want. If there\u0026rsquo;s critical bug/mistake, just emphasise it. If there are some optional improvements or possible refactoring - write it too. But each of your comment should point what\u0026rsquo;s wrong and what is \u0026ldquo;definition of done\u0026rdquo; from your perspective. Avoid short and not descriptive comments that may trigger unnecessary discussion.\nAs any MR participant, if you feel that written discussion does not work, just arrange meeting to resolve it. Sometimes 5 minutes of talk can be much better than several comments that take more time to write/read. There\u0026rsquo;s also difference in perception, written words can be misunderstood and lead to unwanted clashes between you and others.\nBe kind Remember - you don\u0026rsquo;t need to agree with others, but respect them and their work. You\u0026rsquo;re entitled to point anything you want if it\u0026rsquo;s related to the code, project\u0026rsquo;s conventions, business requirements etc. Code review is not a place for personal animosities. Avoid passive-offensive wording, like \u0026ldquo;It\u0026rsquo;s bad\u0026rdquo;, these can do harm and make your arguments less convincing.\nBe distanced It applies mostly to merge requests\u0026rsquo; authors, but reviewers also should remember that all the comments on code review are not there for attacking them, not for proving they are wrong or lame. Merge request discussions are the tool for improving delivered code. Don\u0026rsquo;t take the comments personally - you\u0026rsquo;re entitled to have your own opinion and conventions, so it\u0026rsquo;s natural that sometimes you will do something differently than others would expect. If somebody puts comment that it should be done otherwise, just discuss it without emotions (at least try to 😉).\nAuthor\u0026rsquo;s role As an author you should take care of the Merge Request from the time it\u0026rsquo;s created until it\u0026rsquo;s merged/closed, so you should assign yourself and keep yourself as an assignee as long as it\u0026rsquo;s you, who work on it. If anyone is taking the task from you and continue your work, MR should be assigned to that person (assignee always should reflect person responsible for delivering final product).\nIf you don\u0026rsquo;t have merging rights in the project you should be assigned to merge request until it\u0026rsquo;s ready to be merged. You are responsible for delivering approved code changes, merging is the last technical step, and you can assign different user to MR when it can be merged (but it can be merged without changing assignee too, you can just mention users with merging rights or add label - it should be part of your development process).\nDraft / WIP On most cases you should open Merge Request when code is ready to be reviewed, but you can create merge request and mark it as Draft even if it\u0026rsquo;s not ready. When MR is in Draft mode reviewers should NOT actively do code review, but only respond to author\u0026rsquo;s doubts or questions. Of course, they can do code review, but they should ask author if it makes sense - maybe author wants to change implementation completely and reviewing current code is a waste of time. When author thinks that code is valid and ready, Draft should be taken off and this is the time when proper Code Review starts.\nDo NOT treat Draft status as a way to block merging - there is much better way of doing it by requiring approvals. Keep your branch up to date (by rebasing or merging with main branch) and use Draft flag only for initial development, not for whole Code Review process. Reviewer\u0026rsquo;s role Most important: you\u0026rsquo;re responsible for merge request\u0026rsquo;s code quality and app security as much as author of the changes! As a reviewer, your responsibility is verifying if delivered code changes are acceptable - in terms of overall good practices in programming, team\u0026rsquo;s and company\u0026rsquo;s standards and conventions, security and performance. You do it by checking MR\u0026rsquo;s diff and providing feedback, where\u0026rsquo;s required. You should use diff discussions or suggestions to help merge request\u0026rsquo;s author in providing best code possible. If you start discussion, you should keep an eye on it and mark it as resolved as soon as it\u0026rsquo;s updated by MR\u0026rsquo;s author and provided changes are satisfying (ℹ️ discussions can be automatically resolved if configured that way - this should be discussed within team/company). Remember, your comments will have higher value if you include:\nofficial documentation references links to specialised articles written by experts benchmarks showing advantage of proposed changes suggestion of changes (if your code review platform allows it) references to previous merge requests or comments related to currently reviewed changes In terms of suggestions, use them in your comments to visualise what change you\u0026rsquo;re requesting, it will help the author understand you and even make it easier to apply. If you\u0026rsquo;re not familiar with the concept of suggestions, read Gitlab\u0026rsquo;s or Github\u0026rsquo;s documentation on this topic.\nCode changes\u0026rsquo; author(s) and reviewers are like small team, they should work together. Do not treat code review as your standalone work - keep an eye on other reviewers\u0026rsquo; comments, provide feedback if you agree or disagree (maybe even more with the latter, because it can prevent incorrect changes). Reading other reviewers\u0026rsquo; comments will also help minimise redundancy - without reading others\u0026rsquo; comments you could add the same feedback and that would introduce unnecessary noise.\nWhen code changes are acceptable for you, you should approve MR, that would explicitly tell other people that you agree to merge it.\nDon\u0026rsquo;t waste time Code review is not a place for pointing coding standards. Do not comment about indentations, braces\u0026rsquo; placement and other things that should be automated using for example Easy Coding Standard. Just focus on actual code 🙂.\nTechnical details It depends on the platform where code review is done, but some technical rules apply nonetheless:\nTitle should reflect provided changes. Ensure title is clean and descriptive. If there\u0026rsquo;s naming convention (e.g. with JIRA ticket ID), use it. Use Draft to indicate that code is not finished yet (if applicable). Use description for providing additional information for reviewers. You can describe background of the change, explain why it\u0026rsquo;s implemented this way, how to verify it manually and so on. Put there everything that could be helpful for better and faster code review. Assign yourself to Merge Request, that will make easier to track its progress Assign reviewers for code review (if you don\u0026rsquo;t have this automated). If project allows it, you can fine-tailor approval rules - it\u0026rsquo;s recommended that 2 approvals are required to allow merging, but in more complex MRs you can demand more approvals, also from specific people. If your merge request is related to other MR(s) you can define merge request dependencies which will prevent merging your changes before merging other MRs. Summary of recommended Code Review flow Configure your projects\u0026rsquo; settings in order to optimise merge request flow (approvals, code owners, requirements that must match before merging). Automate everything what\u0026rsquo;s possible from QA perspective (#coding standards, #static analysis, tests, linters etc.) so you can focus on actual business logic and/or technical change in Merge Request. Open MR as soon as you need feedback (from #CI or from reviewers), but mark it as Draft if it\u0026rsquo;s not ready for final review. Remove Draft when you think it\u0026rsquo;s ready (it indicates that you finished your work and want feedback). Assign reviewers when merge request is in state that requires feedback (Draft with things to discuss or when it\u0026rsquo;s ready for review). Keep MR open as short as possible. Opened merge requests in most cases should have higher priority than other work in order to finalise it and deliver product. Keep in touch with other participants - author and reviewers should work as team responsible for finalising task. Track merge requests where you\u0026rsquo;re participating, make it your daily routine. Merge MR when it meets all requirements (green pipeline, discussions resolved, required approvals gathered). Sometimes it\u0026rsquo;s a good idea to rebase your branch before merging, but it depends on project\u0026rsquo;s flow. Code Review topic is really wide, I focused here on soft side of it. More technical follow-up posts will be published in the future, so stay tuned and come back here regularly! 😎 Happy reviewing!\n","permalink":"https://blog.codito.dev/2022/07/code-review-good-practices/","summary":"\u003cp\u003eWhat makes Code Review process good? What should you do, and what you should not? Let me share with you my experience on this 🙂\u003c/p\u003e","title":"Code Review: good practices"},{"content":"Today I came across fantastic idea that was implemented as Proof of Concept in Gitlab some time ago: CI Workflows! I\u0026rsquo;m pretty excited about it and really hope it will be shipped soon 😁\nThis post may contain information related to upcoming products, features, and functionality.\nIt is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes.\nAs with all projects, the items mentioned in this document and linked pages are subject to change or delay. The development, release and timing of any products, features, or functionality remain at the sole discretion of GitLab Inc.\nThe concept Currently, CI jobs can be included in the pipelines, while rules can be applied to define when to do it (e.g. only if certain files were modified). Pipeline can be triggered by several events: commit push, merge request creation, through API or manually using user interface. It is already flexible and awesome, but in terms of automating tasks based on projects\u0026rsquo; content (build, test, release, deploy), not necessarily automation in general.\nGitlab CI Workflows\u0026rsquo; idea is pretty simple, yet brilliant - let CI jobs to be triggered by system\u0026rsquo;s events. This way possibilities become almost endless, limited only by supported events and people\u0026rsquo;s imagination 😉. For example: you want to triage new issues? Run workflow on issue\u0026rsquo;s creation event. Want to run pipeline in the merge request when it\u0026rsquo;s marked as ready (Draft flag removed)? No problem, just hook into event and call the API.\nPossible usage Proposed syntax is based on new on option, at this point it\u0026rsquo;s not clear if it can be single event or collection of events. Anyway, it could look like:\nimage: alpine stages: - triage - verify reviewer_roulette: stage: triage # New option introduced, defines which event we want subscribe on: my-workflow-1/webhooks/issues/created script: - echo \u0026#34;Draw and assign reviewers...\u0026#34; - ... verify_standards: stage: verify needs: - reviewer_roulette script: - echo \u0026#34;Check title, description etc.\u0026#34; - echo \u0026#34;Check if reproducer repository link is provided\u0026#34; - ... These 2 jobs are not regular jobs included in pipelines. reviewer_roulette uses mentioned on option, so it\u0026rsquo;s triggered when issue is created, while verify_standards depends on it through needs. Both will be included in the workflow\u0026rsquo;s pipeline, but won\u0026rsquo;t be included in regular pipelines based on branches, merge requests etc.\nPoC DEMO Video with demo of this Proof of Concept was shared here, initially as private but after my question it was made public 🎉 Asking costs nothing 😉\nSummary I can\u0026rsquo;t wait for it! Of course, there are many questions and doubts, but these will be addressed in the epic. We, as Gitlab users, also can be part of this - collectively this feature can be engineered even better! So don\u0026rsquo;t hesitate and provide feedback if you have ideas about CI workflows 🙂\n","permalink":"https://blog.codito.dev/2022/07/gitlab-ci-workflows-poc/","summary":"\u003cp\u003eToday I came across fantastic idea that was \u003ca href=\"https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91244\"  target=\"_blank\"\u003eimplemented as Proof of Concept\u003c/a\u003e in Gitlab some time ago: \u003cstrong\u003eCI Workflows\u003c/strong\u003e! I\u0026rsquo;m pretty excited about it and really hope it will be shipped soon 😁\u003c/p\u003e","title":"Gitlab CI Workflows (PoC)"},{"content":"In recent days Laravel Pint was introduced and caused havoc of ambivalence - some love it, some hate it. Personally I have mixed feelings about it, and I wanted to share my thoughts with the other developers.\nAppreciation and admiration First, I would like to emphasise my admiration for Nuno Maduro\u0026rsquo;s passion. He is truly dedicated to Open Source community, mostly Laravel part, and brings his ideas to life, providing tools for many people in PHP land. It\u0026rsquo;s truly amazing.\nThe thing is I am not completely fine with his latest work and would like to address some concerns here for discussion.\nAnd now for something completely different This is first thing which triggered me: Nuno\u0026rsquo;s initial tweet is all about announcing new package, a zero-dependency code style fixer. There is not any mention about PHP CS Fixer, actual fixer package, on top of which Pint was built. Of course, there is information about it in Pint\u0026rsquo;s readme, but let\u0026rsquo;s do not fool ourselves - it surely wasn\u0026rsquo;t read by large part of the announcement\u0026rsquo;s audience. Stating it explicitly in the announcement would be fair to PHP CS Fixer\u0026rsquo;s creators and its maintainers\u0026hellip;\nEspecially that Pint is not adding much in terms of coding standard fixing - it focuses on Developer Experience.\nLifelong wait is over I saw many tweets from people that \u0026ldquo;have been looking for this\u0026rdquo;, like:\nThis is AWESOME! I have been looking for something like this I can integrate into projects without nagging people about their editor settings.\n\u0026mdash; Morgan Breden (@bredmor) June 22, 2022 I was pretty confused. Where exactly those people were looking? Into Laravel Docs?\nThis is of course not Nuno\u0026rsquo;s fault that people in Laravel community don\u0026rsquo;t know basic tools that are present in PHP world for years now. It is just saddening that people don\u0026rsquo;t look for existing, battle-tested solutions further than laravel/* namespace. For all of those people, who \u0026ldquo;were looking for it\u0026rdquo;, it would be better to share upstream package explicitly in the announcement and teach them about existence of such tools.\nBut I get it\u0026rsquo;s not in Laravel\u0026rsquo;s interest\u0026hellip;\nSubjectively great DX One of the most hyped features of Pint is its CLI user interface. Announcement had some nice, simple screenshot showing the output of the command. But in my opinion CLI commands above all should be useful and at some point Pint loses it:\n@TattedTechBro, Twitter\nList of applied rules is presented horizontally in one line, so if multiple fixers were applied, the list may be trimmed, and you won\u0026rsquo;t know which were used. For people looking for fancy output, not actual important data, it won\u0026rsquo;t be a problem, probably. Personally, looking at the screenshot above I see messy block of text with some information lost. For me, it is not an advantage over regular CS Fixer\u0026rsquo;s output (at least at this point, it\u0026rsquo;s still 0.* release).\nZero dependency, non-zero problems Pint is built with Laravel Zero, which means it\u0026rsquo;s shipped as executable which contains all of its dependencies. It surely will minimise amount of Composer conflicts, but please have in mind that it also hides actual dependency to PHP CS Fixer. You\u0026rsquo;ll get what\u0026rsquo;s bundled in Pint, not what\u0026rsquo;s available at Packagist. Effectively it means that you need to wait for Pint release to get fixes or new features from CS Fixer. It\u0026rsquo;s totally different from PHPStan or Rector, which also ship with \u0026ldquo;zero dependency\u0026rdquo; (PHAR file or prefixed, built-in vendors) - these are top-level tools while Pint is a wrapper for actual tool.\nSometimes it may be tempting to install Pint globally\u0026hellip; Stahp. Don\u0026rsquo;t do it 😉. Each project should have its own set of dependencies with explicit versions, so every developer install exact same toolset and works the same way.\nYour code, your rules Another Pint\u0026rsquo;s hyped feature is \u0026ldquo;zero config\u0026rdquo;. People are delighted that \u0026ldquo;it just works!\u0026rdquo;.\nI tried Laravel Pint today, I loved how flawless it worked, and how many styling issues I had, 400+\nYou would think that you have great stylistic consistency untill pint comes along😅\n\u0026mdash; 89 (@eighty9nine) June 27, 2022 But let me ask: were all of these 400+ styling issues actually violations? Well, it depends. If you use \u0026ldquo;zero config\u0026rdquo; approach, you\u0026rsquo;re giving away control over your coding standards to the tool. It\u0026rsquo;s not your coding standard then. It may lead to situations when code that you like needs to be changed to satisfy tool. Of course, you don\u0026rsquo;t need to apply all the fixes and configure PHP CS Fixer Pint, so some rules may be disabled or behave differently. But then it\u0026rsquo;s not zero-config anymore, so is it really that good to be advertised? 🤔\nIn my opinion it is good to have explicit config. It ensures that you\u0026rsquo;ll get exactly what you want, not what tool\u0026rsquo;s wrapper\u0026rsquo;s creator delivered as defaults. It\u0026rsquo;s especially important in initial development period (version 0.*), when you don\u0026rsquo;t have any backward compatibility promise.\nAutomate it: yes, but no! Last thing I would like to point is CS automation. I saw many tweets about integrating Pint in Github Actions:\nLaravel Pint json set and Github workflow template update. pic.twitter.com/tm1tamSnQB\n\u0026mdash; Samuel Mwangi (@SamuelMwangiW) June 22, 2022 I did discuss it under several tweets (like here), but it\u0026rsquo;s out of control. So I would like to emphasise here that automating CS checks is highly encouraged, but automating CS fixes not. You should integrate CS tool in your development workflow (CLI habit, Git pre-commit hook, #CI job), and check if code satisfies your coding standards definition.\nIf check fails, you can decide if each violation should be fixed or if rule should be disabled/reconfigured. At some point maybe you\u0026rsquo;ll trust the tool enough to enable automatic fixes, but personally I don\u0026rsquo;t do it. Especially in large, legacy apps where standards are not fully applied, it may lead to unintentional code logic change (e.g. CS Fixer may change == to === when risky rules are enabled) that will cause the problems to the users.\nRemember, tools are just tools, they do what programmers made them to do. And some scenarios or edge cases may be not tested or just implemented badly. Don\u0026rsquo;t take result of such tools as 100% safe.\nIn the end it\u0026rsquo;s your decision Having said all the above, we can agree that in the end it\u0026rsquo;s up to you what fits your needs. You can use whatever tool you like, but do it responsibly and knowingly. Appreciate and support people working hard on tools you use, like Dariusz \u0026ldquo;keradus\u0026rdquo; Rumiński, creator and maintainer of PHP CS Fixer, which drives Laravel Pint.\n","permalink":"https://blog.codito.dev/2022/06/laravel-pint-a-study-of-ambivalence/","summary":"\u003cp\u003eIn recent days \u003ca href=\"https://github.com/laravel/pint\"  target=\"_blank\"\u003eLaravel Pint\u003c/a\u003e was introduced and caused havoc of ambivalence - some love it, some hate it. Personally I have mixed feelings about it, and I wanted to share my thoughts with the other developers.\u003c/p\u003e","title":"Laravel Pint: a study of ambivalence"},{"content":"PHPStan 1.7.15 brings small, but helpful feature: support for relative paths in editorUrl for virtualised environments. It\u0026rsquo;s even better when it makes you a repository collaborator - yes, it\u0026rsquo;s my initiative! 🥹\nWorking with PHPStan is great when you use editorUrl configuration. It adds clickable link in the CLI output, which opens reported file at specific line. It improves productivity because you need less time to jump between analysis report and the codebase. Of course, you can configure your IDE to do real-time analysis, but it\u0026rsquo;s whole different story\u0026hellip; maybe next time 😉\nAnyway, having editorUrl is great, but until 1.7.15 it was not helpful when you run PHPStan analysis in virtualised environment, like Docker or Vagrant. It\u0026rsquo;s because local paths are most often different from paths in container / virtual machine - clickable link will have wrong path and IDE won\u0026rsquo;t open it properly. It also wasn\u0026rsquo;t possible to determine actual paths because PHPStan inside container doesn\u0026rsquo;t have information about host\u0026rsquo;s filesystem.\nIn this release, support for relFile was added, so you can configure PHPStan like this:\n# phpstan.neon includes: - phpstan.neon.dist parameters: editorUrl: \u0026#39;phpstorm://open?file=/your/local/path/%%relFile%%\u0026amp;line=%%line%%\u0026#39; This will generate clickable links based on your local path to the project\u0026rsquo;s root directory, so IDE will open it correctly 🎉.\nWhen you follow official guidelines and use phpstan.neon.dist for common configuration, and phpstan.neon for per-user overrides, it\u0026rsquo;s possible that each developer has different local path and thus editorUrl value. In the end, everyone can use the same virtualised runtime but with custom local setup. ","permalink":"https://blog.codito.dev/2022/06/phpstan-in-virtualised-environments/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/phpstan/phpstan/releases/tag/1.7.15\"  target=\"_blank\"\u003ePHPStan 1.7.15\u003c/a\u003e brings small, but helpful feature: support for relative paths in \u003ccode\u003eeditorUrl\u003c/code\u003e for virtualised environments. It\u0026rsquo;s even better when it makes you a repository collaborator - yes, it\u0026rsquo;s my initiative! 🥹\u003c/p\u003e","title":"PHPStan in virtualised environments"},{"content":"Gitlab CI definition might get really complex. In order to reduce redundancy you can use many useful features and syntax sugar, like !reference.\nAlthough Gitlab CI\u0026rsquo;s YAML syntax is not as friendly as many may expect, it can do wonders. Reference tags are one of these features that can help you with organising your CI definition in maintainable way.\nImagine this simple scenario:\njob1: script: - echo \u0026#34;Diff base is \u0026#39;$CI_MERGE_REQUEST_DIFF_BASE_SHA\u0026#39;\u0026#34; - GIT_DIFF_CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only ${CI_MERGE_REQUEST_DIFF_BASE_SHA}...) - echo \u0026#34;$(echo $GIT_DIFF_CHANGED_FILES | wc -l) file(s) changed\u0026#34; job2: script: - echo \u0026#34;Diff base is \u0026#39;$CI_MERGE_REQUEST_DIFF_BASE_SHA\u0026#39;\u0026#34; - GIT_DIFF_CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only ${CI_MERGE_REQUEST_DIFF_BASE_SHA}...) - echo $GIT_DIFF_CHANGED_FILES Determining Git diff is redundant, it is exactly the same in both jobs. It may lead to inconsistencies between jobs if something needs to be improved - one may be updated, while the other may not. We can simply fix it with !reference:\n.diff: script: - echo \u0026#34;Diff base is \u0026#39;$CI_MERGE_REQUEST_DIFF_BASE_SHA\u0026#39;\u0026#34; - GIT_DIFF_CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only ${CI_MERGE_REQUEST_DIFF_BASE_SHA}...) job1: script: - !reference [.diff, script] - echo \u0026#34;$(echo $GIT_DIFF_CHANGED_FILES | wc -l) file(s) changed\u0026#34; job2: script: - !reference [.diff, script] - echo $GIT_DIFF_CHANGED_FILES Now common logic is extracted to some template (job starting with .) and referenced in other scripts. If diff logic needs to be changed, it will affect both jobs. Even though this can be achieved also with anchors, !reference has one big advantage: referenced section can be defined and used in different files, while anchor usage is limited only to file where it\u0026rsquo;s defined.\nUntil today, I thought that !reference has one drawback: lack of nesting. Documentation states:\nYou can\u0026rsquo;t reuse a section that already includes a !reference tag. Only one level of nesting is supported.\nBut it seems to be working on Gitlab 14.10, so I\u0026rsquo;ve created an issue and asked for providing current information (and most probably updating docs). We found it out after coincidence, colleague just used nested references because he didn\u0026rsquo;t know that it can\u0026rsquo;t be done 😅. Now, we can improve some parts of our Gitlab CI definition, but probably will wait for an answer in the issue to know actual limitations (if any).\nAfter discussing the problem it turned out that !reference supports 10 levels of nesting in script, before_script and after_script, so I created Merge Request, that made me a Gitlab collaborator 😉 Happy referencing!\n","permalink":"https://blog.codito.dev/2022/06/referencing-sections-in-gitlab-ci/","summary":"\u003cp\u003eGitlab CI definition might get really complex. In order to reduce redundancy you can use many useful features and syntax sugar, like \u003ccode\u003e!reference\u003c/code\u003e.\u003c/p\u003e","title":"Referencing sections in Gitlab CI"},{"content":"I really don\u0026rsquo;t know when it happened, but it\u0026rsquo;s been a year since I joined #GetResponse as Software Architect 😵 It was an intense and fruitful time worth summing up!\nWorking with GetResponse When I was looking for a new job a year ago, I went through a lot of recruitment processes, but in the end I only considered 2 offers, from which I chose GetResponse. In retrospect, I do not regret this decision, and I am very happy that I joined this team 🙂. The company offers 100% remote work, which was especially important to me after years spent on the train, between home and the office. It is also essential that the service itself is large, complicated and demanding - it is a real challenge in many aspects. The IT department is large and composed of many specialists from whom you can learn a lot. The technology stack is also substantial, although - there is no need to hide - there is also a large tail of technical debt\u0026hellip;\nMy role in GR I joined GetResponse as a Software Architect and became a member of a cross-area team, so I was kind of a free electron for a year - sure, I had some projects to do, but beyond that I could find potential optimisation spots, point the problems and implement things that affect the software development process. I will not hide that this role suits me very well, because it gives me a chance to show off. It requires some kind of self-discipline and organisation, but thanks to this it is possible to undertake work that probably would not qualify for sprints focused on business goals\u0026hellip;\nCompleted initiatives During this year, I managed to do many small and big things, a very large part of which was my initiative. Simply carrying out tasks, I encountered various problems, nuisances or complications, which I signaled, consulted, and which often turned into new tasks or projects. So let\u0026rsquo;s see what we managed to ship, more or less chronologically 😉\nChanges described below show the process of software\u0026rsquo;s natural evolution - tools are evolving, knowledge level and experience grow, new challenges and requirements arise. It means that solutions implemented some time ago may require improvements, redesign or even replacement. Ability to find out those places and willingness to take up the challenge, which optimisation of code and processes undoubtedly is, are traits worthy of praise and imitation. Luckily, in GetResponse it\u0026rsquo;s well understood - many people constantly try to improve quality, not only of main product, but also everything related to it (tools, processes) 😁 Implementation of Rector in the development process #Rector can help with upgrading outdated dependencies, as it includes a series of rules to eliminate discontinued usages. Of course, not only can it help us with this, it also contains a lot of other rules that improve the existing code (e.g. by using newer language syntax features), without changing its logic (this makes it safe, but remember to never trust any code modification tool in 100%).\nI was able to implement this tool so that it would non-invasively, step by step, refactor the application - in the CI process there is a job that is allowed to fail, thanks to which it does not block the process (it analyses files changed within the Merge Request and suggests potential improvements). Locally, any developer can use handy Composer scripts to automatically apply Rector\u0026rsquo;s suggestions.\nWe also managed to write non-standard rules, thanks to which we can systematically improve the code in the legacy area (elimination of Hungarian notation, direct use of constants).\nImproving PHPStan implementation #PHPStan was already implemented in the process when I joined GR, but unfortunately some things were done not optimally, so it didn\u0026rsquo;t have as much value as it could. On my initiative, we managed to introduce support for baseline files, raise the level of analysis, add extensions (Symfony, Prophecy, MyCLabs enum) that improved the quality of the analysis.\nIn the CI process, it was possible to parallelize the analysis by running nested pipelines. For various reasons, we had to depart from the suggested by the author full analysis, and we have separated 4 areas, analysed separately. In the past, these 4 areas were analysed one after another as part of one job, which took very long, but also failure in any area (except the last one) resulted in a lack of analysis in subsequent areas, which made it difficult to correct errors at once. However, I was able to modify this process so that the main pipeline triggers child pipeline with 4 separate jobs that can be executed in parallel, which shortens the total analysis time.\nIn the meantime, I also initiated and performed a PHPStan upgrade from version 0. * to version 1.*, which was necessary to keep receive updates and be able to use the new features of the tool.\nThere is still a lot to improve in this area, but a lot has already been achieved over these 12 months.\nEasy Coding Standard implementation When I joined GR, there was no automation of the coding standards (other than the guidelines on Confluence 😉) - it\u0026rsquo;s not a problem per se, but it increases risk of inconsistencies. So I took the initiative to introduce #ECS into the project and then, together with one of my colleagues, I implemented it. In the initial phase, the CI job also was allowing failures, so developers had time to apply fixes in their areas. At some point, however, the job became required and now it blocks merge when violations of the standards are found in the code. Developers can of course apply these patches locally, using prepared Composer scripts.\nCentral Traefik In GetResponse, apart from the main application, there is a lot of other projects, and each of them (or the vast majority) offers a Docker stack for local development. Applications within these stacks expose domains, and they do it using Traefik. Unfortunately, the implementation was not optimal, because each stack had its own Traefik service, which made it impossible to run multiple stacks at the same time (because only one Traefik could connect to 80/443 ports). Of course, it was totally possible to work with these applications, but it was harder to integrate them with each other, also switching context was not developer-friendly (necessity of turning off one stack before starting another).\nOn my initiative, we have introduced the central Traefik, which runs in the background as an independent stack, and the remaining stacks expose domains using labels in services that need to be available via HTTPS. Thanks to this, you can run any number of environments, and the stacks can communicate with each other (via a shared Docker network).\nAn additional advantage of this solution is that SSL certificates only need to be updated in one place 🙂\nCaptain Hook implementation All the QA tools that had been implemented / improved so far had one major drawback - the emphasis on their usage was placed on the CI process. We wanted to lighten the runners\u0026rsquo; load a bit and introduce fail fast approach, so more problems would be discovered on the developers\u0026rsquo; side, on their computers. To achieve this, we implemented CaptainHook 🪝\nThanks to this, it is possible to:\nvalidate PHP files (linter) check compliance with coding standards static analysis with PHPStan validate commit title (Jira task required 😉) Some of these tasks are performed before commit, others before push, some for both. Overall, the goal is for the developer to get instant feedback before sending the code to the central repository. It happens automatically, because hooks are installed automatically when the project is launched (specifically after installing Composer packages).\nDirection: PHP8! Coming to GR, I had a few months of coding in PHP8 behind me, and here I had to take a step backwards as the main system is still based on PHP 7.4. Again, it\u0026rsquo;s not a problem per se, because system works stable - it\u0026rsquo;s all about unavailability of some language features introduced in newer PHP versions, which simplify the code and make working with it more comfortable.\nSo I dug the topic down and managed to make a number of changes to ensure compatibility with PHP8 by myself or with the help of others: updating dependencies to versions supporting PHP8 (or getting rid of them, if they turned out to be unnecessary), improving the existing application code and internal dependencies (e.g. improving signatures of methods implemented from interfaces, getting rid of the word resource from namespaces, etc.). There is still a long way to go, because we are blocked by other conditions, but when it comes to the code itself, we are much closer to PHP8 than when I started working with the project 🙂\nGitlab as a Code Review Tool I see this as one of my biggest successes - I managed to convince decision makers in the IT department to change the Code Review tool: from Crucible we switched to Gitlab Premium! I was the initiator of this project, but also one of the coordinators of the entire process - from gathering information about the requirements set by development teams and about the tool\u0026rsquo;s capabilities, through supporting the migration process, to preparing a document describing the Code Review process in Gitlab.\nPreviously, when code review was performed on Crucible, Gitlab was also in the toolset, but not in the Premium version, and was only used to store the code and carry out the CI/CD process. This had a major disadvantage - code review was performed in isolation from the QA process result, so the reviewers either verified the pipeline result manually or left it on the shoulders of the author of the changes and focused only on what changes were made, not if they worked.\nMore subjectively: Crucible\u0026rsquo;s UI is specific. People who are used to Gitlab / Github do not feel comfortable there 😅. From my perspective, this change drastically improved the way you work with the code review of delivered changes.\nA separate topic is the transition to the Premium version, which offers many features to improve processes:\ncode owners merged result pipelines merge trains (we do not use these yet) However, for people who felt a strong habit to the previous process, just today we have completed the integration of Gitlab-Jira through development panel - and yes, I initiated and piloted this too 😉\nOrganisational structure in Gitlab I proposed and implemented (so far manually managed) an organisational tree structure that defines development teams and area meta-teams. Thanks to this, it is possible to:\nassign specific groups of people as code owners mention groups (teams) in Merge Requests grant permissions to projects or entire project groups by sharing them with a group (which significantly simplifies access management, although has its drawbacks) Operating on groups greatly facilitates onboarding/offboarding or any inter-team reshuffling, because users only need to be added/removed to/from groups, instead of tediously going through countless places in order to grant/revoke individual permissions.\nChanging the deployment process The main GetResponse application\u0026rsquo;s deployment is quite complicated, and I won\u0026rsquo;t describe it. In any case, it takes a long time because it is multi-stage and requires various types of verification. When I joined GR, the process of such a deployment basically was blocking the main development branch into which developers were integrating their changes. Therefore, it was not possible to consider it as #Continuous Integration, which had obvious drawbacks (e.g. late finding out conflicts between changes introduced by different people).\nSo I proposed some minor changes that made the main development branch unlocked, and developers can integrate into it at any time (when the changes pass the code review and QA process, and are approved by reviewers). In addition to outlining the concept, I also dealt with the adaptation of the documentation on Confluence and the modernisation of the Gitlab CI definition.\nWe\u0026rsquo;re still looking at this process and thinking how it could look like. I\u0026rsquo;m sure there\u0026rsquo;s much more to improve in order to make delivering value to GetResponse\u0026rsquo;s clients more comfortable, enjoyable and efficient from developers\u0026rsquo; perspective.\nShared .idea directory I don\u0026rsquo;t know the exact data, but it seems the vast majority of GR developers use PHPStorm. It\u0026rsquo;s a great development environment that helps a lot in working with projects efficiently. We have a local runtime environment based on Docker, all kinds of scripts and tools that unify the production process. But how to automate it within the IDE?\nThe .idea folder comes in handy - it\u0026rsquo;s a metadata container in which PHPStorm stores a lot of information about how the project should work. It\u0026rsquo;s common to see this directory added to .gitignore, but is that right? I used to think so, but when I started working with GetResponse, I kept finding myself having to configure a lot of things by hand and thought to myself, \u0026ldquo;Why does every new employee have to waste time setting up something that should be available immediately after cloning? project?\u0026quot;😉\nSo I took the initiative to follow JetBrains recommendations of sharing some settings by adding specific files from .idea directory to the #Git repository, which I then implemented. Thanks to this, all developers share the following configurations:\nPHP version required to run the application remote PHP interpreter based on Docker container in local stack XDebug using a remote interpreter connections to databases running in the Docker stack Symfony plugin #PHPStan and PHPCodeSniffer inspections based on a remote interpreter Run/Debug configurations (running various make or Composer scripts from the IDE menu) The main advantage of this approach is the minimisation of the work that needs to be done to start working with the project. Everything you need just works right after setting up the project. And if new configurations are added as the project develops, they will appear automatically for each developer.\nModernisation of Renovate Bot integration Renovate is a tool that automates the tedious process of maintaining up-to-date dependencies. It was already implemented when I joined GR, but I wouldn\u0026rsquo;t be myself if I hadn\u0026rsquo;t found a hole in the whole 😉\nThere were several details that were affecting package upgrading process:\nRenovate Bot was running from a non-standard image which generated additional maintenance overhead and caused technical debt rangeStrategy was set so that the bot modified composer.json by setting the latest versions of packages as constraints, which could lead to problems with dependency resolution (in fact, the lowest version should be required, those that actually contain what is needed to run application - all newer versions are only optional from the perspective of our project, and the wider range of supported versions means the higher probability that the dependencies of other packages will not conflict with our requirements). At the same time, modification of composer.json was causing a change of content-hash in composer.lock, which, due to the distributed work of many people, often led to conflicts in Git\u0026hellip; Merge Requests issued by the bot contained mass package updates, which made their verification difficult, and when problems were encountered it wasn\u0026rsquo;t clear which package caused them Merge Request titles were not friendly So I sat down and modified the process. We started using the official Renovate image, which includes everything we need to analyse the dependencies of many popular package managers. I created presets, which defined our expectations for PHP packages (ignoring 0.* development versions; version updates only within the declared constraint, with preservation of Semantic Versioning; updating individual packages within single MR; linking internal packages from the company\u0026rsquo;s on-premise Private Packagist in the table summarising the changes). These presets have been created in a separate repository so that they can be used in any PHP project. Support for other managers has been added - Renovate is also keeping track of Dockerfile and .gitlab-ci.yml. We also used the code owners mechanism (initiated earlier by me) to automatically assign reviewers to created merge requests.\nAll these changes allowed us to update dependencies on a daily basis without much work. Merge Requests contain single package updates, so the diff is small, the content-hash does not change, so you can merge all issued MRs once they have been approved by reviewers and passed the pipeline. In theory, we could even let the bot automatically merge changes after passing the pipeline, but we haven\u0026rsquo;t matured for this yet 😅\nMulti-stage Docker build In my opinion, the Docker-based local environment is a must-have to think about achieving repeatable results for anyone working with the project. Such an environment should reflect the production environment, maybe not in the context of a full infrastructure, but at least the runtime environment: PHP version, available extensions, system libraries - all this should be consistent.\nIn GetResponse Dockerfile had some discrepancies in terms of production state, also it was structured in such a way that build cache got outdated quite easily and each image rebuild took a long time. During this year, me and my colleagues with DevOps flairs managed to carry out a lot of work in this area, thanks to which:\nmulti-stage build was introduced with an explicit division into the runtime environment (operating system, packages) and building the application itself (installing project dependencies, generating application cache or other static files) we separated development targets (used in CI to run QA tasks) and production targets (intended to run, surprisingly, in production 😉), having a common base, but differing in many aspects from each other to work with the application locally, we used a target that does not contain the application itself, because the application\u0026rsquo;s directory in the image is overridden by the docker compose volume. This reduces the time required to build an image locally and virtually eliminates the risk of build cache invalidation (operating system and packages change extremely rarely). we managed to align the Docker runtime with the current state of the production (yes, we haven\u0026rsquo;t got there with Docker yet, but we\u0026rsquo;re close 😅) and create tasks to verify whether the extensions we added based on the production state are actually required (or maybe the code that used them is just dead) It was a really good job 👍\nClean up Composer dependencies When I started working with GetResponse, there were a lof of #Composer dependencies marked as abandoned. This means that they were neither developed nor supported, and thus they were a technical debt and a potential source of problems. I managed to clean up the dependencies so that there was not even a single abandoned package.\nA lot of packages were also locked on a specific version, so I allowed them to update to newer ones. The record holder was aws/aws-sdk-php, which was updated from version 3.103.2 to 3.208.5 😅 Remember: if you have a problem with your application after updating the package, try to diagnose the cause of the problem and eliminate it, if you can\u0026rsquo;t do it - report the bug on Github / Gitlab. Only as a last resort, block the faulty version of the dependency, still avoiding rigid constraints, because this is a straight path to a huge technical debt that will sooner or later explode.\nUpdated Symfony components When I installed project\u0026rsquo;s dependencies for the first time, there were Symfony components even in the 3.4 version! We have successively upgraded the versions of these packages - first to 4.*, and in recent days my Merge Request has been added to the main branch, which updated all components to the 5.4 version. It was not as easy as it looks, because the application uses a lot of internal components that are based on Symfony components and unraveling these dependencies in a dozen repositories required a lot of gymnastics. Unfortunately, we will have to wait for v6, because of PHP8 mentioned before\u0026hellip;\nGetting rid of the oldest versions of the components allowed usage of symfony/framework-bundle and implementing a kernel that can automatically build a DI container (for people working with the regular Symfony it seems natural, but GR does not use the Symfony Framework, but only its individual components - the whole is glued together in a custom way).\nOther The initiatives described above were often spread over time and consisted of many stages. In between, various other, smaller tasks were resolved. I have provided advice and assistance many times, be it in the form of verbal discussions or as a reviewer in Merge Requests. I have implemented a lot of small improvements regarding the tools or processes used, but it makes no sense to describe all of them, because it would be difficult to remember them all 😉\nOngoing projects Direction: Kubernetes! All the work mentioned above with Dockerfile and the rest of the build-related files had one goal: migrating the application to a Kubernetes cluster. We use Kubernetes clusters extensively at the development stage (test environments, Gitlab runners) and partially in production. As I mentioned, the GetResponse application does not use Docker (let alone k8s) in production, but we are close to that!\nPerhaps this month we will manage to run an instance on the cluster and redirect part of the traffic there. However, it will not be the end of the work, because the GR infrastructure is more complex - the migration process to k8s consists of many stages, and I am one of the coordinators of this process 😁\nRoad to Cloud In the meantime, I was honored with the role of Chief Architect in the Road to Cloud project, which aims to migrate GR services to the cloud. Before I took this role, a few small applications had already moved there, but for the entire organisation it was just a warm-up. I have been trusted, which I really appreciate, because let\u0026rsquo;s face it - I do not have much experience in this area, because I have not had the opportunity to use cloud solutions before. Fortunately, it is not that I have to do this migration myself - I work with the DevOps team and developers by coordinating the work, organising meetings, writing notes, creating and carrying out tasks and managing a roadmap. The project is demanding and time-consuming, and it has to be reconciled with other ongoing tasks, so it can be tough with the progress of work sometimes.\nThe mentioned migration of GetResponse applications to Kubernetes is in fact a stage of the Road to Cloud project: simply wanting to move to the cloud, we want to have a proven solution based on Docker. It will also make it easier for us to migrate to PHP8, because having a runtime environment wrapped in an image, we can freely change it without going beyond the development process (implementation is only an image replacement, no infrastructure work is required).\nNumerical trivia So far, 125 of my merge requests on Gitlab (I do not know the number of MRs on Crucible, because it has already been turned off), I had a dozen or so additional requests on behalf of Renovate Bot (in the case of PHPStan updates, additional changes were often required) I participated in about 60 merge requests on Gitlab as a reviewer (again I do not know the number from Crucible, but it was probably twice as much or even more), adding an endless number of comments 😉 I created 182 tasks in Jira (of which 106 have already been completed) I work on a MacBook for the 1st time, and during these 12 months I had as many as 3 🤪 Summary It was a very successful year! Not everything went as expected. I didn\u0026rsquo;t do everything as I should. However, looking at the overall picture, taking into account all the things that have been achieved, I can confidently say that I am satisfied 🙂\n","permalink":"https://blog.codito.dev/2022/05/a-year-at-getresponse/","summary":"\u003cp\u003eI really don\u0026rsquo;t know when it happened, but it\u0026rsquo;s been \u003cstrong\u003ea year\u003c/strong\u003e since I joined \u003ca href=\"/tags/getresponse\" target=\"_blank\"\u003e#GetResponse\u003c/a\u003e as Software Architect 😵 It was an intense and fruitful time worth summing up!\u003c/p\u003e","title":"A Year at GetResponse"},{"content":"PHP 8.2 is set to be released at the end of 2022, but final date will be announced at some point in the future. In this post I will evaluate all the features, improvements and deprecations. I\u0026rsquo;ll try to keep this article up-to-date with RFCs that will be accepted after publishing.\nThis is living post, updated when new RFCs are accepted for PHP 8.2 - RFCs added in newer version of the post will be marked with 🆕 rfc Disjunctive normal form types We already had in PHP union and intersection types — time for disjunctive ones! From version 8.2 it will be possible to use complex types like A|(B\u0026amp;C). The type system in PHP is not perfect, but it is being gradually enriched and developed. This time the change is a typical evolution of existing types, or rather the way how we can use types - it will be possible to combine union types (logical OR: |) and intersection types (logical AND: \u0026amp;), thanks to which it will be possible to model expected / returned types in a more flexible manner.\nIt\u0026rsquo;s hard to talk about unequivocal profit here because this change has an infinite number of usages. At the same time, it brings a lot of challenges for developers of tools operating on Abstract Syntax Tree, such as #PHPStan or #Rector - all of them have the difficult task of adapting to changes in syntax (although a large part will of course be done in the parser) and adding rules to verify the correctness of the code or to allow its refactoring.\nIt is worth noting that the RFC introduces DNF types, the syntax of which is clearly defined: it is a OR set, in which individual elements can take the form of the AND set. So A|(B\u0026amp;C) is the correct type, while A\u0026amp;(B|C) is not, and will cause a parsing error.\nThis change introduces backward incompatibility, because from version 8.2 ReflectionUnionType can now contain ReflectionIntersectionType elements. rfc Read-only classes PHP 8.1 introduced readonly modifier, which can be used for marking class\u0026rsquo; properties, so their value can be only set once, and then it can be only read. PHP 8.2 goes one step further and allows using readonly in class\u0026rsquo; context — effectively it\u0026rsquo;s like adding modifier to all class\u0026rsquo; properties, but at once! I am really happy that PHP follows the blow and introduces ability to use readonly on class level. It\u0026rsquo;ll be now possible to model data transferring objects (DTO) even easier than before. In PHP 8.1 usage was like this:\nclass Foo { public function __construct( readonly public string $foo, readonly public string $bar, readonly public string $baz ) {} } Starting with 8.2, in order to achieve full object immutability, readonly will have to be used only once:\nreadonly class Foo { public function __construct( public string $foo, public string $bar, public string $baz ) {} } No doubts here — it\u0026rsquo;s a great addition that will be widely used both in standard PHP library and in open source packages. Of course, you should read the full RFC, especially section about restrictions, so you\u0026rsquo;ll know what readonly usage is not allowed.\nInteresting point from Frank de Jonge:\nreadonly without a way to do withers makes it a half measure for me. It\u0026#39;s great for signalling immutability but now my immutable toolbox is cut into sections where the capability of one is explicit where it can\u0026#39;t be for another. I can generate that stuff, but still.\n\u0026mdash; Frank de Jonge (@frankdejonge) May 24, 2022 It means that readonly disallows modification of properties\u0026rsquo; values after their initialisation, so it\u0026rsquo;s harder to create fluid API that returns immutable object created from another immutable object (e.g. (new \\DateTimeImmutable())-\u0026gt;modify('+1 day')) based on read only properties. In my opinion it\u0026rsquo;s not something that readonly is intended for - it is great for modeling simple, immutable data structures (DTO), but for objects containing logic and API you should use private properties and methods (modifiers and getters).\nrfc Constants in Traits 🆕 Until now, traits allowed declaration of properties and methods only, which effectively limited the scope of potential usages. From version 8.2 it will also be possible to declare constants on the trait level. A trait is a symbol that allows the same code to be reused in many places without interfering with the inheritance tree or the composition applied. Class can inherit from other class, implement interfaces, and at the same time it can use traits completely independently (of course there is a risk of name conflicts, but these can be resolved at the import level). Unfortunately, the lack of support for constants resulted in the developer being faced with a choice:\nignore the encapsulation rule and use, within trait, constants declared outside of it (in places where the trait is used) do not use trait and replace them with another architectural solution The first option is unacceptable to me. Personally, I strictly adhere to the fact that the trait is a closed set of properties and methods, so that it does not use anything declared outside the trait. If needed, trait can define abstract methods that must be implemented in a class that uses that trait.\nThis is bad practice to use methods/properties within traits\u0026#39; methods, that don\u0026#39;t belong to this trait. Trait should not know anything about its parent, it should be a piece of reusable code with self-contained context and parameters passed from the outside.\n\u0026mdash; Greg Korba 🛠️🛹 Codito (@_Codito_) July 9, 2022 So, as you can guess, the accepted RFC brings a change that I support and which I will probably use sooner or later 🙂 From version 8.2, the following code will be correct:\ntrait Example { public const ONE = 1; protected const TWO = 2; private const THREE = 3; public function count(): string { return implode(\u0026#39;, \u0026#39;, [self::ONE, self::TWO, self::THREE]); } } This will allow better symbol modeling, and while traits aren\u0026rsquo;t one of my favorites, I\u0026rsquo;m glad that the number of potential bad uses will be limited. The rest is in the hands of the developers 😉.\nrfc Fetch properties of enums in constant expressions 🆕 PHP 8.1 introduced support for enums, but real-world usage has shown that in practice theirs usage is limited. This RFC improves the way how we can work with enums - it allows fetching properties of enums in constant expressions. Introduction to this section is a bit enigmatic, doesn\u0026rsquo;t it? No different with enums, their usage seems to be not-so-easy for some people. The problem is a fact that enums are objects - their usage is based on instances of specific enum symbol, which improves type system for arguments and return values. Unfortunately, it\u0026rsquo;s not possible to fetch properties in constant expressions. Below you can see examples of usages that will be allowed in PHP 8.2:\nenum E: string { case Foo = \u0026#39;foo\u0026#39;; } const C = E::Foo-\u0026gt;name; function f() { static $v = E::Foo-\u0026gt;value; } #[Attr(E::Foo-\u0026gt;name)] class C {} function f( $p = E::Foo-\u0026gt;value, ) {} class C { public string $p = E::Foo-\u0026gt;name; } // The rhs of -\u0026gt; allows other constant expressions const VALUE = \u0026#39;value\u0026#39;; class C { const C = E::Foo-\u0026gt;{VALUE}; } Voting ended with 24:11 result, so decision wasn\u0026rsquo;t unambiguous. Discussion was barely existent, at least not in official channel 😉. Main arguments against was inconsistency in the language, by in my opinion deviations from conventions are acceptable if they resolve actual community problems.\nPersonally, I would go with support based on constant-like usage, so it would be possible to use Some::thing instead of Some::thing-\u0026gt;value. But as far as I know it wouldn\u0026rsquo;t be fully compatible with declare(strict_types=1); (well, not without some internal hacking\u0026hellip;). Accepted format enforces some boilerplate code, but it solves the problem 🤷.\nIt is worth mention that there is another discussion about enums and if this RFC get accepted, it will be even easier to work with enums. rfc Redacting parameters in back traces To prevent sensitive parameters from appearing within a stack trace this RFC proposes a new standardized \\SensitiveParameter attribute that can be applied to a function\u0026rsquo;s parameter to indicate that the parameter contains sensitive information that must not appear in back traces. Probably each of us, while wandering around the Internet, encountered an error connecting to the database like:\nPDOException: SQLSTATE[HY000] [2002] No such file or directory in /var/www/html/test.php:3 Stack trace: #0 /var/www/html/test.php(3): PDO-\u0026gt;__construct(\u0026#39;mysql:host=loca...\u0026#39;, \u0026#39;root\u0026#39;, \u0026#39;password\u0026#39;) #1 {main} With a bit of luck (or common sense), the owner of the site used a password long enough that the error displayed was truncated and didn\u0026rsquo;t open access to the database for every visitor (well, at least made it more difficult). Of course, the problem does not apply only to databases and errors displayed explicitly to the user - the proposed attribute hides the values in stack traces, and these can also be sent to external services, e.g. #Sentry, which can also anonymise various data. But firstly, it must be properly configured, and secondly, these mechanisms most probably will not work with stack traces. So it\u0026rsquo;s good to be able to hide sensitive data on the application side.\nThe new attribute works as follows:\n\u0026lt;?php function test( $foo, #[\\SensitiveParameter] $bar, $baz ) { throw new \\Exception(\u0026#39;Error\u0026#39;); } test(\u0026#39;foo\u0026#39;, \u0026#39;bar\u0026#39;, \u0026#39;baz\u0026#39;); /* Fatal error: Uncaught Exception: Error in test.php:8 Stack trace: #0 test.php(11): test(\u0026#39;foo\u0026#39;, Object(SensitiveParameterValue), \u0026#39;baz\u0026#39;) #1 {main} thrown in test.php on line 8 */ I believe that this is a useful feature that can improve the #security of an application. I don\u0026rsquo;t quite like the way Object(SensitiveParameterValue) is presented because this form is also used for scalar values, which introduces some discrepancy between the actual type and the one displayed in stack trace. I would prefer something like SensitiveParameter\u0026lt;string\u0026gt;, SensitiveParameter\u0026lt;Foo\u0026gt; etc. so essential information about the passed parameter is not lost. However, it is a detail that does not reduce the usefulness of the attribute itself.\nrfc Deprecate dynamic properties The creation of dynamic properties on classes that aren\u0026rsquo;t marked with the #[AllowDynamicProperties] attribute is deprecated in PHP 8.2 and becomes an Error exception in PHP 9.0. All used properties should be declared in the class declaration. Anyone who has worked with legacy systems (or simply in teams where code quality was not a priority\u0026hellip;) knows that dynamically defined object properties are a real nightmare. Sure, modern IDEs can warn us and point out such places, but without the support of tools, these properties are difficult to work with. They can be quite confusing, because when looking at the code in which some properties are used and at the class definition, we can get the impression that the code is referring to non-existent data, while it has just been dynamically declared somewhere else. On the other hand, the fact that PHP implicitly creates new properties means that we are not protected against common typos or mistakes, which, as we know, can happen even to the best 😉\nLet me demonstrate example straight from RFC:\nclass User { public $name; } $user = new User(); // Assigns declared property User::$name. $user-\u0026gt;name = \u0026#39;foo\u0026#39;; // Oops, a typo: $user-\u0026gt;nane = \u0026#39;foo\u0026#39;; This kind of usage up to PHP 8.1 would just create new property, which could lead to several problems (especially in the projects not taking advantage from #static analysis). From version 8.2 E_DEPRECATED warning will be raised, while PHP9 will just throw an Error exception.\nAt the same time this RFC introduces new #[AllowDynamicProperties] attribute, which allows you to maintain the current behavior (no deprecation warning in 8.2 and no exception in PHP9), so it will be possible to migrate existing project to newer PHP versions without significantly modifying the code. Note, however, that it will not be possible to use the #[AllowDynamicProperties] attribute in classes marked as readonly - such usage will throw an exception.\nThis kind of language-level enhancements improve code quality and help build good practices. I can only applaud 👏\nrfc Deprecate partially supported callables Some callables are currently accepted by the callable type, the is_callable() function and call_user_func(), but are not supported by $callable(). This RFC aims for cleaning this inconsistency. There is currently a callable meta-type in PHP that can be used to type arguments and function returns. Unfortunately, not all the data accepted by the callable type can actually be called as$callable(), which is very inconsistent and introduces the risk of serious errors. The purpose of this RFC is to remove support for those callables that cannot actually be called - as of PHP 8.2, functions that use callable (e.g. call_user_func()) will trigger an E_DEPRECATED warning informing about the deprecated usage. Ultimately, in version 9.0, such support will be completely removed and the callable type will not accept the following formats:\n\u0026#34;self::method\u0026#34; \u0026#34;parent::method\u0026#34; \u0026#34;static::method\u0026#34; [\u0026#34;self\u0026#34;, \u0026#34;method\u0026#34;] [\u0026#34;parent\u0026#34;, \u0026#34;method\u0026#34;] [\u0026#34;static\u0026#34;, \u0026#34;method\u0026#34;] [\u0026#34;Foo\u0026#34;, \u0026#34;Bar::method\u0026#34;] [new Foo, \u0026#34;Bar::method\u0026#34;] Most of theme have their own replacements (details in the RFC), I also suspect that sooner or later there will be rules in #Rector that will enable automatic code refactoring.\nI am glad that PHP cleans up these kinds of things, because each change like this improves the quality of the language and increases its stability from the developers\u0026rsquo; perspective. More strictness on the language level also means fewer needs in tools like #PHPStan.\nrfc Expand deprecation notices for partially supported callables This RFC complements the previously approved change by introducing warnings about deprecated callable formats where it was previously decided not to trigger them. The purpose of this RFC is to warn people early that their code will stop working on PHP 9.0. As some callable formats were dropped in the earlier RFC, but some specific usage paths were not taken into account in the context of generating E_DEPRECATED errors, this could lead to a situation where code working correctly on PHP 8.2 and not generating any deprecation warnings about abandoned formats, could stop working in PHP 9.0.\nI am glad that the PHP developers care about the language users and have decided to change their previous decision and introduce warnings in these places. Image-wise, it is definitely a good decision, as any potential problems after the 9.0 release would have a negative impact on the perception of PHP.\nrfc Locale-independent case conversion PHP string functions will do ASCII case conversion, sort function will do sorting by ASCII case folding. This change should interest those who use multilingualism in their systems and operate on strings containing characters outside of the standard A-Z range - in PHP 8.2, functions that operate on strings will correctly handle diacritics. This means that strtoupper ('ą') will finally return Ą, not ą as before. The sort functions will also start behaving as expected.\nrfc Allow null and false as standalone types Add support for using null and false as stand-alone type declarations, wherever type declarations are currently allowed. As we know perfectly well, PHP has a long backwards-compatibility tail behind it, which in some way creates a bad image of the language and can be a source of the jokes. In any case, many PHP built-in functions historically have signatures that do not fit with modern technology. For example, strpos() returns a numeric value indicating where the specified string occurs OR just false when the specified string is not present in another string. The problem with this signature is that until version 8.0 it could not be described other than with the @return int|false in phpDoc, which was not really of great value. PHP8 made it possible to use false in the union type, so signatures like strpos(/* ... */): int|false are fully valid. This RFC goes a step further and allows the use of false and null as standalone types.\nAt first glance, this change is completely useless (who would want to use false as the return type 🤔?). However, when reading the RFC, we can see examples describing covariance and contravariance, and then this change makes sense (a little, but still). Let\u0026rsquo;s look at an example:\nclass User {} interface UserFinder { function findUserByEmail(): User|null; } class AlwaysNullUserFinder implements UserFinder { function findUserByEmail(): null { return null; } } With changes from this RFC, it is possible to implement the interface and comply with LSP, while limiting the original signature of the return type User|null to only null. In my opinion, this is not something that will find wide adoption - personally, I see a field for showing off in tests or development environments, where some parts of the system will simply be replaced with \u0026ldquo;empty\u0026rdquo; implementations. Though maybe I just haven\u0026rsquo;t encountered scenarios where this change would solve the real problem 😉\nrfc true as a standalone type Following the earlier RFC PHP also introduces true as a standalone type into the language. There is not much to write about it: in PHP 8.2, true will become a standalone type, just like false and null were accepted before. Like in the previous RFC\u0026rsquo;s context, I personally do not see any real added value here. I suspect that this has implications in the standard PHP library and may apply to specific open source libraries. However, even if I do not see the value, it does not mean that this change is not needed - it is good to have more room for maneuver.\nrfc Deprecate ${} string interpolation This RFC proposes to deprecate some string interpolation syntax in PHP 8.2 and remove them in PHP 9.0. Currently, PHP has four ways to dynamically inject values into strings: \u0026quot;$foo\u0026quot;, \u0026quot;{$foo}\u0026quot;, \u0026quot;${foo}\u0026quot; and \u0026quot;${$foo}\u0026quot; (the last case is the so-called variable variable: a dynamic reference to a variable by a value assigned to another variable - quite niche syntax and rather rarely used). This RFC is deprecating the last two methods and support for them will be completely removed in PHP9.\nThe reason that ${} interpolation was deprecated is that they both had nearly identical syntax but completely different behavior. Although it seems that such conflicts are edge cases, it is good that PHP cleans it up and reduces the amount of supported interpolation syntax. New types of interpolation may be added in the future (such as that suggested in RFC \u0026quot;{$:func()}\u0026quot;), but before that it would be great to standardise what is already available in the language.\nrfc Remove support for libmysql from mysqli The proposal is to remove the support for building mysqli against libmysql in the next version. As mysqlnd is the default option, most users should not observe any difference. As of PHP 8.2, it will not be possible to link mysqli against any version of libmysql. If the option --with-mysqli will be supplied, mysqli will be built with mysqlnd. This change is so low-level, that most PHP users will never see the difference. However, there is a significant backward incompatibility aspect of this change - mysqlnd does not and will not support automatic reconnection. Libraries or systems that have used this functionality will have to find other ways to achieve this behavior.\nrfc Deprecation of utf_encode() and utf8_decode() IN PHP 8.2 these function will trigger E_DEPRECATED, and will be completely removed in PHP 9.0. The motivation behind this RFC is that the mentioned functions are not well-defined. They have a limited functionality while their name might suggest that they are more universal. In general, there are many encoding formats and this is a complicated topic as a whole, so people often make mistakes when using these functions. They will therefore be marked as deprecated in PHP 8.2 and then removed in PHP 9.0.\n","permalink":"https://blog.codito.dev/2022/05/new-in-php-8.2/","summary":"\u003cp\u003ePHP 8.2 is set to be released at the end of 2022, but final date will be announced at some point in the future. In this post I will evaluate all the features, improvements and deprecations. I\u0026rsquo;ll try to keep this article up-to-date with \u003ca href=\"https://wiki.php.net/rfc#php_82\"  target=\"_blank\"\u003eRFCs\u003c/a\u003e that will be accepted after publishing.\u003c/p\u003e","title":"New in 🐘 PHP 8.2"},{"content":"I don\u0026rsquo;t like code redundancy, so I wanted to create convention for sharing snippets between language versions of the posts (because code is always in English 😉). As it turned out, it\u0026rsquo;s not that obvious in Hugo.\nResearch on this matter It would seem that the matter is very simple: we create a directory, refer to the snippets from each language version, display the same in each one, and after making any changes in one place, we see the effect in each. Well, not really 😉\nI\u0026rsquo;ve tried many approaches, from headless bundles, through leaf bundles, to shortcodes with .Site.GetPage usage (as I found in several articles). There always were problems, and I think it\u0026rsquo;s related to chosen translation by content directory convention.\nWhen I was starting to think about changing convention and throwing everything into one content directory (and using *.\u0026lt;lang\u0026gt;.md for multi-language support), I stumbled upon one Hugo\u0026rsquo;s built-in function: readFile. Finally it was possible to refer files from the outside of contentDir.\nSnippet implementation I went with such file structure:\nhugo/ ├─ content/ │ ├─ _snippets/ │ │ ├─ 2022/ │ │ │ ├─ 04/ │ │ │ │ ├─ some-post/ │ │ │ │ │ ├─ snippet1.md │ │ │ │ │ ├─ snippet2.md │ ├─ en/ │ │ ├─ 2022/ │ │ │ ├─ 04/ │ │ │ │ ├─ some-post.md │ ├─ pl/ │ │ ├─ 2022/ │ │ │ ├─ 04/ │ │ │ │ ├─ jakis-post.md Next, I\u0026rsquo;ve created shortcode for rendering code snippets:\n\u0026lt;!-- layouts/shortcodes/snippet.html --\u0026gt; {{ $path := (printf \u0026#34;/content/_snippets/%s\u0026#34; (.Get 0))}} {{ if eq (path.Ext $path) \u0026#34;\u0026#34; }} {{ $path = (printf \u0026#34;%s.md\u0026#34; $path)}} {{ end }} {{ if os.FileExists (path.Clean $path) }} {{ $page := os.ReadFile (path.Clean $path) }} {{ $page | markdownify }} {{ else }} \u0026lt;div class=\u0026#34;banner banner-danger\u0026#34;\u0026gt;{{ i18n \u0026#34;invalid_snippet_reference\u0026#34; }}\u0026lt;/div\u0026gt; {{ end }} Thanks to it, in posts I can do this:\n{{\u0026lt;/* snippet \u0026#34;2022/04/some-post/snippet1\u0026#34; */\u0026gt;}} {{\u0026lt;/* snippet \u0026#34;2022/04/some-post/snippet2.md\u0026#34; */\u0026gt;}} Details worth mentioning:\nFragment of the path (content/_snippets) is hardcoded within shortcode, in order to use shorter paths in actual posts. It will allow simpler migration if there\u0026rsquo;s need for moving all snippets to different directory *.md extension is optional: shortcode will automatically add it if omitted just in case, path is validated with os.FileExists and if file does not exist, error banner is rendered instead. Thanks to this I can at any point easily display additional information or add error reporting (e.g. link for creating issue or JavaScript sending some request) Important fact is that hugo server does not react to the changes in the snippets. It\u0026rsquo;s weird, since these are located in content directory, which is being watched by the server. Unfortunately, if you want to see changes in snippets, you need to run hugo server --disableFastRender AND modify file, which includes the snippet. Probably mount would fix it, but I did not try it because it complicates the config too much. The advantage of this approach is also the fact, that in future posts I will be able to refer to the code used previously, by including a selected snippet without having to duplicate the code in another file. Are there cons? It will turn out in practice 😅\n","permalink":"https://blog.codito.dev/2022/04/reusable-code-snippets-in-hugo/","summary":"\u003cp\u003eI don\u0026rsquo;t like code redundancy, so I wanted to create convention for sharing snippets between language versions of the posts (because code is always in English 😉). As it turned out, it\u0026rsquo;s not that obvious in Hugo.\u003c/p\u003e","title":"Reusable code snippets in Hugo"},{"content":"Building images introduces many challenges, one of them is safety of persisted data. How to avoid publishing sensitive data, that should remain private?\nI don\u0026rsquo;t speak about code itself, because if we build images for internal usage, we must ensure safe storage and usage, which is totally different story. Imagine though we build publicly available tool that helps in day by day work, and its code is also public (e.g. composer). So where is the place for secrets in the release process?\nWhat are secrets? Secrets are data, as far as its name suggests, that should remain only for ourselves. These can be passwords, tokens or certificates used for integrating with external services. It was already possible in Docker to use such secrets in Swarm stacks based on docker compose. Thanks to that it\u0026rsquo;s really easy to pass sensitive data to the application, but only in runtime — secrets were not available during build, aren\u0026rsquo;t persisted inside build image (hence it\u0026rsquo;s not possible to get there from build layers\u0026rsquo; cache), in different words: can be used only after passing them to running container.\nSensitive data in a build process Mentioned #Composer and other package managers, during image build process are used for resolving and downloading dependencies. In case of Open Source projects most often these dependencies are also publicly available on GitHub, Gitlab or other. But those providers secure themselves from improper usage, so for mass downloading you probably need to authorise, hence you need tokens on build stage.\nWhile building images that use packages from external registries (Composer, NPM, PYPI), we may need to integrate with private registry (Private Packagist, Artifactory, Verdaccio). While the presence of the packages\u0026rsquo; code inside the image does not have to be a problem for us, we would not necessarily want to share the authentication data with others 😉\nAnyway, it\u0026rsquo;s not only about packages — we may want to download some artifacts: translations\u0026rsquo; definition, static files\u0026hellip; And these files may be stored in external systems integrated through API, in which we need authenticate too.\nSecrets during build Few days ago in docker/compose repository, long-awaited feature (issue from 2018!) was implemented and merged into main v2 branch — secrets during build 🎉. When it gets released, it can be used like:\nservices: ssh: image: build-test-secret build: context: . secrets: - mysecret secrets: mysecret: file: ./secret.txt FROM alpine RUN echo \u0026#34;foo\u0026#34; \u0026gt; /tmp/expected RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret \u0026gt; /tmp/actual RUN diff /tmp/expected /tmp/actual Having such Dockerfile and #Docker Compose stack we can safely pass sensitive data into Docker\u0026rsquo;s build context. Of course storing it into the file is not a great idea, this example only shows how to pass and use the secret 😉. If we use it correctly, it won\u0026rsquo;t be available in built image and within its build history (image layers can be browsed).\nIt will enable consistent build process using secrets both for #CI and for local development.\nNow we only need to wait for new Docker Compose release 😁\nGeneral availability Support for secrets during build was released in v2.5.0.\n","permalink":"https://blog.codito.dev/2022/04/docker-compose-secrets-available-during-build/","summary":"\u003cp\u003eBuilding images introduces many challenges, one of them is safety of persisted data. How to avoid publishing sensitive data, that should remain private?\u003c/p\u003e","title":"Docker Compose: secrets available during build"},{"content":"Rector is a tool for automatic code refactoring. ECS allows keeping coding standards in the project. From few days, configuration of both tools is more developer friendly.\nAbout #Rector and #ECS I\u0026rsquo;ll write several times for sure because these are extraordinary tools. But for now I wanted to focus on one refreshing change that was added recently.\nNew configurator API is available in:\nRector 0.12.21+ ECS 10.2.0+ Historical context Since both of these tools use symfony/dependency-injection under the hood, for a long time they were forced to operate within API exposed by this component, because many of its classes or methods are marked as final and don\u0026rsquo;t allow extending nor overriding. Everything has changed when Rector implemented architecture based on development repository and so-called \u0026ldquo;prefixed\u0026rdquo; one, which is the result of automatic refactoring and downgrading\u0026hellip; with Rector 😉 Such approach opened many new possibilities and authors took this chance by patching Symfony components, covering their original API with custom one.\nAPI changes genesis While using Rector, at some point I had a problem related to tool configuration in terms of #static-analysis, so I\u0026rsquo;ve started discussion which resulted in my Pull Request. In the end it wasn\u0026rsquo;t merged, but it became base for actual changes in API, introduced by the author, Tomas Votruba. As he said himself, my idea opened new universe of options 😅\nOld vs New Changes in API are not revolution, but nice evolution. Before, you had to configure like this:\nuse Rector\\Core\\Configuration\\Option; use Rector\\Php74\\Rector\\Property\\TypedPropertyRector; use Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $parameters = $containerConfigurator-\u0026gt;parameters(); $parameters-\u0026gt;set(Option::PARALLEL, true); $parameters-\u0026gt;set(Option::AUTO_IMPORT_NAMES, true); $services = $containerConfigurator-\u0026gt;services(); $services-\u0026gt;set(TypedPropertyRector::class); }; After changes instead of using Symfony\u0026rsquo;s configurator, custom Rector configurator is available:\nuse Rector\\Php74\\Rector\\Property\\TypedPropertyRector; use Rector\\Config\\RectorConfig; return static function (RectorConfig $rectorConfig): void { $rectorConfig-\u0026gt;parallel(); $rectorConfig-\u0026gt;importNames(); $rectorConfig-\u0026gt;rule(TypedPropertyRector::class); }; Similar changes were introduced in ECS, of course its API is slightly different than Rector\u0026rsquo;s, but in general it\u0026rsquo;s all about the same idea — using custom configurator methods instead of too general $containerConfigurator-\u0026gt;parameters()-\u0026gt;set().\nEaster is coming... have you find the easter 🥚 ? pic.twitter.com/tF39Xqgy0N\n\u0026mdash; Tomas Votruba (@VotrubaT) April 14, 2022 My proposal of simpler ECS API Personally, I\u0026rsquo;ve switched to new API really quickly, but while working with ecs.php I discovered that new API exposes 2 methods for adding rules: rule() and ruleWithConfiguration(). In my opinion it\u0026rsquo;s cumbersome, so I\u0026rsquo;ve created Pull Request with proposal of simplifying API. Unfortunately it was rejected, though I don\u0026rsquo;t agree with provided reasoning 😉\nWhat do you think?\n","permalink":"https://blog.codito.dev/2022/04/rector-and-ecs-new-configurator-api/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/rectorphp/rector\"  target=\"_blank\"\u003eRector\u003c/a\u003e is a tool for automatic code refactoring. \u003ca href=\"https://github.com/symplify/easy-coding-standard\"  target=\"_blank\"\u003eECS\u003c/a\u003e allows keeping coding standards in the project. From few days, configuration of both tools is more developer friendly.\u003c/p\u003e","title":"Rector and ECS: new configurator API"},{"content":"As I wrote in introduction post - I tend to overthink. I decided to start with absolute minimum, as if against myself. So, how did I manage to design this site?\nDesignated goals By saying \u0026ldquo;absolute minimum\u0026rdquo; I think of:\nusing static site generator in order to be able to work with Git, not with CMS based on database and web interface #continuous deployment, so I can publish in automated way exactly when I want it minimal, though tidy and functional user interface multi language support (#i18n), both for content and URLs site available under HTTPS protocol no costs required (one-time or recurrent) Decisions made Having my tendency to overthinking in mind, I didn\u0026rsquo;t want to spend much time on research for potential solutions. I chose what I\u0026rsquo;ve already known plus brand-new things that emerged during implementation process, but I\u0026rsquo;ll cover it later.\nGitlab For the code I chose Gitlab because:\nI use it on daily basis for years, and I know its capabilities much more than Github\u0026rsquo;s it allows nested project groups, which I personally really like in terms of project organisation Cloudflare At the beginning I wanted to use Cloudflare only for DNS management and for handling SSL certificates, but after configuring domains I saw Pages section, and I decided to try it out. I did not know this feature, even though general availability was announced year ago.\nHugo - Static Site Generator Hugo is a static site generator, which is highly flexible, extendable and performant. I did not use it before, but I saw many technical websites built with it, so I assumed it\u0026rsquo;s right fit for content management.\nAction plan Having decided what building block I\u0026rsquo;ll use I did research how to glue them together. The plan was to create Cloudflare Pages, point custom domains there and prepare automatic deployment for content built with Hugo, using Gitlab CI.\ngraph LR; A[Content creation] --\u003e|git push| B[Gitlab Pipeline] B --\u003e C[Cloudflare Pages PL] B --\u003e D[Cloudflare Pages EN] It looks simple, but as it turned out later, there were complications 😅\nSite skeleton Of course at the very beginning Gitlab project and basic Hugo installation had to be created. There are many installation methods, I\u0026rsquo;ve used brew install hugo. After creating new project using hugo new site command, I could start working on it.\nChoosing theme I\u0026rsquo;ve decided to use Minima, because I wanted to focus on content, not on visual effects. Feature list was good enough for my needs.\nInstallation of themes is done with Git submodules:\ngit submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/paper-mod The only files that we commit to repository are .gitmodules and directory entry, that in fact is a pointer to the submodule\u0026rsquo;s commit, which was installed (and can be later restored using git submodule init).\nIn order to use theme we must configure Hugo by adding theme: paper-mod in config/_default/config.yml\nSupport for i18n I own 2 domains, so I wanted to make use of them and serve language versions separately, without using infix in URL\u0026rsquo;s path. Like in documentation I\u0026rsquo;ve created separate content/en and content/pl directories with proper configuration:\n# config/_default/languages.yml pl: contentDir: content/pl baseURL: https://blog.codito.pl/ languageName: \u0026#39;:poland:\u0026#39; languageCode: pl paginatePath: strona title: Codito.pl weight: 1 taxonomies: category: kategorie tag: tagi series: serie params: # Must be set under languages.xx.params since Hugo 0.112 (instead of languages.xx) # see: https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120 languageAltTitle: Polski en: contentDir: content/en baseURL: https://blog.codito.dev/ languageName: \u0026#39;:gb:\u0026#39; languageCode: en title: Codito.dev weight: 2 taxonomies: category: categories tag: tags series: series params: # Must be set under languages.xx.params since Hugo 0.112 (instead of languages.xx) # see: https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120 languageAltTitle: English It\u0026rsquo;s not complete configuration (I\u0026rsquo;ve omitted menu definition), but it shows the concept - I wanted not only possibility of publishing in two languages, but also full language support in URLs (hence separate taxonomy definitions).\nAnother important feature I wanted guarantee, is ability to navigate between language versions of the same site. In chosen theme it wasn\u0026rsquo;t possible out of the box, so I had to override header.html layout, this thread in Hugo\u0026rsquo;s community platform helped me a lot. But in order to make language switching available, site\u0026rsquo;s versions must be bound together with translationKey which has to be defined in site\u0026rsquo;s front matter:\n--- title: \u0026#34;Hello World!\u0026#34; translationKey: \u0026#34;2022-04-10-hello-world\u0026#34; date: 2022-04-10T01:04:46+02:00 --- Thanks to that, it\u0026rsquo;s possible to match other language versions for specific page and generate proper links.\nComment system Minima theme, which I chose, supports Disqus and Utteranc.es. As a reader I used both of these, and each has its own pros and cons. However, having in mind that this site is for technical content, targeting mostly people from IT, I decided to use system based on Github: utteranc.es 🙂\nIntegration is really simple, so I won\u0026rsquo;t describe it. There is one detail though, which I wanted to point out - permanent relationship between pages and comments. Because URL addresses can change with time, and support for multiple languages is required, again I chose translationKey for that. Thanks to that, regardless of which language version is displayed and commented, everything will go under the same GitHub issue. We\u0026rsquo;ll see if this decision was good, but here\u0026rsquo;s how it was done, with another layout overriding:\n\u0026lt;!-- layouts/partials/utterances.html (for Minima theme) --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; const repo = \u0026#39;{{ .Site.Params.utterances.repo }}\u0026#39; const issueTerm = \u0026#39;{{ if .Params.translationKey }}{{ .Params.translationKey }}{{ else }}{{ .Site.Params.utterances.issueTerm }}{{ end }}\u0026#39; const theme = localStorage.theme ? `github-${localStorage.theme}` : \u0026#39;preferred-color-scheme\u0026#39;; const script = document.createElement(\u0026#39;script\u0026#39;) script.src = \u0026#39;https://utteranc.es/client.js\u0026#39; script.async = true script.crossOrigin = \u0026#39;anonymous\u0026#39; script.setAttribute(\u0026#39;repo\u0026#39;, repo) script.setAttribute(\u0026#39;issue-term\u0026#39;, issueTerm) script.setAttribute(\u0026#39;theme\u0026#39;, theme) script.setAttribute(\u0026#39;label\u0026#39;, \u0026#39;comment\u0026#39;) document.querySelector(\u0026#39;main\u0026#39;).appendChild(script) \u0026lt;/script\u0026gt; Content in sections Another little detail is related to managing content, because I wanted to treat pages and posts differently. The former I wanted to be available under simplest URLs possible, while the latter with publish date structure.\npermalinks: posts: \u0026#39;/:year/:month/:slug/\u0026#39; pages: \u0026#39;/:slug/\u0026#39; Such definition ensures that regardless of how I arrange files under content/\u0026lt;language\u0026gt;/posts, in the end they will be available under proper URL.\nWhen I was verifying correctness of permalinks and taxonomies I\u0026rsquo;ve found out that Minima theme has problems with i18n, and after trying multiple variations of config I decided to switch to PaperMod. I had to fix taxonomy handling too, but in this case it was possible to reach the goal and handle URLs properly. Worth to mention is setting removePathAccents: true if you want to publish in language that contains special chars that don\u0026rsquo;t suit URLs well.\nAutomating deployment As I said at the beginning, site is served through Cloudflare Pages. Configuration is well documented, process and interface are intuitive. What we need:\nconnect Gitlab / Github account with Cloudflare choose repository from which site will be deployed set project\u0026rsquo;s name (will be later displayed on projects\u0026rsquo; list and will be used for creating *.pages.dev subdomain) configure deployment choose branch used for production deployment set build command and output directory with content to serve set HUGO_VERSION environment variable, it should be the latest one (but most important is that you should use the same version for local development) The form allows to choose setting from template, so you can choose Hugo. In my case though, I\u0026rsquo;ve customised settings to reflect multi-language configuration, so I\u0026rsquo;ve set public/pl as \u0026ldquo;Build output directory\u0026rdquo;. Setting hugo --verbose --log --verboseLog --debug as build command might be useful, you\u0026rsquo;ll get more details from build process.\nAfter approving configuration, deployment will start, and if everything have gone well, site will be available under specific subdomain. Of course, it\u0026rsquo;s better to use custom domain, especially considering easy and no-cost SSL certificate.\nFor preview environments you probably want to set HUGO_buildDrafts=true and HUGO_buildFuture=true in order to be able to review drafts and posts with publish date in the future. It can be done after project creation, in its settings (Settings → Environment variables). At this stage, each git push to specified branch will trigger deployment process to Cloudflare Pages. It\u0026rsquo;s possible because Cloudflare creates webhook in source repository, so it gets notified about every commit activity.\nOk, we have one language version deployed, it\u0026rsquo;s time to configure second one. So again, \u0026ldquo;Create a project\u0026rdquo;, choose repository and\u0026hellip; we get warning that repository is already in use 😩\nProblems Challenges encountered Cloudflare Pages 1:1 Gitlab Project It turned out that Cloudflare Pages may use Github / Gitlab repository only once, so it\u0026rsquo;s still possible to serve multi-language sites, but only with language as URL path\u0026rsquo;s segment, like https://example.com/pl/. If you want to handle it like me, with multiple domains, additional work is required.\nDo you remember what I wrote about overthinking? Really, I wanted to avoid time-consuming research, so I quickly decided to choose first thing that came to my head, and I used Gitlab Pages for english version of the site 😅\nI did not want to modify existing Hugo project and split it into two separate projects, so I came up with brilliant idea and in project\u0026rsquo;s .gitlab-ci.yml I\u0026rsquo;ve added trigger for other meta-project\u0026rsquo;s pipeline:\n# Trigger build for Gitlab Pages (english version of the site) gitlab-pages: stage: Deploy trigger: project: codito-net/codito-net.gitlab.io branch: main strategy: depend rules: - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH Gitlab Pages domain convention is pretty straightforward, but considering custom domain that will be used, it does not really matter how we name it. However, personally I like tidiness, so I\u0026rsquo;ve created codito-net/codito-net.gitlab.io so Gitlab\u0026rsquo;s technical domain still is as friendly as it could be.\nThis project contains only Gitlab CI definition:\nimage: registry.gitlab.com/pages/hugo/hugo_extended:0.96.0 variables: GIT_SUBMODULE_STRATEGY: recursive HUGO_ENV: production pages: script: - apk add --update --no-cache git go - git clone --depth 1 --shallow-submodules https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/codito-pl/landing-page.git - cd landing-page - git submodule update --init --recursive - hugo # Workaround for: https://gitlab.com/gitlab-org/gitlab-pages/-/issues/668 - cd .. - mkdir public - cp -R landing-page/public/en/* public artifacts: paths: - public rules: # See: https://docs.gitlab.com/ee/ci/triggers/index.html#configure-cicd-jobs-to-run-in-triggered-pipelines - if: $CI_PIPELINE_SOURCE == \u0026#34;pipeline\u0026#34; In the end Gitlab\u0026rsquo;s pipeline with continuous deployment looks like:\nLet\u0026rsquo;s sum it up:\ngitlab-pages job is only a trigger that starts child pipeline in meta-project. It\u0026rsquo;s important to set strategy: depend in order to get this job\u0026rsquo;s status only when child pipeline ends. Cloudflare Pages job on External stage is an automatic Hugo build triggered by webhook, which will deploy polish version of the site to Cloudflare Pages pages job on Build stage in Downstream pipeline builds english version of the site and saves it as artifact, which is then automatically deployed to Gitlab Pages. Naming convention is important here — Gitlab requires pages job for deployment. As you can read in documentation:\nFor GitLab Pages, this job has a specific name, called pages. This setting tells the runner you want the job to deploy your website with GitLab Pages\nGitlab Pages only with artifact from public Attentive eye could see that in pages job some workaround was used 😉 Unfortunately at this stage of work I\u0026rsquo;ve encountered problem with Gitlab\u0026rsquo;s convention (I\u0026rsquo;ve used different job\u0026rsquo;s name myself at the beginning), and also with requirement of publishing from public directory. Problem is reported and most probably will be solved at some point and Gitlab Pages\u0026rsquo; publishing will be possible from any directory.\nCloudflare Preview Environment baseUrl Another challenge was related to Cloudflare Pages and its preview environments\u0026rsquo; baseUrl. So long as production domains are hardcoded in the configuration (inside repository), preview environments are created under random subdomain. You can\u0026rsquo;t workaround that randomness, but you can use environment aliases. Alias is created for each non-production branch, but if you want to make use of it, you must stick to one development branch which will be used for preview deployments. In my case it\u0026rsquo;s develop branch, so preview environment is available under develop.codito-pl.pages.dev.\nOK, but we don\u0026rsquo;t want other people to access our development instance, so how it can be protected? \u0026ldquo;Access policy\u0026rdquo; comes for the help, you can find it in Cloudflare Pages\u0026rsquo; settings:\nHaving preview environment available under stable address, we can improve build process and set environment\u0026rsquo;s alias as baseUrl. To be fair, I\u0026rsquo;ve stuck here for a while, because Hugo\u0026rsquo;s documentation is not clear enough here. While searching for the solution I\u0026rsquo;ve found this issue, also I\u0026rsquo;ve created community thread. In the end I was able to set base URL with HUGO_LANGUAGES_pl_baseurl environment variable, which I\u0026rsquo;ve set for preview environments:\nWhy this particular variable? Documentation says:\nNames must be prefixed with HUGO_ and the configuration key must be set in uppercase when setting operating system environment variables.\nTo set config params, prefix the name with HUGO_PARAMS_\nBecause I use language versions as sections, my baseUrl parameters also are defined in sections. I use config directory, so according to documentation:\nEach file represents a configuration root object, such as params.toml for [Params], menu(s).toml for [Menu], languages.toml for [Languages] etc…\nHUGO_LANGUAGES_pl_baseurl means:\nHUGO_ guarantees that Hugo will process this variable LANGUAGES_ correspond to main Languages object in the application pl_baseurl corresponds to configuration structure in config/_default/languages.yml Final proces After facing all of these challenges I ended with such process:\ngraph LR; A[Tworzenie treści] --\u003e|git push main| B[Gitlab Pipeline] A[Tworzenie treści] --\u003e|git push develop| G[Gitlab Pipeline] B --\u003e C[Cloudflare External Job] B --\u003e D[Trigger External Pipeline] C --\u003e|Deploy PL| E[Cloudflare Pages] D --\u003e|Deploy EN| F[Gitlab Pages] G --\u003e H[Cloudflare External Job] H --\u003e|Deploy Preview PL| I[Cloudflare Pages] Probably it\u0026rsquo;s not ideal, but it does exactly what it\u0026rsquo;s supposed to. My goal was creating no-cost, two-language site with HTTPS support and automatic deployment process, and it was achieved.\nAt this stage there are cons:\nthere is no preview environment for english version of the site there is no scheduled deployment, so it\u0026rsquo;s not possible to create content with publishDate in the future (site is built after git push, so if we push such content to repository it won\u0026rsquo;t get published until another git push after that date) Rome wasn\u0026rsquo;t built in a day 😉\nSummary I was able to configure Hugo in a way that satisfies my needs (URLs, multi-language under separate domains) Continuous Deployment process ensures really fast content deliverability (commit → push → publish) I can publish from any place and device, I need only access to Gitlab I\u0026rsquo;ve learnt new tools/features: Hugo, Cloudflare Pages, Cloudflare Zero Trust, Gitlab Pages And all of it totally for free 😎 ","permalink":"https://blog.codito.dev/2022/04/hugo-i18n-multi-domain-site/","summary":"\u003cp\u003eAs I wrote \u003ca href=\"https://blog.codito.dev/2022/04/hello-world/\" \u003ein introduction post\u003c/a\u003e - I tend to overthink. I decided to start with absolute minimum, as if against myself. So, how did I manage to design this site?\u003c/p\u003e","title":"Hugo: i18n with multi-domain site"},{"content":"9 years. It took NINE years to finally create landing page for Codito. Yeah, there was site, but it was always \u0026ldquo;in progress\u0026rdquo;, inviting to come back later.\nWhy I wasn\u0026rsquo;t able to do it through these years? There are several reasons, but the most important is: overthinking! 😅\nReally, every time I wanted to do it I was like \u0026ldquo;Mhmmm, so let\u0026rsquo;s start with A.. No, it does not support B. Maybe C - nah, it won\u0026rsquo;t handle D. So I need E! Damn, too much work for now, I don\u0026rsquo;t have time for this\u0026rdquo;.\nSo I was developing myself for a long time, worked with several companies and on multiple projects, gave my best everywhere - except my own portfolio 🙄\nBut at this point of my career I decided that I really need to create it. There is so much knowledge I can share, problems that I solve at work, things I deal with, that I\u0026rsquo;m sure other people would find useful. It will also work as a notepad, so I can benefit from it too 😉\nFrom now on, I\u0026rsquo;ll try to write many different stuff: how-tos, tech news, subjective articles etc. I\u0026rsquo;ll also try to create posts about things from the past, that I find important and want them to be here.\nSo let\u0026rsquo;s do it! 😎\n","permalink":"https://blog.codito.dev/2022/04/hello-world/","summary":"\u003cp\u003e9 years. It took \u003cem\u003eNINE years\u003c/em\u003e to finally create landing page for Codito. Yeah, there \u003cem\u003ewas\u003c/em\u003e site, but it was always \u0026ldquo;in progress\u0026rdquo;, inviting to come back later.\u003c/p\u003e\n\u003cp\u003eWhy I wasn\u0026rsquo;t able to do it through these years? There are several reasons, but the most important is: \u003cem\u003eoverthinking\u003c/em\u003e! 😅\u003c/p\u003e","title":"Hello World!"},{"content":" I am open to work! I am self-didacted IT specialist with experience both as developer and team/tech leader. I like clean code, team-discussed solutions, well-fitted technologies, consistency, automating things and up-to-date tools/dependencies. I really enjoy learning new things and am open to new opportunities.\nIn the meantime I try to be good father for my 3 children and good husband for my fantastic wife. My hobbies: skateboarding, music, football, comics.\n","permalink":"https://blog.codito.dev/about/","summary":"I am open to work! I am self-didacted IT specialist with experience both as developer and team/tech leader. I like clean code, team-discussed solutions, well-fitted technologies, consistency, automating things and up-to-date tools/dependencies. I really enjoy learning new things and am open to new opportunities.\nIn the meantime I try to be good father for my 3 children and good husband for my fantastic wife. My hobbies: skateboarding, music, football, comics.","title":"About"}]