プロジェクト

全般

プロフィール

Bug(バグ) #1594

Yuya Watanabe約12年前に更新

h3. Overview (現象)

デイリーニュースに1つ以上ガジェットが登録されている場合、
デイリーニュース配信タスクを実行するとエラーが表示され、デイリーニュースが1通も送信されない。

h5. 実行するタスク

<pre>
$./symfony openpne:send-daily-news
</pre>

h5. 表示されるエラー

<pre>
Call to undefined method myUser::getMember.
</pre>

h3. 再現バージョン

h5. OpenPNE

* OpenPNE 3.7.0-dev
* OpenPNE 3.6beta12

h5. php

* PHP 5.2.13
* PHP 5.3.3
* PHP 5.3.5

h3. Causes (原因)

h3. Way to fix (修正内容)

h3. 報告元

http://sns.openpne.jp/diary/25133 より転載

<pre>
デイリーニュースが送れない、です。
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.

というエラーがでて、デイリーニュースが送れない。
</pre>

h3. 原因

下記コマンドを実行した時にデイリーニュース用のガジェットが表示可能かのロジックが正しくない.
<pre>
$ ./symfony openpne:send-daily-news
</pre>

lib/task/openpneSendDailyNewsTask.class.php の下記部分が実行され,61 行目が実行される.
<pre>
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 }
</pre>

ここで isEnabled() を見てみると,sfContext で得られる getUser() で使えるかどうかを決定している.しかし,タスクで実行しているためここで得られる User は実際にメールを送信したい Member を含む User ではなくタスクを実行した時の User (ここでは apps/api/lib/myUser.class.php )である.エラー自体はここで User から getMember() を呼び出すことができないという問題であるが,エラーが発生していなくても正しく動作しないものと思われる.

lib/model/doctrine/Gadget.class.php
<pre>
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 }
</pre>

h3. 修正案

表示可能かどうかを見たいメンバを Gadget の isEnable() メソッドの引数に与えるようにする.
<pre>
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()),
</pre>

h2. 問題2

# 携帯メールアドレスのみを持つメンバを追加する
# 携帯デイリーニュースにガジェットを追加する
# 「symfony openpne:send-daily-news」を実行する
** 下記エラーが発生する
<pre>
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
</pre>

h3. 原因

pc_frontend $gadget->isEnabled() の中の sfContext::getInstance()->getController()->componentExists() でコンポーネントの存在確認される際にロードされるコンポーネントのアプリケーションが実際に送信する際のコンテキストとは別の場合があることが原因であると思われる.そのため,最初に pc_frontend で sfContext::createInstance() が呼び出されているので携帯メールアドレス向けにデイリー・ニュースを送信しようとするとエラーが発生する.直前の sfContext::createInstance()mobile_frontend の defaultComponent が両方共ロードされることで redeclare としてエラーが発生する状態でした.

h3. 修正方針
componentExists() のインスタンスが一致する場合はエラーが発生しないため,PCメールアドレスに送信するときにエラーが発生しない.

同時に同じクラスがロードされることが原因だったため,実行する php のプロセスをそれぞれ別にすることでこの問題を回避する方針を取ります.

また,この修正のために php のバイナリを探しだす方法として下記 URL 先のものを参考にしました.

http://www.serverphorums.com/read.php?7,415337
具体的には,member_id=1 のメンバが $member->getEmailAddress() でPCメールアドレスを取得でき,member_id=2 のメンバが携帯メールアドレスを取得できるとするとき,下記のような感じで直前に生成されたコンテキストと送信時のコンテキストが一段階ずつずれている.
https://github.com/sebastianbergmann/phpunit/issues/432 # 最初にpc_frontendがコンテキストで設定される (openpneSendDailyNewsTask.class.php 31行目)
https://github.com/symfony/Process/blob/379b35a41a2749cf7361dda0f03e04410daaca4c/PhpExecutableFinder.php

h3. 修正案2

コンテキストの変更を 送信時ではなく $gadget->isEnabled() よりも前に行うことで原因で発生するような齟齬が発生しなくなる.
** componentExists時: 未 送信時: 未
<pre> # componentExists() が呼び出される (openpneSendDailyNewsTask.class.php 61行目)
diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php ** componentExists時: pc_frontend 送信時: 未
index e1c511f..303e9d6 100644 # opBaseMailTask::getContextByEmailAddress() 内で sfContext::createInstance() が呼び出される (openpneSendDailyNewsTask.class.php 72行目)
--- a/lib/task/openpneSendDailyNewsTask.class.php ** componentExists時: pc_frontend 送信時: pc_frontend
+++ b/lib/task/openpneSendDailyNewsTask.class.php # opMailSend::sendTemplateMail() が呼び出される (openpneSendDailyNewsTask.class.php 80行目)
@@ -22,20 +22,58 @@ Call it with:

