It's been a year since my first blog post here. 🖤

For me this journey has meant so much, specially because I get the chance to write about different topics in a very entertaining way. And you know what's my favorite part? That I choose what to share with you and this has help me to improve my communication skills... I'm still pretty shy,  but I'm still here. 🤟🏻

Anyway, let's get down to business.

Let's start from the very beginning: what's Craft CMS?

It's a flexible and reliable content management system. That's it!

I have been using it for a few months now and some benefits that I found are:

  • The interface is intuitive and easy to use. It will not be difficult for you to adapt if you come from using WordPress.
  • It includes flexible fields that allow you to better control the way the content is structured. You don't need to install a plugin for this. 🎉 (cough ACF) 🤭
  • There's a big community of developers using it and a big app store with plugins to improve the CMS.

One of the things I did in Craft CMS was a massive update of images in various entries of a specific section. I needed to implement this because I had approximately 700 entries with people's information and each entry needed to replace the profile image, that was a lot of manual work. The solution for this was creating a module that helped me to make an easier and controlled work.

"Mod­ules in Craft are a way to extend a Yii appli­ca­tion. Yii is the PHP frame­work on which Craft is built". Craft CMS have modules and plugins, but for this case using module is a good option because I had a specific requirement and I didn't need this functionality to be used in other projects. (If you want to read more about modules and plugins, i highly recommend you to read this blog post

So, I want to share with you how to create the module and how  to solve the problem that I mention above step by step.

One

First is important to know the structure of the modules. Following the conventions is the main rule.

https://craftcms.com/docs/3.x/extend/module-guide.html#set-up-the-basic-file-structure

Where "foo" is the module ID or the name that you can identify the module.

Two

The next step is to create the module's folders.

To get started I used https://pluginfactory.io/ where you can bootstrap a module or plugin (as you need). It generates a README.md with helpful information on how to bootstrap the module and get composer to autoload it.

In this case, I only use Controllers as a module component and set a name for this. Then click "Build my module"

After that a zip is downloaded. Then you need to uncompress that zip and add it inside the modules folder (view the basic files structure image).

The module ID for this is: "updateprofileimagesmodule".

Three

The next step is to read the instructions in the README.md file. It states that inside /config folder there is app.php which contains information about the module.

return [
	'modules' => [
		'update-profile-images-module' => [
			'class' => \\modules\\updateprofileimagesmodule\\UpdateProfileImagesModule::class,
	   ],
   ],

	'bootstrap' => ['update-profile-images-module'],
];

Four

Now the README.md also says that is important to configure the composer.json file adding the module in the node "autoload" > "psr-4"

"autoload": {
	"psr-4": {
		"modules\\\\updateprofileimagesmodule\\\\": "modules/updateprofileimagesmodule/src/"
	}
},

After this, you will need to do:

composer dump-autoload

And the module is installed.

Five

Now, the next step is coding in BioProfilesController.

The controller has two functions "actionIndex" and "actionDoSomething" but for this case is better to create another one. I like to name it: "actionSetBioImage"

In order to update each bio is important to identify what is the volume where the images are. "Volumes are storage containers. A volume can be a directory on the web server, or a remote storage service like Amazon S3."

To retrieve the images for a specific volume we write this line:

$assets = Asset::find()->volume('peopleUploads')->all();

Six

Step six is to iterate each asset to get the ID and filename

for($i=0; $i < count($assets); $i++) {
	$featureImageId = $assets[$i]['id'];
	$peopleName = $assets[$i]['filename'];
}

In the code before we assume that naming convention of each image is "name-lastname" (with middle dash) no matter if it is *.jpg or *.png

Seven

Now, inside the for loop find $peopleName variable all entries for a specific section:

for($i=0; $i < count($assets); $i++) {
	$featureImageId = $assets[$i]['id'];
	$peopleName = $assets[$i]['filename'];

	$entry = Entry::find()
								->section('people')
								->slug($peopleName.'*')
                 ->one();
}

In this case filter by slug. It is an efficient way to find $peopleName because the slug is conformed by middle dash.

The asterisk in slug is because you can find the string if it begins with $peopleName

Eight

After that, create an array that contains the parameter and value related to the field that store the image in each people bio.

$fieldValues = [ 'bioImage' => [$featureImageId]];

Nine

Now we check if the previous entry exists.

