AutocompleteController::autoCompleteAction()   C
last analyzed

Complexity

Conditions 10
Paths 21

Size

Total Lines 99

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 99
rs 6.1551
c 0
b 0
f 0
cc 10
nc 21
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
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\DoctrinePHPCRAdminBundle\Controller;
15
16
use Doctrine\ODM\PHPCR\Translation\Translation;
17
use PHPCR\AccessDeniedException;
18
use PHPCR\Util\PathHelper;
19
use Sonata\AdminBundle\Admin\Admin;
20
use Sonata\AdminBundle\Admin\AdminInterface;
21
use Sonata\AdminBundle\Admin\Pool;
22
use Sonata\DoctrinePHPCRAdminBundle\Model\ModelManager;
23
use Symfony\Component\HttpFoundation\JsonResponse;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpFoundation\Response;
26
27
class AutocompleteController
28
{
29
    /**
30
     * @var \Sonata\AdminBundle\Admin\Pool
31
     */
32
    protected $pool;
33
34
    public function __construct(Pool $pool)
35
    {
36
        $this->pool = $pool;
37
    }
38
39
    /**
40
     * @throws AccessDeniedException
41
     *
42
     * @return Response
43
     */
44
    public function autoCompleteAction(Request $request)
45
    {
46
        /** @var Admin $admin */
47
        $admin = $this->pool->getInstance($request->get('code'));
48
        $admin->setRequest($request);
49
50
        // check user permission
51
        if (false === $admin->isGranted('LIST')) {
52
            throw new AccessDeniedException();
53
        }
54
55
        // subject will be empty to avoid unnecessary database requests and keep auto-complete function fast
56
        $admin->setSubject($admin->getNewInstance());
57
        $fieldDescription = $this->retrieveFieldDescription($admin, $request->get('field'));
58
        $formAutocomplete = $admin->getForm()->get($fieldDescription->getName());
59
60
        if ($formAutocomplete->getConfig()->getAttribute('disabled')) {
61
            throw new AccessDeniedException('Autocomplete list can`t be retrieved because the form element is disabled or read_only.');
62
        }
63
64
        $class = $formAutocomplete->getConfig()->getOption('class');
65
        $property = $formAutocomplete->getConfig()->getAttribute('property');
66
        $minimumInputLength = $formAutocomplete->getConfig()->getAttribute('minimum_input_length');
67
        $itemsPerPage = $formAutocomplete->getConfig()->getAttribute('items_per_page');
68
        $reqParamPageNumber = $formAutocomplete->getConfig()->getAttribute('req_param_name_page_number');
69
        $toStringCallback = $formAutocomplete->getConfig()->getAttribute('to_string_callback');
70
71
        $searchText = $request->get('q');
72
        if (mb_strlen($searchText, 'UTF-8') < $minimumInputLength) {
73
            return new JsonResponse(['status' => 'KO', 'message' => 'Too short search string.'], 403);
74
        }
75
76
        $page = $request->get($reqParamPageNumber);
77
        $offset = ($page - 1) * $itemsPerPage;
78
79
        /** @var ModelManager $modelManager */
80
        $modelManager = $formAutocomplete->getConfig()->getOption('model_manager');
81
        $dm = $modelManager->getDocumentManager();
82
83
        if ($class) {
84
            /** @var $qb \Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder */
85
            $qb = $dm->getRepository($class)->createQueryBuilder('a');
86
            $qb->where()->fullTextSearch("a.$property", '*'.$searchText.'*');
87
            $qb->setFirstResult($offset);
88
            //fetch one more to determine if there are more pages
89
            $qb->setMaxResults($itemsPerPage + 1);
90
            $query = $qb->getQuery();
91
            $results = $query->execute();
92
        } else {
93
            /** @var $qb \PHPCR\Util\QOM\QueryBuilder */
94
            $qb = $dm->createPhpcrQueryBuilder();
95
            // TODO: node type should probably be configurable
96
            $qb->from($qb->getQOMFactory()->selector('a', 'nt:unstructured'));
97
            $qb->where($qb->getQOMFactory()->fullTextSearch('a', $property, '*'.$searchText.'*'));
98
            // handle attribute translation
99
            $qb->orWhere($qb->getQOMFactory()->fullTextSearch('a', $dm->getTranslationStrategy('attribute')->getTranslatedPropertyName($request->getLocale(), $property), '*'.$searchText.'*'));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\ODM\PHPCR\Trans...lationStrategyInterface as the method getTranslatedPropertyName() does only exist in the following implementations of said interface: Doctrine\ODM\PHPCR\Trans...ractTranslationStrategy, Doctrine\ODM\PHPCR\Trans...buteTranslationStrategy, Doctrine\ODM\PHPCR\Trans...hildTranslationStrategy.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
100
            $qb->setFirstResult($offset);
101
            //fetch one more to determine if there are more pages
102
            $qb->setMaxResults($itemsPerPage + 1);
103
104
            $results = $dm->getDocumentsByPhpcrQuery($qb->getQuery());
105
        }
106
107
        //did we max out x+1
108
        $more = (\count($results) === $itemsPerPage + 1);
109
        $method = $request->get('_method_name');
110
111
        $items = [];
112
        foreach ($results as $path => $document) {
113
            // handle child translation
114
            if (0 === strpos(PathHelper::getNodeName($path), Translation::LOCALE_NAMESPACE.':')) {
115
                $document = $dm->find(null, PathHelper::getParentPath($path));
116
            }
117
118
            if (!method_exists($document, $method)) {
119
                continue;
120
            }
121
122
            $label = $document->{$method}();
123
            if (null !== $toStringCallback) {
124
                if (!\is_callable($toStringCallback)) {
125
                    throw new \RuntimeException('Option "to_string_callback" does not contain callable function.');
126
                }
127
128
                $label = \call_user_func($toStringCallback, $document, $property);
129
            }
130
131
            $items[] = [
132
                'id' => $admin->id($document),
133
                'label' => $label,
134
            ];
135
        }
136
137
        return new JsonResponse([
138
            'status' => 'OK',
139
            'more' => $more,
140
            'items' => $items,
141
        ]);
142
    }
143
144
    /**
145
     * Retrieve the field description given by field name.
146
     *
147
     * @param string $field
148
     *
149
     * @throws \RuntimeException
150
     *
151
     * @return \Symfony\Component\Form\FormInterface
152
     */
153
    private function retrieveFieldDescription(AdminInterface $admin, $field)
154
    {
155
        $admin->getFormFieldDescriptions();
156
157
        $fieldDescription = $admin->getFormFieldDescription($field);
158
159
        if (!$fieldDescription) {
160
            throw new \RuntimeException(sprintf('The field "%s" does not exist.', $field));
161
        }
162
163
        if ('sonata_type_model_autocomplete' !== $fieldDescription->getType()) {
164
            throw new \RuntimeException(sprintf('Unsupported form type "%s" for field "%s".', $fieldDescription->getType(), $field));
165
        }
166
167
        return $fieldDescription;
168
    }
169
}
170