Completed
Push — develop ( 292cbe...631186 )
by Alexander
15:04
created

ArticleController::persistDocument()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 26
ccs 10
cts 11
cp 0.9091
rs 8.8571
cc 3
eloc 14
nc 2
nop 3
crap 3.0067
1
<?php
2
3
/*
4
 * This file is part of Sulu.
5
 *
6
 * (c) MASSIVE ART WebServices GmbH
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Sulu\Bundle\ArticleBundle\Controller;
13
14
use FOS\RestBundle\Controller\Annotations\Post;
15
use FOS\RestBundle\Routing\ClassResourceInterface;
16
use JMS\Serializer\SerializationContext;
17
use ONGR\ElasticsearchBundle\Service\Manager;
18
use ONGR\ElasticsearchDSL\Query\BoolQuery;
19
use ONGR\ElasticsearchDSL\Query\IdsQuery;
20
use ONGR\ElasticsearchDSL\Query\MatchAllQuery;
21
use ONGR\ElasticsearchDSL\Query\MatchQuery;
22
use ONGR\ElasticsearchDSL\Query\MultiMatchQuery;
23
use ONGR\ElasticsearchDSL\Query\TermQuery;
24
use ONGR\ElasticsearchDSL\Sort\FieldSort;
25
use Sulu\Bundle\ArticleBundle\Admin\ArticleAdmin;
26
use Sulu\Bundle\ArticleBundle\Document\ArticleDocument;
27
use Sulu\Bundle\ArticleBundle\Document\Form\ArticleDocumentType;
28
use Sulu\Bundle\ArticleBundle\Metadata\ArticleViewDocumentIdTrait;
29
use Sulu\Component\Content\Form\Exception\InvalidFormException;
30
use Sulu\Component\Content\Mapper\ContentMapperInterface;
31
use Sulu\Component\DocumentManager\DocumentManagerInterface;
32
use Sulu\Component\Rest\Exception\MissingParameterException;
33
use Sulu\Component\Rest\Exception\RestException;
34
use Sulu\Component\Rest\ListBuilder\FieldDescriptor;
35
use Sulu\Component\Rest\ListBuilder\ListRepresentation;
36
use Sulu\Component\Rest\RequestParametersTrait;
37
use Sulu\Component\Rest\RestController;
38
use Sulu\Component\Security\SecuredControllerInterface;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\Response;
41
42
/**
43
 * Provides API for articles.
44
 */
