Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Anders Björkland
Anders Björkland

Posted on

     

Bolt CMS for developers

In this third article about Bolt CMS we will explore how to work with Bolt CMS as developers. We are going to create a project we callThe Elephpant Experience. We will build a widget for the backend to fetch a random elephant picture and have it being displayed on our homepage. Topics we are going to touch on are:

If you just want the code, you can explore theGithub Repo

Warning! Do not use this code in a production environment. We do not delve into security issues in this one, but as little homework for you - can you spot what would need to be secured in the code?

Answer

The routes are open for anyone! We can solve this by adding a single row to the beginning of thestoreImage-method in our controller:

$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');

Installing Bolt CMS on a development machine

Requirements

On theGetting started page of Bolt CMS we find most of any questions we have on how to set up our local development environment. We are going to be using Bolt version 5 and its basic requirements are PHP >7.2, SQLite, and a few PHP-extensions.

PHP-extensions

  • pdo
  • openssl
  • curl
  • gd
  • intl (optional but recommended)
  • json
  • mbstring (optional but recommended)
  • opcache (optional but recommended)
  • posix
  • xml
  • fileinfo
  • exif
  • zip

We can runphp -m to see a list of all the extensions installed and enabled. If anything is missing, it may be a question of enabling them inphp.ini or otherwise download them fromPECL.

If you find it difficult setting up PHP-extensions on a Windows machine, you can find aninstruction of installing extensions in the official PHP docs. Feel free to ask any questions about this, I know I had many when starting out.

CLI tools

  • Composer - A tool for managing dependencies for PHP projects.
  • Symfony CLI - A tool for managing Symfony projects, comes bundled with a great virtual server.Nice to have.

Installing Bolt CMS

Having Composer as a CLI tool we can use it to install Bolt CMS. From a terminal we runcomposer create-project bolt/project elephpant-experience. This will install the latest stable version of Bolt CMS into a new folder calledelephpant-experience.

Installing Bolt CMS with Composer

Moving into the folder we just created, we can change any configurations needed. For the purpose of this article we will leave the defaults as they are. If you don't want to use SQLite you need to change the .env file to use a different database.

Next up we will initialize our database and populate it with some content. Let's add an admin user and then leverage the fixtures that Bolt provides. We do this withphp bin/console bolt:setup.

Initialize the database, create an admin user and load fixtures

We can now launch the project. We can runsymfony serve -d directly from our project-root orphp -S localhost:8000 from./public. When the server is running we can visitlocalhost:8000 in our browser. The first load of the page will take a few seconds to load. This will build up a cache so the next time we visit the page it will be faster.

A benefit of usingsymfony serve is that it provides us with SSL-certificate if that is something you would prefer to have. It will prompt you with a notice if you have not installed a certificate for Symfony and the necessary command to run if you want it.

Good job! We have successfully installed Bolt CMS on our development machine ✔

Building a Controller and add a route for it

As Bolt CMS is built on top of Symfony there are many things we can do the Symfony-way. Let's start by creating the fileElephpantController.php in thesrc/Controller/ folder. We will use the@Route annotation for each function we want to return a response. So here's a basic example:

<?phpnamespaceApp\Controller;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Annotation\Route;classElephpantController{/**     * @Route("/random-elephpant", name="random_elephpant")     */publicfunctionindex(){returnnewResponse('Hello Elephpants!');}}
Enter fullscreen modeExit fullscreen mode

When we visitlocalhost:8000/elephpant we will see the textHello Elephpants!. As simple as that, we can add our own logic to our Bolt-project.

The reason Bolt manages to connect the route to our controller is because Bolt has a configuration inroutes.yaml to look for @Route-annotations in files located in thesrc/Controller/ folder.

Knowing the basics of how to build a controller, we want to expand on it and build a way to fetch and store an elephant picture. It will return a json-response with a status-code. This will be useful when we build a widget for the backend - as we want to know if we were successful or not. We are going to do a simple crawl on Unsplash and fetch a random picture from the result set where we have searched onelephant.

First we are going to get a new dependency to the project. Have Composer get this for us:

composer require symfony/dom-crawler
Enter fullscreen modeExit fullscreen mode

