User API Tests

<?php

class UserCest
{
    public function _before(ApiTester $I)
    {
    }

    public function registerUser(ApiTester $I)
    {
        $userClass = \App\Entity\User::class;

        $registerUser = [
            'username' => 'codeception_username',
            'email' => 'codeception_email@jonnydevine.com',
            'password' => 'codeception',
            'name' => 'Codeception Name'
        ];

        $I->dontSeeInRepository($userClass, $registerUser);

        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->sendPOST('/api/v1/user', $registerUser);

        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $expectedResponse = [
            'username' => $registerUser['username']
        ];
        $I->seeResponseContainsJson($expectedResponse);
    }

    public function registerExistingUser(ApiTester $I)
    {
        $userClass = \App\Entity\User::class;

        $registerUser = [
            'username' => 'codeception_username',
            'email' => 'codeception_email@jonnydevine.com',
            'name' => 'Codeception Name'
        ];

        $I->seeInRepository($userClass, $registerUser);

        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->sendPOST('/api/v1/user', $registerUser);

        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseIsJson();

        $I->seeResponseContains("USERNAME_IS_ALREADY_IN_USE");
        $I->seeResponseContains("EMAIL_IS_ALREADY_IN_USE");
    }

    public function registerButAlreadyLoggedIn(ApiTester $I)
    {
        $userClass = \App\Entity\User::class;
        $clientClass = \App\Entity\Client::class;

        $username = 'user2';
        $password = 'password';

        $I->seeInRepository($userClass, [
            'username' => $username,
            'enabled' => true
        ]);

        $I->seeInRepository($clientClass, [
            'randomId' => '5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s'
        ]);

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('/oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        $registerUser = [
            'username' => 'codeception_username',
            'email' => 'codeception_email@jonnydevine.com',
            'name' => 'Codeception Name'
        ];

        $I->sendPOST('/api/v1/user', $registerUser);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseIsJson();

        $I->seeResponseContains("Already Logged In");
    }

    public function registerInvalidEmail(ApiTester $I)
    {
        $userClass = \App\Entity\User::class;

        $registerUser = [
            'username' => 'codeception_username_2',
            'email' => 'codeception_email',
            'password' => 'codeception_2',
            'name' => 'Codeception Name 2'
        ];

        $I->dontSeeInRepository($userClass, $registerUser);

        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->sendPOST('/api/v1/user', $registerUser);

        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseIsJson();

        $I->seeResponseContains("Not an email address");
    }

