Test Failed
Push — master ( 9d4e41...1cd52c )
by Sebastian
03:24
created

Name::__construct()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
cc 5
eloc 17
nc 9
nop 2
1
<?php
2
/**
3
 * citeproc-php
4
 *
5
 * @link        http://github.com/seboettg/citeproc-php for the source repository
6
 * @copyright   Copyright (c) 2016 Sebastian Böttger.
7
 * @license     https://opensource.org/licenses/MIT
8
 */
9
10
namespace Seboettg\CiteProc\Rendering\Name;
11
use Seboettg\CiteProc\CiteProc;
12
use Seboettg\CiteProc\Style\InheritableNameAttributesTrait;
13
use Seboettg\CiteProc\Style\SubsequentAuthorSubstituteRule;
14
use Seboettg\CiteProc\Styles\AffixesTrait;
15
use Seboettg\CiteProc\Styles\DelimiterTrait;
16
use Seboettg\CiteProc\Styles\FormattingTrait;
17
use Seboettg\CiteProc\Util\Factory;
18
use Seboettg\CiteProc\Util\StringHelper;
19
20
21
/**
22
 * Class Name
23
 *
24
 * The cs:name element, an optional child element of cs:names, can be used to describe the formatting of individual
25
 * names, and the separation of names within a name variable.
26
 *
27
 * @package Seboettg\CiteProc\Rendering\Name
28
 *
29
 * @author Sebastian Böttger <[email protected]>
30
 */
31
class Name
32
{
33
    use InheritableNameAttributesTrait,
34
        FormattingTrait,
35
        AffixesTrait,
36
        DelimiterTrait;
37
38
    /**
39
     * @var array
40
     */
41
    protected $nameParts;
42
43
    /**
44
     * Specifies whether all the name-parts of personal names should be displayed (value “long”, the default), or only
45
     * the family name and the non-dropping-particle (value “short”). A third value, “count”, returns the total number
46
     * of names that would otherwise be rendered by the use of the cs:names element (taking into account the effects of
47
     * et-al abbreviation and editor/translator collapsing), which allows for advanced sorting.
48
     *
49
     * @var string
50
     */
51
    private $form = "long";
52
53
54
    /**
55
     * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”).
56
     * @var
57
     */
58
    private $delimiter = ", ";
59
60
61
    /**
62
     * @var Names
63
     */
64
    private $parent;
65
66
    /**
67
     * @var \SimpleXMLElement
68
     */
69
    private $node;
70
71
    /**
72
     * Name constructor.
73
     * @param \SimpleXMLElement $node
74
     * @param Names $parent
75
     */
76
    public function __construct(\SimpleXMLElement $node, Names $parent)
77
    {
78
        $this->node = $node;
79
        $this->parent = $parent;
80
81
        $this->nameParts = [];
82
83
        /** @var \SimpleXMLElement $child */
84
        foreach ($node->children() as $child) {
85
86
            switch ($child->getName()) {
87
                case "name-part":
88
                    /** @var NamePart $namePart */
89
                    $namePart = Factory::create($child, $this);
90
                    $this->nameParts[$namePart->getName()] = $namePart;
91
            }
92
        }
93
94
        foreach ($node->attributes() as $attribute) {
95
            switch ($attribute->getName()) {
96
                case 'form':
97
                    $this->form = (string) $attribute;
98
                    break;
99
            }
100
101
        }
102
103
        $this->initFormattingAttributes($node);
104
        $this->initAffixesAttributes($node);
105
        $this->initDelimiterAttributes($node);
106
    }
107
108
    public function render($data, $citationNumber)
109
    {
110
        if (!$this->attributesInitialized) {
111
            $this->initInheritableNameAttributes($this->node);
112
        }
113
        $resultNames = [];
114
        $etAl = false;
115
        $count = 0;
116
117
        $hasPreceding = CiteProc::getContext()->getCitationItems()->hasKey($citationNumber-1);
118
        if ($hasPreceding) {
119
            /** @var \stdClass $preceding */
120
            $preceding = CiteProc::getContext()->getCitationItems()->get($citationNumber-1);
121
        }
122
123
        $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
124
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule();
125
126
        $useSubseqSubstitution = !empty($subsequentSubstitution) && !empty($subsequentSubstitutionRule);
127
128
        /**
129
         * @var string $type
130
         * @var array $name
131
         */
132
        foreach ($data as $rank => $name) {
133
            ++$count;
134
135
            if ($hasPreceding && $useSubseqSubstitution) {
136
                if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::PARTIAL_EACH && $rank > 0) {
137 View Code Duplication
                    if ($preceding->author[$rank]->family === $name->family) {
0 ignored issues
show
Bug introduced by
The variable $preceding does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
138
                        $resultNames[] = $subsequentSubstitution;
139
                    } else {
140
                        $resultNames[] = $this->formatName($name, $rank);
141
                    }
142
                } else if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::PARTIAL_FIRST && $rank === 0) {
143 View Code Duplication
                    if ($preceding->author[0]->family === $name->family) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
144
                        $resultNames[] = $subsequentSubstitution;
145
                    } else {
146
                        $resultNames[] = $this->formatName($name, $rank);
147
                    }
148
                }
149
            }