This dependency will work in tandem with Symfony's HttpClient. This comes with Bolt as one of its dependencies and is used to fetch data from the internet. It can make cURL request which we will use to fetch a response from Unsplash. Before we do anything fancy like storing and building a widget to switch out the image, we are expanding our controller to fetch sush an image randomly and display it on the URI for/random-elephant. Each time we reload that page, another image will be loaded.

<?phpnamespaceApp\Controller;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\DomCrawler\Crawler;useSymfony\Component\HttpClient\HttpClient;useSymfony\Component\HttpFoundation\Request;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Annotation\Route;useSymfony\Contracts\HttpClient\HttpClientInterface;classElephpantControllerextendsAbstractController{/**     * @Route("/random-elephpant", name="random_elephpant")     */publicfunctionindex(Request$request,HttpClientInterface$httpClient):Response{$response=$httpClient->request('GET','https://unsplash.com/s/photos/elephants');$crawler=newCrawler($response->getContent());$crawler=$crawler->filterXPath('descendant-or-self::img');$imageArray=[];$crawler->each(function(Crawler$node)use(&$imageArray){$src=$node->attr('src');$alt=$node->attr('alt');if($src&&$alt){$imageArray[]=['src'=>$src,'alt'=>$alt];}});$imageIndex=rand(0,count($imageArray)-1);$imageAtRandom=false;if($imageIndex>0){$imageAtRandom=$imageArray[$imageIndex];}returnnewResponse('<html><body><h1>Elephpant</h1><img src="'.$imageAtRandom['src'].'" alt="'.$imageAtRandom['alt'].'"> </body></html>');}}
Enter fullscreen modeExit fullscreen mode

This gets us a simple random image:

A random image loaded from Unsplash of an elephant.

Putting a pin in the controller for now, we are going to build a contentType of it where we will store the reference of a single random image.

Creating and storing ContentType based on API-calls

We are going to create a ContentType that will be a singleton to store just the URL/source and alt-text of the image we fetched in the previous step. We will call this typeElephpant. In./config/bolt/contenttypes.yaml we will create this type:

elephpant:name:Elephpantsingular_name:Elephpantfields:name:type:textlabel:Name of this elephpantsrc:type:textalt:type:textslug:type:sluguses:[name]group:Metadefault_status:publishedicon_one:"fa:heart"icon_many:"fa:heart"singleton:true
Enter fullscreen modeExit fullscreen mode

After updatingcontenttypes we want to update Bolt's database to use it. We do this by runningphp bin\console cache:clear. This will lay the ground for us to be able to store the image-reference in Bolt. Which leads us into the next step where we will do just that.

We return to the controller and itsindex-function. We will continue to fetch an image when we visit the route. We will return a json-response with the image-url and alt-text when there's a GET-request. Upon a POST-request (which we will use later on) we will store the image-url and alt-text in Bolt. Let's build this out in the controller:

<?phpnamespaceApp\Controller;useBolt\Factory\ContentFactory;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\DomCrawler\Crawler;useSymfony\Component\HttpClient\HttpClient;useSymfony\Component\HttpFoundation\Request;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Annotation\Route;useSymfony\Contracts\HttpClient\HttpClientInterface;classElephpantControllerextendsAbstractController{/**     * @Route("/random-elephpant", name="random_elephpant", methods={"GET"})     */publicfunctionindex(Request$request,HttpClientInterface$httpClient):Response{$response=$httpClient->request('GET','https://unsplash.com/s/photos/elephants');$crawler=newCrawler($response->getContent());$crawler=$crawler->filterXPath('descendant-or-self::img');$imageArray=[];$crawler->each(function(Crawler$node)use(&$imageArray){$src=$node->attr('src');$alt=$node->attr('alt');if($src&&$alt){$imageArray[]=['src'=>$src,'alt'=>$alt];}});$imageIndex=rand(0,count($imageArray)-1);$imageAtRandom=false;if($imageIndex>=0){$imageAtRandom=$imageArray[$imageIndex];}if($imageAtRandom){return$this->json(['status'=>Response::HTTP_OK,'image'=>$imageAtRandom,]);}return$this->json(['status'=>Response::HTTP_NOT_FOUND,'message'=>'No image found',]);}/**     * @Route("/random-elephpant", name="store_elephpant", methods={"POST"})     */publicfunctionstoreImage(Request$request,ContentFactory$contentFactory):Response{$query=$request->getContent();$query=json_decode($query,true);$src="";$alt="";if(isset($query['src'])){$src=$query['src'];}if(isset($query['alt'])){$alt=$query['alt'];}if($src&&$alt){$elephpantContent=$contentFactory->upsert('elephpant',['name'=>'Random Elephpant',]);$elephpantContent->setFieldValue('src',$src);$elephpantContent->setFieldValue('alt',$alt);$elephpantContent->setFieldValue('slug','random-elephpant');$contentFactory->save($elephpantContent);return$this->json(['status'=>Response::HTTP_OK,'image'=>['src'=>$src,'alt'=>$alt]]);}return$this->json(['status'=>Response::HTTP_INTERNAL_SERVER_ERROR]);}}
Enter fullscreen modeExit fullscreen mode

