Completed
Push — master ( 61818c...b240d3 )
by Sam
03:00
created

TopEditsController::singlePageTopEdits()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 57
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 57
rs 9.0309
cc 4
eloc 36
nc 4
nop 3

How to fix   Long Method   

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
namespace AppBundle\Controller;
4
5
use AppBundle\Helper\ApiHelper;
6
use AppBundle\Helper\LabsHelper;
7
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
8
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9
use Symfony\Component\HttpFoundation\Request;
10
use Xtools\Page;
11
use Xtools\PagesRepository;
12
use Xtools\Project;
13
use Xtools\ProjectRepository;
14
use Xtools\User;
15
16
class TopEditsController extends Controller
17
{
18
19
    /** @var LabsHelper */
20
    private $lh;
21
22
    /**
23
     * Get the Project.
24
     * @todo This can probably be made more common.
25
     * @param string $projectIdent The domain name, database name, or URL of a project.
26
     * @return Project
27
     */
28
    protected function getProject($projectIdent)
29
    {
30
        $project = new Project($projectIdent);
31
        $projectRepo = new ProjectRepository();
32
        $projectRepo->setMetaConnection($this->get('doctrine')->getManager("meta")->getConnection());
33
        $projectRepo->setCache($this->get('cache.app'));
34
        if ($this->container->getParameter('app.single_wiki')) {
35
            $projectRepo->setSingleMetadata([
36
                'url' => $this->container->getParameter('wiki_url'),
37
                'dbname' => $this->container->getParameter('database_replica_name'),
38
            ]);
39
        }
40
        $project->setRepository($projectRepo);
41
        return $project;
42
    }
43
44
    /**
45
     * @Route("/topedits", name="topedits")
46
     * @Route("/topedits", name="topEdits")
47
     * @Route("/topedits/", name="topEditsSlash")
48
     * @Route("/topedits/index.php", name="topEditsIndex")
49
     */
50
    public function indexAction(Request $request)
51
    {
52
        $this->lh = $this->get("app.labs_helper");
53
        $this->lh->checkEnabled("topedits");
54
55
        $project = $request->query->get('project');
56
        $username = $request->query->get('username');
57
        $namespace = $request->query->get('namespace');
58
        $article = $request->query->get('article');
59
60
        if ($project != "" && $username != "" && $namespace != "" && $article != "") {
61
            return $this->redirectToRoute("TopEditsResults", [
62
                'project'=>$project,
63
                'username' => $username,
64
                'namespace'=>$namespace,
65
                'article'=>$article,
66
            ]);
67
        } elseif ($project != "" && $username != "" && $namespace != "") {
68
            return $this->redirectToRoute("TopEditsResults", [
69
                'project'=>$project,
70
                'username' => $username,
71
                'namespace'=>$namespace,
72
            ]);
73
        } elseif ($project != "" && $username != "") {
74
            return $this->redirectToRoute("TopEditsResults", [
75
                'project' => $project,
76
                'username' => $username,
77
            ]);
78
        } elseif ($project != "") {
79
            return $this->redirectToRoute("TopEditsResults", [ 'project'=>$project ]);
80
        }
81
82
        // set default wiki so we can populate the namespace selector
83
        if (!$project) {
84
            $project = $this->container->getParameter('default_project');
85
        }
86
87
        $project = $this->getProject($project);
88
89
        return $this->render('topedits/index.html.twig', [
90
            'xtPageTitle' => 'tool-topedits',
91
            'xtSubtitle' => 'tool-topedits-desc',
92
            'xtPage' => 'topedits',
93
            'project' => $project,
94
        ]);
95
    }
96
97
    /**
98
     * @Route("/topedits/{project}/{username}/{namespace}/{article}", name="TopEditsResults")
99
     */
100
    public function resultAction($project, $username, $namespace = 0, $article = "")
101
    {
102
        /** @var LabsHelper $lh */
103
        $this->lh = $this->get('app.labs_helper');
104
        $this->lh->checkEnabled('topedits');
105
106
        $project = $this->getProject($project);
107
        $user = new User($username);
108
109
        if ($article === "") {
110
            return $this->namespaceTopEdits($user, $project, $namespace);
111
        } else {
112
            return $this->singlePageTopEdits($user, $project, $article);
113
        }
114
    }
115
116
    /**
117
     * List top edits by this user for all pages in a particular namespace.
118
     * @param User $user The User.
119
     * @param Project $project The project.
120
     * @param integer|string $namespace The namespace ID or 'all'
121
     * @return \Symfony\Component\HttpFoundation\Response
122
     */
123
    protected function namespaceTopEdits(User $user, Project $project, $namespace)
124
    {
125
        // Get list of namespaces.
126
        $namespaces = $project->getNamespaces();
127
128
        // Get the basic data about the pages edited by this user.
129
        $params = ['username'=>$user->getUsername()];
130
        $nsClause = '';
131
        $namespaceMsg = 'namespaces_all';
132
        if (is_numeric($namespace)) {
133
            $nsClause = 'AND page_namespace = :namespace';
134
            $params['namespace'] = $namespace;
135
            $namespaceMsg = str_replace(' ', '_', strtolower($namespaces[$namespace]));
136
        }
137
        $revTable = $this->lh->getTable('revision', $project->getDatabaseName());
138
        $pageTable = $this->lh->getTable('page', $project->getDatabaseName());
139
        $query = "SELECT page_namespace, page_title, page_is_redirect, COUNT(page_title) AS count
140
                FROM $pageTable JOIN $revTable ON page_id = rev_page
141
                WHERE rev_user_text = :username $nsClause
142
                GROUP BY page_namespace, page_title
143
                ORDER BY count DESC
144
                LIMIT 100";
145
        $conn = $this->getDoctrine()->getManager('replicas')->getConnection();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Persistence\ObjectManager as the method getConnection() does only exist in the following implementations of said interface: Doctrine\ORM\Decorator\EntityManagerDecorator, Doctrine\ORM\EntityManager.

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...
146
        $editData = $conn->executeQuery($query, $params)->fetchAll();
147
148
        // Inform user if no revisions found.
149
        if (count($editData) === 0) {
150
            $this->addFlash("notice", ["nocontribs"]);
0 ignored issues
show
Documentation introduced by
array('nocontribs') is of type array<integer,string,{"0":"string"}>, but the function expects a string.

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...
151
        }
152
153
        // Get page info about these 100 pages, so we can use their display title.
154
        $titles = array_map(function ($e) {
155
            return $e['page_title'];
156
        }, $editData);
157
        /** @var ApiHelper $apiHelper */
158
        $apiHelper = $this->get('app.api_helper');
159
        $displayTitles = $apiHelper->displayTitles($project->getDomain(), $titles);
0 ignored issues
show
Security Bug introduced by
It seems like $project->getDomain() targeting Xtools\Project::getDomain() can also be of type false; however, AppBundle\Helper\ApiHelper::displayTitles() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
160
161
        // Put all together, and return the view.
162
        $edits = [];
163
        foreach ($editData as $editDatum) {
164
            $pageTitle = $editDatum['page_title'];
165
            // If 'all' namespaces, prepend namespace to display title.
166
            $nsTitle = !is_numeric($namespace) ? $namespaces[$editDatum['page_namespace']].':' : '';
167
            $editDatum['displaytitle'] = $nsTitle.$displayTitles[$pageTitle];
168
            $edits[] = $editDatum;
169
        }
170
        return $this->render('topedits/result_namespace.html.twig', [
171
            'xtPage' => 'topedits',
172
            'project' => $project,
173
            'user' => $user,
174
            'namespace' => $namespace,
175
            'edits' => $edits,
176
            'content_title' => $namespaceMsg,
177
        ]);
178
    }
179
180
    /**
181
     * List top edits by this user for a particular page.
182
     * @param User $user
183
     * @param Project $project
184
     * @param string $pageName
185
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
186
     */
187
    protected function singlePageTopEdits(User $user, Project $project, $pageName)
188
    {
189
        $page = new Page($project, $pageName);
190
        $pageRepo = new PagesRepository();
191
        $page->setRepository($pageRepo);
192
        
193
        if (!$page->exists()) {
194
            // Redirect if the page doesn't exist.
195
            $this->addFlash("notice", ["noresult", $pageName]);
0 ignored issues
show
Documentation introduced by
array('noresult', $pageName) is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

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...
196
            return $this->redirectToRoute("topedits");
197
        }
198
199
        // Get all revisions of this page by this user.
200
        $revTable = $this->lh->getTable('revision', $project->getDatabaseName());
201
        $query = "SELECT
202
                    revs.rev_id AS id,
203
                    revs.rev_timestamp AS timestamp,
204
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
205
                    revs.rev_comment AS comment
206
                FROM $revTable AS revs
207
                    LEFT JOIN $revTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
208
                WHERE revs.rev_user_text in (:username) AND revs.rev_page = :pageid
209
                ORDER BY revs.rev_timestamp DESC
210
            ";
211
        $params = ['username' => $user->getUsername(), 'pageid' => $page->getId()];
212
        $conn = $this->getDoctrine()->getManager('replicas')->getConnection();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Persistence\ObjectManager as the method getConnection() does only exist in the following implementations of said interface: Doctrine\ORM\Decorator\EntityManagerDecorator, Doctrine\ORM\EntityManager.

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...
213
        $revisionsData = $conn->executeQuery($query, $params)->fetchAll();
214
215
        // Loop through all revisions and format dates, find totals, etc.
216
        $totalAdded = 0;
217
        $totalRemoved = 0;
218
        $revisions = [];
219
        foreach ($revisionsData as $revision) {
220
            if ($revision['length_change'] > 0) {
221
                $totalAdded += $revision['length_change'];
222
            } else {
223
                $totalRemoved += $revision['length_change'];
224
            }
225
            $time = strtotime($revision['timestamp']);
226
            $revision['timestamp'] = $time; // formatted via Twig helper
227
            $revision['year'] = date('Y', $time);
228
            $revision['month'] = date('m', $time);
229
            $revisions[] = $revision;
230
        }
231
232
        // Send all to the template.
233
        return $this->render('topedits/result_article.html.twig', [
234
            'xtPage' => 'topedits',
235
            'project' => $project,
236
            'user' => $user,
237
            'page' => $page,
238
            'total_added' => $totalAdded,
239
            'total_removed' => $totalRemoved,
240
            'revisions' => $revisions,
241
            'revision_count' => count($revisions),
242
        ]);
243
    }
244
}
245