Test Failed
Push — master ( 483f83...a6b1f2 )
by Edgar
03:01
created

SVGElement::setXLinkAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
namespace nstdio\svg;
3
4
use Doctrine\Instantiator\Instantiator;
5
use nstdio\svg\attributes\Transformable;
6
use nstdio\svg\container\ContainerInterface;
7
use nstdio\svg\container\Defs;
8
use nstdio\svg\container\SVG;
9
use nstdio\svg\traits\ChildTrait;
10
use nstdio\svg\traits\ElementTrait;
11
use nstdio\svg\util\Identifier;
12
use nstdio\svg\util\Inflector;
13
use nstdio\svg\util\KeyValueWriter;
14
use nstdio\svg\util\Transform;
15
use nstdio\svg\util\TransformInterface;
16
17
/**
18
 * Class SVGElement
19
 * The base class for all elements.
20
 *
21
 * @property string $id
22
 * @property string $fill The fill color.
23
 * @property float $height The height attribute of element.
24
 * @property float $width The width attribute of element.
25
 *
26
 * @package nstdio\svg
27
 * @author  Edgar Asatryan <[email protected]>
28
 */
29
abstract class SVGElement implements ContainerInterface, ElementFactoryInterface
30
{
31
    use ElementTrait, ChildTrait;
32
33
    /**
34
     * This attribute will not be converted.
35
     *
36
     * @see SVGElement::__get;
37
     * @var array
38
     */
39
    private static $notConvertable = ['patternContentUnits', 'patternTransform', 'patternUnits', 'diffuseConstant', 'pointsAtX', 'pointsAtY', 'pointsAtZ', 'limitingConeAngle', 'tableValues', 'filterUnits', 'gradientUnits', 'viewBox', 'repeatCount', 'attributeName', 'attributeType', 'stdDeviation'];
40
41
    /**
42
     * The parent of `$element`.
43
     *
44
     * @var XMLDocumentInterface | ElementFactoryInterface | ContainerInterface
45
     */
46
    protected $root;
47
48
    /**
49
     * The element itself.
50
     *
51
     * @var XMLDocumentInterface | ElementFactoryInterface | ContainerInterface
52
     */
53
    protected $element;
54
55 149
    public function __construct(ElementInterface $parent)
56
    {
57 149
        $this->child = new ElementStorage();
58 149
        $this->root = $parent;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parent of type object<nstdio\svg\ElementInterface> is incompatible with the declared type object<nstdio\svg\XMLDocumentInterface> of property $root.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
59 149
        $this->element = $this->createElement($this->getName());
60 149
        $this->root->append($this);
61 149
    }
62
63
    /**
64
     * @inheritdoc
65
     */
66 149
    public function createElement($name, $value = null, $attributes = [])
67
    {
68 149
        return $this->root->createElement($name, $value, $attributes);
0 ignored issues
show
Unused Code introduced by
The call to XMLDocumentInterface::createElement() has too many arguments starting with $attributes.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
69
    }
70
71
    /**
72
     * @inheritdoc
73
     */
74
    abstract public function getName();
75
76
    /**
77
     * @inheritdoc
78
     */
79 27
    final public function getRoot()
80
    {
81 27
        return $this->root;
82
    }
83
84
    /**
85
     * @inheritdoc
86
     */
87 149
    final public function getElement()
88
    {
89 149
        return $this->element;
90
    }
91
92
    /**
93
     * @param string[] $except
94
     *
95
     * @see XMLDocumentInterface::attributes()
96
     * @return array
97
     */
98 9
    public function allAttributes(array $except = [])
99
    {
100 9
        return $this->element->attributes($except);
101
    }
102
103
    /**
104
     * In order to have short access to the attributes of an element. Any attribute can be obtained as a public
105
     * property object. Attributes that are written with a hyphen can be obtained through a simple conversion.
106
     *
107
     * ```php
108
     * // ...
109
     *
110
     * echo $circle->strokeWidth; // Property strokeWidth will be transformed into stroke-width and so on.
111
     * echo $blur->stdDeviation; // Property stdDeviation will not converted into std-deviation.
112
     * ```
113
     *
114
     * For properties `$fill` and `$filter` will be returned something like this `url(#idTheRefToElement)`. In order to
115
     * have direct access to `idTheRefToElement` part,
116
     * ```php
117
     * // ...
118
     * $circle->filter = "url(#someFilterId)";
119
     * $id = $circle->filterUrl;
120
     * echo $id; // will print someFilterId and same story with fillUrl
121
     * ```
122
     * You can also have access to attributes with `xlink` namespace as mentioned above.
123
     * For example you need to get `xlink:href` value. If given element doest not have `href` attribute (with no
124
     * namespace prefix) it will try to get `xlink:href`.
125
     * ```php
126
     * $id = $mPath->href;
127
     * echo $id; // will print #someHref.
128
     * ```
129
     *
130
     * @param string $name The name of property.
131
     *
132
     * @see SVGElement::$notConvertable
133
     * @return null|string
134
     */
135 109
    public function __get($name)
136
    {
137 109
        if ($name === 'filterUrl' || $name === 'fillUrl') {
138 1
            return $this->getIdFromUrl($name);
139
        }
140
141 109
        $name = $this->convertAttributeName($name);
142 109
        $value = $this->element->getAttribute($name);
143 109
        if ($value === '') {
144 81
            $value = $this->getXLinkAttribute($name);
145 81
        }
146
147 109
        return $value === '' ? null : $value;
148
    }
149
150
    /**
151
     * Has same propose as `__get()` and same `$filterUrl`, `$fillUrl`, name converting policy.
152
     * You can generate an id for the element by assigning null.
153
     * ```php
154
     * // ...
155
     * $circle->id = null;
156
     * echo $circle->id; // will print something like this __circle12345.
157
     * ```
158
     *
159
     * @param string $name  The name of property.
160
     * @param mixed  $value The value of property.
161
     */
162 117
    public function __set($name, $value)
163
    {
164 117
        if ($name === 'id' && $value === null) {
165 37
            $this->element->setAttribute($name, Identifier::random('__' . $this->getName(), 5));
166
167 37
            return;
168
        }
169
170 107
        if ($value === null || $value === false || $value === '') {
171 8
            return;
172
        }
173
174 107
        if ($name === 'filterUrl' || $name === 'fillUrl') {
175 23
            $this->handleUrlPostfixAttribute($name, $value);
176 23
        }
177
178 107
        $name = $this->convertAttributeName($name);
179 107
        $this->element->setAttribute($name, $value);
180 107
    }
181
182
    /**
183
     * @param string $name The local name of `xlink` namespaced attribute.
184
     *
185
     * @return string The value of attribute.
186
     */
187 81
    public function getXLinkAttribute($name)
188
    {
189 81
        return $this->element->getAttributeNS('xlink', $name);
190
    }
191
192
    /**
193
     * @param string $name  The local name of `xlink` attribute.
194
     * @param mixed  $value The value of attribute.
195
     */
196 1
    public function setXLinkAttribute($name, $value)
197
    {
198 1
        $this->element->setAttributeNS('xlink', "xlink:$name", $value);
199 1
    }
200
201
    /**
202
     * If attribute name is convertable converts it from camelCase to dashed.
203
     *
204
     * @param string $name The string to convert.
205
     *
206
     * @return string The converted string.
207
     */
208 120
    private function convertAttributeName($name)
209
    {
210 120
        return !in_array($name, self::$notConvertable) ? Inflector::camel2dash($name) : $name;
211
    }
212
213
    /**
214
     * @inheritdoc
215
     */
216 50
    public function apply(array $assoc)
217
    {
218 50
        $filtered = [];
219 50
        foreach ($assoc as $attribute => $value) {
220 49
            $filtered[$this->convertAttributeName($attribute)] = $value;
221 50
        }
222 50
        KeyValueWriter::apply($this->element, $filtered);
223
224 50
        return $this;
225
    }
226
227
    /**
228
     * It removes the object itself from the DOM and from the list of the children of its parent.
229
     */
230 3
    protected function selfRemove()
231
    {
232 3
        $this->getRoot()->removeChild($this);
0 ignored issues
show
Bug introduced by
The method removeChild() does not seem to exist on object<nstdio\svg\XMLDocumentInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
233 3
    }
234
235
    /**
236
     * @return SVG The root element of hierarchy.
237
     */
238 3
    protected function getSVG()
239
    {
240 3
        if ($this->root instanceof SVG) {
241 3
            return $this->root;
242
        }
243
        $element = $this->root;
244
245
        do {
246
            $element = $element->getRoot();
247
        } while (!($element instanceof SVG));
248
249
        return $element;
250
    }
251
252
    /**
253
     * Returns standard `defs` element for `svg`.
254
     *
255
     * @param ElementInterface $container Where to search `defs` element.
256
     *
257
     * @return Defs The `defs` element.
258
     */
259 61
    protected static function getDefs(ElementInterface $container)
260
    {
261 61
        if ($container instanceof Defs) {
262 11
            return $container;
263
        }
264
265 50
        if ($container instanceof SVG) {
266 50
            $defs = $container->getFirstChild();
267 50
        } else {
268
            /** @var SVGElement $container */
269
            $defs = $container->getSVG()->getFirstChild();
270
        }
271
272 50
        return $defs;
273
    }
274
275
    /**
276
     * @inheritdoc
277
     */
278 2
    public function copy(array $apply = [], array $ignore = [], ContainerInterface $parent = null)
279
    {
280
        /** @var SVGElement $instance */
281 2
        $instance = (new Instantiator())->instantiate(get_class($this));
282 2
        $instance->root = $parent === null ? $this->getRoot() : $parent;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parent === null ? $this->getRoot() : $parent can also be of type object<nstdio\svg\container\ContainerInterface>. However, the property $root is declared as type object<nstdio\svg\XMLDocumentInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
283 2
        $instance->child = new ElementStorage();
284 2
        $instance->element = $this->createElement($this->getName());
285 2
        $instance->id = null;
286
287 2
        if ($instance instanceof TransformInterface && $this instanceof Transformable) {
0 ignored issues
show
Bug introduced by
The class nstdio\svg\util\TransformInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
288
            $instance->transformImpl = Transform::newInstance($this->getTransformAttribute());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class nstdio\svg\SVGElement as the method getTransformAttribute() does only exist in the following sub-classes of nstdio\svg\SVGElement: nstdio\svg\container\G, nstdio\svg\container\Pattern, nstdio\svg\shape\Circle, nstdio\svg\shape\Ellipse, nstdio\svg\shape\Line, nstdio\svg\shape\Path, nstdio\svg\shape\Polygon, nstdio\svg\shape\Polyline, nstdio\svg\shape\Rect, nstdio\svg\shape\RoundedShape, nstdio\svg\shape\Shape. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
289
        }
290 2
        $ignore[] = 'id';
291 2
        $apply = array_merge($this->allAttributes($ignore), $apply);
292 2
        $instance->apply($apply);
293 2
        $parent === null ? $this->root->append($instance) : $parent->append($instance);
0 ignored issues
show
Bug introduced by
The method append() does not exist on nstdio\svg\XMLDocumentInterface. Did you maybe mean appendChild()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
294
295 2
        return $instance;
296
    }
297
298
    /**
299
     * Places the `$value` into `url(#$value)` if `url(#` is not present.
300
     *
301
     * @param string $attribute The `$filterUrl` or `$fillUrl` properties.
302
     * @param string $value     The wrapped string.
303
     */
304 23
    private function handleUrlPostfixAttribute(&$attribute, &$value)
305
    {
306 23
        $attribute = substr($attribute, 0, strrpos($attribute, 'U'));
307 23
        if (strpos($value, "url(#") !== 0) {
308 23
            $value = "url(#" . $value . ")";
309 23
        }
310 23
    }
311
312
    /**
313
     * Retrieves the id from the `url(#id)` string.
314
     *
315
     * @param string $attribute The `$filterUrl` or `$fillUrl` properties.
316
     *
317
     * @return string The extracted string.
318
     */
319 1
    private function getIdFromUrl($attribute)
320
    {
321 1
        $attribute = substr($attribute, 0, strrpos($attribute, 'U'));
322 1
        return str_replace(['url(#', ')'], '', $this->element->getAttribute($attribute));
323
    }
324
}