In this updated version of the controller we specify that the index-version will be used forGET-requests and thestoreImage-version forPOST-requests. We also use theContentFactory to create a newelephpant-content. We then save the content and return a json-response with the status. This will be used in a widget for the admin interface, which we are going to build next.

Build a Bolt Widget for controlling when to fetch a new picture

Next up is for us to build a widget to show on the dashboard. This will enable us to control when to switch out our random image. For this to work, three different parts are required of us. One is theelephpant.html.twig file, which will be used to render the widget. The other two are theElephpantExtension andElephpantWidget files which will be responsible to inject this template to the admin interface. We start with thetwig-template.

The twig-template will be responsible for displaying our widget, asynchroneously fetch a random image (with the help of our controller) and display it, and also asynchroneously store the image (again with the help of our controller) if we like it.

./templates/elephpant-widget.html.twig

<style>.elephpantimg{width:auto;height:10rem;max-width:20rem;}</style><divclass="widget elephpant"><divclass="card mb-4"><divclass="card-header"><iclass="fas fa-plug"></i>{{extension.name}}</div><divclass="card-body"><p>Update the random image?</p>{%setcontentelephpant='elephpant'%}<div><p>Current Image</p><divclass="image card-img-top"><divid="elephpant-img-container">{%ifelephpantisdefinedandelephpantisnotnull%}<imgsrc="{{elephpant.src}}"alt="{{elephpant.alt}}"/>{%else%}<p>No image</p>{%endif%}</div></div></div><divclass="mt-4"><buttonclass="btn btn-secondary"onclick="fetchElephpantImg()">Fetch new image</button><div><divid="elephpant-img-preview"></div><buttonid="elephpant-img-store"class="btn btn-secondary d-none"onclick="storeElephpantImg()">Store</button></div></div></div></div></div><script>constelephpantImgContainer=document.getElementById('elephpant-img-container');constelephpantImgPreview=document.getElementById('elephpant-img-preview');conststoreButton=document.getElementById('elephpant-img-store');/**     * Fetch a new image from our controller's GET-route     */functionfetchElephpantImg(){fetch('/random-elephpant').then(response=>response.json()).then(data=>{elephpantImgPreview.innerHTML=`<img src="${data.image.src}" alt="${data.image.alt}" />`;storeButton.classList.remove('d-none');}).catch(error=>{console.error(error);});}/**     * Store the current preview-image through our controller's POST-route     */functionstoreElephpantImg(){constelephpantImg=elephpantImgPreview.querySelector('img');constrandomImg=elephpantImgPreview.querySelector('img');constrandomImgSrc=randomImg.getAttribute('src');constrandomImgAlt=randomImg.getAttribute('alt');fetch('/random-elephpant',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({src:randomImgSrc,alt:randomImgAlt})}).then(response=>response.json()).then(data=>{storeButton.classList.add('d-none');elephpantImgContainer.innerHTML=`<img src="${data.image.src}" alt="${data.image.alt}" />`;elephpantImgPreview.innerHTML='';}).catch(error=>{console.error(error);});}</script>
Enter fullscreen modeExit fullscreen mode

Next we need to create theElephpantExtension-class. This class will be responsible for registering our widget. We create a new folder calledExtension in oursrc-folder and create a newElephpantExtension.php-file there:

<?phpnamespaceApp\Extension;useApp\Extension\ElephpantWidget;useBolt\Extension\BaseExtension;classElephpantExtensionextendsBaseExtension{publicfunctiongetName():string{return'elephpant extension';}publicfunctioninitialize($cli=false):void{$this->addWidget(newElephpantWidget($this->getObjectManager()));}}
Enter fullscreen modeExit fullscreen mode

