Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
ChangeEntry
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
17 / 17
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 compare
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 getSignificance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSignificance
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 compareSignificance
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTimestamp
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 compareTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSubheading
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSubheading
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 compareSubheading
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 getAuthor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAuthor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 jsonUnserialize
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Class representing a change entry.
4 *
5 * @package automattic/jetpack-changelogger
6 */
7
8namespace Automattic\Jetpack\Changelog;
9
10use DateTime;
11use InvalidArgumentException;
12use JsonSerializable;
13
14/**
15 * Class representing a change entry.
16 */
17class ChangeEntry implements JsonSerializable {
18
19    /**
20     * Entry significance.
21     *
22     * @var string|null
23     */
24    protected $significance = null;
25
26    /**
27     * Entry timestamp.
28     *
29     * @var DateTime
30     */
31    protected $timestamp;
32
33    /**
34     * Subheading the entry is under.
35     *
36     * @var string
37     */
38    protected $subheading = '';
39
40    /**
41     * Author of the entry.
42     *
43     * @var string
44     */
45    protected $author = '';
46
47    /**
48     * Content of the entry.
49     *
50     * @var string
51     */
52    protected $content = '';
53
54    /**
55     * Constructor.
56     *
57     * @param array $data Data for entry fields. Keys correspond to the setters, e.g. key 'content' calls `setContent()`.
58     * @throws InvalidArgumentException If an argument is invalid.
59     */
60    public function __construct( array $data = array() ) {
61        $data += array( 'timestamp' => 'now' );
62        foreach ( $data as $k => $v ) {
63            $func = array( $this, 'set' . ucfirst( $k ) );
64            if ( is_callable( $func ) ) {
65                $func( $v );
66            } else {
67                throw new InvalidArgumentException( __METHOD__ . ": Unrecognized data item \"$k\"." );
68            }
69        }
70    }
71
72    /**
73     * Compare two ChangeEntry objects.
74     *
75     * @param ChangeEntry $a First ChangeEntry.
76     * @param ChangeEntry $b Second ChangeEntry.
77     * @param array       $config Comparison configuration. Keys are:
78     *        - ordering: (string[]) Order in which to consider the fields. Field
79     *          names correspond to getters, e.g. 'significance' =>
80     *          `getSignificance()`. Default ordering is subheading, content.
81     *        - knownSubheadings: (string[]) Change entries with these headings will
82     *          compare, in this order, after those with no subheading and before any
83     *          other subheadings.
84     * @return int <0 if $a should come before $b, >0 if $b should come before $a, or 0 otherwise.
85     * @throws InvalidArgumentException If an argument is invalid.
86     */
87    public static function compare( ChangeEntry $a, ChangeEntry $b, array $config = array() ) {
88        $config += array(
89            'ordering'         => array( 'subheading', 'content' ),
90            'knownSubheadings' => array(),
91        );
92
93        foreach ( $config['ordering'] as $field ) {
94            // First, check for a custom comparison function.
95            $func = array( static::class, 'compare' . ucfirst( $field ) );
96            if ( is_callable( $func ) ) {
97                $ret = $func( $a, $b, $config );
98            } else {
99                // Otherwise, just use `strnatcasecmp()`.
100                $func = 'get' . ucfirst( $field );
101                if ( ! is_callable( array( $a, $func ) ) || ! is_callable( array( $b, $func ) ) ) {
102                    throw new InvalidArgumentException( __METHOD__ . ': Invalid field in ordering' );
103                }
104                $aa  = call_user_func( array( $a, $func ) );
105                $bb  = call_user_func( array( $b, $func ) );
106                $ret = strnatcasecmp( $aa, $bb );
107            }
108            if ( 0 !== $ret ) {
109                return $ret;
110            }
111        }
112
113        return 0;
114    }
115
116    /**
117     * Get the significance.
118     *
119     * @return string|null
120     */
121    public function getSignificance() {
122        return $this->significance;
123    }
124
125    /**
126     * Set the significance.
127     *
128     * While entries coming in from users should always have a significance, we allow null here
129     * because entries created programmatically, particularly when parsing an existing changelog,
130     * may not include significance information.
131     *
132     * @param string|null $significance 'patch', 'minor', or 'major'.
133     * @return $this
134     * @throws InvalidArgumentException If an argument is invalid.
135     */
136    public function setSignificance( $significance ) {
137        if ( ! in_array( $significance, array( null, 'patch', 'minor', 'major' ), true ) ) {
138            throw new InvalidArgumentException( __METHOD__ . ": Significance must be 'patch', 'minor', or 'major' (or null)" );
139        }
140        $this->significance = $significance;
141        return $this;
142    }
143
144    /**
145     * Compare significance values.
146     *
147     * @param ChangeEntry $a First entry.
148     * @param ChangeEntry $b Second entry.
149     * @param array       $config Passed from `compare()`, but unused here.
150     * @return int
151     */
152    protected static function compareSignificance( ChangeEntry $a, ChangeEntry $b, array $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
153        static $values = array( 'major', 'minor', 'patch', null );
154        $aa            = array_search( $a->getSignificance(), $values, true );
155        $bb            = array_search( $b->getSignificance(), $values, true );
156        return $aa - $bb;
157    }
158
159    /**
160     * Get the timestamp.
161     *
162     * @return DateTime
163     */
164    public function getTimestamp() {
165        return $this->timestamp;
166    }
167
168    /**
169     * Set the timestamp.
170     *
171     * @param DateTime|string $timestamp Timestamp to set.
172     * @return $this
173     * @throws InvalidArgumentException If an argument is invalid.
174     */
175    public function setTimestamp( $timestamp ) {
176        if ( ! $timestamp instanceof DateTime ) {
177            try {
178                $timestamp = new DateTime( $timestamp );
179            } catch ( \Exception $ex ) {
180                throw new InvalidArgumentException( __METHOD__ . ': Invalid timestamp', 0, $ex );
181            }
182        }
183        $this->timestamp = $timestamp;
184        return $this;
185    }
186
187    /**
188     * Compare timestamps.
189     *
190     * @param ChangeEntry $a First entry.
191     * @param ChangeEntry $b Second entry.
192     * @param array       $config Passed from `compare()`, but unused here.
193     * @return int
194     */
195    protected static function compareTimestamp( ChangeEntry $a, ChangeEntry $b, array $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
196        return $a->getTimestamp() <=> $b->getTimestamp();
197    }
198
199    /**
200     * Get the subheading.
201     *
202     * @return string
203     */
204    public function getSubheading() {
205        return $this->subheading;
206    }
207
208    /**
209     * Set the subheading.
210     *
211     * @param string $subheading Subheading to set.
212     * @return $this
213     */
214    public function setSubheading( $subheading ) {
215        $this->subheading = (string) $subheading;
216        return $this;
217    }
218
219    /**
220     * Compare subheadings.
221     *
222     * @param ChangeEntry $a First entry.
223     * @param ChangeEntry $b Second entry.
224     * @param array       $config Passed from `compare()`, used for 'knownSubheadings'.
225     * @return int
226     */
227    protected static function compareSubheading( ChangeEntry $a, ChangeEntry $b, array $config ) {
228        $aa = $a->getSubheading();
229        $bb = $b->getSubheading();
230
231        // If they're equal, just return that.
232        $cmp = strnatcasecmp( $aa, $bb );
233        if ( 0 === $cmp ) {
234            return 0;
235        }
236
237        // Empty string comes first.
238        if ( '' === $aa ) {
239            return -1;
240        }
241        if ( '' === $bb ) {
242            return 1;
243        }
244
245        // Search for known values.
246        foreach ( $config['knownSubheadings'] as $v ) {
247            if ( strnatcasecmp( $aa, $v ) === 0 ) {
248                return -1;
249            }
250            if ( strnatcasecmp( $bb, $v ) === 0 ) {
251                return 1;
252            }
253        }
254
255        // Fallback.
256        return $cmp;
257    }
258
259    /**
260     * Get the author.
261     *
262     * @return string
263     */
264    public function getAuthor() {
265        return $this->author;
266    }
267
268    /**
269     * Set the author.
270     *
271     * @param string $author Author to set.
272     * @return $this
273     */
274    public function setAuthor( $author ) {
275        $this->author = (string) $author;
276        return $this;
277    }
278
279    /**
280     * Get the content.
281     *
282     * @return string
283     */
284    public function getContent() {
285        return $this->content;
286    }
287
288    /**
289     * Set the content.
290     *
291     * @param string $content Content to set.
292     * @return $this
293     */
294    public function setContent( $content ) {
295        $this->content = (string) $content;
296        return $this;
297    }
298
299    /**
300     * Return data for serializing to JSON.
301     *
302     * @return array
303     */
304    #[\ReturnTypeWillChange]
305    public function jsonSerialize() {
306        return array(
307            '__class__'    => static::class,
308            'significance' => $this->significance,
309            'timestamp'    => $this->timestamp->format( DateTime::ISO8601 ),
310            'subheading'   => $this->subheading,
311            'author'       => $this->author,
312            'content'      => $this->content,
313        );
314    }
315
316    /**
317     * Unserialize from JSON.
318     *
319     * @param array $data JSON data as returned by self::jsonSerialize().
320     * @return static
321     * @throws InvalidArgumentException If the data is invalid.
322     */
323    public static function jsonUnserialize( $data ) {
324        $data = (array) $data;
325        if ( ! isset( $data['__class__'] ) ) {
326            throw new InvalidArgumentException( 'Invalid data' );
327        }
328        $class = $data['__class__'];
329        unset( $data['__class__'] );
330        if ( ! class_exists( $class ) || ! is_a( $class, static::class, true ) ) {
331            throw new InvalidArgumentException( "Cannot instantiate $class via " . static::class . '::' . __FUNCTION__ );
332        }
333        return new $class( $data );
334    }
335}