[php symfony openpne:send-birthday-mail|INFO]
EOF;
** componentExists時: pc_frontend 送信時: pc_frontend -> メールが送信される
+ # opBaseMailTask::getContextByEmailAddress() 内で sfContext::createInstance() が呼び出される (openpneSendDailyNewsTask.class.php 72行目)
+ $this->addOptions( ** componentExists時: pc_frontend 送信時: mobile_frontend
+ array( # opMailSend::sendTemplateMail() が呼び出される (openpneSendDailyNewsTask.class.php 80行目)
+ new sfCommandOption('app', null, sfCommandOption::PARAMETER_OPTIONAL, 'send to pc or mobile', null), ** componentExists時: pc_frontend 送信時: mobile_frontend -> エラーが発生する

lib/task/openpneSendDailyNewsTask.class.php

+ )
+ );
}

protected function execute($arguments = array(), $options = array())
{
parent::execute($arguments, $options);

-
<pre>
30
31
sfContext::createInstance($this->createConfiguration('pc_frontend', 'prod'), 'pc_frontend');
32

+ $expectedOptions = array('pc_frontend', 'mobile_frontend');
+
+ if (isset($options['app']))
+
...
60
{
+

61
if (in_array($options['app'], $expectedOptions))
+
($gadget->isEnabled($member))
62
{
+ $this->sendDailyNews($options['app']);
+ }
+ else
+ {
+ throw new Exception('invalid option');
+ }
+ }
+ else{
+ $php
...
71
72 $context
= $this->findPhpBinary();
+ foreach ($expectedOptions as $app)
+ {
+ exec($php.' '.sfConfig::get('sf_root_dir').'/symfony openpne:send-daily-news --app='.$app);
+ }
+ }
+
$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);

+ </pre>

lib/task/opBaseSendMailTask.class.php

+ private <pre>
44 protected
function sendDailyNews($app)
+
getContextByEmailAddress($address)
45
{
+ $isAppMobile

46 $application
= 'mobile_frontend' === $app;
+ $dailyNewsName
'pc_frontend';
47 if (opToolkit::isMobileEmailAddress($address))
48 {
49 $application
= $isAppMobile ? 'mobileDailyNews' : 'dailyNews';
+
'mobile_frontend';
50 }
51
52 if (!sfContext::hasInstance($application))
53 {
54
$context = sfContext::createInstance($this->createConfiguration($app, sfContext::createInstance($this->createConfiguration($application, 'prod'), $app);

- $pcGadgets
$application);
55 }
56 else
57 {
58 $context
= Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('dailyNews'); sfContext::getInstance($application);
59 }
60
61 return $context;
62 }

- $mobileGadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('mobileDailyNews'); </pre>

h3. 修正案2

コンテキストの変更を 送信時ではなく $gadget->isEnabled() よりも前に行うことで原因で発生するような齟齬が発生しなくなる.

+ $gadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName($dailyNewsName); <pre>
+ $gadgets = $gadgets[$dailyNewsName.'Contents'];

$targetMembers = Doctrine::getTable('Member')->findAll();
foreach ($targetMembers as $member)
{
diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php
+ $address = $member->getEmailAddress(); index 3a7d9a9..e1c511f 100644
+ if ($isAppMobile !== opToolkit::isMobileEmailAddress($address)) --- a/lib/task/openpneSendDailyNewsTask.class.php
+ { +++ b/lib/task/openpneSendDailyNewsTask.class.php
+ continue;
+ }
+
$dailyNewsConfig = $member->getConfig('daily_news');
if (null !== $dailyNewsConfig && 0 === (int)$dailyNewsConfig)
{
@@ -46,13 +84,6 -48,6 +48,7 @@ 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 -69,7 +70,6 @@ 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
$context = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array('');
+ foreach ($suffixes as $suffix)
+ {
+ if (is_executable($php
$this->getContextByEmailAddress($address);
$params
= PHP_BINDIR.DIRECTORY_SEPARATOR.'php'.$suffix))
+ {
+ return $php;
+ }
+ }
+
+ if ($php = getenv('PHP_PEAR_PHP_BIN')) {
+ if (is_executable($php)) {
+ return $php;
+ }
+ }
+
+ return sfToolkit::getPhpCli();
+
array(
'member'
}
+
}
=> $member,
'gadgets' => $filteredGadgets,

</pre>

戻る