Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
69 / 69
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
ChangelogEntry
100.00% covered (success)
100.00%
69 / 69
100.00% covered (success)
100.00%
17 / 17
40
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
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setVersion
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getLink
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLink
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 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
4
 getPrologue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPrologue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getEpilogue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setEpilogue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getChanges
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setChanges
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 insertChange
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 appendChange
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getChangesBySubheading
n/a
0 / 0
n/a
0 / 0
3
 jsonSerialize
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 jsonUnserialize
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 * Class representing a changelog 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 changelog entry.
16 */
17class ChangelogEntry implements JsonSerializable {
18
19    /**
20     * Entry version.
21     *
22     * @var string
23     */
24    protected $version;
25
26    /**
27     * Entry link.
28     *
29     * @var string|null
30     */
31    protected $link = null;
32
33    /**
34     * Entry timestamp.
35     *
36     * @var DateTime|null
37     */
38    protected $timestamp;
39
40    /**
41     * Content before the changes themselves.
42     *
43     * @var string
44     */
45    protected $prologue = '';
46
47    /**
48     * Content after the changes themselves.
49     *
50     * @var string
51     */
52    protected $epilogue = '';
53
54    /**
55     * Changes.
56     *
57     * @var ChangeEntry[]
58     */
59    protected $changes = array();
60
61    /**
62     * Constructor.
63     *
64     * @param string $version Version for the new entry.
65     * @param array  $data Data for other entry fields. Keys correspond to the setters, e.g. key 'link' calls `setLink()`.
66     * @throws InvalidArgumentException If an argument is invalid.
67     */
68    public function __construct( $version, array $data = array() ) {
69        $data = array( 'version' => $version ) + $data + array( 'timestamp' => 'now' );
70        foreach ( $data as $k => $v ) {
71            $func = array( $this, 'set' . ucfirst( $k ) );
72            if ( is_callable( $func ) ) {
73                $func( $v );
74            } else {
75                throw new InvalidArgumentException( __METHOD__ . ": Unrecognized data item \"$k\"." );
76            }
77        }
78    }
79
80    /**
81     * Get the version.
82     *
83     * @return string
84     */
85    public function getVersion() {
86        return $this->version;
87    }
88
89    /**
90     * Set the version.
91     *
92     * @param string $version Version to set.
93     * @return $this
94     * @throws InvalidArgumentException If an argument is invalid.
95     */
96    public function setVersion( $version ) {
97        $version = (string) $version;
98        if ( '' === $version ) {
99            throw new InvalidArgumentException( __METHOD__ . ': Version may not be empty' );
100        }
101        $this->version = $version;
102        return $this;
103    }
104
105    /**
106     * Get the link.
107     *
108     * @return string|null
109     */
110    public function getLink() {
111        return $this->link;
112    }
113
114    /**
115     * Set the link.
116     *
117     * @param string|null $link Link to set.
118     * @return $this
119     * @throws InvalidArgumentException If an argument is invalid.
120     */
121    public function setLink( $link ) {
122        if ( '' === $link ) {
123            $link = null;
124        } elseif ( null !== $link ) {
125            $link = filter_var( $link, FILTER_VALIDATE_URL );
126            if ( ! is_string( $link ) ) {
127                throw new InvalidArgumentException( __METHOD__ . ': Invalid URL' );
128            }
129        }
130        $this->link = $link;
131        return $this;
132    }
133
134    /**
135     * Get the timestamp.
136     *
137     * The timestamp may be null, which should be interpreted as "unreleased".
138     *
139     * @return DateTime|null
140     */
141    public function getTimestamp() {
142        return $this->timestamp;
143    }
144
145    /**
146     * Set the timestamp.
147     *
148     * The timestamp may be null, which should be interpreted as "unreleased".
149     *
150     * @param DateTime|string|null $timestamp Timestamp to set.
151     * @return $this
152     * @throws InvalidArgumentException If an argument is invalid.
153     */
154    public function setTimestamp( $timestamp ) {
155        if ( null !== $timestamp && ! $timestamp instanceof DateTime ) {
156            try {
157                $timestamp = new DateTime( $timestamp );
158            } catch ( \Exception $ex ) {
159                throw new InvalidArgumentException( __METHOD__ . ': Invalid timestamp', 0, $ex );
160            }
161        }
162        $this->timestamp = $timestamp;
163        return $this;
164    }
165
166    /**
167     * Get the prologue content.
168     *
169     * @return string
170     */
171    public function getPrologue() {
172        return $this->prologue;
173    }
174
175    /**
176     * Set the prologue content.
177     *
178     * @param string $prologue Prologue content to set.
179     * @return $this
180     */
181    public function setPrologue( $prologue ) {
182        $this->prologue = (string) $prologue;
183        return $this;
184    }
185
186    /**
187     * Get the epilogue content.
188     *
189     * @return string
190     */
191    public function getEpilogue() {
192        return $this->epilogue;
193    }
194
195    /**
196     * Set the epilogue content.
197     *
198     * @param string $epilogue Epilogue content to set.
199     * @return $this
200     */
201    public function setEpilogue( $epilogue ) {
202        $this->epilogue = (string) $epilogue;
203        return $this;
204    }
205
206    /**
207     * Get the list of changes.
208     *
209     * @return ChangeEntry[]
210     */
211    public function getChanges() {
212        return $this->changes;
213    }
214
215    /**
216     * Set the list of changes.
217     *
218     * This replaces all existing changes. The caller is responsible
219     * for making sure the changes are sorted properly.
220     *
221     * @param ChangeEntry[] $changes Changes.
222     * @return $this
223     * @throws InvalidArgumentException If an argument is invalid.
224     */
225    public function setChanges( array $changes ) {
226        foreach ( $changes as $i => $change ) {
227            if ( ! $change instanceof ChangeEntry ) {
228                $what = is_object( $change ) ? get_class( $change ) : gettype( $change );
229                throw new InvalidArgumentException( __METHOD__ . ": Expected a ChangeEntry, got $what at index $i" );
230            }
231        }
232        $this->changes = array_values( $changes );
233        return $this;
234    }
235
236    /**
237     * Add a new change.
238     *
239     * The new change is inserted before the first existing change where
240     * `ChangeEntry::compare()` says the existing change should come after.
241     *
242     * @param ChangeEntry $change New change.
243     * @param array       $compareconfig Comparison config, see `ChangeEntry::compare()`.
244     * @return $this
245     */
246    public function insertChange( ChangeEntry $change, $compareconfig = array() ) {
247        foreach ( $this->changes as $i => $e ) {
248            if ( ChangeEntry::compare( $change, $e, $compareconfig ) < 0 ) {
249                array_splice( $this->changes, $i, 0, array( $change ) );
250                return $this;
251            }
252        }
253        $this->changes[] = $change;
254        return $this;
255    }
256
257    /**
258     * Append a new change.
259     *
260     * @param ChangeEntry $change New change.
261     * @return $this
262     */
263    public function appendChange( ChangeEntry $change ) {
264        $this->changes[] = $change;
265        return $this;
266    }
267
268    /**
269     * Get the changes grouped by subheading.
270     *
271     * @param string|null $subheading Subheading to retrieve. @deprecated since 4.2.0; now throws error. Do `->getChangesBySubheading()[ $subheading ]` instead.
272     * @return array<string,ChangeEntry[]> Change entries by subheading.
273     * @throws \InvalidArgumentException If the deprecated `$subheading` parameter is used.
274     */
275    public function getChangesBySubheading( $subheading = null ) {
276        if ( $subheading !== null ) {
277            // @todo Remove this next major version bump.
278            throw new \InvalidArgumentException( 'Passing a value for $subheading is deprecated. Do `->getChangesBySubheading()[ $subheading ]` instead.' );
279        }
280
281        $ret = array();
282        foreach ( $this->changes as $entry ) {
283            $ret[ $entry->getSubheading() ][] = $entry;
284        }
285
286        return $ret;
287    }
288
289    /**
290     * Return data for serializing to JSON.
291     *
292     * @return array
293     *
294     * @todo Once we drop support for PHP <8.0, we should remove the attribute and add a `mixed` return type to match JsonSerializable.
295     */
296    #[\ReturnTypeWillChange]
297    public function jsonSerialize() {
298        return array(
299            '__class__' => static::class,
300            'version'   => $this->version,
301            'link'      => $this->link,
302            'timestamp' => null === $this->timestamp ? null : $this->timestamp->format( DateTime::ISO8601 ),
303            'prologue'  => $this->prologue,
304            'epilogue'  => $this->epilogue,
305            'changes'   => $this->changes,
306        );
307    }
308
309    /**
310     * Unserialize from JSON.
311     *
312     * @param array $data JSON data as returned by self::jsonSerialize().
313     * @return static
314     * @throws InvalidArgumentException If the data is invalid.
315     */
316    public static function jsonUnserialize( $data ) {
317        $data = (array) $data;
318        if ( ! isset( $data['__class__'] ) || ! isset( $data['version'] ) ) {
319            throw new InvalidArgumentException( 'Invalid data' );
320        }
321        $class   = $data['__class__'];
322        $version = $data['version'];
323        unset( $data['__class__'], $data['version'] );
324        if ( ! class_exists( $class ) || ! is_a( $class, static::class, true ) ) {
325            throw new InvalidArgumentException( "Cannot instantiate $class via " . static::class . '::' . __FUNCTION__ );
326        }
327        if ( isset( $data['changes'] ) ) {
328            $data['changes'] = array_map( array( ChangeEntry::class, 'jsonUnserialize' ), $data['changes'] );
329        }
330        return new $class( $version, $data );
331    }
332}