Twitter Bootstrap を使う際に a:link などに color を設定するとボタンの色まで上書きされちゃう問題の対策

1年ぶりくらいに更新しますが、引き続きクロコスにおりまして (てかこの前の記事が去年の7月なんでその後ヤフーに買収されるなどいろいろあったりしましたが) 、まあまあ元気にやっております。

      • -

さて本題。普段 CSS 書くときは大抵、 Twitter Bootstrap からいくつかの LESS をインポートしつつ、必要に応じてスタイルを上書きする形式をとっています。
そんな中、なんかどうしてもボタン(buttons.less でスタイルをあてているやつ)の文字色が反映されない状況に陥りまして、なんでだろうと調べて一応解決したので書いておきます。

問題の概要

今回のケースでは、デフォルトの文字色を次のように設定していました。

@import "bootstrap/buttons.less";

a, a:link {
  color: #666666;
}
a:visited {
  color: #999999;
}

Bootstrap では .btn クラスをつけるとボタンの形式になり、 .btn-primary のようなボタンに意味を付与するクラスをつけると意味に応じた色がつく、といった仕組みになっています。

/* bootstrap/buttons.less */
.btn {
  ...
}
.btn-primary {
  color: #ffffff;
}

で、例えばこの状態で次のようにHTMLがあるとします。

<p><a href="#">いえーい</a></p>

<p><a href="#" class="btn btn-primary">いえーい</a></p>

実際にこの状態で画面を見てみると次のようになります。

f:id:Fivestar:20131113204105p:plain

.btn-primary を設定しているのに文字色が #ffffff になっていません。

僕は最初、「CSS は要素よりもクラスの方が優先度が高いはずなので .btn-primary が適応されないのはおかしい」と思ってしまったのですが、よくよく調べたら擬似クラスもクラスと同じだけ重み付けされるんで、「要素 + 擬似クラス」と「クラス」の差で a:link などのスタイルが単なるクラスのみの指定である .btn-primary より優先されてしまった、ということでした。

対策

で、最初は .btn に疑似クラスや要素をつけて上書きする方法を考えたのですが、無駄な感じでやりたくないなーと思って別の方法を探した結果、否定疑似クラス (:not()) を使えばもっとも簡単に制御できるという結論にいたりました。

a:not(.btn), a:link:not(.btn) {
  color: #666666;
}
a:visited:not(.btn) {
  color: #999999;
}

このようにしておくと .btn の色に干渉することがなくなるので、デフォルトのボタン色がそのまま使われるようになります。

f:id:Fivestar:20131113205539p:plain

Bootstrap みたいな CSS フレームワークを用いる際、特定のスタイルに干渉してほしくない場合なんかは :not() 知っておくとよさそうです。

Symfony勉強会#6で効率本の紹介などをしてきました

6/30に行われた"日本Symfonyユーザー会"主催のSymfony勉強会に参加&発表してきました。

今回はメインセッションで「効率的なWebアプリケーションの作り方」について話してよいとのことだったので、本を書くに至った経緯などを小一時間お話ししました。それともう1本、Symfonyアーキテクチャについて、主にsymfony 1系のことを知ってる人を対象にお話ししました。以下スライドです。

それと、LTやるときにタイマーうんぬん言ってて、アプリを入れるのも面倒だし、と思ってその場で作りました。ブラウザから見られます。スマホでもたぶん大丈夫です。あと、一応ドラの音がでるんですけど、音源側の音量が少ないのでいいやつがあったらPull Requestください。

LT.Timer

雑感

今回も長丁場でしたが、参加者の皆さんお疲れ様でした。朝から晩まで、ずっと誰かしゃべってるというハードな会でしたけど、充実していて楽しかったです。ただ2回もしゃべるのはとても疲れたので、次は別の方(できればこれまでに発表したことない方!)が登壇してくれるといいなーと思います。

さいごに

スタッフのみなさま、お疲れ様でした。そして、会場提供していただいたVOYAGE GROUPさん、今回もありがとうございました。

「効率的なWebアプリケーションの作り方」を執筆した経緯など

僕は今年でエンジニアとして6年目になるのですが、昔は深く勉強したわけでもないのにMVCオブジェクト指向もわかってる気でいて、デザインパターンや設計原則の話を聞いても「難しいこといってて意味わからないし、Javaだけでやってれば?」なんて思っていた時期がありました。

4年目くらいのとき、当時在籍していたアシアルでPHPスクールでオブジェクト指向編の講師を務めることになったため渋々設計原則やデザインパターンを調べていたところ、目からうろこが落ちることばかりでした。

それまでは、複雑な処理を実装しようとした際にひたすら考えた挙げ句、うまく実装したつもりでメンテナンスが困難なコードになったりしたこともあったのですが、原則やパターンを学んだことで難しい場面でも悩むことも減ったし、コード自体もより素直で扱いやすいようになっていきました。

そういったことがあって、現場の苦労を減らせるよう、現場の開発者に向けて書いた本が「効率的なWebアプリケーションの作り方」です。主に「現場の初心者」の方々がよりスムーズにコードを書けるよう、前述のような原則などの知識から実際にアプリを開発する流れを1冊で解説しています。基礎知識を身につけたい、改めて学習したいという方はぜひ読んでみてください。

効率的なWebアプリケーションの作り方 ~PHPによるモダン開発入門

効率的なWebアプリケーションの作り方 ~PHPによるモダン開発入門

PHP勉強会@東京#58でPhakeの紹介をしました

第58回PHP勉強会@東京 - events.php.gr.jp

久々のPHP勉強会だったのでPhakeについて発表をしてきました。とても久々のPHP勉強会でしたが、懇親会で色々お話もできて楽しかったです。

訂正

Phake::mock()の第2引数以降がうんたら書いてますけど、第2引数以降がコンストラクタの引数になるのはパーシャルモックのときだけでした。


id:sotarokPHP 5.4の話のときにもいってたんですけど、「ゲッターセッター用意するの面倒だからAccessorってトレイトつくったよ!」「おれも!」みたいなことがあったりして、考えることはまったく同じですね。僕が作ってたときのソースさらしておきます。__call()をトレイトで実装しちゃうと、__call()が衝突しちゃうので一応メソッドわけてます。MagicCallMixerトレイトとか作ったけど消しちゃったらしい。

<?php
namespace Fivestar;

trait Accessor
{
    public function __call($method, $args)
    {
        return $this->callMagicAccessor($method, $args);
    }

    public function magicCallAccessor($method, $args)
    {   
        $processMethod = strtolower($method);
        if ('set' === ($verb = substr($processMethod, 0, 3)) 
            || 'get' === ($verb = substr($processMethod, 0, 3)) 
            || 'is' === ($verb = substr($processMethod, 0, 2)) 
        ) { 
            $camelName = lcfirst(substr($method, strlen($verb)));
            $underscoreName = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $camelName));

            $property = null;
            if (property_exists($this, $camelName)) {
                $property = $camelName;
            } elseif (property_exists($this, $underscoreName)) {
                $property = $underscoreName;
            }   

            if (null !== $property) {
                if ('set' === $verb) {
                    $this->{$property} = array_shift($args);

                    return;
                } elseif ('get' === $verb || 'is' === $verb) {
                    return $this->{$property};
                }   
            }   
        }   

        throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s', get_class($this), $method));
    } 
}

最後に、会場を提供していただいたVOYAGE GROUPさん、ありがとうございました。

CrocosSecurityBundleの新リリースについて

crocos/CrocosSecurityBundle · GitHub

CrocosSecurityBundleは認証管理用のバンドルで、Symfony2標準のSecurityBundleが複雑で使いづらかったのでシンプルに管理できるように作成したものです。

最初のリリース後に何回かアップデートを重ねていまして、公開当初よりも機能が増えたりしています。本日バーション1.4をリリースしたので、これまでリリースとあわせて紹介します。

1.2

Comparing 1.0...1.2 · crocos/CrocosSecurityBundle · GitHub

1.1はhotfixで使ったので2回目のリリースは1.2になります。ここでの主な変更は、AuthException例外の追加とリファクタリングバグフィックスになります。AuthExceptionはどこかでスローされると、ログイン画面へ遷移(forward)するようになっています。

<?php

use Crocos\SecurityBundle\Exception\AuthException;

$security = $this->get('crocos_security.context');

if (!$security->isAuthenticated()) {
    throw new AuthException('Authentication required');
}

1.3

Comparing 1.2...1.3 · crocos/CrocosSecurityBundle · GitHub

1.3から、Twigのテンプレート内でSecurityContextオブジェクトが _security 変数として参照できるようになりました。

{% if _security.isAuthenticated %}
  <p>Logged in as {{ _security.user }}</p>
{% endif %}

1.4

Comparing 1.3...1.4 · crocos/CrocosSecurityBundle · GitHub

本日リリースした1.4では、Basic認証のサポート、エンティティでログインしたい人向けのSessionEntityAuthの追加などを行いました。

Basic認証のサポート

SecureConfigアノテーションにbasic属性を指定するだけでBasic認証が行われるようになります。basic属性には"ユーザ名:パスワード"形式の文字列を渡すだけです。

/**
 * @SecureConfig(domain="admin", basic="admin:adminpass")
 */

現時点ではコンテナパラメータなどは使えないので設定は固定になります。"%basic_user%:%basic_pass%"のような指定ができてもよいかなと思いますが、とりあえず要望があるまでは保留です。

SessionEntityAuth

SessionEntityAuthはデフォルトの認証ロジックであるSessionAuthの拡張です。SessionAuthはログイン状態をセッションで管理する仕組みで、具体的にはセッションにログインフラグとユーザ情報を格納しておくだけなのですが、このうちユーザ情報はログイン時に渡された値をそのままセッションに格納しているため、エンティティを渡したときにあまりよろしくないことが起こっていました。そこで作成したのが今回のSessionEntityAuthです。

SessionAuthだと何が悪いかというと、1つはオブジェクトがそのままシリアライズされることと、2つめがデシリアライズした時にエンティティがDoctrineの管理対象にならない点です。これらを回避するため、SessionEntityAuthではIDとクラス名のみをセッションへ入れて、セッションから復元する際にクラス名を元にリポジトリを特定し、IDを元にデータを取得しています。これによりセッションには2つのスカラ値が格納されるだけで済み、常にDoctrineの管理対象になるため、更新などもスムーズに行えるようになります。

SessionEntityAuthを使う場合はauth属性に"session.entity"を指定します。

/**
 * @SecureConfig(auth="session.entity")
 */

またログインユーザを表すエンティティにはgetId()メソッドが実装されている必要があります。また、ID以外にも何らかの状態(削除フラグなど)をチェックしなければならない場合は、エンティティにisEnabled()メソッドを実装して、その中で状態を判別して、ログイン状態を復元させない場合はfalseを返すようにします。


現時点で僕が必要な機能はある程度そろっており、あとは要望があった時か、バグなどがあったときに拡張していこうかと思います。

あと、READMEを英語に翻訳してくれる方募集中です。

asset()関数の第2引数に"request"を指定すると絶対URLを返す拡張

fivestar/FivestarAssetsExtraBundle - GitHub

テンプレートで次のように、asset()関数の第2引数にrequestと指定すると、絶対URLとして展開します。

<img src="{{ asset('path/to/image.png', 'request') }}" alt="..." />

スマートな方法があればそれでPull Requestでも送ってたのですが、この方法はどうもいけてない感じがして手元で止めていました。

これを作ったのはもうだいぶ前なのですが、今はasset()関数の戻り値を絶対URLにする方法があるのでしょうか。最近Symfonyの情報を追っていないのでこういう細かい部分がどうなってるかがさっぱりわかりません。

DIコンテナが生成されたあとに何らかの処理を実行する方法

DIコンテナの定義がすべて読み込まれ使用する準備が整ってから、リクエストの処理が始まるまでの間に、特定のメソッドを呼び出したかったり、手動でDIコンテナの設定をしたい場合ってあると思います。
そんなときはバンドルクラスにboot()メソッドを実装すると大体解決します。

たとえば、EntityRepositoryに何かオブジェクトをDIしたいとします。ですがEntityRepositoryはEntityManagerから取得するようになっており、DIコンテナからだと設定できなくなっています。
そこで次のようにboot()メソッド内部で直接設定しておけば、それ以降オブジェクトがセットされた状態で使えます。

<?php

namespace Acme\DemoBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeDemoBundle extends Bundle
{
    public function boot()
    {
        $userRepository = $this->container->get('doctrine')->getRepository('AcmeDemoBundle:User');
        
        $userRepository->setFooService($this->container->get('acme_demo.foo_service'));
    }
}

上記のように、設定ファイルベースではどうやっていいかわからない場合や、処理が始まる場合に共通で呼び出しておきたい処理などがある場合など、boot()メソッドを知っておくと操作の幅が広がるので、知っておくとよいと思います。