_onichannn

【Symfony2.3】stof/doctrine-extensionsのSoftDeleteable設定方法。

エンティティクラスのアノテーションで下記のような感じで指定してやればOK。

例)
deletedAtカラムを論理削除管理カラムとして使用する場合。


use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * User
 *
 * @ORM\Table(name="users")
 * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
 */
class User
{
}

 

【Symfony2.3】基本的に追加している外部バンドルまとめ。


...

    "require": {

        ...

        "jms/di-extra-bundle": "1.4.*@dev",
        "friendsofsymfony/user-bundle": "2.0.*@dev",
        "friendsofsymfony/rest-bundle": "1.5.*@dev",
        "stof/doctrine-extensions-bundle": "1.2.*@dev",
        "doctrine/doctrine-fixtures-bundle": "2.2.*@dev",
        "doctrine/migrations": "1.0.*@dev",
        "doctrine/doctrine-migrations-bundle": "2.1.*@dev",
        "jms/serializer-bundle": "0.13.*@dev",
        "knplabs/knp-paginator-bundle": "2.4.*@dev"
    },

...

 

【Mysql/Doctrine2】where句の記述順によるパフォーマンスの違い。

select時のそういった最適化は全てオプティマイザーが最適解を求めてくれているのかと思い込んでいたがどうやらそういうわけでもない模様。

下記に示す二つのDQLで大きくパフォーマンスが違う。

■パターン1

    select
        p
    from
        HogeFugaBundle:Post as p
    where
        p.body like :str
    and
        p.deletedAt is null
    and
        p.createdAt >= :date

■パターン2

    select
        p
    from
        HogeFugaBundle:Post as p
    where
        p.createdAt >= :date
    and
        p.deletedAt is null
    and
        p.body like :str

だいたいレコード25万くらいのテーブルで実験したところ、パターン1が30秒前後、パターン2が2~3秒といった結果になった。
ある意味素直に最初に指定した条件から順番に絞り込むってことだね。そりゃ25万件のlikeは重いわ。

っていうお話。

 

【Symfony2.3/AngularJS】FOSRestBundleとFOSUserBundleで構築するRESTログインAPI。

説明が皆無なのでかなり不親切です。超絶自分用まとめ。もしくはある程度解る人用。

「/」以下をフロント、「/admin」以下を管理画面(要ログイン)とする、よくあるアプリケーションの構築テンプレート。

□□サーバーサイド

■SecurityController.php

namespace Hoge\FugaBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

use Hoge\FugaBundle\Entity\User;

/**
 * @Route("api")
 */
class SecurityController extends Controller
{
    protected function getUserManager()
    {
        return $this->get('fos_user.user_manager');
    }

    protected function loginUser(User $user)
    {
        $security = $this->get('security.context');
        $providerKey = $this->container->getParameter('fos_user.firewall_name');
        $roles = $user->getRoles();
        $token = new UsernamePasswordToken($user, null, $providerKey, $roles);
        $security->setToken($token);
    }

    protected function logoutUser()
    {
        $security = $this->get('security.context');
        $token = new AnonymousToken(null, new User());
        $security->setToken($token);
        $this->get('session')->invalidate();
    }
    
    protected function checkUser()
    {
        $security = $this->get('security.context');
        
        if ($token = $security->getToken()) {
            $user = $token->getUser();
            if ($user instanceof User) {
                return $user;
            }
        }
        
        return false;
    }

    protected function checkUserPassword(User $user, $password)
    {
        $factory = $this->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        if(!$encoder){
            return false;
        }
        return $encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt());
    }

    /**
     * @Route("/login.{_format}", name="hoge_fuga_admin_api_security_login", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function loginAction()
    {
        $request = $this->getRequest();
        $username = $request->get('username');
        $password = $request->get('password');
        
        $um = $this->getUserManager();
        $user = $um->findUserByUsername($username);
        if(!$user){
            $user = $um->findUserByEmail($username);
        }
        
        if(!$user instanceof User){
            throw new NotFoundHttpException('ユーザーIDが存在しません');
        }
        if(!$this->checkUserPassword($user, $password)){
            throw new AccessDeniedException('パスワードが不正です');
        }
        
        $this->loginUser($user);
        return [
            'success' => true,
            'user' => $user
        ];
    }

    /**
     * @Route("/logout.{_format}", name="hoge_fuga_admin_api_security_logout", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function logoutAction()
    {
        $this->logoutUser();
        
        return [
            'success' => true
        ];
    }
    
    /**
     * @Route("/loginCheck.{_format}", name="hoge_fuga_admin_api_security_login_check", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function loginCheckAction()
    {
        if ($user = $this->checkUser()) {
            return [
                'success' => true,
                'user' => $user
            ];
        } else {
            throw new AccessDeniedException();
        }
    }
}

■app/config/config.yml

fos_user:
    db_driver: orm 
    firewall_name: admin_area
    user_class: Hoge\FugaBundle\Entity\User

fos_rest:
    view:
        view_response_listener: force
        force_redirects:
            html: true
        formats:
            jsonp: true
            json: true
            xml: true
            rss: false
        failed_validation: HTTP_BAD_REQUEST
        default_engine: twig

■app/config/security.yml

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
        ROLE_ADMIN: ROLE_USER
        ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
        fos_userbundle:
            id: fos_user.user_provider.username_email

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        admin_area:
            pattern: ^/admin
            anonymous: ~
            form_login:
                login_path: /admin/login
                check_path: /admin/login_check

    access_control:
        - { path: ^/admin/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/api/login.*$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: [ ROLE_ADMIN ] }

■app/config/routing.yml
※FOSUserBundleのインストールに必要

fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

■src/Hoge/FugaBundle/Resources/config/routing.yml

hoge_fuga_admin_index:
    resource: "@HogeFugaBundle/Controller/DefaultController.php"
    type:     annotation
    prefix:   /admin

hoge_fuga_admin_security:
    resource: "@HogeFugaBundle/Controller/SecurityController.php"
    type:     annotation
    prefix:   /admin

■DefaultController.php

namespace Hoge\FugaBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

class DefaultController extends Controller
{
    
    /**
     * @Route("")
     * @Route("/{_all}", name="affiliate_cms_admin_index_all", requirements={"_all" = "^(?!.*(api)).*$"})
     * @Method("GET")
     */
    public function indexAction()
    {
        return $this->render('HogeFugaBundle:Default:index.html.twig');
    }
}

「api」を含まないURLを全てDefaultControllerのIndexActionで拾い、あとはAngularのルーターに任せる。

□□フロントサイド

■adminLoginController.js(ログイン画面で用いるコントローラー)

void function() {
    
    var adminLoginController = function(securityResource, $state, UserService) {
        
        var that = this;
        
        this.data = {};
        
        this.loginFailure = false;
        
        this.login = function() {
            securityResource.save({action: 'login.json'}, {
                username: that.data.username,
                password: that.data.password
            }, function(response) {
                UserService.setUser(response.user);
                $state.go('admin.main.dashboard');
            }, function(response) {
                if (response.status >= 400) {
                    that.loginFailure = true;
                }
            });
        }
    }

    angular
        .module('hogeControllers')
        .controller('adminLoginController', ['securityResource', '$state', 'UserService', adminLoginController])
    ;
    
}();

■adminController.js(ログイン画面を含むログイン後の全てのコントローラーが継承している基底コントローラー)

void function() {
    
    var adminController = function(securityResource, $state, UserService) {

        this.user = null;
        
        var that = this;
        
        this.logout = function() {
            securityResource.save({action: 'logout.json'}, {}, function() {
                UserService.clear();
                $state.go('admin.login');
            });
        }
        
        this.initUser = function() {
            that.user = UserService.getUser();
            if (!that.user) {
                securityResource.save({action: 'loginCheck.json'}, {}, function(response) {
                    that.user = response.user;
                }, function() {
                    UserService.clear();
                    $state.go('admin.login');
                });
            }
        }
    }
    
    angular
        .module('hogeControllers')
        .controller('adminController', ['securityResource', '$state', 'UserService', adminController])
    ;
    
}();

■SecurityResource.js

void function() {
    
    angular
        .module('hogeResources')
        .factory('securityResource', ['$resource', function($resource) {
            return $resource('/admin/api/:action', {
                action: '@action',
            }, {
            }
        )}])
        ;
    
}();

■UserService.js

void function() {
    
    var UserService = function() {
        
        this.user = null;
        
        this.getUser = function() {
            return this.user;
        }
        
        this.setUser = function(user) {
            this.user = user;
        }
        
        this.clear = function() {
            this.user = null;
        }
        
    }
    
    angular
        .module('hogeServices')
        .service('UserService', [UserService])
    ;
    
}();

■app.js

void function() {
    
    angular.module('hogeApp', [
        // コア
        'ngAnimate',
        'ngResource',
        'ngSanitize',
        'ngTouch',
        
        // 必要に応じて
        'ui',
        'ui.utils',
        'ui.router',
        'ui.bootstrap',
        'ui.bootstrap.datetimepicker',
        'kendo.directives',
        'ngCookies',
        'angular-loading-bar',
        'angularFileUpload',
        'cgNotify',
        
        // 自分のやつら
        'hogeConstants',
        'hogeControllers',
        'hogeDirectives',
        'hogeFactories',
        'hogeFilters',
        'hogeResources',
        'hogeServices'
    ]);
    angular.module('hogeConstants', []);
    angular.module('hogeControllers', []);
    angular.module('hogeDirectives', []);
    angular.module('hogeFactories', []);
    angular.module('hogeFilters', []);
    angular.module('hogeResources', []);
    angular.module('hogeServices', []);
    
    /**
     * config section
     */
    angular
        .module('hogeApp')
        .config(['$urlRouterProvider', '$stateProvider', '$locationProvider',
            function($urlRouterProvider, $stateProvider, $locationProvider) {
            $stateProvider
                
                /**
                 * admin
                 */
                .state('admin', {
                    url: '/admin',
                    views: {
                        main: {
                            templateUrl: '/admin.html',
                            controller: 'adminController',
                            controllerAs: 'admin'
                        }
                    }
                })
                
                /**
                 * admin login
                 */
                .state('admin.login', {
                    url: '/login',
                    views: {
                        main: {
                            templateUrl: '/login.html',
                            controller: 'adminLoginController',
                            controllerAs: 'adminLogin'
                        }
                    }
                })
                
                /**
                 * admin main (base template)
                 */
                .state('admin.main', {
                    url: '/',
                    views: {
                        main: {
                            templateUrl: '/main.html',
                            controller: 'adminMainController',
                            controllerAs: 'adminMain'
                        }
                    }
                })
                
                /**
                 * admin main dashboard
                 */
                .state('admin.main.dashboard', {
                    url: 'dashboard',
                    views: {
                        main: {
                            templateUrl: '/dashboard/index.html',
                            controller: 'adminMainDashboardController',
                            controllerAs: 'adminMainDashboard'
                        }
                    }
                })
            ;
            
            $urlRouterProvider.otherwise('/admin/dashboard');
            
            $locationProvider.html5Mode(true).hashPrefix('!');
            
        }]);
    
}();

こんな感じでベースが完成。

めでたしめでたし。

 

【AngularJS】$animateでshakeアニメーションを実装する。

Macのログイン失敗したときのあの動きね。

まずはCSSに下記を定義。
※数値は好みで変更。

@-webkit-keyframes shake {
    0% {transform: translateX(0);}
    12.5% {transform: translateX(-12px) rotateY(-10deg)}
    37.5% {transform: translateX(10px) rotateY(8deg)}
    62.5% {transform: translateX(-6px) rotateY(-4deg)}
    87.5% {transform: translateX(4px) rotateY(2deg)}
    100% {transform: translateX(0)}
}

@keyframes shake {
    0% {transform: translateX(0);}
    12.5% {transform: translateX(-12px) rotateY(-10deg)}
    37.5% {transform: translateX(10px) rotateY(8deg)}
    62.5% {transform: translateX(-6px) rotateY(-4deg)}
    87.5% {transform: translateX(4px) rotateY(2deg)}
    100% {transform: translateX(0)}
}

.shake {
  animation: shake 800ms ease-in-out;
  -webkit-animation: shake 800ms ease-in-out;
}

んでshakeしたいタイミングでこう。

$animate.addClass(element, 'shake', function() {
    $animate.removeClass(element, 'shake');
});

elementは揺らしたい対象要素。

ディレクティブに逃すと更にイイね。

 

【PHPUnit】のclient->request()メソッドで送信するヘッダーを追加したい際の注意。

PHP側で正しくヘッダーを捕捉させたい場合はヘッダ名に「HTTP_」のプレフィックスをつけてやる必要がある。

例)
X-XSRF-TOKENという名前のヘッダーを送信したい場合。

$this->client->request(
    $method,
    $uri,
    [], // parameters
    [], // files
    ['HTTP_X-XSRF-TOKEN' => $token]
);

こんな感じ。

 

【Symfony2.3】You cannot create a service (“request”) of an inactive scope (“request”)、というエラーが出たら。

AppKernelクラスを下記のように拡張してやれば解決する。

class AppKernel extends Kernel {
    public function registerBundles() {
        // ...
    }

    public function registerContainerConfiguration(LoaderInterface $loader) {
        // ...
    }

    protected function initializeContainer() {
        parent::initializeContainer();
        if (PHP_SAPI == 'cli') {
            $this->getContainer()->enterScope('request');
            $this->getContainer()->set('request', new \Symfony\Component\HttpFoundation\Request(), 'request');
        }
    }
}

 

【PHPUnit】はじめてのPHPUnit。

はじめてテスト書くときに参考にしました。

http://qiita.com/imunew/items/39ac9283d030a8c4f9d4