CodeIgniter ORM (dug out from the graveyard)


/ Published in: PHP
Save to your folder(s)

It's fun to read what I wrote ages ago. An ORM that was heavily influenced (or should I say misguided) by CakePHP, lol.


Copy this code and paste it in your HTML
  1. <?php if (!defined('BASEPATH')) exit('No direct script access allowed');
  2.  
  3. /*===== Subversion ====================================================================
  4. Rev : $ $Date: 2007-05-14 22:13:55 +1000 (Mon, 14 May 2007) $ $
  5. Date : $ $Rev: 19 $ $
  6. =====================================================================================*/
  7.  
  8. /**
  9.  * Object Relational Mapper
  10.  * Maps database tables with associations to programming objects
  11.  * Please read the requirements below carefully!
  12.  *
  13.  * Database table requirements:
  14.  * - there should be a table called 'orm_schemas' for storing table schema data
  15.  * - MySQL query for the 'orm_schemas' table:
  16.  * CREATE TABLE orm_schemas (
  17.  * id mediumint(3) unsigned NOT NULL auto_increment,
  18.  * model varchar(20) NOT NULL default '',
  19.  * recursion tinyint(1) unsigned default NULL,
  20.  * conjTables text,
  21.  * schemaData text,
  22.  * PRIMARY KEY (id)
  23.  * ) ENGINE=MyISAM;
  24.  * - delete the model records if the corresponding table structure have been altered
  25.  *
  26.  * Database table convention requirements:
  27.  * - table name should be one plural word
  28.  * - conjunction table name should be one table name followed by an underscore (_) followed by another table name
  29.  * - model name is the singularised form of the table name
  30.  *
  31.  * Database fields convention requirements:
  32.  * - primary key should be the model name followed by an underscore (_) followed by 'id', e.g. PK of 'posts' should be 'post_id'
  33.  * - other fields should be the model name followed by an underscore (_) followed by descriptive words, e.g. 'post_date' and 'post_updated'
  34.  * - foreign keys should be the foreign model name followed by an underscore (_) followed by 'id', e.g. FK of 'posts' could be 'user_id' and 'category_id'
  35.  * - self reference keys should be one single word (noun) followed by an underscore (_) followed by 'id', e.g. SRK of 'posts' could be 'parent_id' or 'thread_id'
  36.  *
  37.  * Shortcut find methods:
  38.  * $this->find() has two convenient shortcut methods: $this->findField() and $this->searchField()
  39.  * where 'Field' is the actual name of the table field, with OR without the preceding model name and underscore
  40.  * e.g. if you want to find 'post_title' from 'posts': $this->findTitle('post title to look for', 'posts')
  41.  * same for $this->searchTitle(), the difference between these two methods is the former looks for exact matches, the latter look for partial matches
  42.  * you can think of the former as 'something = value' and the latter as 'something LIKE value' in SQL queries
  43.  * Use $this->find() with $this->where() if you want to search by foreign keys
  44.  *
  45.  * Shortened field names (optional):
  46.  * When performing CRUD actions (e.g. $this->findField() and $this->modelName->field = 'field value')
  47.  * field names can be shortened if and only if they're NOT foreign keys
  48.  * e.g. 'post_id' and 'post_title' in 'posts' table can be shortened as 'id' and 'title'
  49.  * however, since this is optional, you can always refer them by their full names
  50.  * foreign key fields can NOT be shortened !!
  51.  *
  52.  * Active record proxies:
  53.  * You may use CodeIginiter's built in active record methods before calling $this->find() or the shortcut methods
  54.  * e.g.
  55.  * $this->where('post_title', 'the post I want');
  56.  * $this->find('posts');
  57.  * equals
  58.  * $this->findTitle('the post I want', 'posts');
  59.  *
  60.  * @package CodeIgniter
  61.  * @subpackage library
  62.  * @author Fred Wu
  63.  * @version 0.1.8
  64.  * @license GNU Lesser General Public License (LGPL) http://www.gnu.org/licenses/lgpl.html
  65.  **/
  66. class ORM {
  67.  
  68. /**
  69. * CI instance
  70. *
  71. * @access protected
  72. * @var object
  73. **/
  74. var $_CI;
  75.  
  76. /**
  77. * Current table name
  78. *
  79. * @access public
  80. * @var string
  81. **/
  82. var $table;
  83.  
  84. /**
  85. * Conjunction table names
  86. * we need it because one conjunction table has two potential name conventions
  87. * array['parentTableName']['tableName']
  88. *
  89. * @access private
  90. * @var array
  91. **/
  92. var $__conjunctionTables;
  93.  
  94. /**
  95. * Conditional queries
  96. *
  97. * @access private
  98. * @var array
  99. **/
  100. var $__conditions;
  101.  
  102. /**
  103. * Record offset number
  104. *
  105. * @access private
  106. * @var integer
  107. **/
  108. var $__offset;
  109.  
  110. /**
  111. * Record limit number
  112. *
  113. * @access private
  114. * @var integer
  115. **/
  116. var $__limit;
  117.  
  118. /**
  119. * Total number of records in a table
  120. *
  121. * @access public
  122. * @var integer
  123. **/
  124. var $tableCount;
  125.  
  126. /**
  127. * Number of records by find condition
  128. *
  129. * @access public
  130. * @var integer
  131. **/
  132. var $findCount;
  133.  
  134. /**
  135. * Modified model
  136. *
  137. * @access private
  138. * @var string
  139. **/
  140. var $__modifiedModel;
  141.  
  142. /**
  143. * Modified model record id
  144. *
  145. * @access private
  146. * @var integer
  147. **/
  148. var $__modifiedModelId;
  149.  
  150. /**
  151. * Constructor
  152. **/
  153. function ORM()
  154. {
  155. $this->_CI =& get_instance();
  156. $this->_CI->load->helper('inflector');
  157. if (phpversion() < 5)
  158. {
  159. }
  160. }
  161.  
  162. /**
  163. * Find
  164. * finds the ORMed data of the specified table
  165. *
  166. * @access public
  167. * @param string $table table name
  168. * @param integer $limit record limit number
  169. * @param integer $page page number (for pagination, must be positive)
  170. * @param integer $recursion recursion level (0 for none)
  171. * @return array table with associated data
  172. **/
  173. function find($table, $limit = null, $page = 1, $recursion = null)
  174. {
  175. $this->table = $table;
  176.  
  177. $model = singular($table);
  178.  
  179. if (empty($page) || !is_numeric($page) || intval($page) < 1)
  180. {
  181. $page = 1;
  182. }
  183.  
  184. if (!isset($recursion))
  185. {
  186. $recursion = 1;
  187. }
  188.  
  189. $this->__limit = $limit;
  190. $this->__offset = $limit * ($page - 1);
  191.  
  192. // builds table relationship structure
  193. $schemaQuery = $this->_CI->db->getwhere('orm_schemas', array('model' => $model, 'recursion' => $recursion));
  194. $schemaResult = $schemaQuery->result_array();
  195.  
  196. if (!$schemaResult)
  197. {
  198. $tableStructure[$table] = $this->__build_table_structure($table, $recursion);
  199.  
  200. // stores model schema
  201. $this->_CI->db->set('model', $model);
  202. $this->_CI->db->set('recursion', $recursion);
  203. $this->_CI->db->set('conjTables', serialize($this->__conjunctionTables));
  204. $this->_CI->db->set('schemaData', serialize($tableStructure[$table]));
  205. $this->_CI->db->insert('orm_schemas');
  206. }
  207. else
  208. {
  209. $this->__conjunctionTables = unserialize($schemaResult[0]['conjTables']);
  210. $tableStructure[$table] = unserialize($schemaResult[0]['schemaData']);
  211. }
  212.  
  213. // populates data according to association
  214. $tableData = $this->__populate_table_data($tableStructure);
  215.  
  216. // record count
  217. $this->tableCount = $this->_CI->db->count_all($table);
  218. $this->findCount = count($tableData[$table]);
  219.  
  220. return $tableData[$table];
  221. }
  222.  
  223. /**
  224. * Number of records
  225. * can use proxy active record methods before calling
  226. * e.g. $this->orm->where('post_title', 'my_post');
  227. *
  228. * @access public
  229. * @param string $table table name
  230. * @return integer number of records
  231. **/
  232. function recordsCount($table)
  233. {
  234. $this->_CI->db->select('COUNT(*)');
  235. $this->__query_conditions();
  236. $query = $this->_CI->db->get($table);
  237. $result = $query->result_array();
  238.  
  239. return $result[0]['COUNT(*)'];
  240. }
  241.  
  242. /**
  243. * Build table structure
  244. * constructs an array according to the table associations
  245. *
  246. * @access private
  247. * @param string $table table name
  248. * @param integer $recursion recursion level (0 for none)
  249. * @return array table associations (structure only)
  250. **/
  251. function __build_table_structure($table, $recursion)
  252. {
  253. $model = singular($table);
  254.  
  255. // table names
  256. $tables = $this->_CI->db->list_tables();
  257. // fields in each table
  258. $fields = $this->_CI->db->list_fields($table);
  259.  
  260. $structure = null;
  261.  
  262. if ($recursion > 0)
  263. {
  264. // determines associations according to the fields
  265. foreach ($fields as $field)
  266. {
  267. $fieldModel = substr($field, 0, -3);
  268. $fieldTable = plural($fieldModel);
  269.  
  270. // relationships based on foreign keys
  271. if (!preg_match("/\b{$model}_/", $field))
  272. {
  273. // belongsTo
  274. if (in_array($fieldTable, $tables))
  275. {
  276. $structure['belongsTo'][$fieldTable] = $this->__build_table_structure($fieldTable, $recursion - 1);
  277. }
  278. // belongsToSelf (is_xxxxx_of)
  279. else
  280. {
  281. $structure['belongsToSelf']["is_{$fieldModel}_of"] = $this->__build_table_structure($table, $recursion - 1);
  282. }
  283. }
  284. }
  285.  
  286. // relationships based on other tables
  287. foreach ($tables as $tableName)
  288. {
  289. // hasOneMany
  290. if (!preg_match("/{$table}/", $tableName))
  291. {
  292. $tableFields = $this->_CI->db->list_fields($tableName);
  293. foreach ($tableFields as $tableField)
  294. {
  295. if (preg_match("/{$model}_id/", $tableField))
  296. {
  297. $structure['hasOneMany'][$tableName] = $this->__build_table_structure($tableName, $recursion - 1);
  298. }
  299. }
  300. }
  301. // hasAndBelongsToMany
  302. elseif (preg_match("/\b{$table}_/", $tableName))
  303. {
  304. $assocTable = substr($tableName, strlen($table)+1);
  305. $this->__conjunctionTables[$table][$assocTable] = $tableName;
  306. $structure['hasAndBelongsToMany'][$assocTable] = $this->__build_table_structure($assocTable, $recursion - 1);
  307. }
  308. elseif (preg_match("/_{$table}\b/", $tableName))
  309. {
  310. $assocTable = substr($tableName, 0, -strlen($table)-1);
  311. $this->__conjunctionTables[$table][$assocTable] = $tableName;
  312. $structure['hasAndBelongsToMany'][$assocTable] = $this->__build_table_structure($assocTable, $recursion - 1);
  313. }
  314. }
  315. }
  316.  
  317. return $structure;
  318. }
  319.  
  320. /**
  321. * Populate table data
  322. * populates table data according to the table structure
  323. *
  324. * @access private
  325. * @param array $tableStructure table structure returned by $this->__build_table_structure() *OR* to be generated in the function
  326. * @param string $parentTable table name of the parent
  327. * @param string $relationship (belongsTo | belongsToSelf | hasOneMany | hasAndBelongsToMany)
  328. * @return array table with associated data
  329. **/
  330. function __populate_table_data($tableStructure, $parentTable = null, $relationship = null, $parentIDValues = null)
  331. {
  332. // for some reason this part has to be separated from the same foreach followed by, a PHP bug perhaps?
  333. foreach ($tableStructure as $table => $relationships)
  334. {
  335. $tableData[$table] = $this->__select_table_data($table, $parentTable, $relationship, $parentIDValues);
  336. }
  337.  
  338. foreach ($tableStructure as $table => $relationships)
  339. {
  340. if ($relationships)
  341. {
  342. for ($i = 0; $i < count($tableData[$table]); $i++)
  343. {
  344. $tableData[$table][$i]['assocData'] = array();
  345.  
  346. // primary and foreign key values of the parent table record
  347. // this is for use in the associated tables
  348. if (preg_match("/\bis_/", $table))
  349. {
  350. $fields = $this->_CI->db->list_fields($parentTable);
  351. }
  352. else
  353. {
  354. $fields = $this->_CI->db->list_fields($table);
  355. }
  356. foreach ($fields as $field)
  357. {
  358. if (preg_match("/_id\b/", $field))
  359. {
  360. $fieldModel = substr($field, 0, -3);
  361. $fieldTable = plural($fieldModel);
  362.  
  363. $parentIDValues[$fieldTable] = $tableData[$table][$i][$field];
  364. }
  365. }
  366.  
  367. // iterates through relationships
  368. foreach ($relationships as $relationship => $assocTables)
  369. {
  370. // always passes the parent table name to all of the belongsToSelf associated tables (on every recursive level)
  371. if (preg_match("/\bis_/", $table))
  372. {
  373. $tableData[$table][$i]['assocData'] += $this->__populate_table_data($assocTables, $parentTable, $relationship, $parentIDValues);
  374. }
  375. else
  376. {
  377. $tableData[$table][$i]['assocData'] += $this->__populate_table_data($assocTables, $table, $relationship, $parentIDValues);
  378. }
  379.  
  380. }
  381. }
  382. }
  383. }
  384.  
  385. return $tableData;
  386. }
  387.  
  388. /**
  389. * Select table data
  390. * retrieves individual table data (to be used in $this->__populate_table_data())
  391. *
  392. * @access private
  393. * @param string $table table name
  394. * @param string $parentTable table name of the parent
  395. * @param string $relationship (belongsTo | belongsToSelf | hasOneMany | hasAndBelongsToMany)
  396. * @return array table data
  397. **/
  398. function __select_table_data($table, $parentTable = null, $relationship = null, $parentIDValues = null)
  399. {
  400. $offset = $this->__offset;
  401. $limit = $this->__limit;
  402.  
  403. // initial table data (whatever the user specifies in $this->find())
  404. if (!$parentTable)
  405. {
  406. $this->__query_conditions();
  407.  
  408. $query = $this->_CI->db->get($table, $limit, $offset);
  409. }
  410. else
  411. {
  412. // changes the table name to its parent table name if it's belongsToSelf (is_xxxxx_of)
  413. if (preg_match("/\bis_/", $table))
  414. {
  415. // model name used in the array (e.g. 'parent' for 'is_parent_of')
  416. $isOfName = substr($table, 3, -3);
  417. $table = $parentTable;
  418. }
  419.  
  420. $model = singular($table);
  421. $parentModel = singular($parentTable);
  422.  
  423. switch ($relationship)
  424. {
  425. case 'belongsToSelf':
  426. $query = $this->_CI->db->getwhere($table, array("{$isOfName}_id" => $parentIDValues[$table]), $limit, $offset);
  427. break;
  428. case 'belongsTo':
  429. $query = $this->_CI->db->getwhere($table, array("{$model}_id" => $parentIDValues[$table]), $limit, $offset);
  430. break;
  431. case 'hasOneMany':
  432. $query = $this->_CI->db->getwhere($table, array("{$parentModel}_id" => $parentIDValues[$parentTable]), $limit, $offset);
  433. break;
  434. case 'hasAndBelongsToMany':
  435. $conjTable = $this->__conjunctionTables[$parentTable][$table];
  436. $this->_CI->db->join($conjTable, "{$conjTable}.{$model}_id = {$table}.{$model}_id", 'left');
  437. $query = $this->_CI->db->getwhere($table, array("{$conjTable}.{$parentModel}_id" => $parentIDValues[$parentTable]), $limit, $offset);
  438. break;
  439. default:
  440. break;
  441. }
  442. }
  443.  
  444. $result = $query->result_array();
  445. return $result;
  446. }
  447.  
  448. /**
  449. * Find neighbours
  450. * finds the specified record data with its neighbours
  451. * useful for pagination (previous, next), etc
  452. *
  453. * @access public
  454. * @param integer $id record id
  455. * @param string $table table name
  456. * @param integer $deep record limit number (both sides, so $deep = 1 means fetching 3 records all together)
  457. * @param mixed $matchingFields array: fields to be matched OR string: the field to be matched
  458. * @param integer $assocLevel recursion level (0 for none) for the records
  459. * @return array table with associated data
  460. **/
  461. function findNeighbours($id, $table, $deep = 1, $matchingFields = null, $assocLevel = 0)
  462. {
  463. $model = singular($table);
  464.  
  465. // self (source record data)
  466. $self = $this->findId($id, $table, $deep, 1, $assocLevel);
  467.  
  468. // neighbours on the left side
  469. $this->_CI->db->where($model.'_id <', $id);
  470. $this->__matchingFields($matchingFields, $table, $self);
  471. $this->_CI->db->orderby($model.'_id', 'DESC');
  472. $neighboursLeft = $this->find($table, $deep, 1, $assocLevel);
  473.  
  474. // neighbours on the right side
  475. $this->_CI->db->where($model.'_id >', $id);
  476. $this->__matchingFields($matchingFields, $table, $self);
  477. $neighboursRight = $this->find($table, $deep, 1, $assocLevel);
  478.  
  479. // resorts data order
  480. if ($deep > 1)
  481. {
  482. $neighboursLeft[$table] = array_reverse($neighboursLeft[$table]);
  483. }
  484.  
  485. $result[$table] = array_merge($neighboursLeft[$table], $self[$table], $neighboursRight[$table]);
  486. return $result;
  487. }
  488.  
  489. /**
  490. * Query conditions
  491. *
  492. * @access private
  493. * @return void
  494. **/
  495. function __query_conditions()
  496. {
  497. if (isset($this->__conditions))
  498. {
  499. foreach ($this->__conditions as $condition)
  500. {
  501. foreach ($condition as $method => $args)
  502. {
  503. @$this->_CI->db->$method($args[0], $args[1]);
  504. }
  505. }
  506. }
  507.  
  508. unset($this->__conditions);
  509. }
  510.  
  511. /**
  512. * Matching fields
  513. * takes the fields to be matched and executes 'where' queries
  514. *
  515. * @access private
  516. * @param mixed $matchingFields array: fields to be matched OR string: the field to be matched
  517. * @param string $table table name
  518. * @param array $self source record data
  519. * @return void
  520. **/
  521. function __matchingFields($matchingFields, $table, $self)
  522. {
  523. if ($matchingFields)
  524. {
  525. if (is_array($matchingFields))
  526. {
  527. foreach ($matchingFields as $matchingField)
  528. {
  529. $this->_CI->db->where($matchingField, $self[$table][0][$matchingField]);
  530. }
  531. }
  532. else
  533. {
  534. $this->_CI->db->where($matchingFields, $self[$table][0][$matchingFields]);
  535. }
  536. }
  537. }
  538.  
  539. /**
  540. * Create
  541. * inserts a new record to the specified table
  542. *
  543. * @access public
  544. * @param string $model model name
  545. * @return void
  546. **/
  547. function create($model = null)
  548. {
  549. if (!isset($model))
  550. {
  551. $table = $this->table;
  552. $model = singular($table);
  553. }
  554. else
  555. {
  556. $table = plural($model);
  557. }
  558.  
  559. $this->__rebuildFieldNames($table);
  560.  
  561. $this->_CI->db->insert($table, $this->$model);
  562.  
  563. // removes the object variable
  564. unset($this->$model);
  565. }
  566.  
  567. /**
  568. * Delete
  569. * deletes a record
  570. *
  571. * @access public
  572. * @param integer $id record id
  573. * @param string $model model name
  574. * @return void
  575. **/
  576. function delete($id, $model = null)
  577. {
  578. if (!isset($model))
  579. {
  580. $table = $this->table;
  581. $model = singular($table);
  582. }
  583. else
  584. {
  585. $table = plural($model);
  586. }
  587.  
  588. $this->_CI->db->delete($table, array($model.'_id' => $id));
  589. }
  590.  
  591. /**
  592. * Edit
  593. * passes $id and $model data to $this->save()
  594. *
  595. * @access public
  596. * @param integer $id record id
  597. * @param string $model model name
  598. * @return void
  599. **/
  600. function edit($id, $model = null)
  601. {
  602. if (!isset($model))
  603. {
  604. $table = $this->table;
  605. $model = singular($table);
  606. }
  607.  
  608. $this->__modifiedModel = $model;
  609. $this->__modifiedModelId = $id;
  610. }
  611.  
  612. /**
  613. * Save
  614. * saves / updates data!
  615. *
  616. * @access public
  617. * @return object modified fields
  618. **/
  619. function save()
  620. {
  621. $model = $this->__modifiedModel;
  622. $id = $this->__modifiedModelId;
  623.  
  624. $table = plural($model);
  625.  
  626. $this->__rebuildFieldNames($table);
  627.  
  628. $this->_CI->db->where($model.'_id', $id);
  629. $this->_CI->db->update($table, $this->$model);
  630.  
  631. $return = $this->$model;
  632.  
  633. // removes the object variable
  634. unset($this->$model);
  635.  
  636. return $return;
  637. }
  638.  
  639. /**
  640. * Rebuild field names
  641. * rebuilds shortened field names to full field names
  642. *
  643. * @access private
  644. * @return void
  645. **/
  646. function __rebuildFieldNames($table = null)
  647. {
  648. $model = singular($table);
  649.  
  650. foreach ($this->$model as $field => $value)
  651. {
  652. // processes shortcut field names
  653. if (!preg_match("/_id\b/", $field) AND !preg_match("/\b{$model}/", $field))
  654. {
  655. // removes old field name
  656. $fieldOld = $field;
  657. unset($this->$model->$fieldOld);
  658. // builds new field name
  659. $field = $model.'_'.$field;
  660. $this->$model->$field = $value;
  661. }
  662. }
  663. }
  664.  
  665. /**
  666. * Method overloading
  667. *
  668. * @access private
  669. * @param string $method method name
  670. * @param array $args method arguments in array
  671. * @return void
  672. **/
  673. function __call($method, $args)
  674. {
  675. // finds data according to a field value
  676. if (preg_match("/\bfind/", $method) OR preg_match("/\bsearch/", $method))
  677. {
  678. // $args[0] = matching content
  679. // $args[1] = table name
  680. // $args[2] and onwards check $this->find();
  681.  
  682. $model = singular($args[1]);
  683.  
  684. // $substrlen is the string length of the keywords (find, search, etc)
  685. // find (WHERE)
  686. if (preg_match("/\bfind/", $method))
  687. {
  688. $substrlen = 4; // strlen('find')
  689. $function = 'where';
  690. }
  691. // search (LIKE)
  692. else
  693. {
  694. $substrlen = 6; // strlen('search')
  695. $function = 'like';
  696. }
  697.  
  698. // completes field name
  699. // foreign key retain its field name
  700. // otherwise prepend it with model name and underscore
  701. if (preg_match("/\b{$model}/", $method) OR preg_match("/_id\b/", $method))
  702. {
  703. $field = strtolower(substr($method, $substrlen));
  704. }
  705. else
  706. {
  707. $field = singular($args[1]) . '_' . strtolower(substr($method, $substrlen));
  708. }
  709.  
  710. $argsCon[] = $field;
  711. $argsCon[] = $args[0];
  712. $this->__conditions[] = array($function => $argsCon);
  713.  
  714. return @$this->find($args[1], $args[2], $args[3], $args[4]);
  715. }
  716. elseif (preg_match("/\badd/", $method))
  717. {
  718. $model = strtolower(substr($method, 3));
  719. $this->create($model);
  720. }
  721. elseif (preg_match("/\bcreate/", $method))
  722. {
  723. $model = strtolower(substr($method, 6));
  724. $this->create($model);
  725. }
  726. elseif (preg_match("/\bedit/", $method))
  727. {
  728. $model = strtolower(substr($method, 4));
  729. $this->create($args[0], $model);
  730. }
  731. elseif (preg_match("/\bdel/", $method))
  732. {
  733. $model = strtolower(substr($method, 3));
  734. $this->delete($args[0], $model);
  735. }
  736. elseif (preg_match("/\bdelete/", $method))
  737. {
  738. $model = strtolower(substr($method, 6));
  739. $this->delete($args[0], $model);
  740. }
  741. else
  742. {
  743. switch ($method)
  744. {
  745. // proxies to CI Active Record functions
  746. case 'where':
  747. case 'orwhere':
  748. case 'like':
  749. case 'orlike':
  750. case 'groupby':
  751. case 'having':
  752. case 'orderby':
  753. case 'limit':
  754. $this->__conditions[] = array($method => $args);
  755. break;
  756. default:
  757. break;
  758. }
  759. }
  760. }
  761. }
  762.  
  763. ?>

Report this snippet


Comments

RSS Icon Subscribe to comments

You need to login to post a comment.