Percona Platform First Steps

https://www.percona.com/blog/wp-content/uploads/2022/05/Percona-Platform.pngPercona Platform

I am a big fan of Percona Monitoring and Management (PMM) and am happy to report that setting up Percona Platform is as easy to set up and offers a lot of value. Percona Platform reached GA status recently and I think you will find it a handy addition to your infrastructure.

What is Percona Platform?

Percona Platform brings together enterprise-level distributions of MySQL, PostgreSQL, and MongoDB plus it includes a range of open source tools for data backup, availability, and management. The core is PMM which provides database management, monitoring, and automated insights, making it easier to manage database deployments. The number of sites with more than 100 separate databases has grown rapidly in the past few years.  Being able to have command and control of that many instances from a CLI has become impossible. Businesses need to move faster in increasingly complex environments which puts ever-increasing pressure on database administrators, developers, and everyone involved in database operations. The spiraling levels of demand make it harder to support, manage, and correct issues in database environments.

What Percona Platform provides is a unified view of the health of your entire database environment to quickly visually identify and remediate issues.  Developers can now self-service many of their database demands quickly and efficiently so they can easily provision and manage databases on a self-service basis across test and production instances. So you spend fewer resources and time on the management of database complexity.

The two keys to Percona Platform are Query Analytics (QAN), which provides granular insights into database behavior and helps uncover new database performance patterns for in-depth troubleshooting and performance optimization, and Percona Advisors, which are automated insights, created by Percona Experts to identify important issues for remediation such as security vulnerabilities, misconfigurations, performance problems, policy compliance, and database design issues. Automated insights within Percona Monitoring and Management ensure your database performs at its best. The Advisors check for replication inconsistencies, durability issues, password-less users, insecure connections, unstable OS configuration, and search for available performance improvements among other functions. 

Percona Platform is a point of control for your database infrastructure and augments PMM to be even more intelligent when connected to the Percona Platform. By connecting PMM with the Percona Platform, you get more advanced Advisors, centralized user account management, access to support tickets, private Database as a Service, Percona Expertise with the fastest SLAs, and more.

Percona Platform architecture

So How Do I Install Percona Platform?

The first step is to install PMM by following the Quick Start Guide. You need version 2.2.7 or later.

Second, You will need a Percona Account.

Third, you will need to connect that account to PMM.

I will assume that you will already have PMM installed.  Did I mention that PMM is free, open source software?

Percona Platform account signup

The signup form allows you to create a new account or use an existing account.

Now you can create a name for your organization.

Form for creating your organization and connecting PMM

After creating your username and password, create your organization

Now login to your PMM dashboard and select the Settings / Percona Platform.  You will need to get your ‘Public Address’ which the browser can populate the value for you if need be.

The PMM Server ID is automatically generated by PMM.  You will need to provide a name for your server, and you will need a second browser window to login into Percona Platform to get the Percona Platform Access Token (this token has a thirty-minute lifetime, so be quick or regenerate another token).

Go back into PMM, paste the Access Token into the Percona Platform Access Token field, and click Connect.

On the Percona Platform page, you will see your PMM instances. Congratulations, you are using Percona Platform!

Advisor Checks

All checks are hosted on Percona Platform. PMM Server automatically downloads them from here when the Advisors and Telemetry options are enabled in PMM under Configuration > Settings > Advanced Settings. Both options are enabled by default.

Depending on the entitlements available for your Percona Account, the set of advisor checks that PMM can download from Percona Platform differ in terms of complexity and functionality.

If your PMM instance is not connected to Percona Platform, PMM can only download the basic set of Anonymous advisor checks. As soon as you connect your PMM instance to Percona Platform, has access to additional checks, available only for Registered PMM instances.

If you are a Percona customer with a Percona Customer Portal account, you also get access to Paid checks, which offer more advanced database health information. A list is provided below.

