Completed
Push — master ( eb83f4...bb238e )
by Maximilian
14s
created

src/Controller/AutocompleteController.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/*
4
 * This file is part of the Sonata Project package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\DoctrinePHPCRAdminBundle\Controller;
13
14
use Doctrine\ODM\PHPCR\Translation\Translation;
15
use PHPCR\AccessDeniedException;
16
use PHPCR\Util\PathHelper;
17
use Sonata\AdminBundle\Admin\Admin;
18
use Sonata\AdminBundle\Admin\AdminInterface;
19
use Sonata\AdminBundle\Admin\Pool;
20
use Sonata\DoctrinePHPCRAdminBundle\Model\ModelManager;
21
use Symfony\Component\HttpFoundation\JsonResponse;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\Response;
24
25
class AutocompleteController
26
{
27
    /**
28
     * @var \Sonata\AdminBundle\Admin\Pool
29
     */
30
    protected $pool;
31
32
    /**
33
     * @param \Sonata\AdminBundle\Admin\Pool $pool
34
     */
35
    public function __construct(Pool $pool)
36
    {
37
        $this->pool = $pool;
38
    }
39
40
    /**
41
     * @param Request $request
42
     *
43
     * @return Response
44
     *
45
     * @throws AccessDeniedException
46
     */
47
    public function autoCompleteAction(Request $request)
48
    {
49
        /** @var Admin $admin */
50
        $admin = $this->pool->getInstance($request->get('code'));
51
        $admin->setRequest($request);
52
53
        // check user permission
54
        if (false === $admin->isGranted('LIST')) {
55
            throw new AccessDeniedException();
56
        }
57
58
        // subject will be empty to avoid unnecessary database requests and keep auto-complete function fast
59
        $admin->setSubject($admin->getNewInstance());
60
        $fieldDescription = $this->retrieveFieldDescription($admin, $request->get('field'));
61
        $formAutocomplete = $admin->getForm()->get($fieldDescription->getName());
62
63
        if ($formAutocomplete->getConfig()->getAttribute('disabled')) {
64
            throw new AccessDeniedException('Autocomplete list can`t be retrieved because the form element is disabled or read_only.');
65
        }
66
67
        $class = $formAutocomplete->getConfig()->getOption('class');
68
        $property = $formAutocomplete->getConfig()->getAttribute('property');
69
        $minimumInputLength = $formAutocomplete->getConfig()->getAttribute('minimum_input_length');
70
        $itemsPerPage = $formAutocomplete->getConfig()->getAttribute('items_per_page');
71
        $reqParamPageNumber = $formAutocomplete->getConfig()->getAttribute('req_param_name_page_number');
72
        $toStringCallback = $formAutocomplete->getConfig()->getAttribute('to_string_callback');
73
74
        $searchText = $request->get('q');
75
        if (mb_strlen($searchText, 'UTF-8') < $minimumInputLength) {
76
            return new JsonResponse(array('status' => 'KO', 'message' => 'Too short search string.'), 403);
77
        }
78
79
        $page = $request->get($reqParamPageNumber);
80
        $offset = ($page - 1) * $itemsPerPage;
81
82
        /** @var ModelManager $modelManager */
83
        $modelManager = $formAutocomplete->getConfig()->getOption('model_manager');
84
        $dm = $modelManager->getDocumentManager();
85
86
        if ($class) {
87
            /** @var $qb \Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder */
88
            $qb = $dm->getRepository($class)->createQueryBuilder('a');
89
            $qb->where()->fullTextSearch("a.$property", '*'.$searchText.'*');
90
            $qb->setFirstResult($offset);
91
            //fetch one more to determine if there are more pages
92
            $qb->setMaxResults($itemsPerPage + 1);
93
            $query = $qb->getQuery();
94
            $results = $query->execute();
95
        } else {
96
            /** @var $qb \PHPCR\Util\QOM\QueryBuilder */
97
            $qb = $dm->createPhpcrQueryBuilder();
98
            // TODO: node type should probably be configurable
99
            $qb->from($qb->getQOMFactory()->selector('a', 'nt:unstructured'));
100
            $qb->where($qb->getQOMFactory()->fullTextSearch('a', $property, '*'.$searchText.'*'));
101
            // handle attribute translation
102
            $qb->orWhere($qb->getQOMFactory()->fullTextSearch('a', $dm->getTranslationStrategy('attribute')->getTranslatedPropertyName($request->getLocale(), $property), '*'.$searchText.'*'));
0 ignored issues
show
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...
103
            $qb->setFirstResult($offset);
104
            //fetch one more to determine if there are more pages
105
            $qb->setMaxResults($itemsPerPage + 1);
106
107
            $results = $dm->getDocumentsByPhpcrQuery($qb->getQuery());
108
        }
109
110
        //did we max out x+1
111
        $more = (count($results) == $itemsPerPage + 1);
112
        $method = $request->get('_method_name');
113
114
        $items = array();
115
        foreach ($results as $path => $document) {
116
            // handle child translation
117
            if (strpos(PathHelper::getNodeName($path), Translation::LOCALE_NAMESPACE.':') === 0) {
118
                $document = $dm->find(null, PathHelper::getParentPath($path));
119
            }
120
121
            if (!method_exists($document, $method)) {
122
                continue;
123
            }
124
125
            $label = $document->{$method}();
126
            if ($toStringCallback !== null) {
127
                if (!is_callable($toStringCallback)) {
128
                    throw new \RuntimeException('Option "to_string_callback" does not contain callable function.');
129
                }
130
131
                $label = call_user_func($toStringCallback, $document, $property);
132
            }
133
134
            $items[] = array(
135
                'id' => $admin->id($document),
136
                'label' => $label,
137
            );
138
        }
139
140
        return new JsonResponse(array(
141
            'status' => 'OK',
142
            'more' => $more,
143
            'items' => $items,
144
        ));
145
    }
146
147
    /**
148
     * Retrieve the field description given by field name.
149
     *
150
     * @param AdminInterface $admin
151
     * @param string         $field
152
     *
153
     * @return \Symfony\Component\Form\FormInterface
154
     *
155
     * @throws \RuntimeException
156
     */
157
    private function retrieveFieldDescription(AdminInterface $admin, $field)
158
    {
159
        $admin->getFormFieldDescriptions();
160
161
        $fieldDescription = $admin->getFormFieldDescription($field);
162
163
        if (!$fieldDescription) {
164
            throw new \RuntimeException(sprintf('The field "%s" does not exist.', $field));
165
        }
166
167
        if ($fieldDescription->getType() !== 'sonata_type_model_autocomplete') {
168
            throw new \RuntimeException(sprintf('Unsupported form type "%s" for field "%s".', $fieldDescription->getType(), $field));
169
        }
170
171
        return $fieldDescription;
172
    }
173
}
174