45
class ArticleController extends RestController implements ClassResourceInterface, SecuredControllerInterface
46
{
47
    const DOCUMENT_TYPE = 'article';
48
49
    use RequestParametersTrait;
50
    use ArticleViewDocumentIdTrait;
51
52
    /**
53
     * Create field-descriptor array.
54
     *
55
     * @return FieldDescriptor[]
56
     */
57
    private function getFieldDescriptors()
58
    {
59
        return [
60
            'uuid' => new FieldDescriptor('uuid', 'public.id', true),
61
            'typeTranslation' => new FieldDescriptor(
62
                'typeTranslation',
63
                'sulu_article.list.type',
64
                !$this->getParameter('sulu_article.display_tab_all'),
65
                false
66
            ),
67
            'title' => new FieldDescriptor('title', 'public.title', false, true),
68
            'creatorFullName' => new FieldDescriptor('creatorFullName', 'sulu_article.list.creator', true, false),
69
            'changerFullName' => new FieldDescriptor('changerFullName', 'sulu_article.list.changer', false, false),
70
            'authorFullName' => new FieldDescriptor('authorFullName', 'sulu_article.author', false, false),
71
            'created' => new FieldDescriptor('created', 'public.created', true, false, 'datetime'),
72
            'changed' => new FieldDescriptor('changed', 'public.changed', false, false, 'datetime'),
73
            'authored' => new FieldDescriptor('authored', 'sulu_article.authored', false, false, 'date'),
74
        ];
75
    }
76
77
    /**
78
     * Returns fields.
79
     *
80
     * @return Response
81
     */
82
    public function cgetFieldsAction()
83
    {
84
        $fieldDescriptors = $this->getFieldDescriptors();
85
86
        return $this->handleView($this->view(array_values($fieldDescriptors)));
0 ignored issues
show
Documentation introduced by
$this->view(array_values($fieldDescriptors)) is of type this<Sulu\Bundle\Article...ller\ArticleController>, but the function expects a object<FOS\RestBundle\View\View>.

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...
87
    }
88
89
    /**
90
     * Returns list of articles.
91
     *
92
     * @param Request $request
93
     *
94
     * @return Response
95
     */
96 11
    public function cgetAction(Request $request)
97
    {
98 11
        $locale = $this->getRequestParameter($request, 'locale', true);
99
100 11
        $restHelper = $this->get('sulu_core.list_rest_helper');
101
102
        /** @var Manager $manager */
103 11
        $manager = $this->get('es.manager.default');
104 11
        $repository = $manager->getRepository($this->get('sulu_article.view_document.factory')->getClass('article'));
105 11
        $search = $repository->createSearch();
106
107 11
        $limit = (int) $restHelper->getLimit();
108 11
        $page = (int) $restHelper->getPage();
109
110 11
        if (null !== $locale) {
111 11
            $search->addQuery(new TermQuery('locale', $locale));
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\TermQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
112
        }
113
114 11
        if (count($ids = array_filter(explode(',', $request->get('ids', ''))))) {
115 1
            $search->addQuery(new IdsQuery($this->getViewDocumentIds($ids, $locale)));
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\IdsQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
116 1
            $limit = count($ids);
117
        }
118
119 11
        if (!empty($searchPattern = $restHelper->getSearchPattern())
120 11
            && 0 < count($searchFields = $restHelper->getSearchFields())
121
        ) {
122 2
            $search->addQuery(new MultiMatchQuery($searchFields, $searchPattern));
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\MultiMatchQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
123
        }
124
125 11
        if (null !== ($type = $request->get('type'))) {
126 10
            $search->addQuery(new TermQuery('type', $type));
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\TermQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
127
        }
128
129 11
        if (null !== ($contactId = $request->get('contactId'))) {
130 1
            $boolQuery = new BoolQuery();
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\BoolQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
131 1
            $boolQuery->add(new MatchQuery('changer_contact_id', $contactId), BoolQuery::SHOULD);
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\MatchQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
132 1
            $boolQuery->add(new MatchQuery('creator_contact_id', $contactId), BoolQuery::SHOULD);
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\MatchQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
133 1
            $boolQuery->add(new MatchQuery('author_id', $contactId), BoolQuery::SHOULD);
0 ignored issues
show
Deprecated Code introduced by
The class ONGR\ElasticsearchDSL\Query\MatchQuery has been deprecated with message: Use the extended class instead. This class is left only for BC compatibility.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
134 1
            $search->addQuery($boolQuery);
135
        }
136
137 11
        if (null === $search->getQueries()) {
138
            $search->addQuery(new MatchAllQuery());
139
        }
140
141 11
        $count = $repository->count($search);
142
143 11
        if (null !== $restHelper->getSortColumn()) {
144 1
            $search->addSort(
145 1
                new FieldSort($this->uncamelize($restHelper->getSortColumn()), $restHelper->getSortOrder())
146
            );
147
        }
148
149 11
        $search->setSize($limit);
150 11
        $search->setFrom(($page - 1) * $limit);
151
152 11
        $result = [];
153 11
        foreach ($repository->execute($search) as $document) {
0 ignored issues
show
Deprecated Code introduced by
The method ONGR\ElasticsearchBundle...e\Repository::execute() has been deprecated with message: Use strict execute functions instead. e.g. executeIterator, executeRawIterator.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
154 9
            if (false !== ($index = array_search($document->getUuid(), $ids))) {
155 1
                $result[$index] = $document;
156
            } else {
157 11
                $result[] = $document;
158
            }
159
        }
160
161 11
        if (count($ids)) {
162 1
            ksort($result);
163 1
            $result = array_values($result);
164
        }
165
166 11
        return $this->handleView(
167 11
            $this->view(
0 ignored issues
show
Documentation introduced by
$this->view(new \Sulu\Co...$page, $limit, $count)) is of type this<Sulu\Bundle\Article...ller\ArticleController>, but the function expects a object<FOS\RestBundle\View\View>.

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...
168 11
                new ListRepresentation(
169
                    $result,
170 11
                    'articles',
171 11
                    'get_articles',
172 11
                    $request->query->all(),
173
                    $page,
174
                    $limit,
175
                    $count
0 ignored issues
show
Bug introduced by
It seems like $count defined by $repository->count($search) on line 141 can also be of type array; however, Sulu\Component\Rest\List...entation::__construct() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
176
                )
177
            )
178
        );
179
    }
180
181
    /**
182
     * Returns single article.
183
     *
184
     * @param string $uuid
185
     * @param Request $request
186
     *
187
     * @return Response
188
     */
189 2
    public function getAction($uuid, Request $request)
190
    {
191 2
        $locale = $this->getRequestParameter($request, 'locale', true);
192 2
        $document = $this->getDocumentManager()->find(
193
            $uuid,
194
            $locale,
195
            [
196 2
                'load_ghost_content' => true,
197
                'load_shadow_content' => false,
198
            ]
199
        );
200
201 2
        return $this->handleView(
202 2
            $this->view($document)->setSerializationContext(
0 ignored issues
show
Bug introduced by
The method setSerializationContext() does not seem to exist on object<Sulu\Bundle\Artic...ller\ArticleController>.

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...
203 2
                SerializationContext::create()->setSerializeNull(true)->setGroups(['defaultPage'])
204
            )
205
        );
206
    }
