Completed
Push — master ( c434f2...7ac5be )
by Sebastian
03:19
created

Name::render()   F

Complexity

Conditions 24
Paths 900

Size

Total Lines 89
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 89
rs 2.248
c 0
b 0
f 0
cc 24
eloc 53
nc 900
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Styles\AffixesTrait;
13
use Seboettg\CiteProc\Styles\DelimiterTrait;
14
use Seboettg\CiteProc\Styles\FormattingTrait;
15
use Seboettg\CiteProc\Util\Factory;
16
use Seboettg\CiteProc\Util\StringHelper;
17
18
19
/**
20
 * Class Name
21
 *
22
 * The cs:name element, an optional child element of cs:names, can be used to describe the formatting of individual
23
 * names, and the separation of names within a name variable.
24
 *
25
 * @package Seboettg\CiteProc\Rendering\Name
26
 *
27
 * @author Sebastian Böttger <[email protected]>
28
 */
29
class Name
30
{
31
    use FormattingTrait,
32
        AffixesTrait,
33
        DelimiterTrait;
34
35
    /**
36
     * @var array
37
     */
38
    protected $nameParts;
39
40
    /**
41
     * Specifies the delimiter between the second to last and last name of the names in a name variable. Allowed values
42
     * are “text” (selects the “and” term, e.g. “Doe, Johnson and Smith”) and “symbol” (selects the ampersand,
43
     * e.g. “Doe, Johnson & Smith”).
44
     *
45
     * @var string
46
     */
47
    private $and;
48
49
    /**
50
     * Determines when the name delimiter or a space is used between a truncated name list and the “et-al”
51
     * (or “and others”) term in case of et-al abbreviation. Allowed values:
52
     * - “contextual” - (default), name delimiter is only used for name lists truncated to two or more names
53
     *   - 1 name: “J. Doe et al.”
54
     *   - 2 names: “J. Doe, S. Smith, et al.”
55
     * - “after-inverted-name” - name delimiter is only used if the preceding name is inverted as a result of the
56
     *   - name-as-sort-order attribute. E.g. with name-as-sort-order set to “first”:
57
     *   - “Doe, J., et al.”
58
     *   - “Doe, J., S. Smith et al.”
59
     * - “always” - name delimiter is always used
60
     *   - 1 name: “J. Doe, et al.”
61
     *   - 2 names: “J. Doe, S. Smith, et al.”
62
     * - “never” - name delimiter is never used
63
     *   - 1 name: “J. Doe et al.”
64
     *   - 2 names: “J. Doe, S. Smith et al.”
65
     *
66
     * @var string
67
     */
68
    private $delimiterPrecedesEtAl;
69
70
    /**
71
     * Determines when the name delimiter is used to separate the second to last and the last name in name lists (if
72
     * and is not set, the name delimiter is always used, regardless of the value of delimiter-precedes-last). Allowed
73
     * values:
74
     *
75
     * - “contextual” - (default), name delimiter is only used for name lists with three or more names
76
     *   - 2 names: “J. Doe and T. Williams”
77
     *   - 3 names: “J. Doe, S. Smith, and T. Williams”
78
     * - “after-inverted-name” - name delimiter is only used if the preceding name is inverted as a result of the
79
     *   name-as-sort-order attribute. E.g. with name-as-sort-order set to “first”:
80
     *   - “Doe, J., and T. Williams”
81
     *   - “Doe, J., S. Smith and T. Williams”
82
     * - “always” - name delimiter is always used
83
     *   - 2 names: “J. Doe, and T. Williams”
84
     *   - 3 names: “J. Doe, S. Smith, and T. Williams”
85
     * - “never” - name delimiter is never used
86
     *   - 2 names: “J. Doe and T. Williams”
87
     *   - 3 names: “J. Doe, S. Smith and T. Williams”
88
     *
89
     * @var string
90
     */
91
    private $delimiterPrecedesLast;
92
93
    /**
94
     * Use of etAlMin (et-al-min attribute) and etAlUseFirst (et-al-use-first attribute) enables et-al abbreviation. If
95
     * the number of names in a name variable matches or exceeds the number set on etAlMin, the rendered name list is
96
     * truncated after reaching the number of names set on etAlUseFirst.
97
     *
98
     * @var int
99
     */
100
    private $etAlMin;
101
102
    /**
103
     * Use of etAlMin (et-al-min attribute) and etAlUseFirst (et-al-use-first attribute) enables et-al abbreviation. If
104
     * the number of names in a name variable matches or exceeds the number set on etAlMin, the rendered name list is
105
     * truncated after reaching the number of names set on etAlUseFirst.
106
     *
107
     * @var int
108
     */
109
    private $etAlUseFirst;
110
111
    /**
112
     * If used, the values of these attributes (et-al-subsequent-min and et-al-subsequent-use-first) replace those of
113
     * respectively et-al-min and et-al-use-first for subsequent cites (cites referencing earlier cited items).
114
     *
115
     * @var int
116
     */
117
    private $etAlSubsequentMin;
118
119
    /**
120
     * If used, the values of these attributes (et-al-subsequent-min and et-al-subsequent-use-first) replace those of
121
     * respectively et-al-min and et-al-use-first for subsequent cites (cites referencing earlier cited items).
122
     *
123
     * @var int
124
     */
125
    private $etAlSubsequentUseFirst;
126
127
    /**
128
     * When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by the name
129
     * delimiter, the ellipsis character, and the last name of the original name list. This is only possible when the
130
     * original name list has at least two more names than the truncated name list (for this the value of
131
     * et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
132
     * et-al-min/et-al-subsequent-use-first).
133
     * A. Goffeau, B. G. Barrell, H. Bussey, R. W. Davis, B. Dujon, H. Feldmann, … S. G. Oliver
134
     *
135
     * @var bool
136
     */
137
    private $etAlUseLast = false;
138
139
    /**
140
     * Specifies whether all the name-parts of personal names should be displayed (value “long”, the default), or only
141
     * the family name and the non-dropping-particle (value “short”). A third value, “count”, returns the total number
142
     * of names that would otherwise be rendered by the use of the cs:names element (taking into account the effects of
143
     * et-al abbreviation and editor/translator collapsing), which allows for advanced sorting.
144
     *
145
     * @var string
146
     */
147
    private $form = "long";
148
149
    /**
150
     * When set to “false” (the default is “true”), given names are no longer initialized when “initialize-with” is set.
151
     * However, the value of “initialize-with” is still added after initials present in the full name (e.g. with
152
     * initialize set to “false”, and initialize-with set to ”.”, “James T Kirk” becomes “James T. Kirk”).
153
     *
154
     * @var bool
155
     */
156
    private $initialize = true;
157
158
    /**
159
     * When set, given names are converted to initials. The attribute value is added after each initial (”.” results
160
     * in “J.J. Doe”). For compound given names (e.g. “Jean-Luc”), hyphenation of the initials can be controlled with
161
     * the global initialize-with-hyphen option
162
     *
163
     * @var string
164
     */
165
    private $initializeWith = "";
166
167
    /**
168
     * Specifies that names should be displayed with the given name following the family name (e.g. “John Doe” becomes
169
     * “Doe, John”). The attribute has two possible values:
170
     *   - “first” - attribute only has an effect on the first name of each name variable
171
     *   - “all” - attribute has an effect on all names
172
     * Note that even when name-as-sort-order changes the name-part order, the display order is not necessarily the same
173
     * as the sorting order for names containing particles and suffixes (see Name-part order). Also, name-as-sort-order
174
     * only affects names written in the latin or Cyrillic alphabets. Names written in other alphabets (e.g. Asian
175
     * scripts) are always displayed with the family name preceding the given name.
176
     *
177
     * @var string
178
     */
179
    private $nameAsSortOrder = "";
180
181
    /**
182
     * Sets the delimiter for name-parts that have switched positions as a result of name-as-sort-order. The default
183
     * value is ”, ” (“Doe, John”). As is the case for name-as-sort-order, this attribute only affects names written in
184
     * the latin or Cyrillic alphabets.
185
     *
186
     * @var string
187
     */
188
    private $sortSeparator = ", ";
189
190
    /**
191
     * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”).
192
     * @var
193
     */
194
    private $delimiter = ", ";
195
196
197
    /**
198
     * @var Names
199
     */
200
    private $parent;
201
202
    /**
203
     * Name constructor.
204
     * @param \SimpleXMLElement $node
205
     * @param Names $parent
206
     */
207
    public function __construct(\SimpleXMLElement $node, Names $parent)
208
    {
209
        $this->nameParts = [];
210
        $this->parent = $parent;
211
212
        /** @var \SimpleXMLElement $child */
213
        foreach ($node->children() as $child) {
214
215
            switch ($child->getName()) {
216
                case "name-part":
217
                    /** @var NamePart $namePart */
218
                    $namePart = Factory::create($child, $this);
219
                    $this->nameParts[$namePart->getName()] = $namePart;
220
            }
221
        }
222
223
224
        /** @var \SimpleXMLElement $attribute */
225
        foreach ($node->attributes() as $attribute) {
226
            switch ($attribute->getName()) {
227
                case 'and':
228
                    $and = (string)$attribute;
229
                    if ("text" === $and) {
230
                        $this->and = CiteProc::getContext()->getLocale()->filter('terms', 'and')->single;
231
                    } elseif ('symbol' === $and) {
232
                        $this->and = '&#38;';
233
                    }
234
                    break;
235
                case 'delimiter-precedes-et-al':
236
                    $this->delimiterPrecedesEtAl = (string) $attribute;
237
                    break;
238
                case 'delimiter-precedes-last':
239
                    $this->delimiterPrecedesLast = (string) $attribute;
240
                    break;
241
                case 'et-al-min':
242
                    $this->etAlMin = intval((string) $attribute);
243
                    break;
244
                case 'et-al-use-first':
245
                    $this->etAlUseFirst = intval((string) $attribute);
246
                    break;
247
                case 'et-al-subsequent-min':
248
                    $this->etAlSubsequentMin = intval((string) $attribute);
249
                    break;
250
                case 'et-al-subsequent-use-first':
251
                    $this->etAlSubsequentUseFirst = intval((string) $attribute);
252
                    break;
253
                case 'et-al-use-last':
254
                    $this->etAlUseLast = boolval((string) $attribute);
255
                    break;
256
                case 'form':
257
                    $this->form = (string) $attribute;
258
                    break;
259
                case 'initialize':
260
                    $this->initialize = boolval((string) $attribute);
261
                    break;
262
                case 'initialize-with':
263
                    $this->initializeWith = (string) $attribute;
264
                    break;
265
                case 'name-as-sort-order':
266
                    $this->nameAsSortOrder = (string) $attribute;
267
                    break;
268
                case 'sort-separator':
269
                    $this->sortSeparator = (string) $attribute;
270
271
            }
272
        }
273
274
        $this->initFormattingAttributes($node);
275
        $this->initAffixesAttributes($node);
276
        $this->initDelimiterAttributes($node);
277
    }
278
279
    public function render($data)
280
    {
281
        $resultNames = [];
282
        $etAl = false;
283
        $count = 0;
284
        /**
285
         * @var string $type
286
         * @var array $name
287
         */
288
        foreach ($data as $rank => $name) {
289
            ++$count;
290
            $resultNames[] = $this->formatName($name, $rank);
291
        }
292
293
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
294
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
295
        names set on et-al-use-first.  */
296
        if (isset($this->etAlMin) && isset($this->etAlUseFirst)) {
297
            $cnt = count($resultNames);
298
            if ($this->etAlMin >= count($cnt)) {
299
                for ($i = $this->etAlUseFirst; $i < $cnt; ++$i) {
300
                    unset($resultNames[$i]);
301
                }
302
            }
303
            if ($this->parent->hasEtAl()) {
304
                $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 288. 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...
Documentation introduced by
$name is of type array, but the function expects a object<stdClass>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
305
            } else {
306
                $etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
307
            }
308
        }
309
310
311
        /* add "and" */
312
        $count = count($resultNames);
313
        if (!empty($this->and) && $count > 1 && !$etAl) {
314
            $new = $this->and . ' ' . end($resultNames); //stick an "and" in front of the last author if "and" is defined
315
            $resultNames[key($resultNames)] = $new;
316
        }
317
318
        $text = implode($this->delimiter, $resultNames);
319
320
        if (!empty($resultNames) && $etAl) {
321
322
            /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
323
            term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
324
            name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
325
            delimiter-precedes-et-al attribute. */
326
            switch ($this->delimiterPrecedesEtAl) {
327
                case 'never':
328
                    $text = $text . " $etAl";
329
                    break;
330
                case 'always':
331
                    $text = $text . "$this->delimiter$etAl";
332
                    break;
333
                default:
334
                    if (count($resultNames) === 1) {
335
                        $text .= " $etAl";
336
                    } else {
337
                        $text .=  $this->delimiter . $etAl;
338
                    }
339
340
            }
341
        }
342
        if ($this->form == 'count') {
343
            if ($etAl === false) {
344
                return (int)count($resultNames);
345
            } else {
346
                return (int)(count($resultNames) - 1);
347
            }
348
        }
349
        // strip out the last delimiter if not required
350
        if (isset($this->and) && count($resultNames) > 1) {
351
            $lastDelimiter = strrpos($text, $this->delimiter . $this->and);
352
            switch ($this->delimiterPrecedesLast) {
353
                case 'always':
354
                    return $text;
355
                    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...
356
                case 'never':
357
                    return substr_replace($text, ' ', $lastDelimiter, strlen($this->delimiter));
358
                    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...
359
                case 'contextual':
360
                default:
361
                    if (count($resultNames) < 3 && $lastDelimiter !== false) {
362
                        return substr_replace($text, ' ', $lastDelimiter, strlen($this->delimiter));
363
                    }
364
            }
365
        }
366
        return $text;
367
    }
368
369
    private function formatName($name, $rank)
370
    {
371
        $nameObj = $this->cloneNamePOSC($name);
372
373
        $useInitials = $this->initialize && !empty($this->initializeWith);
374
        if ($useInitials && isset($name->given)) {
375
            //TODO: initialize with hyphen
376
            //$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...
377
            $nameObj->given = "";
378
            $givenParts = StringHelper::explodeBySpaceOrHyphen($name->given);
379
            foreach ($givenParts as $givenPart) {
380
                $nameObj->given .= substr($givenPart, 0, 1) . $this->initializeWith;
381
            }
382
        }
383
384
        // format name-parts
385
        if (count($this->nameParts) > 0) {
386
            /** @var NamePart $namePart */
387
            foreach ($this->nameParts as $namePart) {
388
                $nameObj->{$namePart->getName()} =   $namePart->render($name);
389
            }
390
        }
391
392
        $return = $this->getNamesString($nameObj, $rank);
393
394
        return trim($return);
395
    }
396
397
    /**
398
     * @param $name
399
     * @return string
400
     */
401
    private function getNamesString($name, $rank)
402
    {
403
        $text = "";
404
405
        if (!isset($name->family)) {
406
            return $text;
407
        }
408
409
        $given = !empty($name->given) ? $this->format(trim($name->given)) : "";
410
        $nonDroppingParticle = isset($name->{'non-dropping-particle'}) ? $name->{'non-dropping-particle'} : " ";
411
        $droppingParticle = isset($name->{'dropping-particle'}) ? $name->{'dropping-particle'} : " ";
412
        $suffix = (isset($name->{'suffix'})) ? ' ' . $name->{'suffix'} : " ";
413
414
        if (isset($name->family)) {
415
            $family = $this->format($name->family);
416
            if ($this->form == 'short') {
417
                $text = (!empty($nonDroppingParticle) ? $nonDroppingParticle . " " : "") . $family;
418
            } else {
419
                switch ($this->nameAsSortOrder) {
420
421
                    case 'all':
422
                    case 'first':
423
                        if ($this->nameAsSortOrder === "first" && $rank !== 0) {
424
                            break;
425
                        }
426
                        /*
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...
427
                        use form "[non-dropping particel] family name,
428
                        given name [dropping particle], [suffix]"
429
                        */
430
                        $text = sprintf(
431
                                    "%s %s, %s %s, %s",
432
                                    $nonDroppingParticle,
433
                                    $family,
434
                                    $given,
435
                                    $droppingParticle,
436
                                    $suffix);
437
                            //remove last comma when no suffix exist.
438
                            $text = trim($text);
439
                            $text = substr($text, -1) === "," ? substr($text, 0, strlen($text)-1) : $text;
440
                        break;
441
                    default:
442
                        /*
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...
443
                        use form "given name [dropping particles] [non-dropping particles] family name [suffix]"
444
                        e.g. [Jean] [de] [La] [Fontaine] [III]
445
                        */
446
                        $text = sprintf(
447
                            "%s %s %s %s %s",
448
                            $given,
449
                            $droppingParticle,
450
                            $nonDroppingParticle,
451
                            $family,
452
                            $suffix);
453
454
                }
455
            }
456
        }
457
458
        return trim(preg_replace("/\s{2,}/", " ", $text));
459
    }
460
461
    public function getOptions()
462
    {
463
        $ignore = ["namePart", "parent", "substitute"];
464
        $options = [];
465
        $reflectedName = new \ReflectionClass($this);
466
467
        foreach ($reflectedName->getProperties() as $property) {
468
            $property->setAccessible(true);
469
            if (in_array($property->getName(), $ignore)) {
470
                continue;
471
            } else if ($property->getName() == "and" && $property->getValue($this) === "&#38;") {
472
                $options["and"] = "symbol";
473
            } else {
474
                $propValue = $property->getValue($this);
475
                if (isset($propValue) && !empty($propValue)) {
476
                    $options[StringHelper::camelCase2Hyphen($property->getName())] = $propValue;
477
                }
478
            }
479
        }
480
        return $options;
481
    }
482
483
    /**
484
     * @param $name
485
     * @return \stdClass
486
     */
487
    private function cloneNamePOSC($name)
488
    {
489
        $nameObj = new \stdClass();
490
        if (isset($name->family)) {
491
            $nameObj->family = $name->family;
492
        }
493
        if (isset($name->given)) {
494
            $nameObj->given = $name->given;
495
        }
496
        if (isset($name->{'non-dropping-particle'})) {
497
            $nameObj->{'non-dropping-particle'} = $name->{'non-dropping-particle'};
498
        }
499
        if (isset($name->{'dropping-particle'})) {
500
            $nameObj->{'dropping-particle'} = $name->{'dropping-particle'};
501
        }
502
        if (isset($name->{'suffix'})) {
503
            $nameObj->{'suffix'} = $name->{'suffix'};
504
        }
505
        return $nameObj;
506
    }
507
508
}
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...
509