Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Assert
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
10 / 10
38
100.00% covered (success)
100.00%
1 / 1
 precondition
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 parameter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 parameterType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 parameterKeyType
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 parameterElementType
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 nonEmptyString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 postcondition
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 invariant
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 hasType
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
12
 isInstanceOf
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Assert;
5
6/**
7 * Assert provides functions for assorting preconditions (such as parameter types) and
8 * postconditions. It is intended as a safer alternative to PHP's assert() function.
9 *
10 * Note that assertions evaluate expressions and add function calls, so using assertions
11 * may have a negative impact on performance when used in performance hotspots. The idea
12 * if this class is to have a neat tool for assertions if and when they are needed.
13 * It is not recommended to place assertions all over the code indiscriminately.
14 *
15 * For more information, see the README file.
16 *
17 * @since 0.1.0
18 *
19 * @license MIT
20 * @author Daniel Kinzler
21 * @author Thiemo Kreuz
22 * @copyright Wikimedia Deutschland e.V.
23 */
24class Assert {
25
26    /**
27     * Checks a precondition, that is, throws a PreconditionException if $condition is false.
28     * For checking call parameters, use Assert::parameter() instead.
29     *
30     * This is provided for completeness, most preconditions should be covered by
31     * Assert::parameter() and related assertions.
32     *
33     * @see parameter()
34     *
35     * @note This is intended mostly for checking preconditions in constructors and setters,
36     * or before using parameters in complex computations.
37     * Checking preconditions in every function call is not recommended, since it may have a
38     * negative impact on performance.
39     *
40     * @since 0.1.0
41     *
42     * @param bool $condition
43     * @param string $description The message to include in the exception if the condition fails.
44     *
45     * @throws PreconditionException if $condition is not true.
46     * @phan-assert-true-condition $condition
47     */
48    public static function precondition( $condition, string $description ): void {
49        if ( !$condition ) {
50            throw new PreconditionException( "Precondition failed: $description" );
51        }
52    }
53
54    /**
55     * Checks a parameter, that is, throws a ParameterAssertionException if $condition is false.
56     * This is similar to Assert::precondition().
57     *
58     * @note This is intended for checking parameters in constructors and setters.
59     * Checking parameters in every function call is not recommended, since it may have a
60     * negative impact on performance.
61     *
62     * @since 0.1.0
63     *
64     * @param bool $condition
65     * @param string $name The name of the parameter that was checked.
66     * @param string $description The message to include in the exception if the condition fails.
67     *
68     * @throws ParameterAssertionException if $condition is not true.
69     * @phan-assert-true-condition $condition
70     */
71    public static function parameter( $condition, string $name, string $description ): void {
72        if ( !$condition ) {
73            throw new ParameterAssertionException( $name, $description );
74        }
75    }
76
77    /**
78     * Checks an parameter's type, that is, throws a InvalidArgumentException if $value is
79     * not of $type. This is really a special case of Assert::precondition().
80     *
81     * @note This is intended for checking parameters in constructors and setters.
82     * Checking parameters in every function call is not recommended, since it may have a
83     * negative impact on performance.
84     *
85     * @note If possible, type hints should be used instead of calling this function.
86     * It is intended for cases where type hints to not work, e.g. for checking union types.
87     *
88     * @since 0.1.0
89     *
90     * @param string|string[] $types The parameter's expected type. Can be the name of a native type
91     *        or a class or interface, or a list of such names.
92     *        For compatibility with versions before 0.4.0, multiple types can also be given separated
93     *        by pipe characters ("|").
94     * @param mixed $value The parameter's actual value.
95     * @param string $name The name of the parameter that was checked.
96     *
97     * @throws ParameterTypeException if $value is not of type (or, for objects, is not an
98     *         instance of) $type.
99     */
100    public static function parameterType( array|string $types, $value, string $name ): void {
101        if ( is_string( $types ) ) {
102            $types = explode( '|', $types );
103        }
104        if ( !self::hasType( $value, $types ) ) {
105            throw new ParameterTypeException( $name, implode( '|', $types ) );
106        }
107    }
108
109    /**
110     * @since 0.3.0
111     *
112     * @param string $type Either "integer" or "string". Mixing "integer|string" is not supported
113     *  because this is PHP's default anyway. It is of no value to check this.
114     * @param array $value The parameter's actual value. If this is not an array, a
115     *  ParameterTypeException is raised.
116     * @param string $name The name of the parameter that was checked.
117     *
118     * @throws ParameterTypeException if one of the keys in the array $value is not of type $type.
119     */
120    public static function parameterKeyType( string $type, $value, string $name ): void {
121        self::parameterType( [ 'array' ], $value, $name );
122
123        if ( $type !== 'integer' && $type !== 'string' ) {
124            throw new ParameterAssertionException( 'type', 'must be "integer" or "string"' );
125        }
126
127        foreach ( $value as $key => $element ) {
128            if ( gettype( $key ) !== $type ) {
129                throw new ParameterKeyTypeException( $name, $type );
130            }
131        }
132    }
133
134    /**
135     * Checks the type of all elements of an parameter, assuming the parameter is an array,
136     * that is, throws a ParameterElementTypeException if any elements in $value are not of $type.
137     *
138     * @note This is intended for checking parameters in constructors and setters.
139     * Checking parameters in every function call is not recommended, since it may have a
140     * negative impact on performance.
141     *
142     * @since 0.1.0
143     *
144     * @param string|string[] $types The elements' expected type. Can be the name of a native type
145     *        or a class or interface. Multiple types can be given in an array (or a string separated
146     *        by a pipe character ("|"), for compatibility with versions before 0.5.0).
147     * @param array $value The parameter's actual value. If this is not an array,
148     *        a ParameterTypeException is raised.
149     * @param string $name The name of the parameter that was checked.
150     *
151     * @throws ParameterTypeException If $value is not an array.
152     * @throws ParameterElementTypeException If an element of $value  is not of type
153     *         (or, for objects, is not an instance of) $type.
154     */
155    public static function parameterElementType( array|string $types, $value, string $name ): void {
156        self::parameterType( [ 'array' ], $value, $name );
157        if ( is_string( $types ) ) {
158            $types = explode( '|', $types );
159        }
160
161        foreach ( $value as $element ) {
162            if ( !self::hasType( $element, $types ) ) {
163                throw new ParameterElementTypeException( $name, implode( '|', $types ) );
164            }
165        }
166    }
167
168    /**
169     * @since 0.3.0
170     *
171     * @param string $value
172     * @param string $name
173     *
174     * @throws ParameterTypeException if $value is not a non-empty string.
175     * @phan-assert non-empty-string $value
176     */
177    public static function nonEmptyString( $value, string $name ): void {
178        if ( !is_string( $value ) || $value === '' ) {
179            throw new ParameterTypeException( $name, 'non-empty string' );
180        }
181    }
182
183    /**
184     * Checks a postcondition, that is, throws a PostconditionException if $condition is false.
185     * This is very similar Assert::invariant() but is intended for use only after a computation
186     * is complete.
187     *
188     * @note This is intended for double checking in the implementation of complex algorithms.
189     * Note however that it should not be used in performance hotspots, since evaluating
190     * $condition and calling postcondition() costs time.
191     *
192     * @since 0.1.0
193     *
194     * @param bool $condition
195     * @param string $description The message to include in the exception if the condition fails.
196     *
197     * @throws PostconditionException
198     * @phan-assert-true-condition $condition
199     */
200    public static function postcondition( $condition, string $description ): void {
201        if ( !$condition ) {
202            throw new PostconditionException( "Postcondition failed: $description" );
203        }
204    }
205
206    /**
207     * Checks an invariant, that is, throws a InvariantException if $condition is false.
208     * This is very similar Assert::postcondition() but is intended for use throughout the code.
209     *
210     * @note The $condition is expected to be falsifiable.  If you are trying
211     * to indicate that a code path is unreachable, use
212     * `throw new UnreachableException( 'why this code is unreachable' )`
213     * instead of `Assert::invariant( false, '…' )`.  Code checking tools
214     * will complain about the latter.
215     *
216     * @note This is intended for double checking in the implementation of complex algorithms.
217     * Note however that it should not be used in performance hotspots, since evaluating
218     * $condition and calling invariant() costs time.
219     *
220     * @since 0.1.0
221     *
222     * @param bool $condition
223     * @param string $description The message to include in the exception if the condition fails.
224     *
225     * @throws InvariantException
226     * @phan-assert-true-condition $condition
227     */
228    public static function invariant( $condition, string $description ): void {
229        if ( !$condition ) {
230            throw new InvariantException( "Invariant failed: $description" );
231        }
232    }
233
234    /**
235     * @param mixed $value
236     * @param string[] $allowedTypes
237     *
238     * @return bool
239     */
240    private static function hasType( $value, array $allowedTypes ): bool {
241        // Apply strtolower because gettype returns "NULL" for null values.
242        $type = strtolower( gettype( $value ) );
243
244        if ( in_array( $type, $allowedTypes ) ) {
245            return true;
246        }
247
248        if ( in_array( 'callable', $allowedTypes ) && is_callable( $value ) ) {
249            return true;
250        }
251
252        if ( is_object( $value ) && self::isInstanceOf( $value, $allowedTypes ) ) {
253            return true;
254        }
255
256        if ( is_array( $value ) && in_array( 'Traversable', $allowedTypes ) ) {
257            return true;
258        }
259
260        if ( $value === false && in_array( 'false', $allowedTypes ) ) {
261            return true;
262        }
263        if ( $value === true && in_array( 'true', $allowedTypes ) ) {
264            return true;
265        }
266
267        return false;
268    }
269
270    /**
271     * @param object $value
272     * @param string[] $allowedTypes
273     *
274     * @return bool
275     */
276    private static function isInstanceOf( $value, array $allowedTypes ): bool {
277        foreach ( $allowedTypes as $type ) {
278            if ( $value instanceof $type ) {
279                return true;
280            }
281        }
282
283        return false;
284    }
285
286}