Check Name Description Tier
MongoDB Active vs Available Connections Checks the ratio between Active and Available connections. Registered, Paid
MongoDB Authentication Warns if MongoDB authentication is disabled. Anonymous, Registered, Paid
MongoDB Security AuthMech Warns if MongoDB is not using the default SHA-256 hashing as SCRAM authentication method. Paid
MongoDB IP Bindings Warns if MongoDB network binding is not set as recommended. Anonymous, Registered, Paid
MongoDB CVE Version Shows an error if MongoDB or Percona Server for MongoDB version is not the latest one with CVE fixes. Anonymous, Registered, Paid
MongoDB Journal Check Warns if journal is disabled. Registered, Paid
MongoDB Localhost Authentication Bypass is Enabled Warns if MongoDB localhost bypass is enabled. Anonymous, Registered, Paid
MongoDB Non-Default Log Level Warns if MongoDB is not using the default log level. Paid
MongoDB Profiling Level Warns when the MongoDB profile level is set to collect data for all operations. Registered, Paid
MongoDB Read Tickets Warns if MongoDB is using more than 128 read tickets. Paid
MongoDB Replica Set Topology Warns if the Replica Set cluster has less than three members. Registered, Paid
MongoDB Version Warns if MongoDB or Percona Server for MongoDB version is not the latest one. Anonymous, Registered, Paid
MongoDB Write Tickets Warns if MongoDB network is using more than 128 write tickets. Paid
Check if Binaries are 32-bits Notifies if version_compatible_machine equals i686. Anonymous, Registered, Paid
MySQL Automatic User Expired Password Notifies if version_compatible_machine equals i686. Registered, Paid
MySQL InnoDB flush method and File Format check Checks the following settings: innodb_file_format, innodb_file_format_max, innodb_flush_method and innodb_data_file_path Registered, Paid
MySQL Checks based on values of MySQL configuration variables Checks the following settings: innodb_file_format,innodb_file_format_max,innodb_flush_method and innodb_data_file_path. Paid
MySQL Binary Logs checks, Local infile and SQL Mode checks Warns about non-optimal settings for Binary Log, Local Infile and SQL mode. Registered, Paid
MySQL Configuration Check Warns if parameters are not following Percona best practices, for infile, replication threads, and replica checksum. Paid
MySQL Users With Granted Public Networks Access Notifies about MySQL accounts allowed to be connected from public networks. Registered, Paid
MySQL User Check Runs a high-level check on user setup Registered, Paid
MySQL Advanced User Check Runs a detailed check on user setup Paid
MySQL Security Check Runs a detailed check on user setup Paid
MySQL Test Database This check returns a notice if there is a database with name ‘test’ or ‘test_%’. Registered, Paid
MySQL Version Warns if MySQL, Percona Server for MySQL, or MariaDB version is not the latest one. Anonymous, Registered, Paid
PostgreSQL Archiver is Failing Verifies if the archiver has failed. Paid
PostgreSQL Cache Hit Ratio Checks database hit ratio and complains when this is too low. Paid
PostgreSQL Configuration Change Requires Restart/Reload Warns when a configuration was changed and requires a server restart/reload Registered, Paid
PostgreSQL fsync is Disabled Shows an error if the fsync configuration is disabled, as this can result in unrecoverable data corruption. Anonymous, Registered, Paid
PostgreSQL Autovacuum Logging Is Disabled Shows an error if the fsync configuration is disabled, as this can result in unrecoverable data corruption. Paid
PostgreSQL Checkpoints Logging Is Disabled Notifies if the log_checkpoints configuration option is not enabled. Paid
PostgreSQL Max_connections is too high Notifies if the max_connections setting is set above 300. Anonymous, Registered, Paid
PostgreSQL Stale Replication Slot Warns for stale replication slots since these can lead to WAL file accumulation and DB server outage. Paid
PostgreSQL Super Role Notifies if there are users with superuser privileges. Anonymous, Registered, Paid
PostgreSQL Version Check Warns if the PostgreSQL minor or major versions are not the latest, and shows an error if the major version is 9.4 or older. Anonymous, Registered, Paid

More Advisors will be added and Perona always welcomes contributions. Check out Percona Platform today!

 

Percona Database Performance Blog

MySQL Partitioned Tables

MySQL Partitioned Tables

Learning Outcomes

  • Learn about List Partitioning.
  • Learn about Range Partitioning.
  • Learn about Columns Partitioning.
  • Learn about Hash Partitioning.
  • Learn about Key Partitioning.
  • Learn about Subpartitioning.

Lesson Material

MySQL supports partitioning of tables. It supports range, list, hash, and key partitioning. Range partitioning lets you partition based on column values that fall within given ranges. List partitioning lets you partition based on columns matching one of a set of discrete values. Hash partitioning lets you partition based on the return value from a user-defined expression (the result from a stored SQL/PSM function). Key partitioning performs like hash partitioning, but it lets a user select one or more columns from the set of columns in a table; a hash manages the selection process for you. A hash is a method of organizing keys to types of data, and hashes speed access to read and change data in tables.

Each of the following subsections discusses one of the supported forms of partitioning in MySQL. Naturally, there are differences between other databases and MySQL’s implementation.

List Partitioning

A MySQL list partition works by identifying a column that contains an integer value, the franchise_number in the following example. Partitioning clauses follow the list of columns and constraints and require a partitioning key to be in the primary key or indexed.

