آموزش سیمفونی روز ششم – مطالب بیشتری از لایه مدل
دیروز روز مهمی بود. یاد گرفتیم که چگونه URLهای زیاد بسازیم و اینکه چگونه از سیمفونی برای خودکار سازی موارد زیادی استفاده کنیم. امروز با پیچیده کردن کدها، قابلیتهای بهتری به jobeet اضافه میکنیم. شما در طول این فرایند، اطلاعات بیشتری راجع به تمامی ویژگیهایی که در روز پنجم به آن اشاره شد دریافت میکنید.
ضوابط شیء Propel
با توجه به قوانین روز دوم:
« هنگامی که کاربری به سایت وارد میشود، لیستی از مشاغل فعال را مشاهده میکند»
اما در حال حاضر تمامی مشاغل لیست میشوند، چه فعال باشند و چه …
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria()); } // ... }
یک شغل فعال نباید بیشتر از ۳۰ روز پیش پست شده باشد. متد doSelect یک شیء Criteria را میگیرد که همان شرح درخواست پایگاه داده برای اجرا است.
در کد بالا، متد Criteria بصورت خالی عبور داده شده، که به معنی برگشت دادن تمامی رکوردهای موجود در جدول میباشد.
حالا آن را طوری تغییر میدهیم که تنها مشاغل فعال را برگرداند:
public function executeIndex(sfWebRequest $request) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CREATED_AT, time() - 86400 * 30, Criteria::GREATER_THAN); $this->jobeet_job_list = JobeetJobPeer::doSelect($criteria); }
متد Criteria::add یک شرط Where را به SQL ساخته شده اضافه میکند. در اینجا Criteria را برای تنها بازگرداندن مشاغل فعال محدود کردیم. متد add عملگرهای مقایسه زیادی را پشتیبانی میکند، که پرکاربردترین آنها را مشاهده میکنید:
- Criteria::EQUAL
- Criteria::NOT_EQUAL
- Criteria::GREATER_THAN, Criteria::GREATER_EQUAL
- Criteria::LESS_THAN, Criteria::LESS_EQUAL
- Criteria::LIKE, Criteria::NOT_LIKE
- Criteria::CUSTOM
- Criteria::IN, Criteria::NOT_IN
- Criteria::ISNULL, Criteria::ISNOTNULL
- Criteria::CURRENT_DATE, Criteria::CURRENT_TIME, Criteria::CURRENT_TIMESTAMP
اشکالزدایی SQLهای ایجاد شده توسط propel
با توجه به اینکه شما کدهای SQL را بطور دستی ایجاد نمیکنید، propel آنها را با توجه به موتور پایگاه دادهای که در روز سوم معرفی کردید ایجاد میکند. اما بعضی اوقات، مشاهده کدهای SQL ایجاد شده توسط propel میتواند بسیار سودمند باشد. برای مثال جهت اشکالزدایی یک query که به درستی کار نمیکند. در محیط dev سیمفونی این queryها را ثبت میکند (به همراه بسیاری از موارد دیگر). برای هر مجموعه از application و محیط یک فایل log وجود دارد. محتوای فایل frontend_dev.log را مشاهده کنید:
# log/frontend_dev.log Dec 6 15:47:12 symfony [debug] {sfPropelLogger} exec: SET NAMES 'utf8' Dec 6 15:47:12 symfony [debug] {sfPropelLogger} prepare: SELECT jobeet_job.ID, jobeet_job.CATEGORY_ID, jobeet_job.TYPE, jobeet_job.COMPANY, jobeet_job.LOGO, jobeet_job.URL, jobeet_job.POSITION, jobeet_job.LOCATION, jobeet_job.DESCRIPTION, jobeet_job.HOW_TO_APPLY, jobeet_job.TOKEN, jobeet_job.IS_PUBLIC, jobeet_job.CREATED_AT, jobeet_job.UPDATED_AT FROM `jobeet_job` WHERE jobeet_job.CREATED_AT>:p1 Dec 6 15:47:12 symfony [debug] {sfPropelLogger} Binding '2008-11-06 15:47:12' at position :p1 w/ PDO type PDO::PARAM_STR
اینجا میتوانید SQL ایجاد شده خودتان توسط propel را که به همراه شرطی که بروی ستون created_at اعمال میشود مشاهدی کنید. (WHERE jobeet_job.CREATED_AT > :p1)
یادداشت: رشته p1 در query نشان دهنده شرح بیان شده SQL ایجاد شده توسط propel است. مقدار واقعی p1 (در مثال بالا «2008-11-06 15:47:12» ) در هنگام اجرای query ثبت میشود و بطور صحیح بوسیله موتور پایگاه داده گریز داده میشود. استفاده از این عبارات آماده شده، قابل نمایش بودن را برای حملات SQL injection کم میکند.
این عالیست، اما سوئیچ کردن بین مرورگرها، IDEها و فایلهای log کمی آزار دهنده است. اینگونه باید بصورت مداوم یک تغییر را آزمایش کنید. از نوار ابزار اشکالزدایی symfony ممنونیم، تمام اطلاعاتی که نیاز دارید براحتی در مرورگر در دسترس میباشد.
ترتیب شیء
کد بالا به هنگام کار کردن، نتیجه بعیدی از چیزی که در روز دوم تعریف کردیم را میدهد:
«یک کاربر میتواند برگردد و دوباره شغل خود را فعال کند و زمان انقضای آن را از ۳۰ روز بیشتر کند …»
اما ایم کد تنها به مقدار موجود در فیلد created_at تکیه میکند، و چون این ستون تنها زمان ایجاد رکورد را ثبت میکند نمیتوانیم این قابلیت را بر مبنای آن پیاده کنیم.
اما اگر نمای پایگاه دادهای که در روز سوم ایجاد کردیم را بخاطر بیاورید، یک ستون به نام expires_at نیز مشخص شده بود. و مقدار پیشفرض برای آن در اطلاعات پایه در نظر گرفته نشد. این ستون میتواند بصورت خودکار مقدار ۳۰ روز پس از تاریخ ثبت را بگیرد.
هنگامی که نیاز است تا برخی مقادیر بصورت خودکار و مرتب بر مبنای پایگاه داده، قبل از یک شیء ایجاد شوند، میتوانید متد save از کلاس مدل را مهمتر کنید.
// lib/model/JobeetJob.php class JobeetJob extends BaseJobeetJob { public function save(PropelPDO $con = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time(); $this->setExpiresAt($now + 86400 * 30); } return parent::save($con); } // ... }
متد isnew هنگامی که شیء هنوز در پایگاه داده مرتب سازی نشده، مقدار true و در غیر این صورت false را بر میگرداند.
حالا اکشن را طوری تغییر میدهیم تا بجای created_at از فیلد expires_at استفاده کند:
public function executeIndex(sfWebRequest $request) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $this->jobeet_job_list = JobeetJobPeer::doSelect($criteria); }
به این صورت، query را برای انتخاب بر حسب ویژگی expires_at محدود کردیم.
اطلاعات بیشتر راجع دادههای اولیه
Refresh کردن صفحه اصلی، تغییری در پی ندارد. زیرا این مشاغل همین چند روز پیش پست شدهاند. این تغییرات را ایجاد کنید تا مشاغلی که expires شدهاند را به جدول بیفزائید:
# data/fixtures/020_jobs.yml JobeetJob: # other jobs expired_job: category_id: programming company: Sensio Labs position: Web Developer location: Paris, France description: | Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: Send your resume to lorem.ipsum [at] dolor.sit is_public: true is_activated: true expires_at: 2005-12-01 token: job_expired email: job@example.com
یادداشت: کدها را به دقت کپی کنید تا فرورفتگیها از بین نرود. expires_job تنها دو تا Space قبل از خود دارد.
همانطور که شغل اصافه شده را در فایل fixture میبینید، مقدار ستون created_at مشخص شده است. حتی اگر بطور خودکار توسط propel پر شود.
مقدار مشخص شده بر هر مقدار پیشفرضی اولویت دارد. Fixture را دوباره بارگذاری کنید و صفحه را refresh کنید:
$ php symfony propel:data-load
میتوانید query زیر را برای مطمئن شدن از اینکه ستون expires_at بطور پیشفرض توسز متد save و بر مبنای ستون created_at مقدار دهی میشوند را اجرا کنید:
SELECT `position`, `created_at`, `expires_at` FROM `jobeet_job`;
پیکرهبندی دستی
در متد JobeetJob::save بوسیله کدهای پیچیدهای تعداد روزهای فعال بودن را مشخص کردیم. اما خیلی بهتر است تا این مقدار را قابل پیکرهبندی کنیم.
Symfony برای هر application یک فایل پیکرهبندی ایجاد میکند تا ویژگیهای خاص در آن تعریف شود. فایل app.yml میتواند شامل هر تنظیمی که شمما میخواهید باشد.
# apps/frontend/config/app.yml all: active_days: 30
در application، این تنظیمات بوسیله کلاس سراسری sfConfig در دسترس هستند.
sfConfig::get('app_active_days')
تنظیمات بهمراه پیشوند app_ هستند، زیرا کلاس sfConfig علاوه بر این دسترسی به تنظیمات symfony را هم مهیا میکند، که در آینده میبینیم.
کدها را بروز میکنیم تا تنظیمات جدید اعمال شود.
public function save(PropelPDO $con = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time(); $this->setExpiresAt($now + 86400 * sfConfig::get('app_active_days')); } return parent::save($con); }
فایل پیکرهبندی app.yml راه فوقالعادهای برای متمرکز کردن تنظیمات سراسری application شما میباشد.
در آخر، اگر نیاز به تعریف تنظیمات سراسری گستردهتری دارید، تنها یک فایل app.yml در پوشه config ریشه ایجاد کنید و …
دوباره سازی – Refactoring
اگر چه این کد کار مورد نظر را انجام میدهد، اما راه سریعی نیست. آیا میتوانید مشکل آن را بیابید؟
کد Criteria متعلق به اکشن نیست (لایه کنترل) بلکه متعلق به لایه مدل است. در ساختار MVC، لایه مدل تمام منطق برنامه را مشخص میکند و لایه کنترل تنها لایه مدل را برای بازیافت دادهها فراخوانی میکند. از آنجایی که این کد مجموعهای از مشاغل را بر میگرداند، آن را به کلاس JobeetJobPeer انتقال میدهیم و متدی بنام getActiveJobs ایجاد میکنیم:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN); return self::doSelect($criteria); } }
حالا کد اکشن میتواند از این متد برای بازیافت مشاغل استفاده کند.
public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = JobeetJobPeer::getActiveJobs(); }
این دوباره سازی چندین مزیت نسبت به کد قبل داشت:
- منظق دریافت مشاغل فعال حالا در لایه مدل قرار دارد، جایی که به آن متعلق است.
- کد موجود در قسمت کنترل بسیار خواناتر از قبل است.
- متد getActiveJobs میتواند دوباره مورد استفاده قرار گیرد (برای مثال در سایر اکشنها)
- کد لایه مدل حالا یک واحد قابل آموزش است.
حالا مرتب سازی مشاغل را بر حسب ستون expired_at صورت میدهیم.
static public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(self::EXPIRES_AT); return self::doSelect($criteria); }
متد addDescendingOrderByColumn یک شرط ORDER BY به SQL ایجاد شده اضافه میکند. (متد addAscendingOrderByColumn هم وجود دارد)
دسته بندیهای صفحه اصلی
طبق قوانین روز دوم:
«مشاغل بر حسب دستهبندی و سپس بر حسب تاریخ انتشارشان لیست میشود (ابتدا مشاغل جدید تر)»
تا حالا، دستهبندی شغل را در اشتراک نمیگرفتیم. مطابق قوانین، صفحه اصلی باید مشاغل را بر حسب دستهبندی نمایش دهد. ابتدا باید تمام دستهبندیها را با کمترین شغل فعال دریافت کنیم.
کلاس JobeetCategoryPeer را باز کنید و متد getWithJobs را به آن اضافه کنید:
// lib/model/JobeetCategoryPeer.php class JobeetCategoryPeer extends BaseJobeetCategoryPeer { static public function getWithJobs() { $criteria = new Criteria(); $criteria->addJoin(self::ID, JobeetJobPeer::CATEGORY_ID); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->setDistinct(); return self::doSelect($criteria); } }
متد Criteria::addJoin یک شرط JOIN را به SQL اضافه میکند. بصورت پیشفرض شرط JOIN به یک شرط WHERE اضافه میشود. در ضمن شما میتوانید عملوند JOIN را با اضافه کردن ۳ آرگومنت زیر تغییر دهید.
(Criteria::LEFT_JOIN ، Criteria::RIGHT_JOIN و Criteria::INNER_JOIN)
بنابراین اکشن index را تغییر میدهیم:
// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { $this->categories = JobeetCategoryPeer::getWithJobs(); }
در قالب، احتیاج داریم تا حلقهای برای مرور همه دستهبندیها و حلقهای برای مرور مشاغل فعال داخل آنها داشته باشیم:
// apps/frontend/modules/job/indexSuccess.php <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <?php foreach ($categories as $category): ?> <div class="category_<?php echo Jobeet::slugify($category->getName()) ?>"> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <table class="jobs"> <?php foreach ($category->getActiveJobs() as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table> </div> <?php endforeach; ?> </div>
یادداشت: برای نمایش نام دستهبندی در قالب از echo استفاده کردیم. جالب نیست؟ $category یک شیء است و ما بطور خارقالعادهای آن را echo میکنیم! جواب این سوال در طول روز سوم بیان شد، هنگامی که متد __toString را برای تمامی کلاسهای مدل تعریف کردیم.
برای اینکار باید متد getActiveJobs را به کلاس JobeetCategory اضافه کنیم. تا مشاغل فعال را از شیء دستهبندی باز گرداند.
// lib/model/JobeetCategory.php public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::getActiveJobs($criteria); }
در فراخوانی add، آرگومنت سوم را از قلم انداختیم زیرا Criteria::EQUAL مقدار پیشفرض است.
متد JobeetCategory::getActiveJobs برای برگرداندن مشاغل فعال از دسته بندی انتخابی از متد JobeetJobPeer::getActiveJobs استفاده میکند.
هنگامی که شما JobeetJobPeer::getActiveJobs را فراخوانی میکنید، محدودیتی برای هموار کردن بوسیله یک دستهبندی ایجاد میکنیم. در عوض عبور شیء دسته بندی، قطعاْ یک شیء Criteria را عبور میدهیم. چون این بهترین راه برای کپسوله کردن یک شرط است.
GetActiveJobs نیاز دارد تا آرگومنت Criteria را با ضوابط (criiteria) خود یکی کند. چون Criteria یک شیء است، کار کاملاْ راحتی است.
// lib/model/JobeetJobPeer.php static public function getActiveJobs(Criteria $criteria = null) { if (is_null($criteria)) { $criteria = new Criteria(); } $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(self::EXPIRES_AT); return self::doSelect($criteria); }
حدود نتایج
هنوز هم قوانینی برای اعمال شدن به لیست مشاغل صفحه اصلی وجود دارد:
«برای هر دسته بندی، تنها ۱۰ لینک میتوانند در لیست نمایش داده شوند و یک پیوند امکان لیست شدن تمام مشاغل در یک دسته بندی را ایجاد میکند»
اینکار بوسیله استفاده از متد getActiveJobs راحت میشود.
// lib/model/JobeetCategory.php public function getActiveJobs($max = 10) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); $criteria->setLimit($max); return JobeetJobPeer::getActiveJobs($criteria); }
اختصاص دادن شرط LIMIT در مدل کار سختی است، اما بهتر است تا این مقدار قابلیت پیکرهبندی داشته باشد. تغییراتی را در قالب ایجاد میکنیم تا نهایت مقدار نمایش مشاغل را از فایل app.yml بخواند.
<!-- apps/frontend/modules/job/indexSuccess.php --> <?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $i => $job): ?>
و این تنظیمات را در فایل app.yml اضافه کنید:
all: active_days: 30 max_jobs_on_homepage: 10