    public function meLoggedInDisabled(ApiTester $I)
    {
        $username = 'user4';
        $password = 'password';

        $I->seeInRepository(\App\Entity\User::class, [
            'username' => $username,
            'enabled' => false
        ]);

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $resposne = json_decode($I->grabResponse(), true);
        $accessToken = $resposne['access_token'];
        $I->amBearerAuthenticated($accessToken);
        $I->sendGet('/api/v1/user/me');
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::UNAUTHORIZED);
        $I->seeResponseIsJson();
        $expectedResponse = [
            'error' => 'access_denied',
            'error_description' => 'User account is disabled.'
        ];
        $I->seeResponseContainsJson($expectedResponse);
    }

    public function meLoggedInEnabled(ApiTester $I)
    {
        $username = 'user19';
        $password = 'password';

        $I->seeInRepository(\App\Entity\User::class, [
            'username' => $username,
            'enabled' => true
        ]);

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];

        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->haveHttpHeader('Authorization', 'Bearer ' . $accessToken);
        $I->sendGet('api/v1/user/me?disableLastLogin=' . true);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
    }

    public function meLoggedOut(ApiTester $I)
    {
        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => 'xxxxxxxx',
            'password' => 'xxxxxxxx'
        ];

        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseIsJson();
        $expectedResponse = [
            'error' => 'invalid_grant',
            'error_description' => 'Invalid username and password combination'
        ];
        $I->seeResponseContainsJson($expectedResponse);

        $I->sendGET('/api/v1/user/me');
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::UNAUTHORIZED);
        $I->seeResponseIsJson();
        $expectedResponse = [
            'error' => 'access_denied',
            'error_description' => 'OAuth2 authentication required'
        ];
        $I->seeResponseContainsJson($expectedResponse);
    }

    public function confirmInvalidUsernameQueryParam(ApiTester $I)
    {
        $username = '';
        $confirmationToken = '123';
        $I->sendGet('api/v1/user/confirm?username='.$username.'&confirmation_token='.$confirmationToken);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::NOT_FOUND);
        $I->seeResponseIsJson();
        $I->seeResponseContains('Username not found');
    }

    public function confirmInvalidConfirmationTokenQueryParam(ApiTester $I)
    {
        $username = 'user1';
        $confirmationToken = '';
        $I->sendGet('api/v1/user/confirm?username='.$username.'&confirmation_token='.$confirmationToken);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::NOT_FOUND);
        $I->seeResponseIsJson();
        $I->seeResponseContains('Confirmation Token not found');
    }

    public function confirmUserNotInDatabase(ApiTester $I)
    {
        $username = 'xxxxx';
        $confirmationToken = 'xxxx';

        $I->dontSeeInRepository(\App\Entity\User::class, [
            'username' => $username
        ]);

        $I->sendGet('api/v1/user/confirm?username='.$username.'&confirmation_token='.$confirmationToken);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::NOT_FOUND);
        $I->seeResponseIsJson();
        $I->seeResponseContains('User not found');
    }

    public function confirmIsUserAlreadyActivated(ApiTester $I)
    {
        $username = 'user1';
        $confirmationToken = 'xxxx';

        $I->seeInRepository(\App\Entity\User::class, [
            'username'  => $username
        ]);

        $I->sendGet('api/v1/user/confirm?username='.$username.'&confirmation_token='.$confirmationToken);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('Already confirmed');
    }

    public function confirmInvalidConfirmationToken(ApiTester $I)
    {
        $userClass = \App\Entity\User::class;

        $username = 'user4';
        $confirmationToken = 'xxxx';

        $I->seeInRepository($userClass, [
            'username'  => $username
        ]);

        $I->sendGet('api/v1/user/confirm?username='.$username.'&confirmation_token='.$confirmationToken);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseIsJson();
        $I->seeResponseContains('Confirmation Token cant be found');
    }

    public function confirmAlreadyLoggedIn(ApiTester $I)
    {
        $userClass = \App\Entity\User::class;

        $username = 'user1';
        $password = 'password';
        $confirmationToken = '4c3fb568d51feb12a0038033890efb5367585af3a';

        $I->seeInRepository($userClass, [
            'username'  => $username
        ]);

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];

        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        $I->sendGet('api/v1/user/confirm?username='.$username.'&confirmation_token='.$confirmationToken);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseContains('Already confirmed');
    }

    public function confirmSuccessfully(ApiTester $I)
    {
        $userClass = App\Entity\User::class;

        $username = 'user5';
        $confirmationToken = '5c3fb568d51feb12a0038033890efb5367585af3a';

        $data = [
            'username' => $username,
            'confirmationToken' => $confirmationToken,
            'activatedAt' => null,
            'enabled' => false
        ];
        $I->seeInRepository($userClass, $data);

        $I->sendGet('api/v1/user/confirm?username='
            .$username.'&confirmation_token='.$confirmationToken.'&disableActivation=true');
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseContains('Successfully confirmed');
    }

    public function resendNoEmail(ApiTester $I)
    {
        $email = null;

        $I->sendGet('api/v1/user/resend');
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::NOT_FOUND);
        $I->seeResponseContains('Email not entered');
    }

    public function resendNonExistingUser(ApiTester $I)
    {
        $email = 'xxxxxxxx';

        $I->sendGet('api/v1/user/resend?email='.$email);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::NOT_FOUND);
        $I->seeResponseContains('User not found');
    }

    public function resendValidEmailNotActivated(ApiTester $I)
    {
        $email = 'user6@email.com';

        $I->sendGet('api/v1/user/resend?email='.$email);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseContains('We have resent a new confirmation email');
    }

    public function resendValidEmailActivated(ApiTester $I)
    {
        $email = 'user1@email.com';

        $I->sendGet('api/v1/user/resend?email='.$email);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseContains('Already confirmed');
    }

    public function resetPasswordInvalidResetKeyParam(ApiTester $I)
    {
        $I->sendPOST('api/v1/user/reset-password', []);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('Reset key not found');
    }

    public function resetPasswordInvalidUsernameParam(ApiTester $I)
    {
        $data = [
            "reset_key" => "xxxxxxxxxxx"
        ];

        $I->sendPOST('api/v1/user/reset-password', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('Username not found');
    }

    public function resetPasswordInvalidPasswordParam(ApiTester $I)
    {
        $data = [
            "reset_key" => "xxxxxxxxxxx",
            "username" => "xxxxxxxxxx"
        ];

        $I->sendPOST('api/v1/user/reset-password', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('Password not found');
    }

    public function resetPasswordUserDoesNotExist(ApiTester $I)
    {
        $data = [
            "reset_key" => "xxxxxxxxxxx",
            "username" => "xxxxxxxxxx",
            "password" => "xxxxxxxxxx"
        ];

        $I->sendPOST('api/v1/user/reset-password', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::FORBIDDEN);
        $I->seeResponseContains('User does not exist');
    }

    public function resetPasswordInvalidResetKey(ApiTester $I)
    {
        $data = [
            "reset_key" => "xxxxxxxxxxx",
            "username" => "user4",
            "password" => "xxxxxxxxxx"
        ];

        $I->sendPOST('api/v1/user/reset-password', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::FORBIDDEN);
        $I->seeResponseContains('Reset key does not exist');
    }

    public function resetPasswordIsRequestExpired(ApiTester $I)
    {
        $data = [
            "reset_key" => "4c3fb568d51feb12a0038033890efb5367585af3a",
            "username" => "user4",
            "password" => "xxxxxxxxxx"
        ];

        $I->sendPOST('api/v1/user/reset-password', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::FORBIDDEN);
        $I->seeResponseContains('Reset key expired');
    }

    public function resetPasswordSuccessfully(ApiTester $I)
    {
        $data = [
            "reset_key" => "5c3fb568d51feb12a0038033890efb5367585af3a",
            "username" => "user5",
            "password" => "xxxxxxxxxx"
        ];

        $I->sendPOST('api/v1/user/reset-password', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseContains('New password set');
    }

    public function forgotUsernameNoEmailParam(ApiTester $I)
    {
        $I->sendPost('api/v1/user/forgot-username', []);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('Email not found');
    }

    public function forgotUsernameUserNotFound(ApiTester $I)
    {
        $data = [
            'email' => 'xxxxxx'
        ];

        $I->sendPost('api/v1/user/forgot-username', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('User not found');
    }

    public function forgotUsernameSuccessful(ApiTester $I)
    {
        $data = [
            'email' => 'user1@email.com'
        ];

        $I->sendPost('api/v1/user/forgot-username', json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseContains('Email has been sent');
    }

    public function getUserInvalidUsername(ApiTester $I)
    {
        $username = 'xxxxxxxxxx';

        $I->sendGET('api/v1/user/' . $username);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::NOT_FOUND);
        $I->seeResponseContains('User cannot be found');
    }

    public function getUserSuccessful(ApiTester $I)
    {
        $username = 'user1';

        $I->sendGET('api/v1/user/' . $username);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);

        $expectedResponse = [
            "name" => "John Smith",
            "username" => "user1",
            "avatar" => "http://localhost:8000/uploads/avatar/0d91cca62a1a31a612b2a6366c7ef56b3e468ce8.jpg",
            "roles" => [
                "ROLE_ADMIN",
                "ROLE_USER"
            ],
        ];

        $I->seeResponseContainsJson($expectedResponse);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $user = $userService->getUserByUsername($username);
        $I->assertNotNull($user->getCreatedAt());
    }

    public function editUserNotLoggedIn(ApiTester $I)
    {
        $userId = 1;
        $data = [];

        $I->sendPATCH('api/v1/user/' . $userId, $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::UNAUTHORIZED);
        $I->seeResponseIsJson();
        $expectedResponse = [
            'error' => 'access_denied',
            'error_description' => 'OAuth2 authentication required'
        ];
        $I->seeResponseContainsJson($expectedResponse);
    }

    public function editUserLoggedInButInvalidUser(ApiTester $I)
    {
        $userId = 999999;
        $username = 'user2';
        $password = 'password';

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        $data = [];
        $I->sendPATCH('api/v1/user/' . $userId, json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::FORBIDDEN);
        $I->seeResponseContains('User does not exist');
    }

    public function editUserButDifferentUser(ApiTester $I)
    {
        $username = 'user2';
        $password = 'password';

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $userId = $userService->getUserByUsername("user1")->getId();

        $data = [];
        $I->sendPATCH('api/v1/user/' . $userId, json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::FORBIDDEN);
        $I->seeResponseContains('You are not allowed to edit a different user');
    }

    public function editUserButDifferentUsername(ApiTester $I)
    {
        $username = 'user2';
        $password = 'password';

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $existingUser = $userService->getUserByUsername($username);

        $data = [
            'name' => $existingUser->getName(),
            'username' => 'user1'
        ];
        $I->sendPATCH('api/v1/user/' . $existingUser->getId(), json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('USERNAME_IS_ALREADY_IN_USE');
    }

    public function editUserSuccessfully(ApiTester $I)
    {
        $username = 'user2';
        $password = 'password';

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $existingUser = $userService->getUserByUsername($username);

        $data = [
            'name' => $existingUser->getName(),
            'username' => 'user99999'
        ];
        $I->sendPATCH('api/v1/user/' . $existingUser->getId(), json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $expectedResponse = [
            "name" => "Sarah McCarthy",
            "username" => "user99999"
        ];
        $I->seeResponseContainsJson($expectedResponse);

        $data = [
            'name' => $existingUser->getName(),
            'username' => $username
        ];
        $I->sendPatch('api/v1/user/' . $existingUser->getId(), json_encode($data));
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $expectedResponse = [
            "name" => "Sarah McCarthy",
            "username" => "user2"
        ];
        $I->seeResponseContainsJson($expectedResponse);
    }

    public function changePasswordNotLoggedIn(ApiTester $I)
    {
        $username = "user1";
        $data = [];

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $user = $userService->getUserByUsername($username);

        $I->sendPOST('api/v1/user/' . $user->getId() . '/change-password', $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::UNAUTHORIZED);

        $expectedResponse = [
            'error' => 'access_denied',
            'error_description' => 'OAuth2 authentication required'
        ];
        $I->seeResponseContainsJson($expectedResponse);
    }

    public function changePasswordNotLoggedInUser(ApiTester $I)
    {
        $username = 'user3';
        $password = 'password';

        $otherUsername = 'user4';

        $data = [];

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $otherUser = $userService->getUserByUsername($otherUsername);

        $I->assertEquals($otherUsername, $otherUser->getUsername());

        $I->sendPOST('api/v1/user/' . $otherUser->getId() . '/change-password', $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::FORBIDDEN);
        $I->seeResponseContains('You cannot change password of someone else');
    }

    public function changePasswordNewPasswordMustBeBetween6And40Chars(ApiTester $I)
    {
        $username = 'user1';
        $currentPassword = 'xxxxx';
        $newPassword = 'xxxxx';
        $confirmPassword = 'xxxxx';

        $data = [
            'currentPassword' => $currentPassword,
            'newPassword' => $newPassword,
            'confirmPassword' => $confirmPassword
        ];

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => 'password'
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $user = $userService->getUserByUsername($username);

        $I->sendPOST('api/v1/user/' . $user->getId() . '/change-password', $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('New Password must be between 6 and 40 characters');
    }

    public function changePasswordWrongCurrentPassword(ApiTester $I)
    {
        $username = 'user1';
        $currentPassword = 'xxxxxxxx';
        $newPassword = 'xxxxxxxx';
        $confirmPassword = 'xxxxxxxx';

        $data = [
            'currentPassword' => $currentPassword,
            'newPassword' => $newPassword,
            'confirmPassword' => $confirmPassword
        ];

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => 'password'
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $user = $userService->getUserByUsername($username);

        $I->sendPOST('api/v1/user/' . $user->getId() . '/change-password', $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('Password does not match the one in your account');
    }

    public function changePasswordSuccessfully(ApiTester $I)
    {
        $username = 'user19';
        $currentPassword = 'password';
        $newPassword = 'newpassword';
        $confirmPassword = 'newpassword';

        $data = [
            'currentPassword' => $currentPassword,
            'newPassword' => $newPassword,
            'confirmPassword' => $confirmPassword
        ];

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => 'password'
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        /** @var \App\Service\UserService $userService */
        $userService = $I->grabService('App\Service\UserService');
        $user = $userService->getUserByUsername($username);

        $I->sendPOST('api/v1/user/' . $user->getId() . '/change-password', $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);

        $expectedResponse = [
            "name" => "Kathleen Sims",
            "username" => "user19"
        ];
        $I->seeResponseContainsJson($expectedResponse);

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $newPassword
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');
    }

    public function forgotPasswordNoUser(ApiTester $I)
    {
        $data = [
            'username' => 'xxxxxxxxxxxx'
        ];

        $I->sendPOST('api/v1/user/forgot-password', $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('Username cannot be found.');
    }

    public function forgotPasswordSuccessful(ApiTester $I)
    {
        $data = [
            'username' => 'user3'
        ];

        $I->sendPOST('api/v1/user/forgot-password', $data);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseContains('An email has been sent');
    }

    public function listUsersOrderByLastLogin(ApiTester $I)
    {
        $sort = \App\Enum\UserOrderBy::LAST_LOGIN . '-' . \App\Enum\Order::DESC;
        $I->sendGET('api/v1/user?sort=' . $sort);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $items = json_decode($I->grabResponse(), true)['items'];

        for ($i = 0; $i <= 2; $i++) {
            $item = $items[$i];
            if ($i == 0) {
                $I->assertEquals('user2', $item['username']);
            } else if ($i == 1) {
                $I->assertEquals('user3', $item['username']);
            } else if ($i == 2) {
                $I->assertEquals('user1', $item['username']);
            }
        }
    }

    public function listUsersAsAdminOrderByLastLogin(ApiTester $I)
    {
        $username = 'user1';
        $password = 'password';

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        $sort = \App\Enum\UserOrderBy::LAST_LOGIN . '-' . \App\Enum\Order::DESC;
        $I->sendGET('api/v1/user?sort=' . $sort);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $items = json_decode($I->grabResponse(), true)['items'];

        for ($i = 0; $i <= 4; $i++) {
            $item = $items[$i];
            if ($i == 0) {
                $I->assertEquals('user5', $item['username']);
            } else if ($i == 1) {
                $I->assertEquals('user2', $item['username']);
            } else if ($i == 2) {
                $I->assertEquals('user4', $item['username']);
            } else if ($i == 3) {
                $I->assertEquals('user3', $item['username']);
            } else if ($i == 4) {
                $I->assertEquals('user1', $item['username']);
            }
        }
    }

    public function listUsersAsAdminEnabledFalseOrderByLastLogin(ApiTester $I)
    {
        $username = 'user1';
        $password = 'password';

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        $sort = \App\Enum\UserOrderBy::LAST_LOGIN . '-' . \App\Enum\Order::DESC;
        $enabled = 'false';
        $I->sendGET('api/v1/user?sort=' . $sort . '&enabled=' . $enabled);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $items = json_decode($I->grabResponse(), true)['items'];

        for ($i = 0; $i <= 2; $i++) {
            $item = $items[$i];
            if ($i == 0) {
                $I->assertEquals('user2', $item['username']);
            } else if ($i == 1) {
                $I->assertEquals('user3', $item['username']);
            } else if ($i == 2) {
                $I->assertEquals('user1', $item['username']);
            }
        }
    }

    public function listUsersAsGuestOrderByEnabled(ApiTester $I)
    {
        $sort = \App\Enum\UserOrderBy::ENABLED . '-' . \App\Enum\Order::DESC;
        $enabled = 'false';
        $I->sendGET('api/v1/user?sort=' . $sort . '&enabled=' . $enabled);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::BAD_REQUEST);
        $I->seeResponseContains('Only admin can sort by enabled');
    }

    public function listUsersAsAdminEnabledFalseOrderByEnabled(ApiTester $I)
    {
        $username = 'user1';
        $password = 'password';

        $logInDetails = [
            'grant_type' => 'password',
            'client_id' => '1_5w8zrdasdafr4tregd454cw0c0kswcgs0oks40s',
            'client_secret' => 'sdgggskokererg4232404gc4csdgfdsgf8s8ck5s',
            'username' => $username,
            'password' => $password
        ];
        $I->sendPOST('oauth/v2/token', $logInDetails);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContains('access_token');
        $I->seeResponseContains('expires_in');
        $I->seeResponseContains('token_type');
        $I->seeResponseContains('scope');
        $I->seeResponseContains('refresh_token');

        $response = json_decode($I->grabResponse(), true);
        $accessToken = $response['access_token'];
        $I->amBearerAuthenticated($accessToken);

        $sort = \App\Enum\UserOrderBy::ENABLED . '-' . \App\Enum\Order::DESC;
        $enabled = 'false';
        $I->sendGET('api/v1/user?sort=' . $sort . '&enabled=' . $enabled);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);
        $items = json_decode($I->grabResponse(), true)['items'];

        for ($i = 0; $i <= 2; $i++) {
            $item = $items[$i];
            if ($i == 0) {
                $I->assertEquals('user1', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 1) {
                $I->assertEquals('user2', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 2) {
                $I->assertEquals('user3', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 3) {
                $I->assertEquals('user7', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 4) {
                $I->assertEquals('user8', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 5) {
                $I->assertEquals('user10', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 6) {
                $I->assertEquals('user11', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 7) {
                $I->assertEquals('user12', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 8) {
                $I->assertEquals('user13', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 9) {
                $I->assertEquals('user14', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 10) {
                $I->assertEquals('user15', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 11) {
                $I->assertEquals('user16', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 12) {
                $I->assertEquals('user17', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 13) {
                $I->assertEquals('user18', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 14) {
                $I->assertEquals('user19', $item['username']);
                $I->assertEquals(true, $item['enabled']);
            } else if ($i == 15) {
                $I->assertEquals('user4', $item['username']);
                $I->assertEquals(false, $item['enabled']);
            }
        }
    }


}
 

Refactoring Multi-Dimensional Array PHP to Object Oriented PHP

One of my favourite things to do is refactor legacy code. I get to see the thinking process that went into solving the problem. It’s like standing on the shoulders of giants.

private function convertActivityToArray(array $activity)
    {
        $activityArray = array();

        $previousGroupNumber = null;
        /** @var Activity $activityItem */
        foreach ($activity as $activityItem) {
            $type = $activityItem->getType();
            $groupNumber = $activityItem->getGroupNumber();
            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $data = $activityItem->getData();
            $created = $activityItem->getCreatedAt();

            $activityArray[$groupNumber]['type'] = $type;
            $activityArray[$groupNumber]['created'] = $created;

            if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

        return $activityArray;
    }

First we refactor $data.

 $data = $activityItem->getData();

$data is an array stored in the database like this:

Joined Data:

a:2:{s:6:"userId";s:1:"1";s:8:"username";s:8:"mbwagner";}

Watchlisted Data

a:5:{s:13:"documentaryId";i:728;s:16:"documentaryTitle";s:24:"The U.S. vs. John Lennon";s:18:"documentaryExcerpt";s:160:"In retrospect, it seems absurd that the United States government felt so threatened by the presence of John Lennon that they tried to have him deported. But tha";s:20:"documentaryThumbnail";s:24:"cover/usvsjohnlennon.jpg";s:15:"documentarySlug";s:22:"the-u-s-vs-john-lennon";}

The problem with this way is the data is hardcoded into the database, what happens if the data changes? So I decide to populate data on the fly.

Replace:

 $data = $activityItem->getData();

with:

$dataStrategyContext = new DataStrategyContext(
                $type,
                $this->request,
                $this->documentaryService,
                $this->commentService);
$data = $dataStrategyContext->createData($activityItem);

Here we use the Strategy Pattern to populate data.

class DataStrategyContext
{
    /**
     * @var StrategyInterface|null
     */
    private $strategy = null;

    public function __construct(
        string $type,
        Request $request,
        DocumentaryService $documentaryService,
        CommentService $commentService)
    {
        switch ($type) {
            case ActivityType::WATCHLIST:
                $this->strategy = new StrategyWatchlist(
                    $request,
                    $documentaryService
                );
            break;
            case ActivityType::COMMENT:
                $this->strategy = new StrategyComment(
                    $request,
                    $commentService
                );
            break;
            case ActivityType::JOINED:
                $this->strategy = new StrategyJoined();
            break;
            case ActivityType::ADDED:
                $this->strategy = new StrategyAdded(
                    $documentaryService
                );
            break;
        }
    }

    /**
     * @param Activity $activityEntity
     * @return mixed
     */
    public function createData(Activity $activityEntity)
    {
        return $this->strategy->createData($activityEntity);
    }
}

StrategyWatchlist would look like the following:

class StrategyWatchlist implements StrategyInterface
{
    /**
     * @var Request
     */
    private $request;

    /**
     * @var DocumentaryService
     */
    private $documentaryService;

    /**
     * @param Request $request
     * @param DocumentaryService $documentaryService
     */
    public function __construct(
        Request $request,
        DocumentaryService $documentaryService)
    {
        $this->request = $request;
        $this->documentaryService = $documentaryService;
    }

    /**
     * @param Activity $activityEntity
     * @return Data
     */
    public function createData(Activity $activityEntity)
    {
        $documentaryId = $activityEntity->getObjectId();
        $documentary = $this->documentaryService->getDocumentaryById($documentaryId);

        $watchlistData = new WatchlistData();
        $watchlistData->setDocumentaryId($documentary->getId());
        $watchlistData->setDocumentaryTitle($documentary->getTitle());
        $watchlistData->setDocumentarySlug($documentary->getSlug());
        $watchlistData->setDocumentarySummary($documentary->getSummary());
        $poster = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $documentary->getPoster();
        $watchlistData->setDocumentaryPoster($poster);

        return $watchlistData;
    }
}

The next thing is to refactor parent and children from this:

if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

After refactoring and creating ActivityParent and ActivityChild it should look like this:

if ($type == ActivityType::Like) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);

		$activityArray[$groupNumber]['parent'] = $activityParent->toArray();
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
} else if ($type == ActivityType::Comment) {
	$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
} else if ($type == ActivityType::Joined) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
} else if ($type == ActivityType::Added) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
}

$previousGroupNumber = $groupNumber;
}

Notice there are duplicate times when we create ActivityParent and ActivityChild. Only certain types have children:

class ActivityType
{
    const LIKE = "like";
    const COMMENT = "comment";
    const FOLLOW = "follow";
    const JOINED = "joined";
    const ADDED = "added";

    /**
     * @return array
     */
    public static function getAllTypes()
    {
        return [
            self::LIKE,
            self::ADDED,
            self::JOINED,
            self::COMMENT,
            self::FOLLOW
        ];
    }

    /**
     * @return array
     */
    public static function getTypesWithChildren()
    {
        return [
            self::LIKE,
            self::JOINED,
            self::ADDED
        ];
    }

    /**
     * @param string $type
     * @return bool
     */
    public static function hasChildren(string $type)
    {
        return in_array($type, self::getTypesWithChildren());
    }
}

Then we update the convertActivityToArray() function to this:


            $hasChildren = ActivityType::hasChildren($type);
            if ($hasChildren) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityParent = new ActivityParent();
                    $activityParent->setName($name);
                    $activityParent->setUsername($username);
                    $activityParent->setAvatar($avatar);
                    $activityParent->setData($data);

                    $activityArray[$groupNumber]['parent'] = $activityParent->toArray();

                } else {
                    $activityChild = new ActivityChild();
                    $activityChild->setData($data);
                    $activityChild->setUsername($username);
                    $activityChild->setName($name);
                    $activityChild->setAvatar($avatar);

                    $activityArray[$groupNumber]['child'][] = $activityChild->toArray();
                }
            } else {
                $activityParent = new ActivityParent();
                $activityParent->setName($name);
                $activityParent->setUsername($username);
                $activityParent->setAvatar($avatar);
                $activityParent->setData($data);

                $activityArray[$groupNumber]['parent'] = $activityParent->toArray();
            }

Notice ActivityParent and ActivityChild are doing the same thing so I refactored it to just ActivityObject. Plus I moved it above the IF statements.

$activityObject = new ActivityObject();
$activityObject->setName($name);
$activityObject->setUsername($username);
$activityObject->setAvatar($avatar);
$activityObject->setData($data);

$hasChildren = ActivityType::hasChildren($type);
if ($hasChildren) {
	if ($groupNumber != $previousGroupNumber) {
		$activityArray[$groupNumber]['parent'] = $activityObject->toArray();
	} else {
		$activityArray[$groupNumber]['child'][] = $activityObject->toArray();
	}
} else {
	$activityArray[$groupNumber]['parent'] = $activityObject->toArray();
}

Next we add ActivityItemOject to hold the array keys and values.

class ActivityItemObject
{
    /**
     * @var string
     */
    private $type;

    /**
     * @var \DateTime
     */
    private $created;

    /**
     * @var ActivityObject
     */
    private $parent;

    /**
     * @var ActivityObject[]
     */
    private $children;

    public function __construct()
    {
        $this->children = [];
    }
.....

    /**
     * @return array
     */
    public function toArray()
    {
        $children = [];
        foreach ($this->children as $child) {
            $children[] = $child->toArray();
        }

        $array = [
            'type' => $this->type,
            'created' => $this->created,
            'parent' => $this->parent->toArray(),
            'child' => $children
        ];

        return $array;
    }

And finally the completed code:


    /**
     * @param ArrayCollection|Activity[] $activity
     * @return array
     */
    private function convertActivityToArray(array $activity)
    {
        $activityMap = [];

        $previousGroupNumber = 0;
        /** @var Activity $activityEntity */
        foreach ($activity as $activityItem) {
            $groupNumber = $activityItem->getGroupNumber();

            if (array_key_exists($groupNumber, $activityMap) != null) {
                $activityItemObject = $activityMap[$groupNumber];
            } else {
                $activityItemObject = new ActivityItemObject();
            }

            $type = $activityItem->getType();
            $created = $activityItem->getCreatedAt();

            $activityItemObject->setType($type);
            $activityItemObject->setCreated($created);

            $dataStrategyContext = new DataStrategyContext(
                $type,
                $this->request,
                $this->documentaryService,
                $this->commentService);
            $data = $dataStrategyContext->createData($activityItem);

            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $username = $user->getUsername();

            $activityObject = new ActivityObject();
            $activityObject->setName($name);
            $activityObject->setUsername($username);
            $activityObject->setAvatar($avatar);
            $activityObject->setData($data);

            $hasChildren = ActivityType::hasChildren($type);
            if ($hasChildren) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityItemObject->setParent($activityObject);
                } else {
                    $activityItemObject->addChild($activityObject);
                }
            } else {
                $activityItemObject->setParent($activityObject);
            }

            $activityMap[$groupNumber] = $activityItemObject;

            $previousGroupNumber = $groupNumber;
        }

        $display = [];
        /** @var ActivityItemObject $value */
        foreach ($activityMap as $key => $value) {
            $display[$key] = $value->toArray();
        }

        return $display;
    }
 

Technical Test – Javascript (Frontend) & PHP (API Backend)

Please complete the following technical task. I will be reviewing it to asses the following:

* Your ability to follow vague instructions and use your own initiative.
* Your ability to write documented PHP code.
* Your ability to design and abstract database tables.
* Your ability to ability to write a simple RESTful API.
* Your ability to use jQuery and ajax to interact with your API.
* Your ability to write integration documentation for your code.
* The overall robustness of your code (error handling, validation and security especially).
Here is the task:

* Create a simple RESTful API and PHP 7 application which serves to create, retrieve, update and delete employees to two fictional companies "Company A" and "Company B".

* A database with company and employee tables have been provided for you. Use these tables, but feel free to add any other tables or columns you need.

* The RESTful API must be the backend of your PHP app.

* Ensure that your API returns the proper HTTP status where required: 200/400/403/404/405/406/500 - this will be tested.

* In the frontend of your PHP app, create a simple list view to show all employees from all companies, but provide an option to filter these by company and sort them by first name or last name. Also include action buttons to add a new employee and update/delete existing employees.

* Use bootstrap components for style and responsive layout.

* Use a modal dialog to display the form which creates or updates a user.

* When deleting a user, display a message to ask the user if they are sure they want to proceed.

* When creating, updating or deleting a user, use jQuery ajax to call your API which in turn updates the database.

* When complete, put the project in a .git repository and add a readme.md to explain how to install and run the project on a vanilla linux box.

Source code: https://github.com/JonnyD/Technical-Test-Javascript-and-PHP

Employees:

You can filter by Company:

and sort by first name or last name:

You can read each employee:

Edit:

And Delete

Create employee:

Here is the MySQL to create and populate employees and companies:

CREATE DATABASE  IF NOT EXISTS `test` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `test`;

--
-- Table structure for table `company`
--

DROP TABLE IF EXISTS `company`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `company` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

INSERT INTO `company` (`id`, `name`) VALUES (1, 'Company A');
INSERT INTO `company` (`id`, `name`) VALUES (2, 'Company B');

--
-- Table structure for table `employee`
--

DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `job_title` varchar(45) DEFAULT NULL,
  `email` varchar(45) DEFAULT NULL,
  `phone` varchar(45) DEFAULT NULL,
  `address_line_1` varchar(45) DEFAULT NULL,
  `address_line_2` varchar(45) DEFAULT NULL,
  `town_city` varchar(45) DEFAULT NULL,
  `county_region` varchar(45) DEFAULT NULL,
  `country` varchar(45) DEFAULT NULL,
  `postcode` varchar(45) DEFAULT NULL,
  `company_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

ALTER TABLE employee ADD FOREIGN KEY (company_id) REFERENCES company(id);

INSERT INTO employee (id, first_name, last_name, job_title, email, phone, address_line_1, address_line_2, town_city, county_region, country, postcode, company_id)
VALUES (1, 'Jonathan', 'Devine', 'Software Developer', 'contact@jonnydevine.com', '0862041801', '23 Windmill Road', NULL, 'Drogheda', 'Louth', 'Ireland', 'ABC123', 1);
INSERT INTO employee (id, first_name, last_name, job_title, email, phone, address_line_1, address_line_2, town_city, county_region, country, postcode, company_id)
VALUES (2, 'Steven', 'Andrews', 'Front End Developer', 'contact@steven.com', '0862454944', '2 Fake Road', NULL, 'Dublin', 'Dublin', 'Ireland', 'DEF456', 2);

Here is an example of reading employees using Javascript:

$(document).ready(function(){

    // show list of employees on first load
    showEmployees();

    $(document).on('click', '#select-company', function(){
        showEmployees();
    });

    $(document).on('click', '.order-by', function(){
        showEmployees();
    });
});

// function to show list of employees
function showEmployees(){
    var order_by = $('input[name=orderBy]:checked').val();
    if (typeof order_by === 'undefined' || !order_by) {
        order_by = "first_name";
    }

    var company_id = $("#select-company").val();
    if (typeof company_id === 'undefined' || !company_id) {
        company_id = "All";
    }

    var order_by_html = "";
    if (order_by === "first_name") {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='first_name' checked> First Name";
    } else {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='first_name'> First Name";
    }
    if (order_by === "last_name") {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='last_name' checked> Last Name";
    } else {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='last_name'> Last Name";
    }

    // load list of companies
    $.getJSON("api/company/read.php", function(data) {
        // build companies option html
        // loop through returned list of data
        var companies_options_html = "";
        companies_options_html += "<select id='select-company' name='company_id' class='form-control pull-left'>";
        companies_options_html += "<option value='All'>All</option>";
        $.each(data, function (key, val) {
            if(val.id == company_id) {
                companies_options_html += "<option value='" + val.id + "' selected>" + val.name + "</option>";
            } else {
                companies_options_html += "<option value='" + val.id + "'>" + val.name + "</option>";
            }
        });
        companies_options_html += "</select>";

        // get list of employees from the API
        $.getJSON("api/employee/read.php?company_id=" + company_id + "&order_by=" + order_by, function(data){
            // html for listing employees
            read_employees_html = "";

            read_employees_html += "<div class='row'>";
                read_employees_html += "<div class='col-md-4'>" + companies_options_html + "</div>";

                read_employees_html += "<div class='col-md-4'>" + order_by_html + "</div>";

                // when clicked, it will load the create employee form
                read_employees_html += "<div class='col-md-4'><div id='create-employee' class='btn btn-primary pull-right create-employee-button' data-toggle='modal' data-target='#myModal'>";
                    read_employees_html += "<span class='glyphicon glyphicon-plus'></span> Create Employee";
                read_employees_html += "</div></div>";

            read_employees_html += "</div>";
            // start table
            read_employees_html += "<table class='table table-bordered table-hover'>";

                // creating our table heading
                read_employees_html += "<tr>";
                    read_employees_html += "<th class='w-25-pct'>First Name</th>";
                    read_employees_html += "<th class='w-10-pct'>Last Name</th>";
                    read_employees_html += "<th class='w-15-pct'>Company</th>";
                    read_employees_html += "<th class='w-25-pct text-align-center'>Action</th>";
                read_employees_html += "</tr>";

            // loop through returned list of data
            $.each(data, function(key, val) {

                // creating new table row per record
                read_employees_html += "<tr>";

                    read_employees_html += "<td>" + val.first_name + "</td>";
                    read_employees_html += "<td>" + val.last_name + "</td>";
                    read_employees_html += "<td>" + val.company.name + "</td>";

                    // 'action' buttons
                    read_employees_html += "<td>";
                        // read employee button
                        read_employees_html += "<button class='btn btn-primary read-one-employee-button' data-id='" + val.id + "' data-toggle='modal' data-target='#myModal'>";
                            read_employees_html += "<span class='glyphicon glyphicon-eye-open'></span> Read";
                        read_employees_html += "</button>";

                        // edit button
                        read_employees_html += "<button class='btn btn-info update-employee-button' data-id='" + val.id + "' data-toggle='modal' data-target='#myModal'>";
                            read_employees_html += "<span class='glyphicon glyphicon-edit'></span> Edit";
                        read_employees_html += "</button>";

                        // delete button
                        read_employees_html += "<button class='btn btn-danger delete-employee-button' data-id='" + val.id + "'>";
                            read_employees_html += "<span class='glyphicon glyphicon-remove'></span> Delete";
                        read_employees_html += "</button>";
                    read_employees_html += "</td>";

                read_employees_html += "</tr>";

            });

            // end table
            read_employees_html += "</table>";

            // inject to 'page-content' of our app
            $("#page-content").html(read_employees_html);

        });
    });
}

For more see here https://github.com/JonnyD/Technical-Test-Javascript-and-PHP/tree/master/app/employees

An example of a backend API:

<?php
// required header
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");

// include files
include_once '../config/database.php';
include_once '../objects/employee.php';
include_once '../objects/company.php';
include_once '../repository/employee_repository.php';
include_once '../repository/company_repository.php';

// instantiate database
$database = new Database();
$db = $database->getConnection();

// instantiate employee repository
$employeeRepository = new EmployeeRepository($db);

// instantiate company repository
$companyRepository = new CompanyRepository($db);

// get company ID
$companyId = isset($_GET['company_id']) ? $_GET['company_id'] : null;

// get order by
$orderBy = isset($_GET['order_by']) ? $_GET['order_by'] : null;

// query employees
if ($companyId != "All" && $companyId != null) {
    $employees = $employeeRepository->readByCompany($companyId, $orderBy);
} else {
    $employees = $employeeRepository->read($orderBy);
}

// check if more than 0 records found
if(count($employees) > 0) {
    $employeesArray = [];

    foreach ($employees as $employee) {
        $company = $companyRepository->readOne($employee->getCompanyId());

        $employeesArr[] = [
            'id' => $employee->getId(),
            'first_name' => $employee->getFirstName(),
            'last_name' => $employee->getLastName(),
            'job_title' => $employee->getJobTitle(),
            'email' => $employee->getEmail(),
            'phone' => $employee->getPhone(),
            'address_line_1' => $employee->getAddressLine1(),
            'address_line_2' => $employee->getAddressLine2(),
            'town_city' => $employee->getTownCity(),
            'county_region' => $employee->getCountyReqion(),
            'country' => $employee->getCountry(),
            'postcode' => $employee->getPostcode(),
            'company' => [
                'id' => $company->getId(),
                'name' => $company->getName()
            ]
        ];
    }

    header("HTTP/1.1 200");
    echo json_encode($employeesArr);
} else {
    header("HTTP/1.1 404");
    echo json_encode([
        "message" => "No employees found."
    ]);
}
?>

More here https://github.com/JonnyD/Technical-Test-Javascript-and-PHP/tree/master/api

EmployeeRepository:

<?php

class EmployeeRepository
{
    /**
     * @var string
     */
    private $tableName;

    /**
     * @var PDO
     */
    private $db;

    /**
     * @param PDO $db
     */
    public function __construct(PDO $db)
    {
        $this->tableName = "employee";
        $this->db = $db;
    }

    /**
     * @param string $orderBy
     * @return Employee[]
     */
    public function read(string $orderBy = null)
    {
        $query = "SELECT
                        id, 
                        first_name, 
                        last_name,
                        job_title,
                        email,
                        phone,
                        address_line_1,
                        address_line_2,
                        town_city,
                        county_region,
                        country,
                        postcode,
                        company_id
                    FROM
                        " . $this->tableName;

        if ($orderBy != null) {
            $query .= " ORDER BY " . $orderBy . " ASC";
        }

        $stmt = $this->db->prepare($query);
        $stmt->execute();

        $employees = [];
        if($stmt->rowCount() > 0) {
            // retrieve our table contents
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $employee = new Employee();
                $employee->setId($row['id']);
                $employee->setFirstName($row['first_name']);
                $employee->setLastName($row['last_name']);
                $employee->setJobTitle($row['job_title']);
                $employee->setEmail($row['email']);
                $employee->setPhone($row['phone']);
                $employee->setAddressLine1($row['address_line_1']);
                $employee->setAddressLine2($row['address_line_2']);
                $employee->setTownCity($row['town_city']);
                $employee->setCountyReqion($row['county_region']);
                $employee->setCountry($row['country']);
                $employee->setPostcode($row['postcode']);
                $employee->setCompanyId($row['company_id']);

                array_push($employees, $employee);
            }
        }

        return $employees;
    }

    /**
     * @param int $companyId
     * @param string $orderBy
     * @return Employee[]
     */
    public function readByCompany(int $companyId, string $orderBy = null)
    {
        $query = "SELECT
                        id, 
                        first_name, 
                        last_name,
                        job_title,
                        email,
                        phone,
                        address_line_1,
                        address_line_2,
                        town_city,
                        county_region,
                        country,
                        postcode,
                        company_id
                    FROM
                        " . $this->tableName . "
                    WHERE company_id = :companyId";

        if ($orderBy != null) {
            $query .= " ORDER BY " . $orderBy . " ASC";
        }

        $stmt = $this->db->prepare($query);
        $stmt->bindParam(":companyId", $companyId);
        $stmt->execute();

        $employees = [];
        if($stmt->rowCount() > 0) {
            // retrieve our table contents
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $employee = new Employee();
                $employee->setId($row['id']);
                $employee->setFirstName($row['first_name']);
                $employee->setLastName($row['last_name']);
                $employee->setJobTitle($row['job_title']);
                $employee->setEmail($row['email']);
                $employee->setPhone($row['phone']);
                $employee->setAddressLine1($row['address_line_1']);
                $employee->setAddressLine2($row['address_line_2']);
                $employee->setTownCity($row['town_city']);
                $employee->setCountyReqion($row['county_region']);
                $employee->setCountry($row['country']);
                $employee->setPostcode($row['postcode']);
                $employee->setCompanyId($row['company_id']);

                array_push($employees, $employee);
            }
        }

        return $employees;
    }

    /**
     * @param int $id
     * @return Employee|null
     */
    public function readOne(int $id)
    {
        $query = "SELECT 
                    id,
                    first_name,
                    last_name,
                    job_title,
                    email,
                    phone,
                    address_line_1,
                    address_line_2,
                    town_city,
                    county_region,
                    country,
                    postcode,
                    company_id
				FROM " . $this->tableName . "
				WHERE id = :id
				LIMIT 0,1";

        // prepare query statement
        $stmt = $this->db->prepare($query);

        // bind selected record id
        $stmt->bindParam(":id", $id);

        // execute the query
        $stmt->execute();

        // get record details
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($stmt->rowCount() > 0) {
            // assign values to object properties
            $employee = new Employee();
            $employee->setId($row['id']);
            $employee->setFirstName($row['first_name']);
            $employee->setLastName($row['last_name']);
            $employee->setJobTitle($row['job_title']);
            $employee->setEmail($row['email']);
            $employee->setPhone($row['phone']);
            $employee->setAddressLine1($row['address_line_1']);
            $employee->setAddressLine2($row['address_line_2']);
            $employee->setTownCity($row['town_city']);
            $employee->setCountyReqion($row['county_region']);
            $employee->setCountry($row['country']);
            $employee->setPostcode($row['postcode']);
            $employee->setCompanyId($row['company_id']);

            return $employee;
        } else {
            return null;
        }
    }

    /**
     * @param Employee $employee
     * @return bool
     */
    public function create(Employee $employee)
    {
        $query = "INSERT INTO
					" . $this->tableName . "
				SET
					first_name = :first_name, 
					last_name = :last_name,
					job_title = :job_title,
					email = :email,
					phone = :phone,
					address_line_1 = :address_line_1,
					address_line_2 = :address_line_2,
					town_city = :town_city,
					county_region = :county_region,
					country = :country,
					postcode = :postcode,
					company_id = :company_id";

        // prepare query
        $stmt = $this->db->prepare($query);

        // bind values
        $firstName = $employee->getFirstName();
        $stmt->bindParam(":first_name", $firstName);
        $lastName = $employee->getLastName();
        $stmt->bindParam(":last_name", $lastName);
        $jobTitle = $employee->getJobTitle();
        $stmt->bindParam(":job_title", $jobTitle);
        $email = $employee->getEmail();
        $stmt->bindParam(":email", $email);
        $phone = $employee->getPhone();
        $stmt->bindParam(":phone", $phone);
        $addressLine1 = $employee->getAddressLine1();
        $stmt->bindParam(":address_line_1", $addressLine1);
        $addressLine2 = $employee->getAddressLine2();
        $stmt->bindParam(":address_line_2", $addressLine2);
        $townCity = $employee->getTownCity();
        $stmt->bindParam(":town_city", $townCity);
        $countyRegion = $employee->getCountyReqion();
        $stmt->bindParam(":county_region", $countyRegion);
        $country = $employee->getCountry();
        $stmt->bindParam(":country", $country);
        $postcode = $employee->getPostcode();
        $stmt->bindParam(":postcode", $postcode);
        $companyId = $employee->getCompanyId();
        $stmt->bindParam(":company_id", $companyId);

        // execute query
        if ($stmt->execute()) {
            return true;
        } else {
            echo "<pre>";
            print_r($stmt->errorInfo());
            echo "</pre>";

            return false;
        }
    }

    /**
     * @param Employee $employee
     * @return bool
     */
    public function update(Employee $employee)
    {
        $query = "UPDATE
					" . $this->tableName . "
				SET
					first_name = :first_name, 
					last_name = :last_name,
					job_title = :job_title,
					email = :email,
					phone = :phone,
					address_line_1 = :address_line_1,
					address_line_2 = :address_line_2,
					town_city = :town_city,
					county_region = :county_region,
					country = :country,
					postcode = :postcode,
					company_id = :company_id
				WHERE
					id = :id";

        // prepare query statement
        $stmt = $this->db->prepare($query);

        // bind new values
        $id = $employee->getId();
        $stmt->bindParam(':id', $id);
        $firstName = $employee->getFirstName();
        $stmt->bindParam(":first_name", $firstName);
        $lastName = $employee->getLastName();
        $stmt->bindParam(":last_name", $lastName);
        $jobTitle = $employee->getJobTitle();
        $stmt->bindParam(":job_title", $jobTitle);
        $email = $employee->getEmail();
        $stmt->bindParam(":email", $email);
        $phone = $employee->getPhone();
        $stmt->bindParam(":phone", $phone);
        $addressLine1 = $employee->getAddressLine1();
        $stmt->bindParam(":address_line_1", $addressLine1);
        $addressLine2 = $employee->getAddressLine2();
        $stmt->bindParam(":address_line_2", $addressLine2);
        $townCity = $employee->getTownCity();
        $stmt->bindParam(":town_city", $townCity);
        $countyRegion = $employee->getCountyReqion();
        $stmt->bindParam(":county_region", $countyRegion);
        $country = $employee->getCountry();
        $stmt->bindParam(":country", $country);
        $postcode = $employee->getPostcode();
        $stmt->bindParam(":postcode", $postcode);
        $companyId = $employee->getCompanyId();
        $stmt->bindParam(":company_id", $companyId);

        // execute the query
        if ($stmt->execute()) {
            return true;
        }else{
            return false;
        }
    }

    /**
     * @param int $id
     * @return bool
     */
    public function delete(int $id)
    {
        $query = "DELETE FROM " . $this->tableName . " WHERE id = :id";

        // prepare query
        $stmt = $this->db->prepare($query);

        // bind id of record to delete
        $stmt->bindParam(':id', $id);

        // execute query
        if ($stmt->execute()) {
            return true;
        }

        return false;

    }
}
 

Example of an API

View more here https://github.com/JonnyD/Greetup-API-PHP/tree/master/src/GU

<?php

namespace GU\GangBundle\Controller\API;

use GU\BaseBundle\Controller\BaseController;
use GU\GangBundle\Entity\Gang;
use GU\GangBundle\Entity\GangUser;
use GU\GangBundle\Entity\JoinRequest;
use GU\GangBundle\Enum\Role;
use GU\GangBundle\Form\GangType;
use GU\GangBundle\Service\GangService;
use GU\GangBundle\Service\GangUserService;
use GU\GangBundle\Service\JoinRequestService;
use GU\GangBundle\Specification\CanViewGang;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use FOS\RestBundle\Controller\Annotations\Post;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\QueryParam;

class GangController extends BaseController
{
    /**
     * @return Response
     */
    public function getGangsAction()
    {
        $gangService = $this->getGangService();
        $gangs = $gangService->getGangsWithinRadius(55.55555, 56.55555, 25);

        $canViewGangSpecification = $this->getCanViewGangSpecification();

        $gangsThatCanBeViewed = [];
        foreach ($gangs as $gang) {
            if ($canViewGangSpecification->isSatisfiedBy($gang)) {
                $gangsThatCanBeViewed[] = $gang;
            }
        }

        $response = $this->createApiResponse($gangsThatCanBeViewed);
        return $response;
    }

    /**
     * @param int $id
     * @return Response
     */
    public function getGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param Request $request
     * @return Response
     */
    public function postGangsAction(Request $request)
    {
        $data = json_decode($request->getContent(), true);

        $gang = new Gang();
        $form = $this->createForm(GangType::class, $gang);
        $form->submit($data);

        if ($form->isSubmitted()) {
            $gangService = $this->getGangService();
            $gangService->save($gang);

            $gangUser = new GangUser();
            $gangUser->setUser($this->getLoggedInUser());
            $gangUser->setGang($gang);
            $gangUser->setRole(Role::FOUNDER);

            $gangUserService = $this->getGangUserService();
            $gangUserService->save($gangUser);
        }

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param Request $request
     * @param int $id
     * @return Response|NotFoundHttpException
     */
    public function putGangAction(Request $request, int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $data = json_decode($request->getContent(), true);

        $form = $this->createForm(GangType::class, $gang);
        $form->submit($data);

        if ($form->isSubmitted()) {
            $gangService = $this->getGangService();
            $gangService->save($gang);
        }

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/join", name="join_gang")
     */
    public function joinGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $loggedInUser = $this->getLoggedInUser();

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($gang, $loggedInUser);

        if ($gangUser != null) {
            return $this->createNotFoundException("You are already a member");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestByGangAndUser($gang, $loggedInUser);

        if ($joinRequest != null) {
            return $this->createNotFoundException("You already requested to join this gang");
        }

        $joinRequest = new JoinRequest();
        $joinRequest->setGang($gang);
        $joinRequest->setUser($loggedInUser);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @param int $joinRequestId
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/accept-join-request/{join_request_id}", name="accept-join-request")
     */
    public function acceptJoinRequestAction(int $id, int $joinRequestId)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestById($joinRequestId);

        if ($joinRequest == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($joinRequest->getGang(), $joinRequest->getUser());

        if ($gangUser != null) {
            return $this->createNotFoundException("User is already a member");
        }

        $gangUser = new GangUser();
        $gangUser->setGang($joinRequest->getGang());
        $gangUser->setUser($joinRequest->getUser());
        $gangUser->setRole(Role::USER);

        $gangUserService->save($gangUser);
        $joinRequestService->remove($joinRequest);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @param int $joinRequestId
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/reject-join-request/{join_request_id}", name="reject-join-request")
     */
    public function rejectJoinRequestAction(int $id, int $joinRequestId)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestById($joinRequestId);

        if ($joinRequest == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($joinRequest->getGang(), $joinRequest->getUser());

        if ($gangUser != null) {
            return $this->createNotFoundException("User is already a member");
        }

        $joinRequestService->remove($joinRequest);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/leave", name="leave_gang")
     */
    public function leaveGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $loggedInUser = $this->getLoggedInUser();

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($gang, $loggedInUser);

        if ($gangUser == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService->remove($gangUser);

        return new Response(204);
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @GET("/gangs/{id}/actions/listMembers", name="list_members_gang")
     */
    public function listMembersAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUsers = $gangUserService->getGangUsersByGang($gang);

        $response = $this->createApiResponse($gangUsers);
        return $response;
    }

    /**
     * @return GangUserService
     */
    private function getGangUserService()
    {
        return $this->get('gu.gang_user_service');
    }

    /**
     * @return GangService
     */
    private function getGangService()
    {
        return $this->get('gu.gang_service');
    }

    /**
     * @return JoinRequestService
     */
    private function getJoinRequestService()
    {
        return $this->get('gu.join_request_service');
    }

    /**
     * @return CanViewGang
     */
    private function getCanViewGangSpecification()
    {
        return $this->get('gu.can_view_gang_specification');
    }
}
 

The Criteria Pattern

You’ve probably come across a Repository like the following:

class DocumentaryRepository
{
    public function findFeaturedDocumentaries() {
        //sql..
    }

    public function findPublishedDocumentariesInCategoryOrdededByCreated(Category $category) {
        //sql
    }

    public function findMostPopularDocumentaries() {
        //sql
    }

    public function findMostDiscussedDocumentaries() {
        //sql
    }

    public function findMostWatchlistedDocumentaries() {
        //sql
    }

    public function findRandomDocumentariesInCategory(Category $category) {
        //sql
    }

    public function findPublishedDocumentariesOrderedByCreated() {
        //sql
    }

    public function getLatestDocumentariesOrderedByCreated() {
        //sql
    }
}

This eventually can become cumbersome.

Another way to look at this is using the Criteria Pattern where you just have one function in the repository to handle the querying of the database.

First lets build our DocumentaryCriteria class:

<?php

namespace App\Criteria;

use App\Entity\Category;
use App\Entity\User;
use App\Entity\VideoSource;

class DocumentaryCriteria
{
    /**
     * @var bool
     */
    private $featured;

    /**
     * @var string
     */
    private $status;

    /**
     * @var Category
     */
    private $category;

    /**
     * @var VideoSource
     */
    private $videoSource;

    /**
     * @var int
     */
    private $year;

    /**
     * @var string
     */
    private $duration;

    /**
     * @var User
     */
    private $addedBy;

    /**
     * @var array
     */
    private $sort;

    /**
     * @var int
     */
    private $limit;

    //getter and setters
}

Another class required is DocumentaryOrderBy

<?php

namespace App\Enum;

class DocumentaryOrderBy
{
    const CREATED_AT = "createdAt";
    const UPDATED_AT = "updatedAt";
    const VIEWS = "views";
    const COMMENT_COUNT = "commentCount";
    const WATCHLIST_COUNT = "watchlistCount";
    const YEAR = "year";
}

and Order

<?php

namespace App\Enum;

class Order
{
    const ASC = "ASC";
    const DESC = "DESC";
}

Now lets look at the DocumentaryRepository and add the criteria function

/**
     * @param DocumentaryCriteria $criteria
     * @return QueryBuilder
     */
    public function findDocumentariesByCriteriaQueryBuilder(DocumentaryCriteria $criteria)
    {
        $em = $this->getEntityManager();
        $qb = $em->createQueryBuilder();

        $qb->select('documentary')
            ->from('App\Entity\Documentary', 'documentary');

        if ($criteria->isFeatured() != null) {
            $qb->andWhere('documentary.featured = :featured')
                ->setParameter('featured', $criteria->isFeatured());
        }

        if ($criteria->getStatus()) {
            $qb->andWhere('documentary.status = :status')
                ->setParameter('status', $criteria->getStatus());
        }

        if ($criteria->getCategory()) {
            $qb->andWhere('documentary.category = :category')
                ->setParameter('category', $criteria->getCategory());
        }

        if ($criteria->getVideoSource()) {
            $qb->andWhere('documentary.videoSource = :videoSource')
                ->setParameter('videoSource', $criteria->getVideoSource());
        }

        if ($criteria->getAddedBy()) {
            $qb->andWhere('documentary.addedBy = :addedBy')
                ->setParameter('addedBy', $criteria->getAddedBy());
        }
        
        if ($criteria->getYear()) {
            $qb->andWhere('documentary.year = :year')
                ->setParameter('year', $criteria->getYear());
        }

        if ($criteria->getSort()) {
            foreach ($criteria->getSort() as $column => $direction) {
                $qb->addOrderBy($qb->getRootAliases()[0] . '.' . $column, $direction);
            }
        }

        if ($criteria->getLimit()) {
            $qb->setMaxResults($criteria->getLimit());
        }

        return $qb;
    }

We’re naming the function findDocumentariesByCriteriaQueryBuilder for a reason (which will come in handy later when we implement pagination).

Lets add another function:


    /**
     * @param DocumentaryCriteria $criteria
     * @return ArrayCollection|Documentary[]
     */
    public function findDocumentariesByCriteria(DocumentaryCriteria $criteria)
    {
        $qb = $this->findDocumentariesByCriteriaQueryBuilder($criteria);

        $query = $qb->getQuery();
        $result = $query->getResult();

        return $result;
    }

Now we can add DocumentaryService which will host more finely grained functions but it’s much simpler with DocumentaryCriteria:


    /**
     * @param DocumentaryCriteria $criteria
     * @return QueryBuilder
     */
    public function getDocumentariesByCriteriaQueryBuilder(DocumentaryCriteria $criteria)
    {
        return $this->documentaryRepository->findDocumentariesByCriteriaQueryBuilder($criteria);
    }

    /**
     * @return ArrayCollection|Documentary[]
     */
    public function getFeaturedDocumentaries()
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setFeatured(true);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);

        $documentaries = $this->documentaryRepository->findDocumentariesByCriteria($criteria);
        shuffle($documentaries);
        return $documentaries;
    }

    /**
     * @param Category $category
     * @return ArrayCollection|Documentary[]
     */
    public function getPublishedDocumentariesInCategory(Category $category)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setCategory($category);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostPopularDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::VIEWS => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostDiscussedDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::COMMENT_COUNT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getMostWatchlistedDocumentaries(int $limit) : array
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setLimit($limit);
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::WATCHLIST_COUNT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @return ArrayCollection|Documentary[]
     */
    public function getPublishedDocumentaries()
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

    /**
     * @param int $limit
     * @return ArrayCollection|Documentary[]
     */
    public function getLatestDocumentaries(int $limit)
    {
        $criteria = new DocumentaryCriteria();
        $criteria->setStatus(DocumentaryStatus::PUBLISH);
        $criteria->setLimit($limit);
        $criteria->setSort([
            DocumentaryOrderBy::CREATED_AT => Order::DESC
        ]);

        return $this->documentaryRepository->findDocumentariesByCriteria($criteria);
    }

Now you can see how the criteria can make your life easier and we can easily add pagination with the QueryBuilder and Pagerfanta.

 /**
     * @FOSRest\Get("/documentary", name="get_documentary_list", options={ "method_prefix" = false })
     *
     * @param Request $request
     * @throws \Doctrine\ORM\ORMException
     */
    public function listAction(Request $request)
    {
        $page = $request->query->get('page', 1);

        $criteria = new DocumentaryCriteria();

        $isRoleAdmin = $this->isGranted('ROLE_ADMIN');

        if ($isRoleAdmin) {
            $videoSourceId = $request->query->get('videoSource');
            if (isset($videoSourceId)) {
                $videoSource = $this->videoSourceService->getVideoSourceById($videoSourceId);
                $criteria->setVideoSource($videoSource);
            }

            $status = $request->query->get('status');
            if (isset($status)) {
                $criteria->setStatus($status);
            }

            $featured = $request->query->get('featured');
            if (isset($featured)) {
                $featured = $featured === 'true' ? true: false;
                $criteria->setFeatured($featured);
            }
        }

        if (!$isRoleAdmin) {
            $criteria->setStatus(DocumentaryStatus::PUBLISH);
        }

        $categorySlug = $request->query->get('category');
        if (isset($categorySlug)) {
            $category = $this->categoryService->getCategoryBySlug($categorySlug);
            $criteria->setCategory($category);
        }

        $year = $request->query->get('year');
        if (isset($year)) {
            $criteria->setYear($year);
        }

        $duration = $request->query->get('duration');
        if (isset($duration)) {
            $criteria->setDuration($duration);
        }

        $addedBy = $request->query->get('addedBy');
        if (isset($addedBy)) {
            $user = $this->userService->getUserByUsername($addedBy);
            $criteria->setAddedBy($user);
        }

        $sort = $request->query->get('sort');
        if (isset($sort)) {
            $exploded = explode("-", $sort);
            $sort = [$exploded[0] => $exploded[1]];
            $criteria->setSort($sort);
        } else {
            $criteria->setSort([
                DocumentaryOrderBy::CREATED_AT => Order::DESC
            ]);
        }

        $qb = $this->documentaryService->getDocumentariesByCriteriaQueryBuilder($criteria);

        $adapter = new DoctrineORMAdapter($qb, false);
        $pagerfanta = new Pagerfanta($adapter);
        $pagerfanta->setMaxPerPage($amountPerPage);
        $pagerfanta->setCurrentPage($page);

        $items = (array) $pagerfanta->getCurrentPageResults();

        $serialized = [];
        foreach ($items as $item) {
            $serialized[] = $this->serializeDocumentary($item);
        }

        $data = [
            'items'             => $serialized,
            'count_results'     => $pagerfanta->getNbResults(),
            'current_page'      => $pagerfanta->getCurrentPage(),
            'number_of_pages'   => $pagerfanta->getNbPages(),
            'next'              => ($pagerfanta->hasNextPage()) ? $pagerfanta->getNextPage() : null,
            'prev'              => ($pagerfanta->hasPreviousPage()) ? $pagerfanta->getPreviousPage() : null,
            'paginate'          => $pagerfanta->haveToPaginate(),
        ];

        return new JsonResponse($data, 200, array('Access-Control-Allow-Origin'=> '*'));
    }

Feedback appreciated.