<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Vladimir</title>
    <description>The latest articles on Forem by Vladimir (@vladimir_mvs).</description>
    <link>https://forem.com/vladimir_mvs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F897498%2Ff59ad7ad-5658-4f9b-be3f-159172277e5d.jpeg</url>
      <title>Forem: Vladimir</title>
      <link>https://forem.com/vladimir_mvs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/vladimir_mvs"/>
    <language>en</language>
    <item>
      <title>From Amazon Lex to GPT-4: How to make a bot with your own data?</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Tue, 04 Apr 2023 17:58:42 +0000</pubDate>
      <link>https://forem.com/vladimir_mvs/from-amazon-lex-to-gpt-4-how-to-make-a-bot-with-your-own-data-3cih</link>
      <guid>https://forem.com/vladimir_mvs/from-amazon-lex-to-gpt-4-how-to-make-a-bot-with-your-own-data-3cih</guid>
      <description>&lt;p&gt;The ChatGPT and other OpenAI models is currently on hype. But they are not the only solution available. Or are they? Let's try to figure it out today without code, only general concepts, pain, and suffering.&lt;/p&gt;

&lt;p&gt;Imagine that you need to create a chatbot that can answer user questions based on your own data: products in an online store, a knowledge base of a support service, marketing articles, etc. Or a list of cafes and coworking spaces, in my case.&lt;/p&gt;

&lt;p&gt;Until recently, such dialogues came down to choosing from "&lt;em&gt;Are you interested in this? - Click here. Interested in something else? - Click there. Didn't understand anything? - Wait for the operator&lt;/em&gt;". Not very friendly, but sometimes quite predictable and understandable, only creating such a complex logic takes a long time.&lt;/p&gt;

&lt;p&gt;Ok, let's add some friendliness and create a "natural language interface" like "&lt;strong&gt;&lt;em&gt;Show me 10 cafes with sockets close to me in London&lt;/em&gt;&lt;/strong&gt;" (if marketers are to be believed, people will write even crazier things, just to find what they're looking for).&lt;/p&gt;

&lt;h2&gt;
  
  
  One of the first "human language recognizers" like this was the service Amazon Lex (as well as Google Dialogflow and a dozen others)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Amazon Lex V2 is an AWS service for building conversational interfaces for applications using voice and text. Amazon Lex V2 provides the deep functionality and flexibility of natural language understanding (NLU) and automatic speech recognition (ASR) so you can build highly engaging user experiences with lifelike, conversational interactions, and create new categories of products.&lt;/p&gt;

&lt;p&gt;Amazon Lex V2 enables any developer to build conversational bots quickly. With Amazon Lex V2, no deep learning expertise is necessary—to create a bot, you specify the basic conversation flow in the Amazon Lex V2 console. Amazon Lex V2 manages the dialog and dynamically adjusts the responses in the conversation. Using the console, you can build, test, and publish your text or voice chatbot. You can then add the conversational interfaces to bots on mobile devices, web applications, and chat platforms (for example, Facebook Messenger).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sounds great: it can extract entities from plain text and pass to the data API.&lt;/p&gt;

&lt;p&gt;For my example above, I had to write an utterance "&lt;em&gt;Show me {&lt;strong&gt;count&lt;/strong&gt;} {&lt;strong&gt;type&lt;/strong&gt;} with {&lt;strong&gt;sockets&lt;/strong&gt;} close to me in {&lt;strong&gt;region&lt;/strong&gt;}&lt;/em&gt;" and describe the entities. After that, the JSON &lt;code&gt;{count: 10, type: cafe, sockets: many, region: London}&lt;/code&gt; was obtained from the original phrase.&lt;/p&gt;

&lt;p&gt;But here's the problem, for a similar phrase "&lt;em&gt;Give me 10 coworking in Riga&lt;/em&gt;", a completely different utterance is needed, and for the simplest query "&lt;em&gt;5 workplaces nearby&lt;/em&gt;", a third one is needed. 🤷‍♂️ In general, I stopped after several hundred utterances of stupid word permutations. Running all the tests took about an hour.&lt;/p&gt;

&lt;p&gt;Another pain is dialogues; for example, the seeker of coworking spaces' second request may be "&lt;em&gt;What about cafes?&lt;/em&gt;". In Lex, context can be passed in three ways, but a limited number of times and only through code (Amazon Lambda functions).&lt;/p&gt;

&lt;p&gt;Lex also has another way of use: training on a dataset of hundreds of thousands of questions and answers, and further automatic responses. Probably suitable for call centers with similar queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Well, let's move on to ChatGPT and the capabilities that are rumored about
&lt;/h2&gt;

&lt;p&gt;Debunking myths: models available through API are "dumber" than a chat web interface because they don't have memory or context 🤦‍♂️&lt;/p&gt;

&lt;p&gt;So, to work with a product catalog, you need to transmit the entire catalog in each request, which won't even fit in the 32k token limit of the most expensive gpt-4-32k model. And with each message, you need to transmit all previous requests and responses to maintain context.&lt;/p&gt;

&lt;p&gt;This is how about 99.9(9)% of typical bots that start charging users from the 10th response (for example) work. The curtain falls.&lt;/p&gt;

&lt;p&gt;In general, the second option for implementing the idea is tokenization and vectorization of the source texts, articles, or product catalogs; tokenization and vectorization of the user query and finding several closest suitable vectors based on cosine similarity.&lt;/p&gt;

