ウェブインコ

インコの技術メモ

CakePHP1.2

日本語化
cd /home/hoge/cake/console
chmod 775 cake
./cake i18n
cd /home/hoge/app/locale/
mkdir -p jpn/LC_MESSAGES
cp default.pot ./jpn\LC_MESSAGES/default.po
default.po の中に対応する文字列を記入してリロードすると変わります。
・この時点で存在しているものは上記のコマンドによりviewsを探して一覧にしてくれていますが、今後できたページに関しては、マネして書き足していけばよいです。
・いったんデバックモードにしないと書き換わらないことが多いです。

デバックモードにしたら盛大にエラーがでる。
Strict Standards: Non-static method Debugger::invoke() should not be called statically, assuming $this from incompatible context in /home/test.hoge.com/cake/libs/debugger.php on line 566 Strict Standards: Non-static method Debugger::getInstance() should not be called statically, assuming $this from incompatible context in /home/test.hoge.com/cake/libs/debugger.php on line 566
別件で、同サーバーをPHPがらみでいろいろいじっていたら、出るようになってしまいました。
デバッグモードじゃなければ問題ないっぽいです。
cakephp1.3で動いている別アプリでは出ないです。

なんだかよくわからないけれど、Non-static method に関する通常の力技で対処。
全部のメソッドを追って行って、片っ端から前に static を付けるとす。
ex.)
function invoke(&$debugger) {

static function invoke(&$debugger) {
このあと、これに関連して次々修正するハメになりますが、debugger.phpとconfigure.php内のいくつかのメソッドを修正するとエラーが全部消えました。

ヘルパーの作り方使い方
ビューの下にファイルを置きます。
/app/views/helpers/hoge.php
例)
<?php
class HogeHelper extends AppHelper
{
 function hoge() {
  return 'wwwww';
 }
 function hage($data=null) {
  App::import('Model', 'Hage');
  $Hage = new Hage();
  $params = array(
   'conditions' => array('status'=>0),
   'fields' => array('id', 'name'),
   'order' => array('created DESC'),
  );
  $hageData = $Hage->find('all', $params);
  $data = array();
  foreach($hageData as $val){
   $data[$val['Hage']['id']] = $val['Hage']['name'];
  }
  return $data;
 }
}
?>

コントローラーで宣言します。
/app/controllers/hoge_controller.php
var $helpers = array('Html', 'Form', 'Hoge');

ビューの任意の場所に書きます。
/app/views/hoges/index.ctp
e($hoge->hoge()); // wwwww
e($form->input('hage', array('type'=>'select', 'options'=>$hoge->hage(), 'label'=>false))); // セレクトボックス

ありがちな文
updated = date("Y-m-d H:i:s");

$hogeData = $this->Hoge->read(null,$id);

$params = array(
'conditions' => array(
'status' => 0,
'or' => array(
array(
'Hoge.start_datetime >='=>$this->data['Hage']['start_time'],
'Hoge.start_datetime <='=>$this->data['Hage']['end_time'],
),
array(
'Hoge.end_datetime >='=>$this->dataData['Hage']['start_time'],
'Hoge.end_datetime <='=>$this->data['Hage']['end_time'],
),
)
),
'fields' => array('id', 'name'),
'order'=>array('Hoge.created' => 'DESC'),
'recursive' => -1,
);
$hogeDatas = $this->Hoge->find('all', $params);

foreach($hogeDatas as $rec){
if(){continue;} // 順番飛ばし
}

foreach ($hash as $key => $val) {
if(){break;} // 終了
}

ラベルをいい感じに
#Form label {display:block; float:left; width:160px;}
form div {clear:left;}

セッションに検索条件を保存
$this->Session->write('searchItems', $searchItems);

セッションに保存された検索条件を読込
$searchItems = $this->Session->read('searchItems');

セッションを破棄
$this->Session->delete('searchItems');

店舗にぶら下がっているユーザーにぶら下がっているレコードとか
App::import('Model', 'User');
$User = new User();
$sql = "SELECT id FROM users WHERE shop_id = 104";
$userData = $User->query($sql);
$user_ids=array();
foreach ($userData as $user_id) {
array_push($user_ids,$user_id['users']['id']);
}
$searchItems['conditions'] = array($this->uses[0].'.user_id' => $user_ids);

その他 form.php の初期設定
セレクトボックスのセパレーターをスペースにして、月名を数字に
/app/views/helpers/form.php L1700付近
$elements = array('Day','Month','Year','Hour','Minute','Meridian');
// $defaults = array(
// 'minYear' => null, 'maxYear' => null, 'separator' => '-',
// 'interval' => 1, 'monthNames' => true
// );
$defaults = array(
'minYear' => null, 'maxYear' => null, 'separator' => ' ',
'interval' => 1, 'monthNames' => false
);

年月日
もう日本の場合以下で良いと思う。いちいちオプション書くの手間だし。
/app/views/helpers/form.php L764
//  $dateFormat = 'MDY';
  $dateFormat = 'YMD';

ラジオボタンの初期値
e($form->radio('status',array('0'=>'使用可','1'=>'使用不可'),array('default' => 1)));
又は
e($form->input('status',array('type' => 'radio', 'options' => array('0'=>'使用可','1'=>'使用不可'), 'default' => 1)));
上記の場合、
DBから取り寄せたデーターが0のとき、default値へ強制的に1が代入されてしまう。
これはcakeのバグ(emptyで判定しているので値が0の時におかしくなる)なので、以下を修正。
appの下にコピーして使うかどうかは任意。
/cake/libs/view/helper.php L598
if (is_array($options)) {
//  if (empty($result) && isset($options['default'])) {
  if (is_null($result) && isset($options['default'])) {
    $result = $options['default'];
  }
  unset($options['default']);
}

文字化け
CakePHPだけの問題ならこれ。
/app/config/database.php
var $default = array( とかに以下の1行を追加。(例はUTF8の場合)
'encoding' => 'utf8',

携帯サイトで、ログインできない
正確には、ログインできたかのように見えて、他のページに遷移したらログインページに戻される、という感じです。
結論から言うと core.php の Session.checkAgent を false にすると良いです。
◆調べ方
app以下で解決しなかった場合、セッション関係なら以下の場所を調べます。
なにかしら推測できることがあれば怪しい場所を重点的に調べますが、皆目検討が付かないときは、もう片っ端から error_log を挟んでしらみつぶしに調べます。
/cake/libs/session.php
/cake/libs/controller/components/auth.php
/cake/libs/controller/components/session.php
◆今回の原因
Session.checkAgent を true にしていると、ユーザーエージェント情報が途中で変わったらセッションを切ります。
ユーザーエージェントが途中で変わるなんてことが普通に有り得るの?
と思いますが、何故かありました。犯人は以下。なぜかこいつが割り込んできます。

Nokia6820/2.0 (4.83) Profile/MIDP-1.0 Configuration/CLDC-1.0 (compatible; Mediapartners-Google/2.1; +http://www.google.com/bot.html)

エラーになるパターンには Google AdSense を貼り付けているページがあったらしいので、何かしら影響を及ぼしているのだと思う。設置した人が何をしたのか、Googleの仕様がどうなのか、を調べ上げるより core.php の設定を変えるほうが手っ取り早いのでそうしようそうしよう。
変なサイトへのリンクが設置されていなければ大丈夫だろう。
セッションジャックが怖ければこのへんに任意のカスタマイズをすれば良いです。

$this->redirect ちゃんとリダイレクトしない
携帯で他サイトにあるリンクから戻ってきたときに、しばらくの間はセッションを維持しようと思い、$this->MOBILE_DATA['uid']を利用して実現しようと思ったら、どうもリダイレクトのところで上手くいかない。
URLを直接入力したらOKだったのだけど。
調べて見たら、/cake/libs/session.php L433 で、セキュリティーレベル medium 以上だと外からのリファラは強制的にセッションを切られていました。
switch ($this->security) {
 case 'high':
  $this->cookieLifeTime = 0;
  if ($iniSet) {
   ini_set('session.referer_check', $this->host);
  }
 break;
 case 'medium':
  $this->cookieLifeTime = 7 * 86400;
  if ($iniSet) {
   ini_set('session.referer_check', $this->host);
  }
 break;
 case 'low':
 default:
  $this->cookieLifeTime = 788940000;
 break;
}
ここらへんをコメントで止めるか、low にします。
或いは任意のカスタマイズを行います。
/cake/以下に手を加えるのは好ましくないのですが、どうしても触らないといけない時もありますので、バカの一つ覚えにならないように。
もしバージョンアップがあればチェックし直すのは言うまでもありません。

正規表現
右辺だけで「AとBいずれにもマッチしない」みたいなもの。
テキストエリアのバリデーションの例。
var $validate = array(
  'title' => array(
    'rule' => 'notEmpty',
    'message' => 'タイトルを入力して下さい',
  ),
  'content' => array(
    array(
      'rule' => 'notEmpty',
      'message' => '内容を入力して下さい',
    ),
    "custom" =>
      array("rule" => array("custom", '/(?=^(?:(?!submit).)*$)(?=^(?:(?!javascript).)*$)/si'),"message" => "不正なコードが含まれています。(submit,javascript等)", "allowEmpty" => true),
  ),
);

画面がまっしろ
Fatal error: Call to undefined function
データーベースの接続がおかしいなら以下を疑う。
・MySQLが起動しているか。
・/app_gal/config/database.php が間違ってないか。
・MySQLにgrant でユーザー・パスワード等を設定しているか。
 (例、GRANT SELECT , INSERT , UPDATE , DELETE ON *.* TO username@"%" IDENTIFIED BY 'passwd' WITH GRANT OPTION;)
・yum install php-mysql したか。
・Apache、MySQL 再起動。

conditions で同じカラムをANDで繋ぐ
スペースを入れます。
$shop_id_array=(1,2,3,4,5);
$shop_id_array2=(2,4,6);
$ShopObj = ClassRegistry::init("Shop");
$shop_data = $ShopObj->find("all", array(
  "fields" => array('id','name','domain'), // 後ろのidの前にスペース
  "conditions" => array('id' => $shop_id_array2, ' id' => $shop_id_array),
  "order" => array('name'),
  )
);

belongToとかをコントローラーで使う直前に宣言する。
function anotherAction() {
 // leader.php モデルファイル内には
 // Leader hasMany Principle がないのでここでは
 // Leader のみ取得します。
 $this->Leader->findAll();
 // bindModel() を使用して Leader モデルに新しい関連を
 // 追加しましょう:
 $this->Leader->bindModel(
  array('hasMany' => array(
   'Principle' => array(
    'className' => 'Principle'
   )
  )
  )
 );
 // 正しく関連付けされたので
 // 1回の find 関数で Leader を取得すると
 // 関連する Principle も取得されます:
 $this->Leader->findAll();
}

belongToとかをコントローラーで使う直前に宣言する。(ページネイト)
$this->Model->unbindModel(array('hasOne'=>array('Model_2')),false);
$this->Model->bindModel(array('hasOne'=>array('Model_3')),false);
$result = $this->paginate('Model');

CSSの変更
/app/webroot/css/cake.generic.css

DBからにランダム表示(3件)する
$home_theme = $this->Theme->find('all', array(
'limit'=>3,
'order'=>'rand()',
));
'order'=>'rand()'はCakePHPではなくMySQLの実装("order by rand()")です。

find('all')とpaginate()の違い
find('all)の引数はハッシュ、paginate()の引数は配列みたいな。
例)
$conditions = array("Entry.contents LIKE" => "%".$contents."%");
$this->paginate($conditions);//1つめの引数が条件文というルール。
if($conditions){$where['conditions']=$conditions;}
$list = $this->Entry->find('all',$where);//'conditions'という文字列で条件文とする。

URLとプログラム
ドメイン/コントローラー名/アクション名/
例えば
http://hoge.net/hoge/index/だと
/controller/hoge_controller.php内のindex()が処理をします。ビューは/views/hoge/index.ctpになります。

既存のデーターで認証ができない
CakePHPのAuthコンポーネントはパスワードを自動的に暗号化してしまうので、既存のパスワードはそのまま使えません。解決方法は以下の2点。
・パスワード暗号化を無効化する。
・既存のパスワードを暗号化する。

コンディションズ
$conditions=array();
$conditions=array("EditorBlogComment.editor_blog_id in (select id from editor_blogs where editor_blog_category_id =$kind and del_flg='0')","del_flg"=>0);
$comments = $this->EditorBlogComment->find('all',array('conditions'=>$conditions,'limit' => 10, 'order' => array('EditorBlogComment.id' => 'desc')));
--------------------
$this->paginate = array('order' => array('Hoge.ranking_pv_pc' => 'desc')); 
--------------------
$conditions = array("Entry.editor_blog_category_id" => 1,"Entry.del_flg" => 0);
$orders = array("Entry.id DESC");
$e_blog = $this->Entry->find('all',array('conditions'=>$conditions,'order' => $orders,'limit' =>3));
--------------------
$conditions = array("or" => array (
 "name LIKE" => "%".$keyword."%",
 "telephone LIKE " => "%".$keyword."%",
 "post_address LIKE " => "%".$keyword."%"
), "id" => $form_shops);
$this->set('shoplists', $this->paginate($conditions));
--------------------
$conditions = array("hoge_id" => $hoges, "Fuga.order_no" => 1, "Fuga.del_flg" => 0);
$proshots = $this->Fuga->find('all',array('conditions'=>$conditions));
--------------------
$orderby = 'Hoge.'.$this->params['form']['orderby'];
$this->paginate = array( 'limit' => 18, 'order' => array($orderby => 'desc')); 
$res = $this->Hoge->find('all',array('conditions'=>$conditions));
--------------------
unset($conditions['personality'][$key]);

コントローラーからビューに変数を渡す
コントローラー側
$this->set('hoge', $hoge);
ビュー側
echo $hoge;
連想配列とかも可です。

コントローラーが無いとき
  //既に作成済みのコントローラーを配列に納めとく。
  $reservednames=array('members', 'posts', 'entries', 'tags'); 

  //リクエストされたコントローラ名を取得
  if(! empty($fromUrl)) list($request_controller) = explode("/", $fromUrl); 

 if(! empty($request_controller)){
  //リクエストされたコントローラ名がサイトで使用されてなければ、、、
  if(array_search($request_controller, $reservednames) ===false){
   //リクエストされたコントローラ名をユーザ名として、
   //マイページ用のコントローラとアクションにアクセスする
   Router::connect('/*', array('controller' => 'users', 'action' => 'index'));
  }
 }

コントローラーの最初で宣言するもの
class HogessController extends AppController {
var $name = 'Hogees'; //自分の名前
var $uses = array('Hoge', 'User'); //使用するテーブル名(頭大文字の単数形)複数選択可
var $helpers = array('Html', 'Form'); //使用するヘルパー名
var $components = array('Confirm'); //使用するコンポーネント名
$users, $helpers, $commponents はいらないなら無くても可。
テーブル名に添った形のコントローラーでテーブルを読まないときは以下。
public $uses = null;

コントローラー名以外のビューとかレイアウトとか
public $layout = 'custom'; // コントローラー全体
function index() {
 $this->layout = 'custom'; // レイアウト変更
 $this->render('index2'); // ビュー変更
 $this->render('index', 'custom'); // ビューまで変えるとき。
}

コンポーネント
コントローラーの部品は以下に置きます。
/app/controllers/components/
ビューの部品
/app/views/helpers/

チェックボックスの作り方
echo $form->input('exclusion_shop', array('options' => $shoplists, 'type' => 'select', 'multiple' => 'checkbox', 'selected' => $selectedshops, 'label' => '対象外の個店'));
selectedをlabelの後ろに書いてたらエラーになっていて悩んだ。

ディレクトリ構造
/app/以下の部分だけ触ります。
/cake/lib/以下に各機能の基ネタがありますので、同じ位置関係にあるファイルを/app/以下にコピーしてくればそちらを優先して見ますので、必要があればコピーしたものに手を加えます。
以下よく触る順番です。
/app/controllers プログラム
/app/views HTMLテンプレート とりあえずこの2つが無いと何も表示されません。
/app/models DBとバリデーション DBの読み書きが有る場合。
/app/webroot ブラウザから見える領域です。デザイン素材とかcssとかjsはここに置きます。
/app/config 基本設定 変わったことをしなければ最初に触るだけです。
/app/vendors cake以外のプログラムを取り込むときなどはここに置きます。

データーベース
テーブル名は英単語の複数形にする必要があります。
自分のシークエンス番号の名前は id 。
別テーブルと関連付けられているシークエンス番号は、対象テーブルの英単語名を単数形にしたもの+id。(リレーション設定時に自動検出してくれます)
作成時間は created 。(自動更新してくれます)
更新時間は updated 。(自動更新してくれます)
名前は name 。(自動で太字にしてくれたりします)
タイトルは title 。(自動で太字にしてくれたりします)

データベースを使わない、DBを使わない
var $uses = null;

デザインの変更
/app/views/コントローラー名/ 各ページの中心部分
/app/views/layouts/ 共通で使用する枠。
/webroot/css/ スタイルシート

デザインの変更
以下を
/cake/console/libs/templates/skel/views/layouts/default.ctp:
以下にコピーして変更すると上書きされます。
/app/views/layouts/default.ctp:

トップページを変更する。
以下のディレクトリとファイルを作って置きます。
/app/views/pages/home.ctp

ビューでモデルを呼ぶ
<?php
App::import('Model', 'Shop');
$shp = new Shop();
e($form->input('shop_id',array('options'=>$shp->selectbox())));
?>

ページネイト
1ページに表示されるレコード数
$this->paginate = array(
  'limit' => 6,
  'conditions' => array(
   'status' => 1
  )
);
$this->set('tablenames', $this->paginate());

ページネイト
<p>
<?php echo $paginator->prev('<< '.__('previous', true), array(), null, array('class'=>'disabled'));?>
<?php echo $paginator->numbers();?>
<?php echo $paginator->next(__('next', true).' >>', array(), null, array('class' => 'disabled'));?>
</p>

<?php
if($paginator->numbers()){
$pagenum=preg_replace('/\|/','',$paginator->numbers());
$pagenum=preg_replace('/<span>/','<li class="left">',$pagenum);
$pagenum=preg_replace('/<\/span>/','</li>',$pagenum);
$pagenum=preg_replace('<span class="current">','<li class="left firstChild">',$pagenum);
e($pagenum);
}
?>

ページネイトした後のパラム
こんなのが付いてきますのでオリジナルのページャーを作ることも可能です。
$this->Paginator->params
    [paging] => Array
        (
            [Album] => Array
                (
                    [page] => 2
                    [current] => 12
                    [count] => 2358
                    [prevPage] => 1
                    [nextPage] => 1
                    [pageCount] => 197
                    [defaults] => Array
                        (
                            [limit] => 12
                            [step] => 1
                            [order] => Array
                                (
                                    [id] => desc
                                )

                            [conditions] => Array
                                (
                                )

                        )

                    [options] => Array
                        (
                            [page] => 2
                            [limit] => 12
                            [order] => Array
                                (
                                    [id] => desc
                                )

                            [conditions] => Array
                                (
                                )

                        )

                )

        )


ページネイトに条件
public $paginate = array(
 'MyModel' => array('limit' => 20,
 'order' => array('week' => 'desc'),
 'group' => array('week', 'home_team_id', 'away_team_id'))
);

ページネイトに他のデーターを付加
// ページネイト
$conditions = array("Hoge.id" => $hoges, "portal_show_flg" => 1, "Hoge.del_flg" => 0);
$hogelist = $this->paginate('Hoge',$conditions);
// ページネイトしたデーターに他のデーターを付加
$i=0;
foreach ($hogelist as $val) {
 $res = $this->Fuga->find('first', array('conditions'=>array('hoge_id'=>$val['Hoge']['id']), 'order'=>array('order_no')));
 $hogelist[$i]['Fuga']['large'] = $res['Fuga']['large'];
 $i++;
}
$this->set('hoges', $hogelist);
$this->render('hoge_result');

ページネイトの数を指定
<?php
if($paginator->numbers()){
$options = array('modulus'=>7); 
$pagenum=preg_replace('/\|/','',$paginator->numbers($options));
$pagenum=preg_replace('/span/','li',$pagenum);
$pagenum=preg_replace('/current/','selected',$pagenum);
e($pagenum);
}
?>

ページネイト時のパラメーター値
GETはビューに以下を書きます。
$paginator->options(array('url' => 
    array('?' => array('kind' => '2'))
));

モデルとコントローラーとビュー
hogesというデーターベースに対する処理を行う一連のセットは基本的に以下になります。
【モデル(対象のDBとバリデーションの設定)】/app/models/hoge.php、
【コントローラー(プログラム(或いは各処理へのリンク集))】/app/controller/hoges_controller.php、
【ビュー(HTMLテンプレート)】/app/views/hoges/index.ctp等
モデルのファイル名だけ単数形です。

モデルの紐付け
エントリーにひもづくカテゴリーを1つ一緒にひっぱってくる。
<?php
class Entry extends AppModel {
 var $name = 'Entry';
 var $belongsTo = array(
  'EntryCategory' => array(
   'className' => 'EntryCategory',
   'foreignKey' => 'entry_category_id',
   'conditions' => array('EntryCategory.del_flg' => 0),
   'fields' => '',
   'order' => ''
  )
 );
}
?>
↑の相方で、この場合、カテゴリーにひもづくエントリーを無数にひっぱってきます。
<?php
class EntryCategory extends AppModel {
 var $name = 'EntryCategory';
 var $hasMany = array(
  'Entry' => array(
   'className' => 'Entry',
   'foreignKey' => 'entry_category_id',
   'dependent' => false,
   'conditions' => '',
   'fields' => '',
   'order' => '',
   'limit' => '',
   'offset' => '',
   'exclusive' => '',
   'finderQuery' => '',
   'counterQuery' => ''
  )
 );
}
?>

リダイレクト
$this->redirect('/orders/confirm');
$this->redirect('/orders/thanks'));
$this->redirect('http://www.example.com');
$this->redirect(array('action' => 'edit', $id));
$this->redirect($this->referer());

リンク
$html->link('リンク先の名前', '/pages/hoge')
<?=$html->link('hoge', '/ddd/eee.html' array('piyo' => 123)) ?>
http://aaa.co.jp/bbb/ccc/
<a href="/bbb/ccc/ddd/eee.html" piyo="123">hoge</a>

ルートディレクトリ
以下になります。ブラウザから見えるところです。
/app/webroot/

下の階層でセットしたセッションが上の階層で見えない
/data/hoge/cake/libs/session.php
// ひとまずここに書けば確実ですが推奨できません。
ini_set('session.cookie_path', '/');
ini_set('session.cookie_lifetime', 0);
ini_set('session.cookie_domain', 'hoge.net');

可変長任意のURL
通常の階層構造を持ったようなURLにしたい。
下記の『複数のアプリを配下に付ける』と似たような感じで、ウェブルートに任意のデ
ィレクトリ構造を作成し、index.phpと.htaccessを置くだけで実現可能です。
階層構造自体が引数になる場合は$_SERVER['REQUEST_URI']を解析する部品を作って噛ま
せてやれば良いと思います。

定数の作り方
色々あると思いますが、一箇所にまとめたかったので以下になります。
/home/hoge/app/config/bootstrap.phpに以下の一行を書きます。
config('const');
/home/hoge/app/config/const.phpを作って以下のような感じに書きます。
<?php
  define("IMGURL", "http://img.hoge.tes/");
  define("IMGDIR", "/home/hoge_img/public_html/");
?>

複数のアプリを配下に付ける
SSL申請料を節約する為にワンドメインで運用することとします。
なので、親アプリの配下に子アプリを複数設置します。
親のアプリ(hoge)
 ウェブルート → /home/hoge/app/webroot/
 アプリ → /home/hoge/app
 cake → /home/hoge/cake
子のアプリ(hage)
 ウェブルート → /home/hoge/app/webroot/hage
 アプリ → /home/hoge/app_hage
 cake → /home/hoge/cake(親のと共用)
 修正箇所
 子のウェブルートにあるindex.phpに以下の修正。
 if (!defined('ROOT')) {
//----------------------------------------------------------------------change
//  define('ROOT', dirname(dirname(dirname(__FILE__))));
  define('ROOT', DS.'home'.DS.'hoge');
 }
/**
 * The actual directory name for the "app".
 *
 */
 if (!defined('APP_DIR')) {
//----------------------------------------------------------------------change
//  define('APP_DIR', basename(dirname(dirname(__FILE__))));
  define ('APP_DIR', 'app_hage');
 }
/**
 * The absolute path to the "cake" directory, WITHOUT a trailing DS.
 *
 */
 if (!defined('CAKE_CORE_INCLUDE_PATH')) {
//----------------------------------------------------------------------change
//  define('CAKE_CORE_INCLUDE_PATH', ROOT);
  define('CAKE_CORE_INCLUDE_PATH', DS.'home'.DS.'hoge');
 }


ウェブアクセスは hoge/hage になります。
各リンクは "/controller/action" 等とすると、ドメイン直下のところに飛んでしまうので、相対パスにするか、
"/hoge/controller/action" とするか、定数を作って
"/<?php e(HOGE) ?>/controller/action" としてください。
cssやjsがおかしくならないのは、CakePHPのヘルパーを使って指定しているからだろう。
フォームもヘルパーを使っているところは何もしなくても期待通りのリンクになっています。

超簡単に作るとき
cd ルート/cake/console
./cake bake
M
エンター → DBの数字 → n → n → y → n
C
DBの数字 → n → y → n → y → n
V
DBの数字 → y → n
Q
/app/以下にファイルが出来るので必要なら任意の場所にmv