The following list partition works with literal numeric values. MySQL uses the IN keyword for list partitions. Note that there’s no primary key designated and an index is on the auto-incrementing surrogate key column. A complete example is provided to avoid confusion on how to index the partitioning key:

CREATE TABLE franchise
( franchise_id     INT UNSIGNED AUTO_INCREMENT
, franchise_number INT UNSIGNED
, franchise_name   VARCHAR(20)
, city             VARCHAR(20)
, state            VARCHAR(20)
, index idx (franchise_id))
PARTITION BY LIST(franchise_number)
( PARTITION offshore VALUES IN (49,50)
, PARTITION west VALUES IN (34,45,48)
, PARTITION desert VALUES IN (46,47)
, PARTITION rockies VALUES IN (38,41,42,44));

The inclusion of a PRIMARY KEY constraint on the franchise_id column would trigger an ERROR 1503 when the partitioning key isn’t the primary key. The reason for the error message is that a primary key implicitly creates a unique index, and that index would conflict with the partitioning by list instruction. The use of a non-unique idx index on the franchise_id column is required when you want to partition on a non-primary key column.

Range Partitioning

Range partitioning works only with an integer value or an expression that resolves to an integer against the primary key column. The limitation of the integer drives the necessity of choosing an integer column for range partitioning. You can’t define a range-partitioned table with a PRIMARY KEY constraint unless the primary key becomes your partitioning key, like
the one below.

CREATE TABLE ordering
( ordering_id     INT UNSIGNED AUTO_INCREMENT
, item_id         INT UNSIGNED
, rental_amount   DECIMAL(15,2)
, rental_date     DATE
, index idx (ordering_id))
PARTITION BY RANGE(item_id)
( PARTITION jan2011 VALUES LESS THAN (10000) , PARTITION feb2011 VALUES LESS THAN (20000) , PARTITION mar2011 VALUES LESS THAN (30000));

Range partitioning is best suited to large tables that you want to break into smaller pieces based on the integer column. You can also use stored functions that return integers as the partitioning key instead of the numeric literals shown. Few other options are available in MySQL.

Columns Partitioning

Columns partitioning is a new variant of range and list partitioning. It is included in MySQL 5.5 and forward. Both range and list partitioning work on an integer-based column (using TINYINT, SMALLINT, MEDIUMINT, INT [alias INTEGER], and BIGINT). Columns partitioning extends those models by expanding the possible data types for the partitioning column to include CHAR, VARCHAR, BINARY, and VARBINARY string data types, and DATE, DATETIME, or TIMESTAMP data types. You still can’t use other number data types such as DECIMAL and FLOAT. The TIMESTAMP data type is also available only in range partitions with the caveat that you use a UNIX_TIMESTAMP function, according to MySQL Bug 42849.

Hash Partitioning

Hash partitions ensure an even distribution of rows across a predetermined number of partitions. It is probably the easiest way to partition a table quickly to test the result of partitioning on a large table. You should base hash partitions on a surrogate or natural primary key.

The following provides a modified example of the ordering table:

CREATE TABLE ordering
( ordering_id       INT UNSIGNED PRIMARY KEY AUTO_INCREMENT
, item_id           INT UNSIGNED
, rental_amount     DECIMAL(15,2)
, rental_date       DATE)
PARTITION BY HASH(ordering_id) PARTITIONS 8;

This is the partitioning type that benefits from a PRIMARY KEY constraint because it automatically creates a unique index that can be used by the hash. A non-unique index such as the list partitioning example doesn’t work for a hash partition.

Key Partitioning

Key partitioning is valuable because you can partition on columns that aren’t integers. It performs along the line of hash partitioning, except the MySQL Server uses its own hashing expression.

CREATE TABLE orders_list
( order_list_id     INT UNSIGNED AUTO_INCREMENT
, customer_surname  VARCHAR(30)
, store_id          INT UNSIGNED
, salesperson_id    INT UNSIGNED
, order_date        DATE
, index idx (order_list_id))
PARTITION BY KEY (order_date) PARTITIONS 8;

This is the only alternative when you want to partition by date ranges. Like the hash partition, it’s easy to deploy. The only consideration is the number of slices that you want to make of the data in the table.

Subpartitioning

The concept of subpartitioning is also known as composite partitioning. You can subpartition range or list partitions with a hash, linear hash, or linear key.

A slight change to the previously created ordering table is required to demonstrate composite partitioning: we’ll add a store_id column to the table definition. The following is an example of a range partition subpartitioned by a hash:

CREATE TABLE ordering
INT UNSIGNED
INT UNSIGNED
DATE
 ( ordering_id     INT UNSIGNED AUTO_INCREMENT
, item_id          INT UNSIGNED
, store_id         INT UNSIGNED
, rental_amount    DECIMAL(15,2)
, rental_date      DATE
, index idx (ordering_id))
PARTITION BY RANGE(item_id)
  SUBPARTITION BY HASH(store_id) SUBPARTITIONS 4
( PARTITION jan2011 VALUES LESS THAN (10000)
, PARTITION feb2011 VALUES LESS THAN (20000)
, PARTITION mar2011 VALUES LESS THAN (30000));

Composite partitioning is non-trivial and might require some experimentation to achieve optimal results. Plan on making a few tests of different scenarios before you deploy a solution.

Planet MySQL

Matthew Wright: An introduction to accessing financial data in EDGAR, using Python

Some sources of financial data can be expensive or difficult to find. For example, some is only available from exchanges or vendors who charge a hefty fee for access. However, the financial industry is also heavily regulated, and one of its main regulators provides free access to its data. The (U.S. Securities and Exchange Commission)[https://www.sec.gov] 
 Continue reading An introduction to accessing financial data in EDGAR, using Python

The post An introduction to accessing financial data in EDGAR, using Python appeared first on wrighters.io.

Planet Python

Accurate Stormtroopers

https://theawesomer.com/photos/2022/05/accurate_stormtroopers_t.jpg

Accurate Stormtroopers

Link

(PG-13: Gore) As Star Wars fans know, stormtroopers have a long history of missing their targets. Corridor imagines how differently things might have turned out if the Empire’s plastic-armored troops just a bit more practice. Bonus VFX points for Han Solo’s mustache.

The Awesomer

Archaeologists Have Just Discovered CONCLUSIVE PROOF Of The Bible: A Rock In The Red Sea That Says ‘Moses Wuz Here’

https://media.babylonbee.com/articles/article-11387-1.jpg

CAIRO—Nautical archaeologists have at last discovered the exact location of where the Israelites crossed the Red Sea, having found a rock reading “Moses Wuz Here”.

“It seems a bit juvenile, but the evidence is unmistakable,” said archaeologist Benjamin Ioset. “We found it right next to several Egyptian chariots, along with another rock that said ‘Here The Lord Pwned Pharoah’.”

The revelation has lead to a cascade of breakthroughs in tracing the exodus route of ancient Israel. “With this rock in our possession, experts can now match the handwriting to other markers in the desert and we can follow the steps of Moses,” said Mr. Ioset. “For example, we have already discovered a match at the site of Israel’s battle with the Amalekites, where Moses had to keep his staff raised above his head for several hours. There is a small inscription which simply reads ‘Arm Day FTW!’. We also have found in Ancient Midian, where Moses met his wife, an inscription in a cave reading ‘Moses+Zipporah 4Eva’. We really hit the jackpot here!”

Scholars had hoped that this would lead to finally discovering the famed Ark Of The Covenant. Sources say that an archaeology team reportedly found what they believed may be the Ark, but they accidentally touched it and no one’s heard from them since.

At publishing time, experts discovered Moses’ handwriting also displayed a startling similarity to the writing of Mr. Charlton Heston.

Checkmate, atheists!


In the social justice system, words are considered violence. In New York City, the dedicated detectives who investigate these vicious attacks are members of an elite squad known as the Microaggression Victims Unit. These are their stories.

Subscribe to The Babylon Bee on YouTube

The Babylon Bee

Restructuring a Laravel Controller using Services, Events, Jobs, Actions, and more

https://laravelnews.imgix.net/images/controller-refactor.jpg?ixlib=php-3.3.1

One of the top Laravel questions I hear is “How to structure the project”. If we narrow it down, the largest part of it sounds like “If the logic shouldn’t be in Controllers, then where should we put it?”

The problem is there is no single correct answer to such questions. Laravel gives you the flexibility to choose the structure yourself, which is both a blessing and a curse. You won’t find any recommendations in the official Laravel docs, so let’s try to discuss various options, based on one specific example.

Notice: as there’s no one way to structure the project, this article will be full of side-notes, “what if” and similar paragraphs. I advise you don’t skip them, and read the article in full, to be aware of all the exceptions to the best practices.

Imagine you have a Controller method for registering users that does a lot of things:

1public function store(Request $request)

2{

3 // 1. Validation

4 $request->validate([

5 'name' => ['required', 'string', 'max:255'],

6 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],

7 'password' => ['required', 'confirmed', Rules\Password::defaults()],

8 ]);

9 

10 // 2. Create user

11 $user = User::create([

12 'name' => $request->name,

13 'email' => $request->email,

14 'password' => Hash::make($request->password),

15 ]);

16 

17 // 3. Upload the avatar file and update the user

18 if ($request->hasFile('avatar')) {

19 $avatar = $request->file('avatar')->store('avatars');

20 $user->update(['avatar' => $avatar]);

21 }

22 

23 // 4. Login

24 Auth::login($user);

25 

26 // 5. Generate a personal voucher

27 $voucher = Voucher::create([

28 'code' => Str::random(8),

29 'discount_percent' => 10,

30 'user_id' => $user->id

31 ]);

32 

33 // 6. Send that voucher with a welcome email

34 $user->notify(new NewUserWelcomeNotification($voucher->code));

35 

36 // 7. Notify administrators about the new user

37 foreach (config('app.admin_emails') as $adminEmail) {

38 Notification::route('mail', $adminEmail)

39 ->notify(new NewUserAdminNotification($user));

40 }

41 

42 return redirect()->route('dashboard');

43}