150
            else {
151
                $resultNames[] = $this->formatName($name, $rank);
152
            }
153
        }
154
155
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
156
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
157
        names set on et-al-use-first.  */
158
        if (isset($this->etAlMin) && isset($this->etAlUseFirst)) {
159
            $cnt = count($resultNames);
160
            if ($this->etAlMin >= count($cnt)) {
161
                for ($i = $this->etAlUseFirst; $i < $cnt; ++$i) {
162
                    unset($resultNames[$i]);
163
                }
164
            }
165
            if ($this->parent->hasEtAl()) {
166
                $etAl = $this->parent->getEtAl()->render($name);
0 ignored issues
show
Bug introduced by
The variable $name seems to be defined by a foreach iteration on line 132. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
167
            } else {
168
                $etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
169
            }
170
        }
171
172
173
        /* add "and" */
174
        $count = count($resultNames);
175
        if (!empty($this->and) && $count > 1 && !$etAl) {
176
            if ($this->etAlUseLast) {
177
                /* When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by
178
                the name delimiter, the ellipsis character, and the last name of the original name list. This is only
179
                possible when the original name list has at least two more names than the truncated name list (for this
180
                the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
181
                et-al-min/et-al-subsequent-use-first). */
182
                $new = "… " . end($resultNames); // and prefix of the last author if "and" is defined
183
            } else {
184
                $new = $this->and . ' ' . end($resultNames); // and prefix of the last author if "and" is defined
185
            }
186
            $resultNames[key($resultNames)] = $new;
187
        }
188
189
        $text = implode($this->delimiter, $resultNames);
190
191
        if (!empty($resultNames) && $etAl) {
192
193
            /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
194
            term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
195
            name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
196
            delimiter-precedes-et-al attribute. */
197
            switch ($this->delimiterPrecedesEtAl) {
198
                case 'never':
199
                    $text = $text . " $etAl";
200
                    break;
201
                case 'always':
202
                    $text = $text . "$this->delimiter$etAl";
203
                    break;
204
                default:
205
                    if (count($resultNames) === 1) {
206
                        $text .= " $etAl";
207
                    } else {
208
                        $text .=  $this->delimiter . $etAl;
209
                    }
210
211
            }
212
        }
213
        if ($this->form == 'count') {
214
            if ($etAl === false) {
215
                return (int)count($resultNames);
216
            } else {
217
                return (int)(count($resultNames) - 1);
218
            }
219
        }
220
        // strip out the last delimiter if not required
221
        if (isset($this->and) && count($resultNames) > 1) {
222
            $lastDelimiter = strrpos($text, $this->delimiter . $this->and);
223
            switch ($this->delimiterPrecedesLast) {
224
                case 'always':
225
                    return $text;
226
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
227
                case 'never':
228
                    return substr_replace($text, ' ', $lastDelimiter, strlen($this->delimiter));
229
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
230
                case 'contextual':
231
                default:
232
                    if (count($resultNames) < 3 && $lastDelimiter !== false) {
233
                        return substr_replace($text, ' ', $lastDelimiter, strlen($this->delimiter));
234
                    }
235
            }
236
        }
237
        return $text;
238
    }
239
240
    private function formatName($name, $rank)
241
    {
242
        $nameObj = $this->cloneNamePOSC($name);
243
244
        $useInitials = $this->initialize && !empty($this->initializeWith);
245
        if ($useInitials && isset($name->given)) {
246
            //TODO: initialize with hyphen
247
            //$nameObj->given = $name->given;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
248
            $nameObj->given = "";
249
            $givenParts = StringHelper::explodeBySpaceOrHyphen($name->given);
250
            foreach ($givenParts as $givenPart) {
251
                $nameObj->given .= substr($givenPart, 0, 1) . $this->initializeWith;
252
            }
253
        }
254
255
        // format name-parts
256
        if (count($this->nameParts) > 0) {
257
            /** @var NamePart $namePart */
258
            foreach ($this->nameParts as $namePart) {
259
                $nameObj->{$namePart->getName()} =   $namePart->render($name);
260
            }
261
        }
262
263
        $return = $this->getNamesString($nameObj, $rank);
264
265
        return trim($return);
266
    }
