Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
VersionRequirementResolver
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
5 / 5
25
100.00% covered (success)
100.00%
1 / 1
 getClassRequirement
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getEffectiveRequirement
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
8
 getDeprecation
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 stripIncompatibleProperties
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 clearCache
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Core\File;
6
7use Phpdftk\Pdf\Core\DeprecatedPdfFeature;
8use Phpdftk\Pdf\Core\PdfVersion;
9use Phpdftk\Pdf\Core\PdfVersionAware;
10use Phpdftk\Pdf\Core\RequiresPdfVersion;
11
12/**
13 * Reads {@see RequiresPdfVersion} and {@see DeprecatedPdfFeature} attributes
14 * from PDF object classes via reflection, with static per-class caching.
15 */
16final class VersionRequirementResolver
17{
18    /** @var array<class-string, PdfVersion|null> */
19    private static array $classCache = [];
20
21    /** @var array<class-string, array<string, PdfVersion>> */
22    private static array $propertyCache = [];
23
24    /** @var array<class-string, DeprecatedPdfFeature|null> */
25    private static array $deprecationCache = [];
26
27    /**
28     * Get the class-level minimum PDF version requirement, if any.
29     */
30    public static function getClassRequirement(string|object $class): ?PdfVersion
31    {
32        $className = is_object($class) ? $class::class : $class;
33
34        if (!array_key_exists($className, self::$classCache)) {
35            $version = null;
36            $ref = new \ReflectionClass($className);
37            // Walk the class hierarchy to find inherited attributes
38            while ($ref !== false) {
39                $attrs = $ref->getAttributes(RequiresPdfVersion::class);
40                if ($attrs !== []) {
41                    $found = $attrs[0]->newInstance()->minimumVersion;
42                    $version = $version !== null ? $version->max($found) : $found;
43                }
44                $ref = $ref->getParentClass();
45            }
46            self::$classCache[$className] = $version;
47        }
48
49        return self::$classCache[$className];
50    }
51
52    /**
53     * Get the effective minimum version for an object instance, considering
54     * both the class-level requirement and all non-null property-level
55     * requirements. Returns PdfVersion::V1_0 if nothing is annotated.
56     */
57    public static function getEffectiveRequirement(object $object): PdfVersion
58    {
59        $version = self::getClassRequirement($object) ?? PdfVersion::V1_0;
60
61        // Check PdfVersionAware interface for runtime version requirements
62        if ($object instanceof PdfVersionAware) {
63            $runtimeVersion = $object->getMinimumPdfVersion();
64            if ($runtimeVersion !== null) {
65                $version = $version->max($runtimeVersion);
66            }
67        }
68
69        $className = $object::class;
70        if (!isset(self::$propertyCache[$className])) {
71            self::$propertyCache[$className] = [];
72            $ref = new \ReflectionClass($className);
73            foreach ($ref->getProperties() as $prop) {
74                $attrs = $prop->getAttributes(RequiresPdfVersion::class);
75                if ($attrs !== []) {
76                    self::$propertyCache[$className][$prop->getName()] =
77                        $attrs[0]->newInstance()->minimumVersion;
78                }
79            }
80        }
81
82        foreach (self::$propertyCache[$className] as $propName => $propVersion) {
83            $value = $object->$propName ?? null;
84            if ($value !== null) {
85                $version = $version->max($propVersion);
86            }
87        }
88
89        return $version;
90    }
91
92    /**
93     * Check if a class is marked as deprecated in the PDF specification.
94     */
95    public static function getDeprecation(string|object $class): ?DeprecatedPdfFeature
96    {
97        $className = is_object($class) ? $class::class : $class;
98
99        if (!array_key_exists($className, self::$deprecationCache)) {
100            $found = null;
101            $ref = new \ReflectionClass($className);
102            while ($ref !== false) {
103                $attrs = $ref->getAttributes(DeprecatedPdfFeature::class);
104                if ($attrs !== []) {
105                    $found = $attrs[0]->newInstance();
106                    break;
107                }
108                $ref = $ref->getParentClass();
109            }
110            self::$deprecationCache[$className] = $found;
111        }
112
113        return self::$deprecationCache[$className];
114    }
115
116    /**
117     * Nullify properties on an object whose version requirement exceeds
118     * the given ceiling. Returns a list of stripped property names.
119     *
120     * Only strips property-level requirements — class-level incompatibility
121     * must be handled by the caller (the object itself cannot be stripped).
122     *
123     * @return list<string>
124     */
125    public static function stripIncompatibleProperties(object $object, PdfVersion $ceiling): array
126    {
127        $stripped = [];
128        $className = $object::class;
129
130        // Ensure property cache is populated
131        if (!isset(self::$propertyCache[$className])) {
132            self::getEffectiveRequirement($object);
133        }
134
135        foreach (self::$propertyCache[$className] as $propName => $propVersion) {
136            if ($propVersion->isGreaterThan($ceiling)) {
137                $value = $object->$propName ?? null;
138                if ($value !== null) {
139                    $object->$propName = null;
140                    $stripped[] = $propName;
141                }
142            }
143        }
144
145        return $stripped;
146    }
147
148    /**
149     * Clear all caches (useful for testing).
150     */
151    public static function clearCache(): void
152    {
153        self::$classCache = [];
154        self::$propertyCache = [];
155        self::$deprecationCache = [];
156    }
157}