Seven things, to be precise. You will all probably agree that it’s too much for one controller method, we need to separate the logic and move the parts somewhere. But where exactly?

  • Services?
  • Jobs?
  • Events/listeners?
  • Action classes?
  • Something else?

The trickiest part is that all of the above would be the correct answers. That’s probably the main message you should take home from this article. I will emphasize it for you, in bold and caps.

YOU ARE FREE TO STRUCTURE YOUR PROJECT HOWEVER YOU WANT.

There, I said it. In other words, if you see some structure recommended somewhere, it doesn’t mean that you have to jump and apply it everywhere. The choice is always yours. You need to choose the structure that would be comfortable for yourself and your future team to maintain the code later.

With that, I probably could even end the article right now. But you probably want some “meat”, right? Ok, fine, let’s play around with the code above.


General Refactoring Strategy

First, a “disclaimer”, so it would be clear what we’re doing here, and why. Our general goal is to make the Controller method shorter, so it wouldn’t contain any logic.

Controller methods need to do three things:

  • Accept the parameters from routes or other inputs
  • Call some logic classes/methods, passing those parameters
  • Return the result: view, redirect, JSON return, etc.

So, controllers are calling the methods, not implementing the logic inside the controller itself.

Also, keep in mind, that my suggested changes are only ONE way of doing it, there are dozens of other ways which would also work. I will just provide you with my suggestions, from personal experience.


1. Validation: Form Request classes

It’s a personal preference, but I like to keep the validation rules separately, and Laravel has a great solution for it: Form Requests

So, we generate:

1php artisan make:request StoreUserRequest

We move our validation rules from the controller to that class. Also, we need to add the Password class on top and change the authorize() method to return true:

1use Illuminate\Validation\Rules\Password;

2 

3class StoreUserRequest extends FormRequest

4{

5 public function authorize()

6 {

7 return true;

8 }

9 

10 public function rules()

11 {

12 return [

13 'name' => ['required', 'string', 'max:255'],

14 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],

15 'password' => ['required', 'confirmed', Password::defaults()],

16 ];

17 }

18}

Finally, in our Controller method, we replace Request $request with StoreUserRequest $request and remove the validation logic from the Controller:

1use App\Http\Requests\StoreUserRequest;

2 

3class RegisteredUserController extends Controller

4{

5 public function store(StoreUserRequest $request)

6 {

7 // No $request->validate needed here

8 

9 // Create user

10 $user = User::create([...]) // ...

11 }

12}

Ok, the first shortening of the controller is done. Let’s move on.


2. Create User: Service Class

Next, we need to create a user and upload the avatar for them:

1// Create user

2$user = User::create([

3 'name' => $request->name,

4 'email' => $request->email,

5 'password' => Hash::make($request->password),

6]);

7 

8// Avatar upload and update user

9if ($request->hasFile('avatar')) {

10 $avatar = $request->file('avatar')->store('avatars');

11 $user->update(['avatar' => $avatar]);

12}

If we follow the recommendations, that logic should not be in a Controller. Controllers shouldn’t know anything about the DB structure of the user, or where to store the avatars. It just needs to call some class method that would take care of everything.

A pretty common place to put such logic is to create a separate PHP Class around one Model’s operations. It is called a Service class, but that’s just a “fancy” official name for a PHP class that “provides a service” for the Controller.

That’s why there’s no command like php artisan make:service because it’s just a PHP class, with whatever structure you want, so you can create it manually within your IDE, in whatever folder you want.

Typically, Services are created when there are more than one method around the same entity or model. So, by creating a UserService here, we assume there will be more methods here in the future, not just to create the user.

Also, Services typically have methods that return something (so, “provides the service”). In comparison, Actions or Jobs are called typically without expecting anything back.

In my case, I will create the app/Services/UserService.php class, with one method, for now.

1namespace App\Services;

2 

3use App\Models\User;

4use Illuminate\Http\Request;

5use Illuminate\Support\Facades\Hash;

6 

7class UserService

8{

9 public function createUser(Request $request): User

10 {

11 // Create user

12 $user = User::create([

13 'name' => $request->name,

14 'email' => $request->email,

15 'password' => Hash::make($request->password),

16 ]);

17 

18 // Avatar upload and update user

19 if ($request->hasFile('avatar')) {

20 $avatar = $request->file('avatar')->store('avatars');

21 $user->update(['avatar' => $avatar]);

22 }

23 

24 return $user;

25 }

26}

Then, in the Controller, we can just type-hint this Service class as a parameter of the method, and call the method inside.

1use App\Services\UserService;

2 

3class RegisteredUserController extends Controller

4{

5 public function store(StoreUserRequest $request, UserService $userService)

6 {

7 $user = $userService->createUser($request);

8 

9 // Login and other operations...

Yes, we don’t need to call new UserService() anywhere. Laravel allows you to type-hint any class like this in the Controllers, you can read more about Method Injection here in the docs.

2.1. Service Class with Single Responsibility Principle

Now, the Controller is much shorter, but this simple copy-paste separation of code is a bit problematic.

The first problem is that the Service method should act like a “black box” that just accepts the parameters and doesn’t know where those come from. So this method would be possible to be called from a Controller, from Artisan command, or a Job, in the future.

Another problem is that the Service method violates the Single Responsibility principle: it creates the user and uploads the file.

So, we need two more “layers”: one for file upload, and one for the transformation from the $request to the parameters for the function. And, as always, there are various ways to implement it.

In my case, I will create a second service method that will upload the file.

app/Services/UserService.php:

1class UserService

2{

3 public function uploadAvatar(Request $request): ?string

4 {

5 return ($request->hasFile('avatar'))

6 ? $request->file('avatar')->store('avatars')

7 : NULL;

8 }

9 

10 public function createUser(array $userData): User

11 {

12 return User::create([

13 'name' => $userData['name'],

14 'email' => $userData['email'],

15 'password' => Hash::make($userData['password']),

16 'avatar' => $userData['avatar']

17 ]);

18 }

19}

RegisteredUserController.php:

1public function store(StoreUserRequest $request, UserService $userService)

2{

3 $avatar = $userService->uploadAvatar($request);

4 $user = $userService->createUser($request->validated() + ['avatar' => $avatar]);

5 

6 // ...

Again, I will repeat: it’s only one way of separating the things, you may do it differently.

But my logic is this:

  1. The method createUser() now doesn’t know anything about the Request, and we may call it from any Artisan command or elsewhere
  2. The avatar upload is separated from the user creation operation

You may think that the Service methods are too small to separate them, but this is a very simplified example: in real-life projects, the file upload method may be much more complex, as well as the User creation logic.

In this case, we moved away a bit from the sacred rule “make a controller shorter” and added the second line of code, but for the right reasons, in my opinion.


3. Maybe Action Instead of Service?

In recent years, the concept of Action classes got popular in the Laravel community. The logic is this: you have a separate class for just ONE action only. In our case, the action classes may be:

  • CreateNewUser
  • UpdateUserPassword
  • UpdateUserProfile
  • etc.

So, as you can see, the same multiple operations around users, just not in one UserService class, but rather divided into Action classes. It may make sense, looking from the Single Responsibility Principle point of view, but I do like to group methods into classes, instead of having a lot of separate classes. Again, that’s a personal preference.

Now, let’s take a look at how our code would look in the case of the Action class.

Again, there’s no php artisan make:action, you just create a PHP class. For example, I will create app/Actions/CreateNewUser.php:

1namespace App\Actions;

2 

3use App\Models\User;

4use Illuminate\Http\Request;

5use Illuminate\Support\Facades\Hash;

6 

7class CreateNewUser

8{

9 public function handle(Request $request)

10 {

11 $avatar = ($request->hasFile('avatar'))

12 ? $request->file('avatar')->store('avatars')

13 : NULL;

14 

15 return User::create([

16 'name' => $request->name,

17 'email' => $request->email,

18 'password' => Hash::make($request->password),

19 'avatar' => $avatar

20 ]);

21 }

22}

You are free to choose the method name for the Action class, I like handle().

RegisteredUserController:

1public function store(StoreUserRequest $request, CreateNewUser $createNewUser)

2{

3 $user = $createNewUser->handle($request);

4 

5 // ...

In other words, we offload ALL the logic to the action class that then takes care of everything around both file upload and user creation. To be honest, I’m not even sure if it’s the best example to illustrate the Action classes, as I’m personally not a big fan of them and haven’t used them much. As another source of examples, you may take a look at the code of Laravel Fortify.


4. Voucher Creation: Same or Different Service?

Next, in the Controller method, we find three operations:

1Auth::login($user);

2 

3$voucher = Voucher::create([

4 'code' => Str::random(8),

5 'discount_percent' => 10,

6 'user_id' => $user->id

7]);

8 

9$user->notify(new NewUserWelcomeNotification($voucher->code));

The login operation will remain unchanged here in the controller, because it is already calling an external class Auth, similar to a Service, and we don’t need to know what is happening under the hood there.

But with Voucher, in this case, the Controller contains the logic of how the voucher should be created and sent to the user with the welcome email.

First, we need to move the voucher creation to a separate class: I’m hesitating between creating a VoucherService and putting it as a method within the same UserService. That’s almost a philosophical debate: what this method is related to the vouchers system, the users’ system, or both?

Since one of the features of Services is to contain multiple methods, I decided to not create a “lonely” VoucherService with one method. We’ll do it in the UserService:

1use App\Models\Voucher;

2use Illuminate\Support\Str;

3 

4class UserService

5{

6 // public function uploadAvatar() ...

7 // public function createUser() ...

8 

9 public function createVoucherForUser(int $userId): string

10 {

11 $voucher = Voucher::create([

12 'code' => Str::random(8),

13 'discount_percent' => 10,

14 'user_id' => $userId

15 ]);

16 

17 return $voucher->code;

18 }

19}

Then, in the Controller, we call it like this:

1public function store(StoreUserRequest $request, UserService $userService)

2{

3 // ...

4 

5 Auth::login($user);

6 

7 $voucherCode = $userService->createVoucherForUser($user->id);

8 $user->notify(new NewUserWelcomeNotification($voucherCode));

Something else to consider here: maybe we should move both of those lines into a separate method of UserService that would be responsible for the welcome email, which would in turn call the voucher method?

Something like this:

1class UserService

2{

3 public function sendWelcomeEmail(User $user)

4 {

5 $voucherCode = $this->createVoucherForUser($user->id);

6 $user->notify(new NewUserWelcomeNotification($voucherCode));

7 }

Then, Controller will have only one line of code for this:

1$userService->sendWelcomeEmail($user);


5. Notifying Admins: Queueable Jobs

Finally, we see this piece of code in the Controller:

1foreach (config('app.admin_emails') as $adminEmail) {

2 Notification::route('mail', $adminEmail)

3 ->notify(new NewUserAdminNotification($user));

4}

It is sending potentially multiple emails, which may take time, so we need to put it into the queue, to run in the background. That’s where we need Jobs.

Laravel Notification classes may be queueable, but for this example, let’s imagine there may be something more complex than just sending a notification email. So let’s create a Job for it.

In this case, Laravel provides the Artisan command for us:

1php artisan make:job NewUserNotifyAdminsJob

app/Jobs/NewUserNotifyAdminsJob.php:

1class NewUserNotifyAdminsJob implements ShouldQueue

2{

3 use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

4 

5 private User $user;

6 

7 public function __construct(User $user)

8 {

9 $this->user = $user;

10 }

11 

12 public function handle()

13 {

14 foreach (config('app.admin_emails') as $adminEmail) {

15 Notification::route('mail', $adminEmail)

16 ->notify(new NewUserAdminNotification($this->user));

17 }

18 }

19}

Then, in the Controller, we need to call that Job with the parameter:

1use App\Jobs\NewUserNotifyAdminsJob;

2 

3class RegisteredUserController extends Controller

4{

5 public function store(StoreUserRequest $request, UserService $userService)

6 {

7 // ...

8 

9 NewUserNotifyAdminsJob::dispatch($user);

So, now, we’ve moved all the logic from the Controller to elsewhere, and let’s recap what we have:

1public function store(StoreUserRequest $request, UserService $userService)

2{

3 $avatar = $userService->uploadAvatar($request);

4 $user = $userService->createUser($request->validated() + ['avatar' => $avatar]);

5 Auth::login($user);

6 $userService->sendWelcomeEmail($user);

7 NewUserNotifyAdminsJob::dispatch($user);

8 

9 return redirect(RouteServiceProvider::HOME);

10}

Shorter, separated into various files, and still readable, right? Again, will repeat once more, that it’s only one way to accomplish this mission, you can decide to structure it in another way.

But that’s not all. Let’s also discuss the “passive” way.


6. Events/Listeners

Philosophically speaking, we can divide all the operations in this Controller method, into two types: active and passive.

  1. We’re actively creating the user and logging them in
  2. And then something with that user may (or may not) happen in the background. So we’re passively waiting for those other operations: sending a welcome email and notifying the admins.

So, as one way of separating the code, it should not be called in the Controller at all, but be fired automatically when some event happens.

You can use a combination of Events and Listeners for it:

1php artisan make:event NewUserRegistered

2php artisan make:listener NewUserWelcomeEmailListener --event=NewUserRegistered

3php artisan make:listener NewUserNotifyAdminsListener --event=NewUserRegistered

The event class should accept the User model, which is then passed to ANY listener of that event.

app/Events/NewUserRegistered.php

1use App\Models\User;

2 

3class NewUserRegistered

4{

5 use Dispatchable, InteractsWithSockets, SerializesModels;

6 

7 public User $user;

8 

9 public function __construct(User $user)

10 {

11 $this->user = $user;

12 }

13}

Then, the Event is dispatched from the Controller, like this:

1public function store(StoreUserRequest $request, UserService $userService)

2{

3 $avatar = $userService->uploadAvatar($request);

4 $user = $userService->createUser($request->validated() + ['avatar' => $avatar]);

5 Auth::login($user);

6 

7 NewUserRegistered::dispatch($user);

8 

9 return redirect(RouteServiceProvider::HOME);

10}

And, in the Listener classes, we repeat the same logic:

1use App\Events\NewUserRegistered;

2use App\Services\UserService;

3 

4class NewUserWelcomeEmailListener

5{

6 public function handle(NewUserRegistered $event, UserService $userService)

7 {

8 $userService->sendWelcomeEmail($event->user);

9 }

10}

And, another one:

1use App\Events\NewUserRegistered;

2use App\Notifications\NewUserAdminNotification;

3use Illuminate\Support\Facades\Notification;

4 

5class NewUserNotifyAdminsListener

6{

7 public function handle(NewUserRegistered $event)

8 {

9 foreach (config('app.admin_emails') as $adminEmail) {

10 Notification::route('mail', $adminEmail)

11 ->notify(new NewUserAdminNotification($event->user));

12 }

13 }

14}

What is the advantage of this approach, with events and listeners? They are used like “hooks” in the code, and anyone else in the future would be able to use that hook. In other words, you’re saying to the future developers: “Hey, the user is registered, the event happened, and now if you want to add some other operation happening here, just create your listener for it”.


7. Observers: “Silent” Events/Listeners

A very similar “passive” approach could be also implemented with a Model Observer, in this case.

1php artisan make:observer UserObserver --model=User

app/Observers/UserObserver.php:

1use App\Models\User;

2use App\Notifications\NewUserAdminNotification;

3use App\Services\UserService;

4use Illuminate\Support\Facades\Notification;

5 

6class UserObserver

7{

8 public function created(User $user, UserService $userService)

9 {

10 $userService->sendWelcomeEmail($event->user);

11 

12 foreach (config('app.admin_emails') as $adminEmail) {

13 Notification::route('mail', $adminEmail)

14 ->notify(new NewUserAdminNotification($event->user));

15 }

16 }

17}

In that case, you don’t need to dispatch any events in the Controller, the Observer would be fired immediately after the Eloquent model is created.

Convenient, right?

But, in my personal opinion, this is a bit dangerous pattern. Not only the implementation logic is hidden from the Controller, but the mere existence of those operations is not clear. Imagine a new developer joining the team in a year, would they check all the possible observer methods when maintaining the User registration?

Of course, it’s possible to figure it out, but still, it’s not obvious. And our goal is to make the code more maintainable, so the fewer “surprises”, the better. So, I’m not a big fan of Observers.


Conclusion

Looking at this article now, I realize I’ve only scratched the surface of possible separations of the code, on a very simple example.

In fact, in this simple example, it may seem that we made the application more complex, creating many more PHP classes instead of just one.

But, in this example, those separate code parts are short. In real life, they may be much more complex, and by separating them, we made them more manageable, so every part may be handled by a separate developer, for example.

In general, I will repeat for the last time: you are in charge of your application, and only you decide where you place the code. The goal is so that you or your teammates will understand it in the future, and will not have trouble adding new features and maintaining/fixing the existing ones.

Laravel News