267
268
    /**
269
     * @param $name
270
     * @return string
271
     */
272
    private function getNamesString($name, $rank)
273
    {
274
        $text = "";
275
276
        if (!isset($name->family)) {
277
            return $text;
278
        }
279
280
        $given = !empty($name->given) ? $this->format(trim($name->given)) : "";
281
        $nonDroppingParticle = isset($name->{'non-dropping-particle'}) ? $name->{'non-dropping-particle'} : "";
282
        $droppingParticle = isset($name->{'dropping-particle'}) ? $name->{'dropping-particle'} : "";
283
        $suffix = (isset($name->{'suffix'})) ? $name->{'suffix'} : "";
284
285
        if (isset($name->family)) {
286
            $family = $this->format($name->family);
287
            if ($this->form == 'short') {
288
                $text = (!empty($nonDroppingParticle) ? $nonDroppingParticle . " " : "") . $family;
289
            } else {
290
                switch ($this->nameAsSortOrder) {
291
292
                    case 'all':
293
                    case 'first':
294
                        if ($this->nameAsSortOrder === "first" && $rank !== 0) {
295
                            break;
296
                        }
297
                        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
298
                        use form "[non-dropping particel] family name,
299
                        given name [dropping particle], [suffix]"
300
                        */
301
                        $text  = !empty($nonDroppingParticle) ? "$nonDroppingParticle " : "";
302
                        $text .= $family;
303
                        $text .= !empty($given) ? ", $given" : "";
304
                        $text .= !empty($droppingParticle) ? " $droppingParticle" : "";
305
                        $text .= !empty($suffix) ? ", $suffix" : "";
306
307
                        //remove last comma when no suffix exist.
308
                        $text = trim($text);
309
                        $text = substr($text, -1) === "," ? substr($text, 0, strlen($text)-1) : $text;
310
                        break;
311
                    default:
312
                        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
313
                        use form "given name [dropping particles] [non-dropping particles] family name [suffix]"
314
                        e.g. [Jean] [de] [La] [Fontaine] [III]
315
                        */
316
                        $text = sprintf(
317
                            "%s %s %s %s %s",
318
                            $given,
319
                            $droppingParticle,
320
                            $nonDroppingParticle,
321
                            $family,
322
                            $suffix);
323
324
                }
325
            }
326
        }
327
328
        //contains nbsp prefixed by normal space or followed by normal space?
329
        $text = htmlentities($text);
330
        if (strpos($text, " &nbsp;") !== false || strpos($text, "&nbsp; ") !== false) {
331
332
            $text = preg_replace("/[\s]+/", "", $text); //remove normal spaces
333
            return preg_replace("/&nbsp;+/", " ", $text);
334
        }
335
        $text = preg_replace("/[\s]+/", " ", $text);
336
        return trim($text);
337
    }
338
339
    public function getOptions()
340
    {
341
        $ignore = ["namePart", "parent", "substitute"];
342
        $options = [];
343
        $reflectedName = new \ReflectionClass($this);
344
345
        foreach ($reflectedName->getProperties() as $property) {
346
            $property->setAccessible(true);
347
            if (in_array($property->getName(), $ignore)) {
348
                continue;
349
            } else if ($property->getName() == "and" && $property->getValue($this) === "&#38;") {
350
                $options["and"] = "symbol";
351
            } else {
352
                $propValue = $property->getValue($this);
353
                if (isset($propValue) && !empty($propValue)) {
354
                    $options[StringHelper::camelCase2Hyphen($property->getName())] = $propValue;
355
                }
356
            }
357
        }
358
        return $options;
359
    }
360
361
    /**
362
     * @param $name
363
     * @return \stdClass
364
     */
365
    private function cloneNamePOSC($name)
366
    {
367
        $nameObj = new \stdClass();
368
        if (isset($name->family)) {
369
            $nameObj->family = $name->family;
370
        }
371
        if (isset($name->given)) {
372
            $nameObj->given = $name->given;
373
        }
374
        if (isset($name->{'non-dropping-particle'})) {
375
            $nameObj->{'non-dropping-particle'} = $name->{'non-dropping-particle'};
376
        }
377
        if (isset($name->{'dropping-particle'})) {
378
            $nameObj->{'dropping-particle'} = $name->{'dropping-particle'};
379
        }
380
        if (isset($name->{'suffix'})) {
381
            $nameObj->{'suffix'} = $name->{'suffix'};
382
        }
383
        return $nameObj;
384
    }
385
386
387
}
0 ignored issues
show
Coding Style introduced by
As per coding style, files should not end with a newline character.

This check marks files that end in a newline character, i.e. an empy line.

Loading history...
388