if($entry) {
	$entry->setFieldValues($fieldValues);
	$success = Craft::$app->elements->saveElement($entry);

	if (!$success){
		Craft::error('Couldn’t save the entry '.$entry->title, __METHOD__);
	
	} else {
		ArrayHelper::append($updatedBioPeopleArray, $entry->id);
		LogToFile::info($entry->id.': '.$entry->title, 'bio-people-log');
	}

} else {
		LogToFile::info($peopleName, 'bio-people-log');
}

If exists we pass the array $fieldValues to each $entry and save it.

If the entry wasn't saved correctly an error is thrown, otherwise we append to an array the id of the updated entry and create a log with the ID and title updated.

Now if the entry doesn't exists, create a log with name of the file that was not found in the iteration.

💡 To access to log file, go to: storage/logs/bio-people.log

Ten

Don't forget that $updatedBioPeopleArray variable. What does that mean?

Well, we save in array all the bio people updated successfully. It is important to initialize the array. I suggest doing it at the beginning of the function.

$notUpdatedBioPeople = Entry::find()
		                        ->section('people')
		                        ->id(['not', $updatedBioPeopleArray ])
		                        ->all();

The reason to do this is because if one entry is not updated then we set a placeholder image. So now we find the entries in people section where not the ID that comes from the array.

Eleven

The next thing to do is find the placeholder image that we are gonna use

$assetPlaceholder = Asset::find()->filename('bio_placeholder.png')->one()->id;

Twelve

Finally iterate through each entry in $notUpdatedBioPeople

foreach($notUpdatedBioPeople as $notUpdatedBio) {
	$fieldValues = ['bioImage' => [$assetPlaceholder]];
	
	$notUpdatedBio->setFieldValues($fieldValues);
	Craft::$app->elements->saveElement($notUpdatedBio);
}

Basically we follow the same way to update the entry but in this case we set a specific image.

This is the final code:

public function actionSetBioImage() {
	$updatedBioPeopleArray = array(); 
	
	$assets = Asset::find()->volume('peopleUploads')->all();

	for($i=0; $i < count($assets); $i++) {
	 $featureImageId = $assets[$i]['id'];
		$peopleName = $assets[$i]['filename'];

		$entry = Entry::find()
									->section('people')
									->slug($peopleName.'*')
	                 ->one();
		$fieldValues = [ 'bioImage' => [$featureImageId]];

		if($entry) {
			$entry->setFieldValues($fieldValues);
			$success = Craft::$app->elements->saveElement($entry);
		
			if (!$success){
				Craft::error('Couldn’t save the entry '.$entry->title, __METHOD__);
			
			} else {
				ArrayHelper::append($updatedBioPeopleArray, $entry->id);
				LogToFile::info($entry->id.': '.$entry->title, 'bio-people-log');
			}
		
		} else {
				LogToFile::info($peopleName, 'bio-people-log');
		}
	}

	$notUpdatedBioPeople = Entry::find()
			                        ->section('people')
			                        ->id(['not', $updatedBioPeopleArray ])
			                        ->all();
	
	$assetPlaceholder = Asset::find()->filename('bio_placeholder.png')->one()->id;
	
	foreach($notUpdatedBioPeople as $notUpdatedBio) {
		$fieldValues = ['bioImage' => [$assetPlaceholder]];
		
		$notUpdatedBio->setFieldValues($fieldValues);
		Craft::$app->elements->saveElement($notUpdatedBio);
	}

}

Now, how do we test this?

Thirteen

It's important to set a route to access to the function. To define this, go to config/routes.php located in the root of the project.

return [ 
	'set-people-bio-image' =>  'update-profile-images-module/bio-profiles/set-bio-image',
];

There are conventions to write the route:

  • The name of the route can be whatever you want
  • The reference to the controller function needs to be:
    • The name of the module in lowercase and separated by middle dash
    • The name of the controller in lowercase, separated by middle dash and without the word "controller"
    • The name of the function in lower case, separated by middle dash and without the word "action".

Now type in the browser "mycustomdomain.com/set-people-bio-image" and wait until the massive update conclude. After that you can check the logs too.

And that's it! You've done some witchCraft CMS!

To me, Craft CMS is a good discovery and was a real challenge in my daily work, so expect future blog posts about this awesome CMS tool and my process. ✨

Happy Halloween! 🔮