Bug(バグ) #1594
完了デイリーニュースにガジェットが追加されている場合、デイリーニュース配信タスク実行時にエラーになる
100%
説明
Overview (現象)¶
デイリーニュースに1つ以上ガジェットが登録されている場合、
デイリーニュース配信タスクを実行するとエラーが表示され、デイリーニュースが1通も送信されない。
実行するタスク¶
$./symfony openpne:send-daily-news
表示されるエラー¶
Call to undefined method myUser::getMember.
再現バージョン¶
OpenPNE¶
- OpenPNE 3.7.0-dev
- OpenPNE 3.6beta12
php¶
- PHP 5.2.13
- PHP 5.3.3
- PHP 5.3.5
Causes (原因)¶
Way to fix (修正内容)¶
報告元¶
http://sns.openpne.jp/diary/25133 より転載
デイリーニュースが送れない、です。 PHP5.3.3の所為??それとも、3.7.0-devだから?? 【環境】 Powered by OpenPNE 3.7.0-dev # ./symfony plugin:list Installed plugins: symfony 1.4.6-stable openpne 3.7.0dev-beta opAuthMailAddressPlugin 1.3.1-devel opAuthMobileUIDPlugin 1.3.0-devel opAuthOpenIDPlugin 1.3.0-beta opCommunityTopicPlugin 1.0.0.2-stable opWebAPIPlugin 0.4.0-beta opDiaryPlugin 1.3.1-beta opBlogPlugin 1.0.1-stable opOpenSocialPlugin 1.2.0.1-stable opAshiatoPlugin 0.9.1-stable opMessagePlugin 0.9.1-beta opAlbumPlugin 0.9.4-beta opIntroFriendPlugin 0.9.0.1-beta opFavoritePlugin 1.0.0.3-beta opRankingPlugin 1.0.0-beta FreeBSD 7.2-RELEASE-p8 Apache/2.2.16 (FreeBSD) PHP 5.3.3 with Suhosin-Patch Zend Engine v2.3.0 mysql 5.1.36 【現象】 コマンドラインで #./symfony openpne:send-daily-news を実行すると Call to undefined method myUser::getMember. というエラーがでて、デイリーニュースが送れない。
原因¶
下記コマンドを実行した時にデイリーニュース用のガジェットが表示可能かのロジックが正しくない.
$ ./symfony openpne:send-daily-news
lib/task/openpneSendDailyNewsTask.class.php の下記部分が実行され,61 行目が実行される.
27 protected function execute($arguments = array(), $options = array()) 28 { ... 56 $filteredGadgets = array(); 57 if ($gadgets) 58 { 59 foreach ($gadgets as $gadget) 60 { 61 if ($gadget->isEnabled()) 62 { 63 $filteredGadgets[] = array( 64 'component' => array('module' => $gadget->getComponentModule(), 'action' => $gadget->getComponentAction()), 65 'gadget' => $gadget, 66 'member' => $member, 67 ); 68 } 69 } 70 }
ここで isEnabled() を見てみると,sfContext で得られる getUser() で使えるかどうかを決定している.しかし,タスクで実行しているためここで得られる User は実際にメールを送信したい Member を含む User ではなくタスクを実行した時の User (ここでは apps/api/lib/myUser.class.php )である.エラー自体はここで User から getMember() を呼び出すことができないという問題であるが,エラーが発生していなくても正しく動作しないものと思われる.
lib/model/doctrine/Gadget.class.php
66 public function isEnabled() 67 { 68 $list = $this->getGadgetConfigList(); 69 if (empty($list[$this->name])) 70 { 71 return false; 72 } 73 74 $controller = sfContext::getInstance()->getController(); 75 if (!$controller->componentExists($this->getComponentModule(), $this->getComponentAction())) 76 { 77 return false; 78 } 79 80 $member = sfContext::getInstance()->getUser()->getMember(); 81 $isEnabled = $this->isAllowed($member, 'view'); 82 83 return $isEnabled; 84 }
修正案¶
表示可能かどうかを見たいメンバを Gadget の isEnable() メソッドの引数に与えるようにする.
diff --git a/lib/model/doctrine/Gadget.class.php b/lib/model/doctrine/Gadget.class.php index 4cb5053..80ab1cd 100644 --- a/lib/model/doctrine/Gadget.class.php +++ b/lib/model/doctrine/Gadget.class.php @@ -63,7 +63,7 @@ class Gadget extends BaseGadget implements opAccessControlRecordInterface return $list[$this->name]['component'][1]; } - public function isEnabled() + public function isEnabled($member = null) { $list = $this->getGadgetConfigList(); if (empty($list[$this->name])) @@ -77,7 +77,10 @@ class Gadget extends BaseGadget implements opAccessControlRecordInterface return false; } - $member = sfContext::getInstance()->getUser()->getMember(); + if (is_null($member)) + { + $member = sfContext::getInstance()->getUser()->getMember(); + } $isEnabled = $this->isAllowed($member, 'view'); return $isEnabled; diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php index f70f082..3a7d9a9 100644 --- a/lib/task/openpneSendDailyNewsTask.class.php +++ b/lib/task/openpneSendDailyNewsTask.class.php @@ -58,7 +58,7 @@ EOF; { foreach ($gadgets as $gadget) { - if ($gadget->isEnabled()) + if ($gadget->isEnabled($member)) { $filteredGadgets[] = array( 'component' => array('module' => $gadget->getComponentModule(), 'action' => $gadget->getComponentAction()),
問題2¶
- 携帯メールアドレスのみを持つメンバを追加する
- 携帯デイリーニュースにガジェットを追加する
- 「symfony openpne:send-daily-news」を実行する
- 下記エラーが発生する
PHP Fatal error: Cannot redeclare class defaultComponents in /home/hoge/sns/36.example.com/apps/mobile_frontend/modules/default/actions/components.class.php on line 50
- 下記エラーが発生する
原因¶
pc_frontend と mobile_frontend の defaultComponent が両方共ロードされることで redeclare としてエラーが発生する状態でした.
修正方針¶
同時に同じクラスがロードされることが原因だったため,実行する php のプロセスをそれぞれ別にすることでこの問題を回避する方針を取ります.
また,この修正のために php のバイナリを探しだす方法として下記 URL 先のものを参考にしました.
http://www.serverphorums.com/read.php?7,415337
https://github.com/sebastianbergmann/phpunit/issues/432
https://github.com/symfony/Process/blob/379b35a41a2749cf7361dda0f03e04410daaca4c/PhpExecutableFinder.php
修正案2¶
コンテキストの変更を 送信時ではなく $gadget->isEnabled() よりも前に行うことで原因で発生するような齟齬が発生しなくなる.
diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php index e1c511f..303e9d6 100644 --- a/lib/task/openpneSendDailyNewsTask.class.php +++ b/lib/task/openpneSendDailyNewsTask.class.php @@ -22,20 +22,58 @@ Call it with: [php symfony openpne:send-birthday-mail|INFO] EOF; + + $this->addOptions( + array( + new sfCommandOption('app', null, sfCommandOption::PARAMETER_OPTIONAL, 'send to pc or mobile', null), + ) + ); } protected function execute($arguments = array(), $options = array()) { parent::execute($arguments, $options); - sfContext::createInstance($this->createConfiguration('pc_frontend', 'prod'), 'pc_frontend'); + $expectedOptions = array('pc_frontend', 'mobile_frontend'); + + if (isset($options['app'])) + { + if (in_array($options['app'], $expectedOptions)) + { + $this->sendDailyNews($options['app']); + } + else + { + throw new Exception('invalid option'); + } + } + else{ + $php = $this->findPhpBinary(); + foreach ($expectedOptions as $app) + { + exec($php.' '.sfConfig::get('sf_root_dir').'/symfony openpne:send-daily-news --app='.$app); + } + } + } + + private function sendDailyNews($app) + { + $isAppMobile = 'mobile_frontend' === $app; + $dailyNewsName = $isAppMobile ? 'mobileDailyNews' : 'dailyNews'; + $context = sfContext::createInstance($this->createConfiguration($app, 'prod'), $app); - $pcGadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('dailyNews'); - $mobileGadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('mobileDailyNews'); + $gadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName($dailyNewsName); + $gadgets = $gadgets[$dailyNewsName.'Contents']; $targetMembers = Doctrine::getTable('Member')->findAll(); foreach ($targetMembers as $member) { + $address = $member->getEmailAddress(); + if ($isAppMobile !== opToolkit::isMobileEmailAddress($address)) + { + continue; + } + $dailyNewsConfig = $member->getConfig('daily_news'); if (null !== $dailyNewsConfig && 0 === (int)$dailyNewsConfig) { @@ -46,13 +84,6 @@ EOF; { continue; } - $address = $member->getEmailAddress(); - $gadgets = $pcGadgets['dailyNewsContents']; - $context = $this->getContextByEmailAddress($address); - if (opToolkit::isMobileEmailAddress($address)) - { - $gadgets = $mobileGadgets['mobileDailyNewsContents']; - } $filteredGadgets = array(); if ($gadgets) @@ -91,4 +122,36 @@ EOF; return in_array($day, opConfig::get('daily_news_day')); } + + private function findPhpBinary() + { + if (defined('PHP_BINARY') && PHP_BINARY) + { + return PHP_BINARY; + } + + if (false !== strpos(basename($php = $_SERVER['_']), 'php')) + { + return $php; + } + + // from https://github.com/symfony/Process/blob/379b35a41a2749cf7361dda0f03e04410daaca4c/PhpExecutableFinder.php + $suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array(''); + foreach ($suffixes as $suffix) + { + if (is_executable($php = PHP_BINDIR.DIRECTORY_SEPARATOR.'php'.$suffix)) + { + return $php; + } + } + + if ($php = getenv('PHP_PEAR_PHP_BIN')) { + if (is_executable($php)) { + return $php; + } + } + + return sfToolkit::getPhpCli(); + } + }
Kiwa Sakai さんが13年以上前に更新
- 題名 を PHP5.3環境でデイリーニュースが送れない から デイリーニュースにガジェットが追加されている場合、デイリーニュース配信タスク実行時にエラーになる に変更
- 説明 を更新 (差分)
同じ現象に遭遇したのでチケットの内容を変更しました。 PHP 5.2.13 の環境でも再現したので、 PHP のバージョンの問題ではないようです。
発生条件¶
自分が確認した限りでは、ガジェット設定でひとつ以上ガジェットが設定されている場合に発生するようです。
OpenPNE 3.6beta12 時点でデイリーニュースに使用できるガジェットは「フレンド最新日記」「フリーエリア」のみですが、
どちらを登録しても発生しました。
問題の処理¶
lib/task/openpneSendDailyNewsTask.class.php
61 if ($gadget->isEnabled())
この部分の、 isEnabled() 処理内で発生しているようです。
lib/model/doctrine/Gadget.class.php
80 $member = sfContext::getInstance()->getUser()->getMember();
Yuya Watanabe さんがほぼ13年前に更新
- ステータス を New(新規) から Accepted(着手) に変更
- 担当者 を Yuya Watanabe にセット
Yuya Watanabe さんがほぼ13年前に更新
- ステータス を Accepted(着手) から Pending Review(レビュー待ち) に変更
- 進捗率 を 0 から 50 に変更
更新履歴 d67e117ceb908f029519712d4007a25ddcb5e2b3 で適用されました。
Kousuke Ebihara さんがほぼ13年前に更新
- ステータス を Pending Review(レビュー待ち) から Pending Testing(テスト待ち) に変更
- 進捗率 を 50 から 70 に変更
どのアプリケーションが選択されても動作するようにするという意味で、この修正は妥当だと思います。
ここで採ったアプローチの他にも、タスクが使用するアプリケーションや環境を固定するという方法がありえたことを参考までに記しておきます。たとえばタスクが特定のアプリケーションに依存しきっているようなシチュエーションでは、そのようなアプローチを採るほうがより適切です(今回の問題はどちらでもよいと思います)
Yuya Watanabe さんがほぼ13年前に更新
- ステータス を Pending Testing(テスト待ち) から Pending Fixing(修正待ち) に変更
- 進捗率 を 70 から 0 に変更
問題2¶
- 携帯メールアドレスのみを持つメンバを追加する
- 携帯デイリーニュースにガジェットを追加する
- 「symfony openpne:send-daily-news」を実行する
- 下記エラーが発生する
PHP Fatal error: Cannot redeclare class defaultComponents in /home/hoge/sns/36.example.com/apps/mobile_frontend/modules/default/actions/components.class.php on line 50
- 下記エラーが発生する
原因¶
$gadget->isEnabled() の中の sfContext::getInstance()->getController()->componentExists() でコンポーネントの存在確認される際にロードされるコンポーネントのアプリケーションが実際に送信する際のコンテキストとは別の場合があることが原因であると思われる.そのため,最初に pc_frontend で sfContext::createInstance() が呼び出されているので携帯メールアドレス向けにデイリー・ニュースを送信しようとするとエラーが発生する.直前の sfContext::createInstance() と componentExists() のインスタンスが一致する場合はエラーが発生しないため,PCメールアドレスに送信するときにエラーが発生しない.
具体的には,member_id=1 のメンバが $member->getEmailAddress() でPCメールアドレスを取得でき,member_id=2 のメンバが携帯メールアドレスを取得できるとするとき,下記のような感じで直前に生成されたコンテキストと送信時のコンテキストが一段階ずつずれている.- 最初にpc_frontendがコンテキストで設定される (openpneSendDailyNewsTask.class.php 31行目)
- componentExists時: 未 送信時: 未
- componentExists() が呼び出される (openpneSendDailyNewsTask.class.php 61行目)
- componentExists時: pc_frontend 送信時: 未
- opBaseMailTask::getContextByEmailAddress() 内で sfContext::createInstance() が呼び出される (openpneSendDailyNewsTask.class.php 72行目)
- componentExists時: pc_frontend 送信時: pc_frontend
- opMailSend::sendTemplateMail() が呼び出される (openpneSendDailyNewsTask.class.php 80行目)
- componentExists時: pc_frontend 送信時: pc_frontend -> メールが送信される
- opBaseMailTask::getContextByEmailAddress() 内で sfContext::createInstance() が呼び出される (openpneSendDailyNewsTask.class.php 72行目)
- componentExists時: pc_frontend 送信時: mobile_frontend
- opMailSend::sendTemplateMail() が呼び出される (openpneSendDailyNewsTask.class.php 80行目)
- componentExists時: pc_frontend 送信時: mobile_frontend -> エラーが発生する
lib/task/openpneSendDailyNewsTask.class.php
30 31 sfContext::createInstance($this->createConfiguration('pc_frontend', 'prod'), 'pc_frontend'); 32 ... 60 { 61 if ($gadget->isEnabled($member)) 62 { ... 71 72 $context = $this->getContextByEmailAddress($address); 73 $params = array( 74 'member' => $member, 75 'gadgets' => $filteredGadgets, 76 'subject' => $context->getI18N()->__('デイリーニュース'), 77 'today' => time(), 78 ); 79 80 opMailSend::sendTemplateMail('dailyNews', $address, opConfig::get('admin_mail_address'), $params, $context);
lib/task/opBaseSendMailTask.class.php
44 protected function getContextByEmailAddress($address) 45 { 46 $application = 'pc_frontend'; 47 if (opToolkit::isMobileEmailAddress($address)) 48 { 49 $application = 'mobile_frontend'; 50 } 51 52 if (!sfContext::hasInstance($application)) 53 { 54 $context = sfContext::createInstance($this->createConfiguration($application, 'prod'), $application); 55 } 56 else 57 { 58 $context = sfContext::getInstance($application); 59 } 60 61 return $context; 62 }
修正案2¶
コンテキストの変更を 送信時ではなく $gadget->isEnabled() よりも前に行うことで原因で発生するような齟齬が発生しなくなる.
diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php index 3a7d9a9..e1c511f 100644 --- a/lib/task/openpneSendDailyNewsTask.class.php +++ b/lib/task/openpneSendDailyNewsTask.class.php @@ -48,6 +48,7 @@ EOF; } $address = $member->getEmailAddress(); $gadgets = $pcGadgets['dailyNewsContents']; + $context = $this->getContextByEmailAddress($address); if (opToolkit::isMobileEmailAddress($address)) { $gadgets = $mobileGadgets['mobileDailyNewsContents']; @@ -69,7 +70,6 @@ EOF; } } - $context = $this->getContextByEmailAddress($address); $params = array( 'member' => $member, 'gadgets' => $filteredGadgets,
Yuya Watanabe さんがほぼ13年前に更新
- ステータス を Accepted(着手) から Pending Review(レビュー待ち) に変更
- 進捗率 を 0 から 50 に変更
更新履歴 7f1145dd3cca38ee7cc2bb9b9c3a149d8c638b3f で適用されました。
Yuya Watanabe さんがほぼ13年前に更新
問題¶
member_id が最も若いメンバが携帯メールアドレスのみを持っている場合は その後のメンバがどんなメールアドレスを持っていようと成功するのですが,
member_id が最も若いメンバがメンバが PC メールアドレスを持っているときに note-11 と同様のエラーが発生するようになっていました.
note-11 で書いたようなものが原因だったわけではなく,やはり pc_fronend と mobile_frontend の defaultComponent が両方共ロードされることで redeclare としてエラーが発生する状態でした.
member_id が最も若いメンバが携帯メールアドレスのみを持っている場合が成功していた理由は,コンテキストの切り替えが完全ではなく,運良く動いていた状態であったためです.
これは別の問題として取り扱うべきとして,本チケットでは回避策を適用します.
修正方針¶
同時に同じクラスがロードされることが原因だったため,実行する php のプロセスをそれぞれ別にすることでこの問題を回避する方針を取ります.
また,この修正のために php のバイナリを探しだす方法として下記 URL 先のものを参考にしました.
http://www.serverphorums.com/read.php?7,415337
https://github.com/sebastianbergmann/phpunit/issues/432
https://github.com/symfony/Process/blob/379b35a41a2749cf7361dda0f03e04410daaca4c/PhpExecutableFinder.php
修正内容¶
diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php index e1c511f..9de9d5c 100644 --- a/lib/task/openpneSendDailyNewsTask.class.php +++ b/lib/task/openpneSendDailyNewsTask.class.php @@ -22,20 +22,48 @@ Call it with: [php symfony openpne:send-birthday-mail|INFO] EOF; + + $this->addOptions( + array( + new sfCommandOption('app', null, sfCommandOption::PARAMETER_OPTIONAL, 'send to pc or mobile', null), + ) + ); } protected function execute($arguments = array(), $options = array()) { parent::execute($arguments, $options); + if (isset($options['app'])) + { + $this->sendDailyNews($options['app']); + } + else{ + $php = $this->findPhpBinary(); + foreach (array('pc_frontend', 'mobile_frontend') as $app) + { + exec($php.' '.sfConfig::get('sf_root_dir').'/symfony openpne:send-daily-news --app='.$app); + } + } + } - sfContext::createInstance($this->createConfiguration('pc_frontend', 'prod'), 'pc_frontend'); + private function sendDailyNews($app) + { + $isPc = 'pc_frontend' === $app; + $dailyNewsName = $isPc ? 'dailyNews' : 'mobileDailyNews'; + $context = sfContext::createInstance($this->createConfiguration($app, 'prod'), $app); - $pcGadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('dailyNews'); - $mobileGadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('mobileDailyNews'); + $gadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName($dailyNewsName); + $gadgets = $gadgets[$dailyNewsName.'Contents']; $targetMembers = Doctrine::getTable('Member')->findAll(); foreach ($targetMembers as $member) { + $address = $member->getEmailAddress(); + if (!($isPc ^ opToolkit::isMobileEmailAddress($address))) + { + continue; + } + $dailyNewsConfig = $member->getConfig('daily_news'); if (null !== $dailyNewsConfig && 0 === (int)$dailyNewsConfig) { @@ -46,13 +74,6 @@ EOF; { continue; } - $address = $member->getEmailAddress(); - $gadgets = $pcGadgets['dailyNewsContents']; - $context = $this->getContextByEmailAddress($address); - if (opToolkit::isMobileEmailAddress($address)) - { - $gadgets = $mobileGadgets['mobileDailyNewsContents']; - } $filteredGadgets = array(); if ($gadgets) @@ -91,4 +112,36 @@ EOF; return in_array($day, opConfig::get('daily_news_day')); } + + private function findPhpBinary() + { + if (defined('PHP_BINARY') && PHP_BINARY) + { + return PHP_BINARY; + } + + if (false !== strpos(basename($php = $_SERVER['_']), 'php')) + { + return $php; + } + + // from https://github.com/symfony/Process/blob/379b35a41a2749cf7361dda0f03e04410daaca4c/PhpExecutableFinder.php + $suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array(''); + foreach ($suffixes as $suffix) + { + if (is_executable($php = PHP_BINDIR.DIRECTORY_SEPARATOR.'php'.$suffix)) + { + return $php; + } + } + + if ($php = getenv('PHP_PEAR_PHP_BIN')) { + if (is_executable($php)) { + return $php; + } + } + + return sfToolkit::getPhpCli(); + } + }
Kousuke Ebihara さんがほぼ13年前に更新
- ステータス を Pending Review(レビュー待ち) から Rejected(差し戻し) に変更
- 50 行目の else がコーディング規約違反です
- 148 行目と 149 行目の if がコーディング規約違反です
Yuya Watanabe さんがほぼ13年前に更新
- ステータス を Rejected(差し戻し) から Pending Review(レビュー待ち) に変更
更新履歴 d6937c1c57ec8e25a9223592ea372c1d5cab7615 で適用されました。
Kousuke Ebihara さんがほぼ13年前に更新
- ステータス を Pending Review(レビュー待ち) から Pending Testing(テスト待ち) に変更
- 進捗率 を 50 から 70 に変更
Shouta Kashiwagi さんがほぼ13年前に更新
- ステータス を Pending Testing(テスト待ち) から Fixed(完了) に変更
- 進捗率 を 70 から 100 に変更
テストOKです。