If you have intellisense or a reasonable sane IDE, you should get some kind of warning because we do not have the ElephpantWidget-class yet. Let's fix that. In the same folder, create theElephpantWidget.php-file:

<?phpnamespaceApp\Extension;useBolt\Widget\BaseWidget;useBolt\Widget\Injector\RequestZone;useBolt\Widget\Injector\AdditionalTarget;useBolt\Widget\TwigAwareInterface;classElephpantWidgetextendsBaseWidgetimplementsTwigAwareInterface{protected$name='Elephpant Experience';protected$target=ADDITIONALTARGET::WIDGET_BACK_DASHBOARD_ASIDE_TOP;protected$priority=300;protected$zone=REQUESTZONE::BACKEND;protected$template='@elephpant-experience/elephpant-widget.html.twig';}
Enter fullscreen modeExit fullscreen mode

This file is pretty minimal and is responsible for directing Bolt on where to use its Widget and how to render it. We set thetemplate-property to theelephpant-widget.html.twig-file, which is located in the@elephpant-experience-namespace. This namespace is directing to the project root'stemplates-folder. Just be sure to update the namespace if you choose to change the$name-property of this file.

On the backend we will have the following experience in the dashboard:

The widget is displayed on the dashboard and can fetch and store a random image.

We are now on the final stretch. For the last touch we are going to display our image when there is one. We will add it to the homepage for the theme of our choice.

Set up a theme to display the picture

The default theme for Bolt is currentlybase-2021. Usually we would copy this theme and make it our own, but for this tutorial we will modify it directly. Go to./public/theme/base-2021/index.twig and add the following after the secondinclude, on line 8, add:

{# The ELEPHPANT Experience #}{%setcontentelephpant='elephpant'%}{%ifelephpantisdefinedandelephpantisnotnull%}<sectionclass="bg-white border-b py-6"><divclass="container max-w-5xl mx-auto m-8"><divclass="flex flex-col"><h3class="text-3xl text-gray-800 font-bold leading-none mb-3 text-center">The Elephpant Experience</h3>{%ifelephpant%}<divclass="w-full sm:w-1/2 p-6 mx-auto"><imgclass="w-full object-cover object-center"src="{{elephpant.src}}"alt="{{elephpant.alt}}"></div>{%endif%}</div></div></section>{%endif%}
Enter fullscreen modeExit fullscreen mode

The Bolt-2021 theme uses Tailwind CSS, therefore we see its utility classes being used here. We get the elephpant contentType and display the image if it exists by usingsetContent. Then we use the elephpant variable to accesssrc andalt for the image.

We have finally arrived at the goal. We have:

  • Installed Bolt CMS
  • Built a Controller for our routes
  • Created a custom ContentType
  • Used a web crawler to fetch a random image (of elephants)
  • Added a route for storing the image
  • Built a dashboard widget
  • Display the random image on our homepage

It's quite a lot, and if you have any questions or viewpoints you are more than welcome to share them. That's the wrap-up, thank you for reading aboutThe Elephpant Experience.
The Elephpant Experience

Top comments(2)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
tompich profile image
Thomas
Self taught fullstack developer for three year. Before that, I was Physics teacher in High School.
  • Education
    Master of Space Studies / Master of physics for teaching
  • Work
    Freelance web dev
  • Joined

Hey Anders,
That was absolutely great, thank you.
I really felt in love with Bolt, but it's documentation is very thin when it comes to add functionalities. Your article really helped me.
I would really love to read more on this topic.

CollapseExpand
 
andersbjorkland profile image
Anders Björkland
Runner enthusiast and fullstack developer coming from a Political science background. I love coding as much as I do running 🐱‍🏍He/Him.
  • Location
    Stockholm, Sweden
  • Joined

Thanks Thomas! I wouldn't mind exploring how it has evolved over the last couple of years. I've been up in arms with Sylius and SilverStripe lately, so it would be about time to do it.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Runner enthusiast and fullstack developer coming from a Political science background. I love coding as much as I do running 🐱‍🏍He/Him.
  • Location
    Stockholm, Sweden
  • Joined

More fromAnders Björkland

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp