This article was peer-reviewed by Deji Akala and Marco Pivetta. Thanks to all the peer reviewers of SitePoint to get the best content in SitePoint!
Understanding each other's code and how to use it can sometimes become difficult when medium and large teams collaborate on the same code base. To this end, there are multiple solutions. For example, it can be agreed to follow a set of coding standards to improve the readability of your code, or use a framework that is familiar to all team members (the excellent Laravel entry-level course is available here).
However, this is usually not enough, especially when it is necessary to delve into the application section written a while ago to fix bugs or add new features. At this point, it is difficult to remember how a particular class is expected to work, including themselves and how they are combined with each other. At this point, it is easy to accidentally introduce side effects or errors without knowing them.
These errors may be found in quality assurance, but may also be truly ignored. Even if discovered, sending the code back and fixing can take a lot of time.
So, how do we prevent this? The answer is "Poka Yoke".
Poka Yoke is a Japanese term that roughly translates to "anti-error". The term originated from lean manufacturing and refers to any mechanism that helps the machine operator avoid errors.
In addition to manufacturing, Poka Yoke is also frequently used in consumer electronics. For example, SIM cards can only be inserted into the SIM tray in one way due to their asymmetric shape.
The hardware example lacking Poka Yoke is the PS/2 port, which has the exact same shape for the keyboard connector and the mouse connector. They can only be distinguished by using color codes, so it's easy to accidentally swap connectors and insert them into the wrong ports, as they all fit in the same way.
In addition to being used in hardware, the concept of Poka Yoke can also be applied to programming. The idea is to make the public interface of our code as easy to understand as possible and to immediately throw an error when the code is used incorrectly. This seems obvious, but we actually often encounter code that has flaws in this regard.
However, please note that Poka Yoke is not intended to prevent intentional abuse. Its goal is to prevent unexpected errors, not to protect the code from malicious use. As long as someone has access to your code, they are always able to bypass your security measures if they really want to.
Before discussing what specific measures can be taken to make the code more error-proof, it is important to know that the Poka Yoke mechanism can usually be divided into two categories:
Error prevention techniques help to detect errors as early as possible. They are designed to ensure that no one can accidentally use our code incorrectly by making the interface and behavior as simple and straightforward as possible. Think of the example of a SIM card, which can only be inserted into the SIM tray in one way.
On the other hand, the error detection mechanism exists outside our code. They monitor our applications for potential errors and warn us. An example is to detect whether a device connected to a PS/2 port is the correct type of software, and if not, a warning is displayed to the user about the reason why it does not work. This particular software cannot prevent errors because the connector is interchangeable when plugged in, but it can detect errors and warn us so that the error can be fixed.
In the rest of this article, we will explore several methods that can be used to implement error prevention and error detection in our applications. But remember that this list is just a starting point. Depending on your specific application, there may be additional measures that can make your code more error-proof. Also, it is important to remember the upfront cost of Poka Yoke and make sure it is worth it for your specific project. Depending on the complexity and size of the application, some measures can be too expensive compared to potential error costs. Therefore, you and your team need to decide which measures are best for you to take.
Previously called type hints in PHP 5, type declaration is an easy way to start error-proof function and method signature in PHP 7.
By assigning a specific type to the function parameters, obfuscating the order of the parameter becomes more difficult when calling a function.
For example, let's take a look at this notification we might want to send to the user:
<?php class Notification { private $userId; private $subject; private $message; public function __construct( $userId, $subject, $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() { return $this->userId; } public function getSubject() { return $this->subject; } public function getMessage() { return $this->message; } }
Without a type declaration, we can easily inject variables of the wrong type, which may break our application. For example, we can assume that $userId should be a string, and it may actually have to be an integer.
If we inject the wrong type, the error may not be detected until the application tries to actually process the Notification. By then we might get some incomprehensible error messages about unexpected types, but nothing immediately points to our code that injects strings instead of integers.
Therefore, it is usually more interesting to force the application to crash as soon as possible so that such errors can be detected as early as possible during development.
In this case we can simply add some type declaration, and when we obfuscate the parameter type, PHP will stop immediately and warn us of fatal error:
<?php declare(strict_types=1); class Notification { private $userId; private $subject; private $message; public function __construct( int $userId, string $subject, string $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : int { return $this->userId; } public function getSubject() : string { return $this->subject; } public function getMessage() : string { return $this->message; } }
But note that by default, PHP will try to cast incorrect parameters to their expected type. To prevent this, it is important that we enable strict_types so that we will actually receive a fatal error when an error occurs. Therefore, scalar type declarations are not ideal Poka Yoke forms, but they are a good start to reduce errors. Even if strict_types are disabled, they can still serve as an indication of the expected type of the parameter.
In addition, we declare the return type for the method. These make it easier to determine which value you can expect when calling a specific function.
Operatingly defined return types also help avoid using a large number of switch statements when processing return values, because our method can return various types without explicitly declared return types. So, those who use our method must check which type is actually returned in a specific case. These switch statements are obviously forgotten and lead to undetectable errors. Using return types, such errors will be greatly reduced.
One problem that scalar type hints cannot be easily solved for us is that having multiple function parameters makes it possible to confuse the order of said parameters.
PHP can warn us when we obfuscate the order of parameters when all parameters have different scalar types, but in most cases we may have some parameters with the same type.
To solve this problem, we can wrap our parameters in a value object like this:
class UserId { private $userId; public function __construct(int $userId) { $this->userId = $userId; } public function getValue() : int { return $this->userId; } } class Subject { private $subject; public function __construct(string $subject) { $this->subject = $subject; } public function getValue() : string { return $this->subject; } } class Message { private $message; public function __construct(string $message) { $this->message = $message; } public function getMessage() : string { return $this->message; } } class Notification { /* ... */ public function __construct( UserId $userId, Subject $subject, Message $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : UserId { /* ... */ } public function getSubject() : Subject { /* ... */ } public function getMessage() : Message { /* ... */ } }
Because our parameters now have a very specific type, it is almost impossible to confuse them.
Another advantage of using value objects instead of scalar type declaration is that we no longer need to enable strict_types in each file. If we don't have to remember it, we can't accidentally forget it.
When using value objects, we can encapsulate their data verification logic in the object itself. In doing so, we can prevent the creation of value objects with invalid state, which can cause problems in other layers of the application.
For example, we might have a rule that any given UserId should always be positive.
We can obviously verify this rule every time we get UserId as input, but on the other hand, it can also be easily forgotten in one place or another.
Even if this error causes an actual error to occur in another layer of the application, the error message may not clearly indicate what error actually occurred and is difficult to debug.
To prevent such errors, we can add some validation to the UserId constructor:
<?php class Notification { private $userId; private $subject; private $message; public function __construct( $userId, $subject, $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() { return $this->userId; } public function getSubject() { return $this->subject; } public function getMessage() { return $this->message; } }
This way we can always ensure that when using the UserId object, it has a valid state. This prevents us from constantly reverifying our data at all levels of our application.
Note that we can add scalar type declarations instead of using is_int, but this will force us to enable strict_types everywhere we use UserId.
If we do not enable strict_types, PHP will automatically try to cast other types to int when passing them to UserId. This can be problematic, for example, we might inject a floating point number, which may actually be an incorrect variable because the user ID is not a floating point number.
In other cases, such as when we may be using a Price value object, disabling strict_types may result in a rounding error, as PHP will automatically convert floating point variables to int.
By default, objects are passed by reference in PHP. This means that when we make changes to the object, it changes immediately throughout the application.
While this method has its advantages, it also has some disadvantages. Let's take a look at an example of sending notifications to users via text messages and emails:
<?php declare(strict_types=1); class Notification { private $userId; private $subject; private $message; public function __construct( int $userId, string $subject, string $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : int { return $this->userId; } public function getSubject() : string { return $this->subject; } public function getMessage() : string { return $this->message; } }
We are causing unexpected side effects because the Notification object is passed by reference. By shortening the length of the message in SMSNotificationSender, the referenced Notification object is updated throughout the application, meaning it is also shortened when sent later by the EmailNotificationSender.
To solve this problem, we can make our Notification object immutable. Instead of providing a set method to change it, we can add some with methods to create a copy of the original Notification and then apply the changes:
class UserId { private $userId; public function __construct(int $userId) { $this->userId = $userId; } public function getValue() : int { return $this->userId; } } class Subject { private $subject; public function __construct(string $subject) { $this->subject = $subject; } public function getValue() : string { return $this->subject; } } class Message { private $message; public function __construct(string $message) { $this->message = $message; } public function getMessage() : string { return $this->message; } } class Notification { /* ... */ public function __construct( UserId $userId, Subject $subject, Message $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : UserId { /* ... */ } public function getSubject() : Subject { /* ... */ } public function getMessage() : Message { /* ... */ } }
This way, whenever we change the Notification class by, for example, shortening the message length, the changes no longer propagate throughout the application, thus preventing any unexpected side effects.
But note that it is difficult (if not impossible) to make an object truly immutable in PHP. But to make our code more error-proof, it's already helpful if we add "immutable" with methods instead of set methods, because the user of the class no longer needs to remember to clone the object themselves before making changes.
Sometimes we may have functions or methods that can return certain values or null. These nullable return values can cause problems, because they almost always need to check if they are empty before we can use them. Again, this is something we can easily forget. To avoid always having to check the return value, we can return an empty object instead.
For example, we could have a ShoppingCart where discounts are applied or no discounts are applied:
<?php class Notification { private $userId; private $subject; private $message; public function __construct( $userId, $subject, $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() { return $this->userId; } public function getSubject() { return $this->subject; } public function getMessage() { return $this->message; } }
When calculating the final price of ShoppingCart, we now always have to check whether getDiscount() returns null or actual Discount before we can call the applyTo method:
<?php declare(strict_types=1); class Notification { private $userId; private $subject; private $message; public function __construct( int $userId, string $subject, string $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : int { return $this->userId; } public function getSubject() : string { return $this->subject; } public function getMessage() : string { return $this->message; } }
If we do not do this check, we may receive PHP warnings and/or other unexpected effects when getDiscount() returns null.
On the other hand, if we return an empty object when Discount is not set, we can completely delete these checks:
class UserId { private $userId; public function __construct(int $userId) { $this->userId = $userId; } public function getValue() : int { return $this->userId; } } class Subject { private $subject; public function __construct(string $subject) { $this->subject = $subject; } public function getValue() : string { return $this->subject; } } class Message { private $message; public function __construct(string $message) { $this->message = $message; } public function getMessage() : string { return $this->message; } } class Notification { /* ... */ public function __construct( UserId $userId, Subject $subject, Message $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : UserId { /* ... */ } public function getSubject() : Subject { /* ... */ } public function getMessage() : Message { /* ... */ } }
Now, when we call getDiscount(), we always get the Discount object even if there is no discount available. This way we can apply the discount to our total, and even without the discount, we no longer need the if statement:
class UserId { private $userId; public function __construct($userId) { if (!is_int($userId) || $userId <= 0) { throw new \InvalidArgumentException( 'UserId should be a positive integer.' ); } $this->userId = $userId; } public function getValue() : int { return $this->userId; } }
For reasons we want to avoid nullable return values, we may want to avoid optional dependencies, but just make all of our dependencies necessary.
For example, the following class:
interface NotificationSenderInterface { public function send(Notification $notification); } class SMSNotificationSender implements NotificationSenderInterface { public function send(Notification $notification) { $this->cutNotificationLength($notification); // 发送短信... } /** * 确保通知不超过短信长度。 */ private function cutNotificationLength(Notification $notification) { $message = $notification->getMessage(); $messageString = substr($message->getValue(), 0, 160); // 修正截取长度 $notification->setMessage(new Message($messageString)); } } class EmailNotificationSender implements NotificationSenderInterface { public function send(Notification $notification) { // 发送电子邮件... } } $smsNotificationSender = new SMSNotificationSender(); $emailNotificationSender = new EmailNotificationSender(); $notification = new Notification( new UserId(17466), new Subject('Demo notification'), new Message('Very long message ... over 160 characters.') ); $smsNotificationSender->send($notification); $emailNotificationSender->send($notification);
There are two problems with this method:
We can simplify this problem by making LoggerInterface a required dependency:
class Notification { public function __construct( ... ) { /* ... */ } public function getUserId() : UserId { /* ... */ } public function withUserId(UserId $userId) : Notification { return new Notification($userId, $this->subject, $this->message); // 使用新的Notification实例 } public function getSubject() : Subject { /* ... */ } public function withSubject(Subject $subject) : Notification { return new Notification($this->userId, $subject, $this->message); // 使用新的Notification实例 } public function getMessage() : Message { /* ... */ } public function withMessage(Message $message) : Notification { return new Notification($this->userId, $this->subject, $message); // 使用新的Notification实例 } }
This way our public interface becomes less confusing, and whenever someone creates a new instance of SomeService, they know that the class requires an instance of LoggerInterface, so they won't forget to inject an instance.
In addition, we omitted the if statement to check if the logger was injected, which made our doSomething() easier to read and reduced the possibility of errors when someone made changes to it.
If at some point we want to use SomeService without a logger, we can apply the same logic as the return statement, but just use an empty object:
<?php class Notification { private $userId; private $subject; private $message; public function __construct( $userId, $subject, $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() { return $this->userId; } public function getSubject() { return $this->subject; } public function getMessage() { return $this->message; } }
Ultimately, this has the same effect as using the optional setLogger() method, but it makes our code easier to follow and reduces the possibility of errors in the dependency injection container.
To make our code easier to use, it is best to keep the number of public methods on the class to a minimum. This way our code usage becomes less confusing, we maintain less code, and less likely to break backward compatibility when refactoring.
To keep the public method to a minimum, it can be considered a public method as a transaction.
For example, transfer money between two bank accounts:
<?php declare(strict_types=1); class Notification { private $userId; private $subject; private $message; public function __construct( int $userId, string $subject, string $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : int { return $this->userId; } public function getSubject() : string { return $this->subject; } public function getMessage() : string { return $this->message; } }
While the underlying database can provide transactions to ensure that if the deposit cannot be made, no funds will be withdrawn, and vice versa, the database cannot prevent us from forgetting to call $account1->withdraw() or $account2->deposit( ), which will cause the balance to be incorrect.
Luckily, we can easily solve this problem by replacing our two separate methods with a single transaction method:
class UserId { private $userId; public function __construct(int $userId) { $this->userId = $userId; } public function getValue() : int { return $this->userId; } } class Subject { private $subject; public function __construct(string $subject) { $this->subject = $subject; } public function getValue() : string { return $this->subject; } } class Message { private $message; public function __construct(string $message) { $this->message = $message; } public function getMessage() : string { return $this->message; } } class Notification { /* ... */ public function __construct( UserId $userId, Subject $subject, Message $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : UserId { /* ... */ } public function getSubject() : Subject { /* ... */ } public function getMessage() : Message { /* ... */ } }
As a result, our code becomes more robust because only partially completing transactions is more prone to errors.
In contrast to error prevention mechanisms, error detection mechanisms are not intended to prevent errors. Instead, they are designed to warn us when we detect problems.
They exist most of the time outside our applications and run regularly to monitor our code or specific changes to them.
Unit testing can ensure that the new code works properly, but it can also help ensure that existing code still works as expected when someone refactors a part of the system.
Because someone may still forget to actually run our unit tests, it is recommended to use services like Travis CI and Gitlab CI to automatically run them when they make changes. This way, developers will automatically be notified when major changes occur, which also helps us make sure the changes work as expected when reviewing pull requests.
Apart from error detection, unit testing is also a great way to provide examples of how a particular part of the code is expected to work, which in turn prevents other people from making mistakes when using our code.
Because we may always forget to write enough tests, it may be beneficial to use services like Coveralls to automatically generate code coverage reports when running unit tests. Whenever our code coverage goes down, Coveralls sends us notifications so we can add some unit tests and we can also see how our code coverage changes over time.
Another better way to make sure we have enough unit tests for our code is to set up some mutation tests, such as using Humbug. As the name suggests, these tests are designed to verify that we have sufficient code coverage by slightly changing our source code, then running our unit tests and ensuring that the relevant tests start to fail due to mutations.
Using code coverage reporting and mutation testing, we can make sure that our unit tests cover enough code to prevent unexpected errors or errors.
Code analyzers can detect errors in applications early in the development process. For example, IDEs such as PHPStorm use code analyzers to warn us of errors and give suggestions when we write our code. These range from simple syntax errors to detection of repeated codes.
In addition to the built-in analyzers in most IDEs, third-party or even custom analyzers can be incorporated into the build process of the application to discover specific issues. A non-exhaustive list of analyzers suitable for PHP projects can be found in exakat/php-static-analysis-tools, ranging from coding standard analyzers to analyzers that check for security vulnerabilities.
Online solutions exist, such as SensioLabs Insights.
In contrast to most other error detection mechanisms, log messages can help us detect errors in the application when it runs in real time in production.
Of course, our code is first required to actually record the message when an unexpected situation occurs. Even if our code supports loggers, it can be easy to forget everything while setting them up. Therefore, we should try to avoid optional dependencies (see above).
While most applications record at least some messages, the information they provide becomes truly interesting when they are proactively analyzing and monitoring them using tools like Kibana or Nagios. Such tools give us insight into the errors and warnings that occur in applications when users actively use the application, rather than when testing internally. We have an excellent article on monitoring PHP applications using this ELK stack.
Some errors are often suppressed even if they are actively logging error messages. Whenever a "recoverable" error occurs, PHP tends to continue running as if it wants to help us by keeping the application running. However, errors are often very useful when developing or testing new features because they usually indicate errors in our code.
This is why most code analyzers warn you when they detect that you use @ suppression errors, as it can hide errors that inevitably reappear once a visitor actually uses the application.
Generally, it is best to set the error_reporting level of PHP to E_ALL so that even the slightest warnings will be reported. However, make sure that these messages are recorded somewhere and hidden in front of the user so that sensitive information about the application architecture or potential security vulnerabilities is not exposed to the end user.
Besides the error_reporting configuration, be sure to always enable strict_types so that PHP does not attempt to automatically cast function parameters to its expected type, because this is when converting from one type to another (e.g. from float Rounding errors when converting to int) usually lead to difficult to detect errors.
Since Poka Yoke is more of a concept than a specific technology, it can also be applied to areas outside of PHP (but related to PHP).
At the infrastructure level, tools such as Vagrant can be used to share the same development settings as the production environment, which can prevent many errors.
Using server-building such as Jenkins and GoCD can also help prevent errors when deploying changes to our applications, as this may often include a large number of necessary steps that depend on the application, which can easily be forgotten.
When building REST APIs, we can use Poka Yoke in conjunction to make our API easier to use. For example, we can ensure that an error is always returned when passing unknown parameters in the URL query or request body. This may seem strange because we obviously want to avoid "breaking" our API client, but it is usually better to warn developers using our API as soon as possible about incorrect usage so that bugs can be fixed early in the development process.
For example, there might be a color parameter on our API, but someone using our API might accidentally use the colour parameter. Without any warnings, this error can easily go into production until the end user notices it due to unexpected behavior. To understand how to build an API that won't disappoint you later, a good book may help.
Effectively all applications rely on at least some custom configuration. Typically, developers prefer to provide as many default values as possible for configurations, so configuring an application is less work.
However, like the color and colour examples above, it is easy to enter configuration parameters incorrectly, which causes our application to accidentally fall back to the default value. It is difficult to trace such errors when the application does not throw an error, and the best way to throw an error for incorrect configuration is to not provide any default values at all and immediately throw an error when configuration parameters are missing.
Poka Yoke concept can also be applied to prevent or detect user errors. For example, in payment software, a check bit algorithm can be used to verify the account entered by the user. This prevents users from accidentally entering an account with a typo.
While Poka Yoke is more of a concept than a specific set of tools, we can apply various principles to our code and development processes to ensure that errors are prevented or detected as early as possible. Often, these mechanisms will be specific to the application itself and its business logic, but we can use some simple techniques and tools to make any code more error-proof.
The most important thing is that while we obviously want to avoid errors in production, they are very useful in development and we should not be afraid to raise them as soon as possible so that errors can be traced more easily. These errors can be raised by the code itself or by separate processes running separately from our application and monitoring it from the outside.
To further reduce errors, we should strive to make the public interface of the code as simple and clear as possible.
If you have other tips on how to apply Poka Yoke to PHP development or general programming, feel free to share in the comments!
Poka-Yoke is a Japanese term that translates as "mistake prevention". In the context of programming, it is a defensive design approach designed to prevent errors from happening. It involves implementing security measures to help avoid errors and ensure that functions are used correctly. Poka-Yoke's main purpose in programming is to improve the quality of the software and reduce errors, thereby saving time and resources during the development process.
Traditional programming methods usually focus on creating functional code, error handling and error fixing are usually done after initial development. On the other hand, Poka-Yoke takes an active approach, combining error prevention mechanisms in its own development stage. This leads to more powerful and reliable code, reducing the need for a lot of debugging and testing later.
Yes, Poka-Yoke is a concept that can be applied to any programming language. It is not a specific tool or technology, but a way of thinking or method of programming. No matter which language you are using, you can implement the Poka-Yoke principle to make your code more error-resistant.
Examples of Poka-Yoke in programming include input verification (to ensure the data is formatted correctly before processing), using assertions to check the program's state at certain points, and implementing fail-safe defaults (to minimize in case of a failure) The default action for damage).
By preventing errors during the development phase, Poka-Yoke helps improve the overall quality of software products. It reduces the number of errors and defects, resulting in more stable and reliable software. This not only improves the user experience, but also reduces the cost and time of debugging and maintenance.
While implementing Poka-Yoke may take some extra time during the development phase, it can save a lot of time in the long run. By preventing errors before they occur, it reduces the time spent on debugging and fixing errors, thus reducing delivery time and improving development efficiency.
In manufacturing, Poka-Yoke can help prevent errors and defects, thus producing higher quality products. It can also increase efficiency and productivity by reducing the time and resources for rework and repairs.
To start implementing Poka-Yoke in your programming practice, you must first identify common errors or potential failure points in your code. Then, develop strategies to prevent these errors, or handle them gracefully when they occur. This may include input verification, assertions, fail-safe defaults, or other error prevention techniques.
Yes, Poka-Yoke can be used effectively for agile development. In fact, it fits well with the agile principle (frequent delivery of runnable software) because it helps ensure that every iteration of the software is as error-free as possible.
No, Poka-Yoke is beneficial for projects of any size. Even for small projects, preventing errors during the development phase can save time and resources and produce higher quality final products.
The above is the detailed content of Poka Yoke - Saving Projects with Hyper-Defensive Programming. For more information, please follow other related articles on the PHP Chinese website!