دادهای اولیه پویا
برای اینکه نتایج تغییرات را مشاهده کنید، باید گروهی از دادههای اولیه را وارد سیستم کنیم. میتوان اطلاعات موجود در fixture را کپی کرد که شاید وقتگیر و کسل کننده باشد. Symfony اینکار را هم آسان کرده است. شما میتوانید از کدهای php در فایلهای yml استفاده کنید. فایل 020_jobs.yml را باز کرده و کدهای زیر را در انتهای آن اضافه کنید:
JobeetJob: # Starts at the beginning of the line (no whitespace before) <?php for ($i = 100; $i <= 130; $i++): ?> job_<?php echo $i ?>: category_id: programming company: Company <?php echo $i."\n" ?> position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: | Send your resume to lorem.ipsum [at] company_<?php echo $i ?>.sit is_public: true is_activated: true token: job_<?php echo $i."\n" ?> email: job@example.com <?php endfor; ?>
دقت کنید که دندانهها و فرورفتگیها را رعایت کنید زیرا در غیر این صورت پردازش نمیشوند.
این نکات را همیشه بخاطر بسپارید:
- عبارت <?php ?> همیشه باید در ابتدای خط و یا درون یک مقدار جاگذاری شده باشد.
- اگر یک عبارت <?php ?> سطر را تمام کند، باید در خروجی مقدار «\n» را ذکر کنیم.
و میبینیم که در صفحه اصلی تنها ۱۰ شغل آخر اضافه شده است.

