Skip to content

Commit 1333089

Browse files
committed
Adds version object and related files.
1 parent 13b2fed commit 1333089

File tree

3 files changed

+452
-0
lines changed

3 files changed

+452
-0
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
<?php
2+
3+
namespace DevCoding\Object\System\Version;
4+
5+
use DevCoding\Helper\StringHelper;
6+
7+
/**
8+
* Object class representing a semantic software version.
9+
*
10+
* This class is based loosely on PHLAK's SemVer (https://github.com/PHLAK/SemVer), however the parser has been
11+
* rewritten to be more forgiving with version numbers that do not quite match the Semantic Versioning strategy,
12+
* include the build number when making comparisons, and provide separate classes for immutable version objects.
13+
*
14+
* @author AMJones <am@jonesiscoding.com>
15+
* @license https://github.com/deviscoding/objection/blob/main/LICENSE
16+
* @package DevCoding\Object\System\Version
17+
*/
18+
abstract class BaseVersion
19+
{
20+
/** @var string the raw version string given at instantiation */
21+
protected $raw;
22+
/** @var int the major version */
23+
protected $major;
24+
/** @var int the minor version */
25+
protected $minor;
26+
/** @var int|null the patch number */
27+
protected $patch;
28+
/** @var string|null the pre-release string */
29+
protected $pre;
30+
/** @var string|null the build string or number */
31+
protected $build;
32+
33+
public function __construct($version)
34+
{
35+
$this->raw = $version;
36+
37+
if ($parsed = $this->parse($version))
38+
{
39+
$this->major = (int) $parsed['major'];
40+
$this->minor = (int) $parsed['minor'];
41+
$this->patch = (int) $parsed['patch'];
42+
$this->pre = $parsed['pre_release'];
43+
$this->build = $parsed['build'];
44+
}
45+
}
46+
47+
/**
48+
* Creates the object from the given array, which should either contain the keys: major, minor, patch, pre_release,
49+
* and build, or 5 indexed keys in the order of major, minor, patch, pre_release, build.
50+
*
51+
* @return static
52+
*/
53+
public static function fromArray(array $array)
54+
{
55+
$major = $array['major'] ?? $array[0] ?? null;
56+
$minor = $array['minor'] ?? $array[1] ?? null;
57+
$patch = $array['patch'] ?? $array[2] ?? null;
58+
$pre = $array['pre_release'] ?? $array[3] ?? null;
59+
$build = $array['build'] ?? $array[4] ?? null;
60+
$base = implode('.', [$major, $minor, $patch]);
61+
62+
// Create the object
63+
$ver = new static($base);
64+
65+
// Set PreRelease and Build Separately
66+
$ver->pre = $pre;
67+
$ver->build = $build;
68+
69+
return $ver;
70+
}
71+
72+
/**
73+
* @return string
74+
*/
75+
public function __toString()
76+
{
77+
return implode($this->toArray());
78+
}
79+
80+
/**
81+
* @return int
82+
*/
83+
public function getMajor(): int
84+
{
85+
return $this->major;
86+
}
87+
88+
/**
89+
* @return int
90+
*/
91+
public function getMinor()
92+
{
93+
return $this->minor;
94+
}
95+
96+
/**
97+
* @return int|null
98+
*/
99+
public function getPatch()
100+
{
101+
return $this->patch;
102+
}
103+
104+
/**
105+
* @return string|null
106+
*/
107+
public function getPreRelease()
108+
{
109+
return $this->pre;
110+
}
111+
112+
/**
113+
* @return string|null
114+
*/
115+
public function getBuild()
116+
{
117+
return $this->build;
118+
}
119+
120+
/**
121+
* @return bool
122+
*/
123+
public function isPreRelease()
124+
{
125+
return isset($this->pre);
126+
}
127+
128+
/**
129+
* @param bool $assoc
130+
*
131+
* @return array
132+
*/
133+
public function toArray($assoc = true)
134+
{
135+
$arr = [
136+
'major' => $this->getMajor(),
137+
'minor' => $this->getMinor(),
138+
'patch' => $this->getPatch(),
139+
'pre_release' => $this->getPreRelease(),
140+
'build' => $this->getBuild(),
141+
];
142+
143+
return $assoc ? $arr : array_values($arr);
144+
}
145+
146+
/**
147+
* Check if this Version object is greater than another.
148+
*
149+
* @param BaseVersion $version The version to compare to
150+
*
151+
* @return bool True if this Version object is greater than the comparing object, otherwise false
152+
*/
153+
public function gt(BaseVersion $version): bool
154+
{
155+
return $this->compare($this, $version) > 0;
156+
}
157+
158+
/**
159+
* Check if this Version object is less than another.
160+
*
161+
* @param BaseVersion $version The version to compare to
162+
*
163+
* @return bool True if this Version object is less than the comparing object, otherwise false
164+
*/
165+
public function lt(BaseVersion $version): bool
166+
{
167+
return $this->compare($this, $version) < 0;
168+
}
169+
170+
/**
171+
* Check if this Version object is equal to than another.
172+
*
173+
* @param BaseVersion $version The version to compare to
174+
*
175+
* @return bool True if this Version object is equal to the comparing object, otherwise false
176+
*/
177+
public function eq(BaseVersion $version): bool
178+
{
179+
return 0 === $this->compare($this, $version);
180+
}
181+
182+
/**
183+
* Check if this Version object is not equal to another.
184+
*
185+
* @param BaseVersion $version The version to compare to
186+
*
187+
* @return bool True if this Version object is not equal to the comparing object, otherwise false
188+
*/
189+
public function neq(BaseVersion $version): bool
190+
{
191+
return 0 !== $this->compare($this, $version);
192+
}
193+
194+
/**
195+
* Check if this Version object is greater than or equal to another.
196+
*
197+
* @param BaseVersion $version The version to compare to
198+
*
199+
* @return bool True if this Version object is greater than or equal to the comparing object, otherwise false
200+
*/
201+
public function gte(BaseVersion $version): bool
202+
{
203+
return $this->compare($this, $version) >= 0;
204+
}
205+
206+
/**
207+
* Check if this Version object is less than or equal to another.
208+
*
209+
* @param BaseVersion $version The version to compare to
210+
*
211+
* @return bool True if this Version object is less than or equal to the comparing object, otherwise false
212+
*/
213+
public function lte(BaseVersion $version): bool
214+
{
215+
return $this->compare($this, $version) <= 0;
216+
}
217+
218+
/**
219+
* Compares the two given versions, returning an integer similar to the PHP spaceship operator.
220+
* Based on the Comparable trait in PHLAK/SemVer, but expanded to compare the build number if needed.
221+
*
222+
* @return int
223+
*/
224+
protected function compare(BaseVersion $v1, BaseVersion $v2)
225+
{
226+
$vBase1 = array_slice($v1->toArray(false), 0, 3);
227+
$vBase2 = array_slice($v2->toArray(false), 0, 3);
228+
$vBaseC = $vBase1 <=> $vBase2;
229+
230+
if (0 !== $vBaseC)
231+
{
232+
return $vBaseC;
233+
}
234+
235+
// Both version numbers are the same. Compare based on PreRelease...
236+
if ($v1->isPreRelease() && !$v2->isPreRelease())
237+
{
238+
return -1;
239+
}
240+
elseif (!$v1->isPreRelease() && $v2->isPreRelease())
241+
{
242+
return 1;
243+
}
244+
elseif ($v1->isPreRelease() && $v2->isPreRelease())
245+
{
246+
$vPreC = $this->comparePart($v1->getPreRelease(), $v2->getPreRelease());
247+
248+
if (0 !== $vPreC)
249+
{
250+
return $vPreC;
251+
}
252+
}
253+
254+
// Both PreReleases are the same. Compare based on Build Number...
255+
$v1Build = $v1->getBuild();
256+
$v2Build = $v2->getBuild();
257+
258+
if ($v1Build && !$v2Build)
259+
{
260+
return -1;
261+
}
262+
elseif (!$v1Build && $v2Build)
263+
{
264+
return 1;
265+
}
266+
elseif ($v1Build && $v2Build)
267+
{
268+
$vBuildC = $this->comparePart($v1Build, $v2Build);
269+
270+
if (0 !== $vBuildC)
271+
{
272+
return $vBuildC;
273+
}
274+
}
275+
276+
// Everything looks the same...
277+
return 0;
278+
}
279+
280+
/**
281+
* Compares the version parts given, typically the pre-release or build numbers, returning an integer similar to the
282+
* functionality of the PHP Spaceship operator.
283+
*
284+
* @param string $p1 The first version part string to compare
285+
* @param string $p2 the second version part string to compare
286+
*
287+
* @return int
288+
*/
289+
protected function comparePart($p1, $p2)
290+
{
291+
// Normalize
292+
$vNorm1 = str_replace([' ', '_', '-'], '.', $p1 ?? '');
293+
$vNorm2 = str_replace([' ', '_', '-'], '.', $p2 ?? '');
294+
295+
// Break Into Parts
296+
$vParts1 = explode('.', $vNorm1);
297+
$vParts2 = explode('.', $vNorm2);
298+
299+
// Pad the Array
300+
$vPad1 = array_pad($vParts1, count($vParts2), null);
301+
$vPad2 = array_pad($vParts2, count($vParts1), null);
302+
303+
// Return the comparison
304+
return $vPad1 <=> $vPad2;
305+
}
306+
307+
/**
308+
* Parse the version number into an array with the keys: major, minor, patch, pre_release, build. Strings that are
309+
* missing one or more of these parts will return either 0 or null for the key, depending on whether the key is
310+
* essential to identifying the version.
311+
*
312+
* @param string $v
313+
*
314+
* @return array|null
315+
*/
316+
protected function parse($v)
317+
{
318+
if ($this->isStringable($v))
319+
{
320+
// Strip extraneous spaces
321+
$v = trim($v);
322+
323+
// Strip preceeding 'version'
324+
if (0 === stripos($v, 'version'))
325+
{
326+
$v = substr($v, 8);
327+
}
328+
329+
$empty = array_fill_keys(['major', 'minor', 'patch', 'pre_release', 'build'], null);
330+
if (preg_match('#^[vV]?(?<major>\d+)(?:\.(?<minor>\d+)(?:\.(?<patch>\d+))?)?(?<remainder>.*)$#', $v, $m))
331+
{
332+
$parsed = array_intersect_key($m, $empty) + $empty;
333+
334+
if (!empty($m['remainder']))
335+
{
336+
if (preg_match('#(?:[.\s-](?<pre_release>[^\s+]+))?(?:[+\s](?<build>.+))?$#', $m['remainder'], $r))
337+
{
338+
$parsed = array_intersect_key($r, $empty) + $parsed;
339+
}
340+
}
341+
342+
return array_merge(['major' => '0', 'minor' => '0'], $parsed);
343+
}
344+
elseif (preg_match('#Build (?<build>.*)$#', $v, $m))
345+
{
346+
$jbuild = array_merge(['major' => '0', 'minor' => '0'], $m);
347+
348+
return array_intersect_key($jbuild, $empty) + $empty;
349+
}
350+
}
351+
352+
return null;
353+
}
354+
355+
/**
356+
* Determines if the given value is a string, or another type that may be converted to a string.
357+
*
358+
* @param mixed $val
359+
*
360+
* @return bool
361+
*/
362+
protected function isStringable($val)
363+
{
364+
return StringHelper::get()->isStringable($val);
365+
}
366+
}

0 commit comments

Comments
 (0)