Completed
Push — master ( a33308...36ba79 )
by Maximilian
01:58
created

src/Controller/AutocompleteController.php (1 issue)

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