&lt;p&gt;Simplified, this is how semantic search works, and it doesn't require chat models. However, for the "friendliness" of the dialogue and responses, chat models can be used and the found vectors and their corresponding original texts can be transmitted along with the query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://habr.com/ru/post/717576/"&gt;Here&lt;/a&gt; &lt;a href="https://www.mlq.ai/fine-tuning-gpt-3-question-answer-bot/"&gt;are&lt;/a&gt; a &lt;a href="https://betterprogramming.pub/how-to-give-your-chatbot-the-power-of-neural-search-with-openai-ebcff5194170"&gt;few&lt;/a&gt; &lt;a href="https://dylancastillo.co/ai-search-engine-fastapi-qdrant-chatgpt/"&gt;articles&lt;/a&gt; &lt;a href="https://blinkdata.com/openai-embedding-tutorial/"&gt;about&lt;/a&gt; it &lt;a href="https://pub.towardsai.net/build-chatgpt-like-chatbots-with-customized-knowledge-for-your-websites-using-simple-programming-f393206c6626"&gt;and&lt;/a&gt; a &lt;a href="https://medium.com/@nils_reimers/openai-gpt-3-text-embeddings-really-a-new-state-of-the-art-in-dense-text-embeddings-6571fe3ec9d9"&gt;little&lt;/a&gt; &lt;a href="https://qdrant.tech/articles/chatgpt-plugin/"&gt;more&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All actions can be performed on your own models, OpenAI models (such as text-embedding-ada-002) via API, and on any publicly available models, such as &lt;a href="https://nlpcloud.com/"&gt;NLP cloud&lt;/a&gt;. Vectors can be stored in CSV files, but it's better to use specialized vector databases like &lt;a href="https://qdrant.tech/"&gt;Qdrant&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Nowadays, this approach is becoming a de facto standard, and most search plugins for ChatGPT are based on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  There are several advantages:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;low cost of vectorization (it's only performed when the source data is uploaded/modified) and storage, especially in a local database&lt;/li&gt;
&lt;li&gt;some possibility to maintain context by carrying the entire dialogue with each request to the chat model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  There are also some disadvantages:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;source data must be text-based, or more precisely, descriptive (my cafe catalog made of enum parameters in JSON format didn't work)&lt;/li&gt;
&lt;li&gt;there must be a lot of them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Services have already appeared that simplify the implementation of the entire stack from data preparation to obtaining chat code for your website. For example, Databerry and Spellbook. And there are also good alternative models, such as Vicuna.&lt;/p&gt;

&lt;h2&gt;
  
  
  After vector experiments, I switched to the third option - translating human requests into JSON using a chat model
&lt;/h2&gt;

&lt;p&gt;It turned out to be the easiest, cheapest, and fastest way to implement my initial idea 😎&lt;/p&gt;

&lt;p&gt;The model is accompanied by instructions that are transmitted together with the user's request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Convert the question below to JSON data.
Mostly questions are related to cafes and coworkings with different amenities.
Use only following parameters.
Skip unknown parameters and parameters that not in question.
Just output JSON data without explanation, notes or error messages!
Parameters
"""
- count: integer from 0 to 5
- type: one of "Cafe", "Coworking" and "Anticafe"
- region: any city
- sockets: one of "None", "Few" and "Many"
- noise: one of "Quiet", "Medium" and "Noisy"
- size: one of "Small", "Average" and "Big"
- busyness: one of "Low", "Average" and "High"
- view: one of "Street", "Roofs" and "Garden"
- cuisine: one of "Coffee &amp;amp; snacks" and "Full"
- roundclock: one of true and false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In most cases, a perfectly normal JSON response like &lt;code&gt;{count: 5, type: cafe, sockets: many, region: London}&lt;/code&gt; is returned, which can be passed on to a microservice API.&lt;/p&gt;

&lt;p&gt;But a text generation model wouldn't be a text generation model if it always responded the same way (even when frozen with &lt;code&gt;temperature=0&lt;/code&gt;). Approximately 10% of the time, it "gets stuck" and adds non-existent parameters or forgets to close the JSON, and a similar repeated request is processed normally.&lt;/p&gt;

&lt;p&gt;It's pointless to fight this, but you can remove non-existent parameters or invalid values by validating the response against the JSON schema, and also suggest that the user ask the bot again.&lt;/p&gt;

&lt;p&gt;By the way, the latest gpt-4 model turned out to be "smarter", more predictable, and doesn't add random parameters, but it costs 6 times more. We're waiting for gpt-4-turbo.&lt;/p&gt;

&lt;p&gt;You can ask the resulting bot here &lt;a href="https://thttps//t.me/WorkplacesDigitalBot"&gt;@WorkplacesDigitalBot&lt;/a&gt;, its budget is $10/month until it starts earning on its own, and there's no context saving.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>ai</category>
      <category>nlp</category>
      <category>openai</category>
    </item>
    <item>
      <title>Workplaces for digital nomads: the frontend</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Tue, 01 Nov 2022 13:59:17 +0000</pubDate>
      <link>https://forem.com/vladimir_mvs/workplaces-for-digital-nomads-the-frontend-2mk3</link>
      <guid>https://forem.com/vladimir_mvs/workplaces-for-digital-nomads-the-frontend-2mk3</guid>
      <description>&lt;p&gt;A continuation of the story about the development of a pet project about cafés and co-working spaces in sunny Cyprus. Workplaces for digital nomads ヽ(。_°)ノ&lt;/p&gt;

&lt;p&gt;I talked the API microservice in the &lt;a href="https://dev.to/vladimir_mvs/workplaces-for-digital-nomads-the-api-5hk8"&gt;first part&lt;/a&gt;, the frontend site in the second part.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/workplacescy"&gt;project code&lt;/a&gt; is open, website &lt;a href="https://workplaces.cy/"&gt;https://workplaces.cy/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The site is built as a PWA using the &lt;a href="https://vuejs.org/api/sfc-script-setup.html"&gt;Vue 3 Composition API&lt;/a&gt; and the &lt;a href="https://next.vuetifyjs.com/en/"&gt;Vuetify&lt;/a&gt; UI framework. Both tools are ideal for a quick start from scratch and contain less redundant code than earlier versions.&lt;/p&gt;

&lt;p&gt;The major portion of the site is Google Maps in its always-free tier, which displays data from the microservice's REST API, as well as a panel with filters and a list of workplaces.&lt;/p&gt;

&lt;p&gt;The entry component &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/components/Home.vue"&gt;src/components/Home.vue&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manages the display of sections for mobile and desktop versions;&lt;/li&gt;
&lt;li&gt;manages data from the API, which is retrieved from &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/composable/api.js"&gt;src/composable/api.js&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;handles them with computed filters from &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/components/Filters.vu"&gt;src/components/Filters.vue&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;passes the filtered data to component &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/components/Places.vue"&gt;src/components/Places.vue&lt;/a&gt; to display a list of locations;&lt;/li&gt;
&lt;li&gt;handles a 404 error;&lt;/li&gt;
&lt;li&gt;sets meta tags via component &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/components/PlaceHead.vue"&gt;src/components/PlaceHead.vue&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no separate "place card" in the project, a simple scrolling of the place list to the required place is sufficient for now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedPlaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;scrollToPlace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filters are integrated in &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/components/Filters.vue"&gt;src/components/Filters.vue&lt;/a&gt; and stylized further for a more compact appearance.&lt;/p&gt;

&lt;p&gt;Meta tags are handled by the vueuse/head package.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/fawmi/vue-google-maps"&gt;fawmi/vue-google-maps&lt;/a&gt; library, version 0.9.72, is used to display the map. The author broke the map in later versions and did not publish the source code on GitHub (however, these versions are available via npm).&lt;/p&gt;

&lt;p&gt;You may also see local points of interest by displaying your location on a &lt;a href="https://github.com/workplacescy/frontend/blob/develop/src/components/Map.vue"&gt;map&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swiperjs.com/vue"&gt;Swiper/vue&lt;/a&gt; and a number of components are used to display large and small photographs of locations in a carousel.&lt;/p&gt;

&lt;p&gt;In the HSL colour space, a gradient colour rating of places is calculated using a simple function in &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/composable/colors.js"&gt;src/composable/colors.js&lt;/a&gt;. Only the hue changes depending on the rating value, while the saturation and brightness remain fixed.&lt;/p&gt;

&lt;p&gt;On the place cards, there is a &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/components/ComplainButton.vue"&gt;Complain button&lt;/a&gt; to report any inaccuracies - it's all serious:-)&lt;/p&gt;

&lt;p&gt;A simple &lt;a href="https://github.com/workplacescy/frontend/tree/develop/src/router/index.js"&gt;src/router/index.js&lt;/a&gt; router based on &lt;a href="https://v3.router.vuejs.org/"&gt;vue-router&lt;/a&gt; allows you to avoid utilising the store for now (with the current project capabilities) and helps with 404 error handling.&lt;/p&gt;

&lt;p&gt;Most components are loaded asynchronously to speed up page display, for example, &lt;code&gt;const Navigation = defineAsyncComponent() =&amp;gt; import("/Navigation.vue"))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When opening/closing the panel, a portion of the template is wrapped into &lt;a href="https://vuejs.org/guide/built-ins/keep-alive.html"&gt;&amp;lt;KeepAlive&amp;gt;&lt;/a&gt; to cache components. However, it did not work with &lt;a href="https://vuejs.org/guide/built-ins/suspense.html"&gt;Suspense&lt;/a&gt; for displaying stubs :-(&lt;/p&gt;

&lt;p&gt;Instead of using Vuetify's default font icons, the project imports SVG icons from the mdi/js package at &lt;a href="https://materialdesignicons.com"&gt;https://materialdesignicons.com&lt;/a&gt;. This saves approximately 1 MB in the final package.&lt;/p&gt;

&lt;p&gt;Builds the project with &lt;a href="https://vitejs.dev/"&gt;Vite&lt;/a&gt; and some magic in &lt;a href="https://github.com/workplacescy/frontend/tree/develop/vite.config.js"&gt;vite.config.js&lt;/a&gt; to optimise the final code, including CSS and HTML minification and the creation of a lighter version of Sentry.&lt;/p&gt;

&lt;p&gt;PWA is created with &lt;a href="https://github.com/vite-pwa/vite-plugin-pwa"&gt;vite-pwa/vite-plugin-pwa&lt;/a&gt;, and its parameters are set in &lt;a href="https://github.com/workplacescy/frontend/tree/develop/vite.config.js"&gt;vite.config.js&lt;/a&gt;. In summary, the implementation of PWA was not a project aim, but it did provide good caching of all project components and resulted in a very quick reopening of the site.&lt;/p&gt;

&lt;h3&gt;
  
  
  About Vuetify
&lt;/h3&gt;

&lt;p&gt;Overall, I preferred Vuetify over &lt;a href="https://quasar.dev/"&gt;Quazar&lt;/a&gt; and other UI frameworks. However, it has its own drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The difficulty in finely customising visual components. Vuetify is built for Material Design, therefore you can't just remove the indents between checkboxes or make mobile buttons smaller. Because of the high DOM depth, you may need to use &lt;a href="https://vuejs.org/api/sfc-css-features.html#scoped-css"&gt;:deep&lt;/a&gt; or wrap components in additional divs in some circumstances, lowering your Google PageSpeed score.&lt;/li&gt;
&lt;li&gt;Unable to get rid of unused styles. &lt;a href="https://next.vuetifyjs.com/en/features/treeshaking/#manual-imports"&gt;Importing components individually&lt;/a&gt; and &lt;a href="https://next.vuetifyjs.com/en/features/sass-variables/"&gt;setting styles in SASS&lt;/a&gt; can greatly simplify builds, but several common unused styles can't be deleted using &lt;a href="https://purgecss.com/"&gt;PurgeCSS&lt;/a&gt; and analogues due to dynamic class names.&lt;/li&gt;
&lt;li&gt;Some components, such as the &lt;a href="https://next.vuetifyjs.com/en/components/carousels/"&gt;slider&lt;/a&gt;, have few features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But I'm sure, this framework is good enough for many projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;The same Fly.io platform with managed micro VM Firecracker. It's much easier than hosting the microservice API here, and a four-line &lt;a href="https://github.com/workplacescy/frontend/tree/develop/Dockerfile"&gt;Dockerfile&lt;/a&gt; suffices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; pierrezemb/gostatic&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; docker/config/headerConfig.json /config/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; dist/ /srv/http/&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/goStatic", "-fallback", "/index.html"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI/CD
&lt;/h2&gt;

&lt;p&gt;This is slightly more complicated: first, &lt;a href="https://github.com/workplacescy/frontend/tree/develop/.github/workflows/build.yml"&gt;Github Action&lt;/a&gt; creates a build with Vite, then &lt;code&gt;flyctl&lt;/code&gt; creates a container from it and deploys it to the production VM. All secrets are stored in the GitHub production environment in this case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;The same &lt;a href="https://sentry.io/"&gt;Sentry&lt;/a&gt; and &lt;a href="https://www.honeybadger.io/"&gt;Honeybadger&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At this stage, the website is live, hosted in a production and available to all users. The project is fully completed :-)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/workplacescy/frontend"&gt;Frontend repository&lt;/a&gt;, website &lt;a href="https://workplaces.cy/"&gt;https://workplaces.cy/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>tutorial</category>
      <category>vue</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Workplaces for digital nomads: the API</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Mon, 31 Oct 2022 16:03:06 +0000</pubDate>
      <link>https://forem.com/vladimir_mvs/workplaces-for-digital-nomads-the-api-5hk8</link>
      <guid>https://forem.com/vladimir_mvs/workplaces-for-digital-nomads-the-api-5hk8</guid>
      <description>&lt;p&gt;Another pet project: cafés and co-working spaces in sunny Cyprus. Workplaces for digital nomads ヽ(。_°)ノ&lt;/p&gt;

&lt;p&gt;I like to share my development process. The overall development approach is pragmatic and similar to &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-2lph"&gt;the previous project&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project's goals
&lt;/h2&gt;

&lt;p&gt;There are numerous cafes, coffee houses, taverns, restaurants, and bars on the island, but not everyone is good to work for at least a couple of hours.&lt;br&gt;
There are well-known Starbucks, Costa Coffee, Gloria Jeans Coffee, and so on, but there are also very cosy and totally underrated local places.&lt;br&gt;
That is why it was decided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classify locations based on factors relevant to remote working, such as kitchen, outlets, noise, size, occupancy, view, and so on.&lt;/li&gt;
&lt;li&gt;Filter locations by the parameters you've chosen.&lt;/li&gt;
&lt;li&gt;Display a map with relevant locations.&lt;/li&gt;
&lt;li&gt;Create a desktop and mobile web app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, everything was successful and the &lt;a href="https://github.com/workplacescy"&gt;project code&lt;/a&gt; is open. Website: &lt;a href="https://workplaces.cy/"&gt;https://workplaces.cy/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To achieve the objectives, it was decided to build a REST API microservice on Laravel, with an admin panel on Twill and a &lt;a href="https://dev.to/vladimir_mvs/workplaces-for-digital-nomads-the-frontend-2mk3"&gt;frontend web application on Vue&lt;/a&gt;. Deploy on &lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; as before.&lt;/p&gt;
&lt;h2&gt;
  
  
  REST API microservice
&lt;/h2&gt;

&lt;p&gt;The platform chosen is the familiar and lightweight Laravel and PHP 8.1 with promoted- and readonly- properties and strict typing.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/workplacescy/api/blob/develop/composer.json"&gt;composer.json&lt;/a&gt; and &lt;a href="https://github.com/workplacescy/api/tree/develop/config"&gt;project configuration&lt;/a&gt; is lightened as much as possible: unused packages and classes are removed, &lt;a href="https://getcomposer.org/doc/06-config.md#platform-check"&gt;platform-check&lt;/a&gt; is disabled, and &lt;a href="https://getcomposer.org/doc/06-config.md#classmap-authoritative"&gt;classmap-authoritative&lt;/a&gt; is enabled.&lt;/p&gt;

&lt;p&gt;As a result, the number of classes to be loaded decreased 4.5 times from 28247 to 6230, the vendor directory was "thinned" almost 1.5 times, and tests were slightly faster.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The main &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Models/Place.php"&gt;Place&lt;/a&gt; model is a typical Laravel model that extended Twill model (&lt;code&gt;A17\Twill\Models\Model&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/workplacescy/api/tree/develop/app/Enums"&gt;Properties for filtering&lt;/a&gt; - native PHP enum's with a few common methods from &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Traits/EnumValues.php"&gt;EnumValues&lt;/a&gt; trait to obtain values for the admin panel. These are cast in the properties of the model.&lt;/p&gt;

&lt;p&gt;In addition, each property has a coefficient and a weight to calculate an place's rating. For example, the &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Enums/Sockets.php"&gt;availability of outlets&lt;/a&gt; is more important than &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Enums/View.php"&gt;the view&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Sockets&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PropertyEnum&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;EnumValues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'None'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Few&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Few'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Many&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Many'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;WEIGHT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Few&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="cd"&gt;/** @inheritDoc */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;coefficient&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Few&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Many&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;API queries are handled by single-action controllers, validated by Request including enum matching. For example, &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Http/Requests/IndexRequest.php"&gt;IndexRequest&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;#[OA\Parameter(name: 'busyness', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'city', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'size', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'sockets', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'noise', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'type', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'view', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'cuisine', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string'))]&lt;/span&gt;
&lt;span class="c1"&gt;#[OA\Parameter(name: 'vRate', in: 'query', allowEmptyValue: false, schema: new OA\Schema(type: 'string', format: 'float', maximum: 0, minimum: 5))]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IndexRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @return array{busyness: string, city: string, size: string, sockets: string, noise: string, type: string, view: string} */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'busyness'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Busyness&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'city'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;City&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'size'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Size&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'sockets'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Sockets&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'noise'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Noise&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'view'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'cuisine'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Cuisine&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="s1"&gt;'vRate'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'float'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'numeric'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'between:0,5'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native PHP attributes allowed OpenAPI markup to be much more compact than in DocBlocks. The resulting &lt;a href="https://github.com/workplacescy/api/blob/develop/storage/openapi.yaml"&gt;openapi.yaml&lt;/a&gt; is created with &lt;a href="https://zircote.github.io/swagger-php/"&gt;swagger-php&lt;/a&gt; and used to test the API.&lt;/p&gt;

&lt;p&gt;Apart from the validators, requests are passed through &lt;a href="https://github.com/Tucker-Eric/EloquentFilter"&gt;EloquentFilter&lt;/a&gt;-based &lt;a href="https://github.com/workplacescy/api/blob/develop/app/ModelFilters/PlaceFilter.php"&gt;filters&lt;/a&gt; - a very expressive solution instead of a bunch of if's and when's.&lt;/p&gt;

&lt;p&gt;Some places have photos that are transparently uploaded to AWS S3 from the admin and processed by the &lt;a href="https://imgix.com/"&gt;Imgix&lt;/a&gt; service. There is nothing on the API side to handle the pictures.&lt;/p&gt;

&lt;p&gt;To get detailed geodata for the place from Google Maps, &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Services/GooglePlacesService.php"&gt;GooglePlacesService&lt;/a&gt; and &lt;a href="https://github.com/alexpechkarev/google-maps/"&gt;alexpechkarev/google-maps&lt;/a&gt; package are used. In API service all the places are added only with the name, city and properties for rating. The rest data - coordinates, business ID, address and link are obtained from Google Places API.&lt;/p&gt;

&lt;p&gt;To calculate the rating of an place &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Services/VRateService.php"&gt;VRateService&lt;/a&gt; is used.&lt;/p&gt;

&lt;p&gt;Both services are wrapped in their respective actions and are available through console commands and events after recording the place.&lt;/p&gt;

&lt;p&gt;The finished data is wrapped in &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Http/Resources/PlaceResource.php"&gt;PlaceResource&lt;/a&gt; and &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Http/Resources/PlaceCollection.php"&gt;PlaceCollection&lt;/a&gt;. Excessive fields are also removed there. The middleware &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Http/Middleware/JsonResponse.php"&gt;JsonResponse.php&lt;/a&gt; is used to force response in JSON format&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JsonResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @param Closure(Request): (BaseJsonResponse) $next */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;BaseJsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Accept'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Administrative control panel
&lt;/h2&gt;

&lt;p&gt;I've worked with &lt;a href="https://twill.io/"&gt;Twill&lt;/a&gt; before, so I decided to use it for my project: an open, free system with rich features and good support. Why not? :-)&lt;/p&gt;

&lt;p&gt;Installed via &lt;code&gt;composer requires area17/twill&lt;/code&gt;, adds some migrations and communicates transparently with existing models. In some cases, you need to add service fields like `published and activity start/stop dates to them. However, the documentation describes everything in detail.&lt;/p&gt;

&lt;p&gt;Now I recommend to try version 3-beta: it has much more options to programmatically manage the data on pages instead of separate widgets in the blade templates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e8aqcxkz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5yjtunc9z3t3891xfik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e8aqcxkz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5yjtunc9z3t3891xfik.png" alt="Administrative control panel" width="880" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example of a &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Http/Controllers/Twill/PlaceController.php"&gt;controller&lt;/a&gt;, &lt;a href="https://github.com/workplacescy/api/blob/develop/app/Repositories/PlaceRepository.php"&gt;repository&lt;/a&gt; and &lt;a href="https://github.com/workplacescy/api/blob/develop/resources/views/twill/places/form.blade.php"&gt;template&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database
&lt;/h2&gt;

&lt;p&gt;Simple and fast SQLite ¯_(ツ)_/¯&lt;br&gt;
Hosted on a persistent volume. No configuration required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;For the &lt;a href="https://github.com/workplacescy/api/tree/develop/tests"&gt;tests&lt;/a&gt;, &lt;a href="https://pestphp.com/"&gt;Pest&lt;/a&gt; is used with Laravel support, parallel execution of tests, and with disabled throttling (&lt;code&gt;$this-&amp;gt;withoutMiddleware(ThrottleRequests::class&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The endpoints are used to check &lt;a href="https://github.com/workplacescy/api/tree/develop/tests/Datasets"&gt;datasets&lt;/a&gt; responses and their consistency with the OpenAPI specification.&lt;/p&gt;

&lt;p&gt;For manual check there is &lt;a href="https://github.com/rectorphp/rector/"&gt;Rector&lt;/a&gt; with some &lt;a href="https://github.com/workplacescy/api/blob/develop/rector.php"&gt;exceptions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Found one downside: &lt;code&gt;laravel/dusk&lt;/code&gt; and &lt;code&gt;php-webdriver/webdriver&lt;/code&gt; are nailed to Twill and require mandatory installation, although not used in my tests :-(&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;The server is hosted on the &lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; platform with managed micro VM Firecracker. It never sleeps, has a good free tier, and allows you to host both static and any application server, unlike the popular Heroku. There are also different deployment and rollback strategies, health checks, and hosting geographies to choose from.&lt;/p&gt;

&lt;p&gt;The runtime can be set up automatically with the &lt;code&gt;flyctl launch&lt;/code&gt; command from the application directory or you can write your own config and Dockerfile.&lt;/p&gt;

&lt;p&gt;I used my &lt;a href="https://github.com/workplacescy/api/blob/develop/Dockerfile"&gt;Dockerfile&lt;/a&gt; and run API microservice in the easiest way via &lt;code&gt;php artisan serve&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Static distribution (admin assets and robots.txt &amp;amp; Co) can be delegated to the Fly platform by configuring &lt;a href="https://github.com/workplacescy/api/blob/develop/fly.toml"&gt;fly.toml&lt;/a&gt;&lt;br&gt;
&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
[[statics]]&lt;br&gt;
guest_path = "/var/www/html/public/assets"&lt;br&gt;
url_prefix = "/assets"&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD
&lt;/h2&gt;

&lt;p&gt;It's simple here, &lt;a href="https://github.com/workplacescy/api/blob/develop/.github/workflows/deploy.yml"&gt;GitHub Action with one workflow&lt;/a&gt; and the same &lt;code&gt;flyctl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sentry.io/"&gt;Sentry&lt;/a&gt; is used for errors tracking and &lt;a href="https://www.honeybadger.io/"&gt;Honeybadger&lt;/a&gt; is used for uptime and availability checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At this point, the microservice API is live, hosted in production, and accessible to all users. MVP is complete :-)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/workplacescy"&gt;API repository&lt;/a&gt;, website &lt;a href="https://workplaces.cy/"&gt;https://workplaces.cy/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'll tell about creating the frontend with Vue 3 Composition API in the &lt;a href="https://dev.to/vladimir_mvs/workplaces-for-digital-nomads-the-frontend-2mk3"&gt;second part&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>laravel</category>
      <category>php</category>
      <category>api</category>
    </item>
    <item>
      <title>Pragmatic development 3: Telegram bot</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Fri, 14 Oct 2022 13:17:22 +0000</pubDate>
      <link>https://forem.com/vladimir_mvs/pragmatic-development-3-telegram-bot-ed1</link>
      <guid>https://forem.com/vladimir_mvs/pragmatic-development-3-telegram-bot-ed1</guid>
      <description>&lt;p&gt;The conclusion of a simple project about specialty coffee shops in Cyprus. The &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-2lph"&gt;first part&lt;/a&gt; focused on the API microservice, the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-2-frontend-1m79"&gt;second&lt;/a&gt; on the frontend site, and the third on the Telegram bot.&lt;/p&gt;

&lt;p&gt;The bot began with simple tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;/map&lt;/em&gt; - display map of coffee shops&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;/list&lt;/em&gt; - show list of coffee shops&lt;/li&gt;
&lt;li&gt;show coffee shop details&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;/random&lt;/em&gt; - show random coffee shop&lt;/li&gt;
&lt;li&gt;search for a coffee shop by the name&lt;/li&gt;
&lt;li&gt;search for the nearest coffee shop by location or by &lt;em&gt;/nearest&lt;/em&gt; command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the implementation, it was discovered that Telegram cannot display an embedded map with multiple markers, nor does it send the location in the web version. As a result, instead of a real map, I had to show a link to a website with a map and a stub message instead of a response on an empty location. Everything else has been done.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/specialtycoffeecyprus"&gt;project code&lt;/a&gt; is open, bot &lt;a href="https://t.me/SpecialtyCoffeeCyBot"&gt;https://t.me/SpecialtyCoffeeCyBot&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Nutgram was chosen as the bot's base after much deliberation: it is the most lightweight, simple, and modern library. A fully configured DI container comes as a bonus, allowing you to avoid manual service initialization and delivery to customers.&lt;/p&gt;

&lt;p&gt;Using an actual version of PHP 8.1 allowed me to write slightly less code while achieving slightly better performance. Development is made much easier by promoted properties, read-only properties, and strict typing.&lt;/p&gt;

&lt;p&gt;The composer settings are simple as the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-2lph"&gt;API&lt;/a&gt;. The final &lt;a href="https://github.com/specialtycoffeecyprus/bot/blob/develop/composer.json"&gt;composer.json&lt;/a&gt; file.&lt;/p&gt;

&lt;p&gt;Telegram updates are received at the webhook endpoint and transmitted to handlers of commands and message types. Handlers can respond on their own or request data from the REST API. For the unexpected, there are Fallback, Exception, and ApiError handlers.&lt;/p&gt;

&lt;p&gt;The use of short, single-action invokable handlers allowed the &lt;a href="https://github.com/specialtycoffeecyprus/bot/blob/develop/public/index.php"&gt;bot's logic&lt;/a&gt; to be condensed into only 32 lines!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Nutgram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'BOT_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'CONNECT_TIMEOUT'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'logger'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ConsoleLogger&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRunningMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthMiddleware&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FallbackHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onApiError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ApiErrorHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NearestCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SEND_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NotSupportedHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onMessageType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MessageTypes&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SearchHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchRequirementsMiddleware&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onMessageType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MessageTypes&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LOCATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocationHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onMessageType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MessageTypes&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NEW_CHAT_MEMBERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NullHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onMessageType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MessageTypes&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LEFT_CHAT_MEMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NullHandler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ListCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;ListCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ListCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getDescription&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MapCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;MapCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MapCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getDescription&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NearestCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;NearestCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NearestCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getDescription&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RandomCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;RandomCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RandomCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getDescription&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StartCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;StartCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StartCommand&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getDescription&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;registerMyCommands&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'base_uri'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'API_URL'&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addShared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$http&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;a href="https://github.com/specialtycoffeecyprus/bot/blob/develop/app/Handlers/Commands/NearestCommand.php"&gt;/nearest command&lt;/a&gt; example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NearestCommand&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseCommand&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SEND_TEXT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Send location'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'nearest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getDescription&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'Show nearest specialty coffee shop'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getAnswer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Answer&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Send your location to find the nearest coffee shop'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'reply_markup'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ReplyKeyboardMarkup&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resize_keyboard&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KeyboardButton&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SEND_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_location&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/specialtycoffeecyprus/bot/blob/develop/app/Handlers/LocationHandler.php"&gt;Location handler&lt;/a&gt; example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocationHandler&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;Location&lt;/span&gt; &lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Sender&lt;/span&gt; &lt;span class="nv"&gt;$sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;ApiService&lt;/span&gt; &lt;span class="nv"&gt;$api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sender&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Nutgram&lt;/span&gt; &lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?Message&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAnswer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="cd"&gt;/** @inheritDoc */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getAnswer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Answer&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$cafe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNearest&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Formatter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cafe&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'parse_mode'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ParseMode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;

            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VenueAnswer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$cafe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$cafe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cafe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'google_place_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$cafe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;placeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'reply_to_message_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'reply_markup'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'remove_keyboard'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getLocation&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Location&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Location&lt;/span&gt; &lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;LocationHandler&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same short and concise &lt;a href="https://github.com/specialtycoffeecyprus/bot/tree/develop/app/Middleware"&gt;middleware&lt;/a&gt; is used to validate search data as well as check the legitimacy of messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Common options and secret names are stored in the .env file, while local overrides are stored in the .env.local file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;I'm pleased that the Nutgram library provides enough possibilities for writing tests. Unlike simple Guzzle mocks, project's &lt;a href="https://github.com/specialtycoffeecyprus/bot/tree/develop/tests"&gt;tests&lt;/a&gt; use the mocked &lt;a href="https://github.com/specialtycoffeecyprus/bot/blob/develop/tests/ApiClient.php"&gt;ApiClient&lt;/a&gt;, which returns predefined responses an infinite number of times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;The same &lt;a href="https://docs.sentry.io/platforms/php/"&gt;Sentry&lt;/a&gt; as in the API microservice and the frontend, in the .env just specify an empty value of SENTRY_DSN (for clarity), and write the actual value to secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;Still the same &lt;a href="http://fly.io/"&gt;Fly.io&lt;/a&gt; platform, but now with 300ms-load-time &lt;a href="https://fly.io/docs/reference/machines/"&gt;Machines&lt;/a&gt;. Generally, it's FaaS (serverless), but in my case, with the PHP server, it's still a regular VM.&lt;/p&gt;

&lt;p&gt;I used an embedded PHP server instead of the usual combination of PHP-FPM + Nginx/Caddy + Supervisor to speed up the project's launch. The Docker image of course got smaller, but I had to use a separate router:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pass only POST requests to bot handlers&lt;/li&gt;
&lt;li&gt;redirect the dev domain like .fly.dev to the main domain&lt;/li&gt;
&lt;li&gt;distribute statics (robots.txt, favicon.ico, etc.)&lt;/li&gt;
&lt;li&gt;block all other requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final &lt;a href="https://github.com/specialtycoffeecyprus/specialty-bot/blob/develop/app/router.php"&gt;router&lt;/a&gt; and &lt;a href="https://github.com/specialtycoffeecyprus/specialty-bot/blob/develop/Dockerfile"&gt;Dockerfile&lt;/a&gt; (same layered as in API part).&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/specialtycoffeecyprus/specialty-bot/blob/develop/.github/workflows/deploy.yml"&gt;Github Action&lt;/a&gt; is simple enough: update the machine &lt;code&gt;flyctl deploy&lt;/code&gt; and update the webhook registration &lt;code&gt;curl -sS ${{ secrets.APP_URL }}/setup.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All secrets are stored on the hosting platform and partially duplicated in the GitHub production environment for webhook registration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At this stage, the bot is live, hosted in a production and available to all users. The project is fully completed :-)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/specialtycoffeecyprus/specialty-bot/"&gt;Bot repository&lt;/a&gt;, website &lt;a href="https://specialtycoffee.cy/"&gt;https://specialtycoffee.cy/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TODO
&lt;/h2&gt;

&lt;p&gt;The final non-critical tasks for the entire project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;health checks for real service response, not just port 'survivability'.&lt;/li&gt;
&lt;li&gt;optimize the build with Caddy&lt;/li&gt;
&lt;li&gt;try a Buildpack or Nixpack&lt;/li&gt;
&lt;li&gt;replace the built-in PHP server with something more secure&lt;/li&gt;
&lt;li&gt;add strict typing (Typescript)&lt;/li&gt;
&lt;li&gt;add API usage stats&lt;/li&gt;
&lt;li&gt;add bot usage statistics&lt;/li&gt;
&lt;li&gt;expand link and event tracking in Google Analytics&lt;/li&gt;
&lt;li&gt;replace Google Analytics with something lighter and more GDPR compliant.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>telegram</category>
      <category>chatbot</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Pragmatic development 2: frontend</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Thu, 13 Oct 2022 15:45:35 +0000</pubDate>
      <link>https://forem.com/vladimir_mvs/pragmatic-development-2-frontend-1m79</link>
      <guid>https://forem.com/vladimir_mvs/pragmatic-development-2-frontend-1m79</guid>
      <description>&lt;p&gt;Development of a simple project about specialty coffee shops in Cyprus continues. I talked the API microservice in the &lt;a href="https://dev.to/vladimir%20mvs/pragmatic-development-2lph"&gt;first part&lt;/a&gt;, the frontend site in the second part, and the Telegram bot in the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-3-telegram-bot-ed1"&gt;final article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/specialtycoffeecyprus"&gt;project code&lt;/a&gt; is open, website &lt;a href="https://specialtycoffee.cy/"&gt;https://specialtycoffee.cy/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The website has only two pages: a map with coffee shop markers and an About page. You don't need anything else for a startup, but for feedback, I have Telegram and email.&lt;/p&gt;

&lt;p&gt;The implementation is modular vanilla JavaScript and Google Maps in a free tier, which can always be replaced by Mapbox, Mapquest, and other services.&lt;/p&gt;

&lt;p&gt;The map and data from the REST API are loaded concurrently, and when ready, markers from the GeoJson data array are added to the map and collected in clusters. A click on the marker sends a request to the Google Places API for a company id (&lt;em&gt;cid&lt;/em&gt;) and opens a small description (Info Window) with a link to the company card on the larger Google Maps with the ability to build a route, view reviews, and so on. This link is generated using the &lt;em&gt;cid&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If Google Maps is unavailable, a stub with an error message is displayed, but the website navigation remains functional.&lt;/p&gt;

&lt;p&gt;Everything is simple and quick thanks to asynchronous execution.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://vitejs.dev/"&gt;Vite&lt;/a&gt; to build the project, which is also very fast and simple.&lt;/p&gt;

&lt;p&gt;I use Google Analytics with a direct connection instead of Google Tag Manager for analytics and insights about site visitors (a bit less traffic this way). Info Window openings are recorded in analytics as reaching goals in order to calculate site conversion. The plugin used is &lt;a href="https://github.com/stafyniaksacha/vite-plugin-radar"&gt;vite-plugin-radar&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two additional plugins, &lt;a href="https://github.com/Jax-p/vite-plugin-html-purgecss"&gt;vite-plugin-html-purgecss&lt;/a&gt; and &lt;a href="https://github.com/zhuweiyou/vite-plugin-minify"&gt;vite-plugin-minify&lt;/a&gt;, enable the removal of all unused code from the final build. It took 15 minutes to set them up, which is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Common options and secret names are stored in the .env file, while local overrides are stored in the .env.local file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;The same &lt;a href="https://docs.sentry.io/platforms/javascript/"&gt;Sentry&lt;/a&gt; as in the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-2lph"&gt;API microservice&lt;/a&gt;, in the .env just specify an empty value of VITE_SENTRY_DSN (for clarity), and write the actual value to secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;The same &lt;a href="http://fly.io/"&gt;Fly.io&lt;/a&gt; platform with managed micro VM Firecracker. It's much easier than hosting the microservice API here, and a four-line Dockerfile suffices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; pierrezemb/gostatic&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; docker/config/headerConfig.json /config/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; dist/ /srv/http/&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/goStatic", "--fallback", "/index.html"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI/CD
&lt;/h2&gt;

&lt;p&gt;This is slightly more complicated: first, &lt;a href="https://github.com/specialtycoffeecyprus/specialty-frontend/blob/develop/.github/workflows/build.yml"&gt;Github Action&lt;/a&gt; creates a build with Vite, then &lt;code&gt;flyctl&lt;/code&gt; creates a container from it and deploys it to the production VM). All secrets are stored in the GitHub production environment in this case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At this point, the website is live, hosted in a production, and accessible to all users. In fact, the project is completed :-)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/specialtycoffeecyprus/specialty-frontend"&gt;Frontend repository&lt;/a&gt;, website &lt;a href="https://specialtycoffee.cy/"&gt;https://specialtycoffee.cy/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-3-telegram-bot-ed1"&gt;third section&lt;/a&gt;, I'll go over how to make a Telegram bot.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>map</category>
      <category>tutorial</category>
      <category>coffee</category>
    </item>
    <item>
      <title>Pragmatic development</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Thu, 13 Oct 2022 12:07:26 +0000</pubDate>
      <link>https://forem.com/vladimir_mvs/pragmatic-development-2lph</link>
      <guid>https://forem.com/vladimir_mvs/pragmatic-development-2lph</guid>
      <description>&lt;p&gt;I recently had some free time, so I created a simple project about specialty coffee shops in Cyprus: a website and a Telegram chatbot that adhered to "big" enterprise standards. I adore good coffee.&lt;/p&gt;

&lt;p&gt;Now I like to share my development process as well as tips on how to get things done quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project's goals are simple:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Display a map of coffee shops on the website.&lt;/li&gt;
&lt;li&gt;Examine a coffee shop's details&lt;/li&gt;
&lt;li&gt;Use Google Maps for directions, reviews, and so on.&lt;/li&gt;
&lt;li&gt;List of coffee shops in the Telegram chatbot&lt;/li&gt;
&lt;li&gt;Search for local coffee shops in the bot&lt;/li&gt;
&lt;li&gt;Search for the near coffee shop in the bot&lt;/li&gt;
&lt;li&gt;Show a random coffee shop in the bot&lt;/li&gt;
&lt;li&gt;Everything has a minimal and clear style.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also, avoid perfectionism and never-ending development.&lt;/p&gt;

&lt;p&gt;That is actually quite difficult because you may want to outperform others, wear badges of various linters, and express all of your abilities in the code.&lt;/p&gt;

&lt;p&gt;Although it is sufficient for the service to simply work, metrics are tracked and errors are recorded. It is worth 20% of the effort to get 80% of the result.&lt;/p&gt;

&lt;p&gt;So break everything down into small tasks and discard anything that will take more than a few hours or that cannot be estimated.&lt;/p&gt;

&lt;p&gt;To be honest, I failed a few times: the first time, I got hooked on the idea of running a Caddy server without config from the console, but it can only run a reverse proxy and/or file server; I spent two days on it in total.&lt;/p&gt;

&lt;p&gt;Another day was wasted on a bad choice of library for the Telegram chatbot.&lt;/p&gt;

&lt;p&gt;Overall, everything was successful and the &lt;a href="https://github.com/specialtycoffeecyprus"&gt;project code&lt;/a&gt; is open. Website: &lt;a href="https://specialtycoffee.cy/"&gt;https://specialtycoffee.cy/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To achieve my objectives I've decided to build a REST API microservice, a frontend website (more on that in the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-2-frontend-1m79"&gt;second part&lt;/a&gt;), a backend for the bot (more on that in the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-3-telegram-bot-ed1"&gt;third part&lt;/a&gt;) and deploy it to something modern and managed with a free tier, rather than VPS or shared hosting. The bot is generally compatible with the Serverless/FaaS ideology.&lt;/p&gt;

&lt;p&gt;My first step was to register a domain name, which cost me 20 euros out of my own pocket - a good incentive not to squander them.&lt;/p&gt;

&lt;p&gt;Speaking of registration, it is difficult to predict the project's future: it may turn out to be profitable to sell, and you may want to get rid of it quickly. As a result, it is preferable to separate all external services into separate accounts: mail, domain, hosting, analytics, monitoring, and so on. You can also use free tiers and trials.&lt;/p&gt;

&lt;h2&gt;
  
  
  REST API microservice
&lt;/h2&gt;

&lt;p&gt;I have previous experience with Laravel and Symfony, so I chose a familiar and simple-to-use technology for quick implementation. &lt;del&gt;Then I'll almost certainly rewrite it in Go.&lt;/del&gt; Using an actual version of PHP 8.1 allowed me to write slightly less code while achieving slightly better performance. Development is made much easier by promoted properties, read-only properties, and strict typing.&lt;/p&gt;

&lt;p&gt;To make things easier, I remove some unused packages and services from Laravel: it's almost like Lumen.&lt;/p&gt;

&lt;p&gt;Packages can be marked as "installed" in composer.json, but they will not be installed. This is very useful for removing redundant polyfills like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-ctype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-iconv"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-intl-grapheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-intl-idn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-mbstring"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-php72"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-php73"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-php80"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/polyfill-php81"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dragonmantank/cron-expression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"egulias/email-validator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"league/commonmark"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"league/flysystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/mime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony/var-dumper"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tijsverkoyen/css-to-inline-styles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also disable &lt;a href="https://getcomposer.org/doc/06-config.md#platform-check"&gt;platform-check&lt;/a&gt; so that you don't have to check the PHP version on every request and limit your check to installing packages. It's also useful to enable &lt;a href="https://getcomposer.org/doc/06-config.md#classmap-authoritative"&gt;classmap-authoritative&lt;/a&gt; so that classes are only loaded from the map created by the composer, not from each use, but this would interfere with development, so it's sufficient to enable it on deployment.&lt;/p&gt;

&lt;p&gt;There are final &lt;a href="https://github.com/specialtycoffeecyprus/api/blob/develop/composer.json"&gt;composer.json&lt;/a&gt; and &lt;a href="https://github.com/specialtycoffeecyprus/api/blob/develop/config/app.php"&gt;config/app.php&lt;/a&gt;. This kind of optimization took less than an hour, so ok. But deeper optimization will require much more time, so not now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The service is made up of single-action controllers that retrieve data from models. There are no repositories because I consider them unnecessary for simple queries with no additional logic.&lt;/p&gt;

&lt;p&gt;Separate Requests validate input data, and GeoJson Resources wrap output data. One class, one responsibility.&lt;/p&gt;

&lt;p&gt;When I was developing the frontend, there was only one endpoint &lt;em&gt;/cafes&lt;/em&gt; that returned a list of all the coffee shops: this allowed me to quickly get the API up and running without affecting other parts of the project. I added a few more endpoints during bot development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database
&lt;/h2&gt;

&lt;p&gt;To begin, SQLite is used as a database, which allowed me to avoid the time spent on the traditional MySQL/PostgreSQL deployment. Furthermore, I'm confident that SQLite is an excellent choice for microprocessing with a load of 100 hits per day and a few dozen or hundreds of table entries.&lt;/p&gt;

&lt;p&gt;During the deployment process, data is seeded from a regular array in &lt;a href="https://github.com/specialtycoffeecyprus/api/blob/develop/database/seeders/CafeSeeder.php"&gt;database/seeders/CafeSeeder.php&lt;/a&gt;. I intend to write 1-2 console commands to edit data in the future because they are much faster than any visual admin panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search
&lt;/h2&gt;

&lt;p&gt;Scout with a "collection" driver makes the API full-text searchable: it allows the search of each model's fields with a simple "LIKE%smth%" query and does not require any full-text indexes in the database. It only took 15 minutes to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static
&lt;/h2&gt;

&lt;p&gt;There are a few static files that must be present in the service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;robots.txt, which doesn't allow indexing.&lt;/li&gt;
&lt;li&gt;favicon.ico, which is loved by many services&lt;/li&gt;
&lt;li&gt;humans.txt&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;First, &lt;a href="https://zircote.github.io/swagger-php/"&gt;swagger-php&lt;/a&gt; generates &lt;a href="https://github.com/specialtycoffeecyprus/api/blob/develop/storage/openapi.yaml"&gt;openapi.yaml&lt;/a&gt; based on code attributes, and then the &lt;a href="https://github.com/hotmeteor/spectator"&gt;spectator&lt;/a&gt; checks API responses to match openapi specification. The popular &lt;a href="https://github.com/DarkaOnLine/L5-Swagger"&gt;L5-Swagger&lt;/a&gt; is redundant in this case, as it is based on the same &lt;a href="https://zircote.github.io/swagger-php/"&gt;swagger-php&lt;/a&gt; with the addition of &lt;a href="https://github.com/swagger-api/swagger-ui"&gt;Swagger UI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Unlike Symfony, Laravel does not read the.env.local file to override/add to the.env configuration and does not recommend storing the.env in a repository. This is a good approach, but it is ineffective when there are many configuration parameters.&lt;/p&gt;

&lt;p&gt;You can do it a little differently: put the local parameters in.env (but don't put them in the repository) and all the production and secret parameter names in.env.production (but put them in the repository). The hosting and/or deployment tools must write the APP_ENV=production parameter as well as the secrets themselves.&lt;/p&gt;

&lt;p&gt;In this case, .env.local will replace (not complement!) the configuration from.env.production, and listing all used parameters (even without values) in.env.production will help with project understanding. In this case, remove .env.example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;When the first stage of development is complete, add &lt;a href="https://docs.sentry.io/platforms/php/guides/laravel/"&gt;Sentry&lt;/a&gt; to the project: in .env.production just specify an empty value of SENTRY_LARAVEL_DSN (for clarity) and record the actual value in the secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;The server is hosted on the &lt;a href="http://fly.io/"&gt;Fly.io&lt;/a&gt; platform with managed micro VM Firecracker. It never sleeps, has a good free tier, and allows you to host both static and any application server, unlike the popular Heroku. There are also different deployment and rollback strategies, health checks, and hosting geographies to choose from.&lt;/p&gt;

&lt;p&gt;The flyctl launch command from the application directory will detect the required components and build the fly.toml and Dockerfile configurations automatically. You can also create your own configuration and Dockerfile.&lt;/p&gt;

&lt;p&gt;I already had a Dockerfile for similar projects, so I used that. As an added bonus, I could run all services as an unprivileged user.&lt;/p&gt;

&lt;p&gt;Because layer caching has been implemented in relatively recent Docker versions, it makes no sense to write all instructions in a single RUN command. On the contrary, it is preferable to arrange the "thin" RUN and COPY layers in the order of how frequently the data changes.&lt;/p&gt;

&lt;p&gt;Because OS distributions and packages are rarely changed, the &lt;code&gt;RUN apk add...&lt;/code&gt; command may appear at the beginning of the Dockerfile.&lt;/p&gt;

&lt;p&gt;Because composer packages are updated more frequently than project source code, the layers &lt;code&gt;COPY composer.* .&lt;/code&gt; and &lt;code&gt;RUN composer install --no-autoloader --no-dev --no-interaction --no-scripts&lt;/code&gt; can be specified in the middle of a Dockerfile and fetched from cache.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COPY --chown=www-data:www-data . .&lt;/code&gt;, &lt;code&gt;RUN composer dump-autoload --classmap-authoritative --no-interaction&lt;/code&gt;, and possibly other commands affecting the project source code can be placed at the end and executed only if the project code itself changes, rather than the OS packages or composer dependencies.&lt;/p&gt;

&lt;p&gt;I used an embedded PHP server instead of the usual combination of PHP-FPM + Nginx/Caddy + Supervisor to speed up the project's launch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/specialtycoffeecyprus/specialty-api/blob/develop/Dockerfile"&gt;Final Dockerfile&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="http://fly.io/"&gt;Fly.io&lt;/a&gt; platform itself terminates https and manages certificates, so the container application only needs to handle normal http traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD
&lt;/h2&gt;

&lt;p&gt;It's simple here, GitHub Action with one &lt;a href="https://github.com/specialtycoffeecyprus/specialty-api/blob/develop/.github/workflows/deploy.yml"&gt;workflow&lt;/a&gt; and the same flyctl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At this point, the microservice API is live, hosted in production, and accessible to all users. MVP is complete :-)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/specialtycoffeecyprus/specialty-api"&gt;API repository&lt;/a&gt;, website &lt;a href="https://specialtycoffee.cy"&gt;https://specialtycoffee.cy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'll tell about creating the frontend in the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-2-frontend-1m79"&gt;second part&lt;/a&gt; and the chatbot for Telegram in the &lt;a href="https://dev.to/vladimir_mvs/pragmatic-development-3-telegram-bot-ed1"&gt;third part&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>map</category>
      <category>laravel</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Demo Symfony Currency Converter</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Mon, 25 Jul 2022 09:44:00 +0000</pubDate>
      <link>https://forem.com/vladimir_mvs/demo-symfony-currency-converter-3ong</link>
      <guid>https://forem.com/vladimir_mvs/demo-symfony-currency-converter-3ong</guid>
      <description>&lt;p&gt;I recently did a test task on Symfony - currency converter with direct and cross conversion. So I want to share the result with the community as an example of a simple console application according to Symfony's rules: DI, autowiring, service tagging, flexible configuration, that's all. I hope this will be useful for beginners.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vladimirmartsul/symfony-exchange-demo"&gt;Source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application calculates "currency exchange" at direct rates (for example, USD -&amp;gt; EUR), as well as through "intermediate" currencies (for example, BTC -&amp;gt; USD -&amp;gt; EUR). There are also fake rates for tests.&lt;/p&gt;

&lt;p&gt;Rates are taken from &lt;a href="https://ecb.europa.eu"&gt;ecb.europa.eu&lt;/a&gt; (major world currencies against EUR) and &lt;a href="https://coindesk.com"&gt;coindesk.com&lt;/a&gt; (BTC against USD).&lt;/p&gt;

&lt;p&gt;Triangulation is based on the principles from &lt;a href="http://www.dpxo.net/articles/fx_rate_triangulation_sql.html"&gt;http://www.dpxo.net/articles/fx_rate_triangulation_sql.html&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The data storing in the SQLite database.&lt;/p&gt;

&lt;p&gt;The application can be used through local PHP or in Docker.&lt;br&gt;
PHP requirements: version 8.1+, bcmath, ctype, iconv, intl, pdo_sqlite, simplexml, sqlite3 extensions.&lt;/p&gt;

&lt;p&gt;I had little experience with Symfony (I worked with Laravel mostly), so there may be some flaws.&lt;br&gt;
In addition, SQLite imposed some limitations due to the lack of real decimal and numeric formats, and INSERT IGNORE, the calculation accuracy of 16.8 had to be hardcoded.&lt;br&gt;
There was trouble with the ECB rates' dates, so the application uses the last available day from each source.&lt;/p&gt;
&lt;h2&gt;
  
  
  Highlights
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;

&lt;p&gt;The application has two console commands: "&lt;em&gt;currency:update&lt;/em&gt;" - updating exchange rates (&lt;code&gt;\App\Command\CurrencyUpdateCommand&lt;/code&gt;) and "&lt;em&gt;currency:exchange&lt;/em&gt;" - exchangу currencies (&lt;code&gt;\App\Command\CurrencyExchangeCommand&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The commands accept parameters, validate data, pass them to services, catch exceptions and beautifully print the result to the console with the appropriate exit status.&lt;/p&gt;

&lt;p&gt;All services and providers are passed through constructor injection. Rate providers have been tagged with the "&lt;em&gt;app.rates_provider&lt;/em&gt;" tag in config/services.yaml and passed through an iterator to &lt;code&gt;\App\Services\RatesUpdater&lt;/code&gt; by this tag. Very convenient, I think.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;App\Providers\CoinDeskRatesProvider&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'app.rates_provider'&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nc"&gt;App\Providers\EcbRatesProvider&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'app.rates_provider'&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nc"&gt;App\Services\RatesUpdater&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;tagged_iterator&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rates_provider&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RatesUpdater&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;iterable&lt;/span&gt; &lt;span class="nv"&gt;$ratesProviders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data exchange and validation
&lt;/h3&gt;

&lt;p&gt;Data for currency exchange and saving rates are sent via DTO: &lt;code&gt;\App\Dto\Exchange&lt;/code&gt; and &lt;code&gt;\App\Dto\Rate&lt;/code&gt;, respectively.&lt;br&gt;
Validation of "&lt;em&gt;AmountRequirements&lt;/em&gt;" - quantity requirements and "&lt;em&gt;ExchangeCurrencyRequirements&lt;/em&gt;" - currency requirements are imposed on DTO for currency exchange.&lt;br&gt;
In addition, validation applies to the &lt;code&gt;\App\Entity\Pair&lt;/code&gt; and &lt;code&gt;\App\Entity\Rate&lt;/code&gt; entities.&lt;/p&gt;

&lt;p&gt;All validators are custom to hide unnecessary details from consumers. Validators locate in the &lt;code&gt;src/Validator/&lt;/code&gt; classes. Most of them are compounds of simple rules. For example, the quantity requirements are "&lt;em&gt;Non-empty string&lt;/em&gt;", "&lt;em&gt;Numeric type&lt;/em&gt;", and "&lt;em&gt;Positive value&lt;/em&gt;".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AmountRequirements&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Compound&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getConstraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Assert\NotBlank&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Assert\Type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'numeric'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'The value {{ value }} is not a valid {{ type }}'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Assert\Positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a more complex currency existence validator &lt;code&gt;\App\Validator\PairCurrencyExistValidator&lt;/code&gt;. It accesses the currency pair repository and checks the database for &lt;code&gt;SELECT COUNT(1) FROM pair WHERE base = &amp;lt;passed currency ticker&amp;gt;&lt;/code&gt;. Its realized via Doctrine Query Builder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exchange rates update
&lt;/h3&gt;

&lt;p&gt;Everything is quite simple here: &lt;code&gt;\App\Services\RatesUpdater&lt;/code&gt; receives an iterator of currency rate providers in the constructor and calls them one by one (via __&lt;em&gt;invoke&lt;/em&gt;, so you don't need to invent a method name). All providers inherit the &lt;code&gt;\App\Providers\RatesProvider&lt;/code&gt; abstract class and implement their data transformation methods in the &lt;code&gt;\App\Dto\Rate&lt;/code&gt; DTO.&lt;/p&gt;

&lt;p&gt;The abstract provider asks for rates at the address specified in the configuration and &lt;em&gt;.env&lt;/em&gt;, which is embedded in the constructor and the base currency's name. Then the provider parses rates from JSON or XML into a simple array and passes them to the provider-specific transformer.&lt;br&gt;
Parsers locate in &lt;code&gt;src/Parsers/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For tests, &lt;code&gt;\App\Providers\FakeRatesProvider&lt;/code&gt; is used with an overridden fetch method and a couple of rates wired into it.&lt;/p&gt;

&lt;p&gt;The rates received in the form of DTO are stored in the database in direct and reverse form, after which the triangulator &lt;code&gt;\App\Services\RatesTriangulator&lt;/code&gt; is put into operation. It creates all possible combinations of rates through intermediate currencies (so-called cross rates) and records them in the &lt;code&gt;\App\Entity\Pair&lt;/code&gt; entity.&lt;/p&gt;

&lt;p&gt;Triangulation is based on the principles of &lt;a href="http://www.dpxo.net/articles/fx_rate_triangulation_sql.html"&gt;http://www.dpxo.net/articles/fx_rate_triangulation_sql.html&lt;/a&gt;. It is much easier to get one pair of currencies for conversion from a separate table with currency pairs than calculate rates for each conversion.&lt;/p&gt;

&lt;p&gt;If something goes wrong, then the providers or the triangulator throw exceptions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using
&lt;/h3&gt;

&lt;p&gt;If you have PHP installed locally, you need to clone the repository, install packages, create a database, perform migrations and update exchange rates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vladimirmartsul/symfony-exchange-demo.git
&lt;span class="nb"&gt;cd &lt;/span&gt;symfony-exchange-demo
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;
php bin/console currency:update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Calculate exchange
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;php bin/console currency:exchange &amp;lt;amount&amp;gt; &amp;lt;from&amp;gt; &amp;lt;to&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example&lt;br&gt;
&lt;code&gt;php bin/console currency:exchange 2 EUR BTC&lt;/code&gt;&lt;br&gt;
should output&lt;br&gt;
&lt;code&gt;[OK] 2 EUR is 0.00005254 BTC&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can also build and run the application in Docker&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vladimirmartsul/symfony-exchange-demo.git
&lt;span class="nb"&gt;cd &lt;/span&gt;symfony-exchange-demo
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exchange rates are loaded during the build.&lt;/p&gt;

&lt;h4&gt;
  
  
  Calculate exchange
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;docker compose run symfony-exchange-demo currency:exchange &amp;lt;amount&amp;gt; &amp;lt;from&amp;gt; &amp;lt;to&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example&lt;br&gt;
&lt;code&gt;docker compose run symfony-exchange-demo currency:exchange 2 EUR BTC&lt;/code&gt;&lt;br&gt;
Should output the same result as the local PHP run.&lt;/p&gt;
&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;p&gt;A couple of tests have been written for the application to make sure that the main functionality works correctly. The tests use a mocked provider of exchange rates.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\App\Tests\Command\CurrencyUpdateCommandTest&lt;/code&gt; - a simple check for messages about successful download, triangulation and update of courses.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\App\Tests\Command\CurrencyExchangeCommandTest&lt;/code&gt; - a little more complicated: checking the real conversion using a dataProvider with several currency pairs and the expected result. Each time the test is run, the exchange rates are updated.&lt;/p&gt;

&lt;p&gt;You can run tests locally by installing additional dev packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;symfony-exchange-demo
&lt;span class="nb"&gt;echo &lt;/span&gt;&lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env.local
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;
php bin/phpunit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or similar with the Docker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;symfony-exchange-demo
&lt;span class="nb"&gt;echo &lt;/span&gt;&lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env.local
docker compose run symfony-exchange-demo composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;
docker compose run symfony-exchange-demo doctrine:database:create
docker compose run symfony-exchange-demo doctrine:migrations:migrate &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;
docker compose run symfony-exchange-demo bin/phpunit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Welcome to comments and pull requests :-)&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>php</category>
      <category>tutorial</category>
      <category>laravel</category>
    </item>
  </channel>
</rss>