207
208
    /**
209
     * Create article.
210
     *
211
     * @param Request $request
212
     *
213
     * @return Response
214
     */
215 29
    public function postAction(Request $request)
216
    {
217 29
        $action = $request->get('action');
218 29
        $document = $this->getDocumentManager()->create(self::DOCUMENT_TYPE);
219 29
        $locale = $this->getRequestParameter($request, 'locale', true);
220 29
        $data = $request->request->all();
221
222 29
        $this->persistDocument($data, $document, $locale);
223 29
        $this->handleActionParameter($action, $document, $locale);
224 29
        $this->getDocumentManager()->flush();
225
226 29
        return $this->handleView(
227 29
            $this->view($document)->setSerializationContext(
0 ignored issues
show
Bug introduced by
The method setSerializationContext() does not seem to exist on object<Sulu\Bundle\Artic...ller\ArticleController>.

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...
228 29
                SerializationContext::create()->setSerializeNull(true)->setGroups(['defaultPage'])
229
            )
230
        );
231
    }
232
233
    /**
234
     * Update articles.
235
     *
236
     * @param Request $request
237
     * @param string $uuid
238
     *
239
     * @return Response
240
     */
241 7
    public function putAction(Request $request, $uuid)
242
    {
243 7
        $locale = $this->getRequestParameter($request, 'locale', true);
244 7
        $action = $request->get('action');
245 7
        $data = $request->request->all();
246
247 7
        $document = $this->getDocumentManager()->find(
248
            $uuid,
249
            $locale,
250
            [
251 7
                'load_ghost_content' => false,
252
                'load_shadow_content' => false,
253
            ]
254
        );
255
256 7
        $this->get('sulu_hash.request_hash_checker')->checkHash($request, $document, $document->getUuid());
257
258 7
        $this->persistDocument($data, $document, $locale);
259 7
        $this->handleActionParameter($action, $document, $locale);
260 7
        $this->getDocumentManager()->flush();
261
262 7
        return $this->handleView(
263 7
            $this->view($document)->setSerializationContext(
0 ignored issues
show
Bug introduced by
The method setSerializationContext() does not seem to exist on object<Sulu\Bundle\Artic...ller\ArticleController>.

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...
264 7
                SerializationContext::create()->setSerializeNull(true)->setGroups(['defaultPage'])
265
            )
266
        );
267
    }
268
269
    /**
270
     * Deletes multiple documents.
271
     *
272
     * @param Request $request
273
     *
274
     * @return Response
275
     */
276 1
    public function cdeleteAction(Request $request)
277
    {
278 1
        $ids = array_filter(explode(',', $request->get('ids', '')));
279
280 1
        $documentManager = $this->getDocumentManager();
281 1
        foreach ($ids as $id) {
282 1
            $document = $documentManager->find($id);
283 1
            $documentManager->remove($document);
284 1
            $documentManager->flush();
285
        }
286
287 1
        return $this->handleView($this->view(null));
0 ignored issues
show
Documentation introduced by
$this->view(null) is of type this<Sulu\Bundle\Article...ller\ArticleController>, but the function expects a object<FOS\RestBundle\View\View>.

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...
288
    }
289
290
    /**
291
     * Deletes multiple documents.
292
     *
293
     * @param string $id
294
     *
295
     * @return Response
296
     */
297 1
    public function deleteAction($id)
298
    {
299 1
        $documentManager = $this->getDocumentManager();
300 1
        $document = $documentManager->find($id);
301 1
        $documentManager->remove($document);
302 1
        $documentManager->flush();
303
304 1
        return $this->handleView($this->view(null));
0 ignored issues
show
Documentation introduced by
$this->view(null) is of type this<Sulu\Bundle\Article...ller\ArticleController>, but the function expects a object<FOS\RestBundle\View\View>.

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
    }
306
307
    /**
308
     * Trigger a action for given article specified over get-action parameter.
309
     *
310
     * @Post("/articles/{uuid}")
311
     *
312
     * @param string $uuid
313
     * @param Request $request
314
     *
315
     * @return Response
316
     */
317 1
    public function postTriggerAction($uuid, Request $request)