ایمن کردن صفحه شغل
هنگامی که شغلی به انقضاء میرسد، حتی اگر شما url آن را بدانید نباید به آن دسترسی پیدا کنید. سعی کنید تا URL یک شغل تاریخ گذشته را پیدا کنید. (id را با یک id واقعی در پایگاه داده عوض کنید)
SELECT id, token FROM jobeet_job WHERE expires_at < NOW()
/frontend_dev.php/job/sensio-labs/paris-france/ID/web-developer-expired
بجای نمایش شغل باید به صفحه ۴۰۴ منتقل شویم. اما چگونه میتوان بصورت خودکار و بوسیله مسیرها به این نتیجه رسید؟
بطور پیشفرض sfPropelRoute از متد استاندارد doSelectOne برای بازگداندن اشیاء استفاده میکند، و ما میتوانیم مشروط بر اینکه خصیصه method_for_criteria در پیکرهبندی مسیر باشد آن را تغییر دهیم:
# apps/frontend/config/routing.yml job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: model: JobeetJob type: object method_for_criteria: doSelectActive param: { module: job, action: show } requirements: id: \d+ sf_method: [GET]
متد doSelectActive یک شیء Criteria را که بوسیله مسیر ساخته شده را دریافت میکند:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function doSelectActive(Criteria $criteria) { $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); return self::doSelectOne($criteria); } // ... }
حالا، اگر سعی کنید تا شغل تاریخ گذشتهای را ببینید با صفحه ۴۰۴ مواجه میشوید.

پیوند به صفحه مشاغل
حالا، یک پیوند برای صفحه دستهبندی اضافه کرده و این صفحه را ایجاد میکنیم. اما صبر کنید! وقت امروز تمام شده و زمان کافی برای ادامه این بحث را نداریم. البته اگر شما زمان کافی را داشته باشید، اطلاعات لازم برای اینکار را هم دارید. سعی کنید تا اینکار را انجام دهید و یا فردا بهمراه یکدیگر انجامش دهیم.
فردا همدیگر را میبینیم
روی jobeet خود کار کنید، سعی کنید نهایت استفاده از مستندات API و مستندات آزاد موجود در سایت symfony را ببرید. فردا دوباره همدیگر را میبینیم و به ادامه کار میپردازیم.
موفق باشید…