318
    {
319
        // extract parameter
320 1
        $action = $this->getRequestParameter($request, 'action', true);
321 1
        $locale = $this->getRequestParameter($request, 'locale', true);
322
323
        // prepare vars
324 1
        $view = null;
325 1
        $data = null;
326 1
        $userId = $this->getUser()->getId();
327
328
        try {
329
            switch ($action) {
330 1
                case 'unpublish':
331
                    $document = $this->getDocumentManager()->find($uuid, $locale);
332
                    $this->getDocumentManager()->unpublish($document, $locale);
333
                    $this->getDocumentManager()->flush();
334
335
                    $data = $this->getDocumentManager()->find($uuid, $locale);
336
                    break;
337 1
                case 'remove-draft':
338
                    $data = $this->getDocumentManager()->find($uuid, $locale);
339
                    $this->getDocumentManager()->removeDraft($data, $locale);
340
                    $this->getDocumentManager()->flush();
341
                    break;
342 1
                case 'copy-locale':
343 1
                    $destLocales = $this->getRequestParameter($request, 'dest', true);
344 1
                    $data = $this->getMapper()->copyLanguage($uuid, $userId, null, $locale, explode(',', $destLocales));
345 1
                    break;
346
                case 'copy':
347
                    /** @var ArticleDocument $document */
348
                    $document = $this->getDocumentManager()->find($uuid, $locale);
349
                    $copiedPath = $this->getDocumentManager()->copy($document, dirname($document->getPath()));
350
                    $this->getDocumentManager()->flush();
351
352
                    $data = $this->getDocumentManager()->find($copiedPath, $locale);
353
                    break;
354
                default:
355
                    throw new RestException('Unrecognized action: ' . $action);
356
            }
357
358
            // prepare view
359 1
            $view = $this->view($data, $data !== null ? 200 : 204);
360 1
            $view->setSerializationContext(SerializationContext::create()->setGroups(['defaultPage']));
0 ignored issues
show
Bug introduced by
The method setSerializationContext() does not seem to exist on object<Sulu\Bundle\Artic...ller\ArticleController>.

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...
361
        } catch (RestException $exc) {
362
            $view = $this->view($exc->toArray(), 400);
363
        }
364
365 1
        return $this->handleView($view);
0 ignored issues
show
Documentation introduced by
$view is of type this<Sulu\Bundle\Article...ller\ArticleController>, but the function expects a object<FOS\RestBundle\View\View>.

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...
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371 30
    public function getSecurityContext()
372
    {
373 30
        return ArticleAdmin::SECURITY_CONTEXT;
374
    }
375
376
    /**
377
     * Persists the document using the given information.
378
     *
379
     * @param array $data
380
     * @param object $document
381
     * @param string $locale
382
     *
383
     * @throws InvalidFormException
384
     * @throws MissingParameterException
385
     */
386 29
    private function persistDocument($data, $document, $locale)
387
    {
388 29
        $form = $this->createForm(
389 29
            ArticleDocumentType::class,
390
            $document,
391
            [
392
                // disable csrf protection, since we can't produce a token, because the form is cached on the client
393 29
                'csrf_protection' => false,
394
            ]
395
        );
396 29
        $form->submit($data, false);
397
398 29
        if (!$form->isValid()) {
399
            throw new InvalidFormException($form);
400
        }
401
402 29
        $this->getDocumentManager()->persist(
403
            $document,
404
            $locale,
405
            [
406 29
                'user' => $this->getUser()->getId(),
407
                'clear_missing_content' => false,
408 29
                'route_path' => array_key_exists('routePath', $data) ? $data['routePath'] : null,
409
            ]
410
        );
411 29
    }
412
413
    /**
414
     * Returns document-manager.
415
     *
416
     * @return DocumentManagerInterface
417
     */
418 29
    protected function getDocumentManager()
419
    {
420 29
        return $this->get('sulu_document_manager.document_manager');
421
    }
422
423
    /**
424
     * @return ContentMapperInterface
425
     */
426 1
    protected function getMapper()
427
    {
428 1
        return $this->get('sulu.content.mapper');
429
    }
430
431
    /**
432
     * Delegates actions by given actionParameter, which can be retrieved from the request.
433
     *
434
     * @param string $actionParameter
435
     * @param object $document
436
     * @param string $locale
437
     */
438 29
    private function handleActionParameter($actionParameter, $document, $locale)
439
    {
440
        switch ($actionParameter) {
441 29
            case 'publish':
442 10
                $this->getDocumentManager()->publish($document, $locale);
443 10
                break;
444
        }
445 29
    }
446
447
    /**
448
     * Converts camel case string into normalized string with underscore.
449
     *
450
     * @param string $camel
451
     *
452
     * @return string
453
     */
454 1
    private function uncamelize($camel)
455
    {
456 1
        $camel = preg_replace(
457 1
            '/(?!^)[[:upper:]][[:lower:]]/',
458 1
            '$0',
459 1
            preg_replace('/(?!^)[[:upper:]]+/', '_$0', $camel)
460
        );
461
462 1
        return strtolower($camel);
463
    }
464
}
465