A modest collection of PHP libraries used at SparkFun.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

975 lines
26 KiB

  1. <?php
  2. use \SparkLib\DB;
  3. use \SparkLib\Fail;
  4. /**
  5. * This is a modified SparkFinder for use with generated relationships. It
  6. * is automatically available through __get magic on dinosaurs with a
  7. * corresponding has_many defined in configuration/records.php. For now it
  8. * uses a blanket 's' on the end of the class name for plurals, like Orders,
  9. * Customers, or OrdersStatusHistorys =[
  10. *
  11. * See the last section of this file for the available SQL operations.
  12. *
  13. * EXAMPLE 1: Find a customer's 5 most recent orders.
  14. *
  15. * $customer = new CustomerSaurus(id);
  16. * $orders = $customer->Orders->orderBy('date_purchased', 'DESC')->limit(5)->find();
  17. * while($order = $orders->getNext())
  18. * // whatever
  19. *
  20. * It should be noted that while a foreach() on the return of find() will
  21. * also work, it loads everything into memory first and is thus a lot more
  22. * costly than the above while() example, as that iterates over one result at
  23. * a time. So, use while().
  24. *
  25. * EXAMPLE 2: Find a single storefront product by SKU:
  26. *
  27. * $product = StorefrontProductSaurus::finder()
  28. * ->where('products_model')->eq('DEV-00666')
  29. * ->find()->current();
  30. *
  31. * This uses the static finder() method available to all generated records.
  32. * So, find() returns a SparkFriendFinder with the first result (the only
  33. * one, in this case) preloaded, and current() returns it.
  34. */
  35. class SparkFriendFinder extends \SparkLib\Iterator {
  36. const NULLS_LAST = 'LAST';
  37. const NULLS_FIRST = 'FIRST';
  38. protected $_recordClass;
  39. protected $_relationships;
  40. // A class to instantiate with the record instance as its only
  41. // parameter and return
  42. protected $_wrapperClass = null;
  43. // Table and primary key of the record class we're searching for.
  44. protected $_table = null;
  45. protected $_tableKey = null;
  46. protected $_tableRecord = null;
  47. protected $_sparkfinder = null;
  48. protected $_total = null;
  49. // Initial SELECT and WHERE.
  50. protected $_default_query = '';
  51. // Full query with constraints.
  52. protected $_query = '';
  53. // Those constraints:
  54. protected $_distinctOn = '';
  55. protected $_join = '';
  56. protected $_with = '';
  57. protected $_where = '';
  58. protected $_orderBy = '';
  59. protected $_groupBy = '';
  60. protected $_limit = '';
  61. protected $_forUpdate = '';
  62. // For the time being these two will only get set in child classes of this one.
  63. protected $_left_join = '';
  64. protected $_extraselects = '';
  65. protected $_key;
  66. protected $_id;
  67. protected $_buffered = true;
  68. protected $_selectIdOnly = false;
  69. /**
  70. * Get a new finder. Takes a string containing the classname of dinosaur
  71. * to instantiate, and an optional key and id.
  72. *
  73. * If a key and id are passed, we are looking for records in the context of
  74. * another one, e.g. getting a customer's orders. Otherwise, we are
  75. * (probably) using the static SomeRecord::finder() method.
  76. */
  77. public function __construct ($record_class, $key = null, $id = null)
  78. {
  79. $this->_recordClass = $record_class;
  80. $this->_key = $key;
  81. $this->_id = $id;
  82. // It's worth noting that class_exists() probably invokes the Autoloader here
  83. if (! class_exists($this->_recordClass))
  84. throw new InvalidArgumentException("No such record class: {$this->_recordClass}");
  85. // Pull in some metadata from the record class:
  86. $this->handleClass();
  87. }
  88. /**
  89. * Re-run the previous query, starting over from the first record.
  90. */
  91. public function rewind ()
  92. {
  93. if (! $this->_sparkfinder)
  94. return false;
  95. $this->getResults();
  96. }
  97. // Simple getters
  98. public function key () { return $this->current()->getId(); }
  99. public function recordClass () { return $this->_recordClass; }
  100. public function tableKey () { return $this->_tableKey; }
  101. public function tableRecord () { return $this->_tableRecord; }
  102. /**
  103. * Return current position within the resultset.
  104. */
  105. public function position()
  106. {
  107. if (isset($this->_sparkfinder))
  108. return $this->_sparkfinder->position();
  109. else
  110. return 0;
  111. }
  112. /**
  113. * Check if there's a currently available record.
  114. *
  115. * @return bool - yay or nay
  116. */
  117. public function valid ()
  118. {
  119. return $this->_sparkfinder->valid();
  120. }
  121. /**
  122. * How many rows in the current statement results?
  123. */
  124. public function count ()
  125. {
  126. if (! $this->_sparkfinder)
  127. return false;
  128. return $this->_sparkfinder->count();
  129. }
  130. /**
  131. * How many rows total with these params?
  132. *
  133. * Keeps the joins, as they may affect the total; drops the _with because
  134. * that's just overhead.
  135. */
  136. public function total ()
  137. {
  138. $dbi = SparkDBI::getInstance();
  139. if($this->_total)
  140. return $this->_total;
  141. $q = 'SELECT COUNT(' . $this->_table . '.' . $this->_tableKey . ') AS total ' .
  142. ' FROM ' . $this->_table .
  143. $this->_join .
  144. $this->_left_join .
  145. ' WHERE true ' .
  146. $this->_where;
  147. $totalArray = $dbi->fetchRow($q);
  148. $this->_total = (int)$totalArray['total'];
  149. return $this->_total;
  150. }
  151. /**
  152. * How many rows total with these params?
  153. *
  154. * use this if you have a group by statement in your query
  155. */
  156. public function group_total ()
  157. {
  158. $dbi = SparkDBI::getInstance();
  159. if ($this->_total)
  160. return $this->_total;
  161. $q = 'SELECT COUNT(0) AS total FROM (SELECT ' . $this->_table . '.' . $this->_tableKey .
  162. ' FROM "' . $this->_table . '"' .
  163. $this->_join .
  164. $this->_left_join .
  165. ' WHERE true ' .
  166. $this->_where .
  167. $this->_groupBy .
  168. ') g';
  169. $totalArray = $dbi->fetchRow($q);
  170. $this->_total = $totalArray['total'];
  171. return $this->_total;
  172. }
  173. /**
  174. * Advance to the next record.
  175. */
  176. public function next ()
  177. {
  178. if (! $this->_sparkfinder)
  179. throw new Exception('No current results - find() not called?');
  180. return $this->_sparkfinder->next();
  181. }
  182. /**
  183. * Return an object for the current record.
  184. *
  185. * @return SomeSaurus - a $this->_recordClass
  186. */
  187. public function current ()
  188. {
  189. if (! $this->valid())
  190. return false;
  191. if ($this->_wrapperClass) {
  192. $wrapper = $this->_wrapperClass;
  193. // yo dawg, I heard you like classes
  194. return new $wrapper($this->_sparkfinder->current());
  195. }
  196. return $this->_sparkfinder->current();
  197. }
  198. /**
  199. * Load the next record and return an object for it.
  200. *
  201. * @return SomeSaurus - a $this->_recordClass
  202. */
  203. public function getNext ()
  204. {
  205. // Assume we want to go ahead and find() if there aren't
  206. // already results to get
  207. if (! isset($this->_sparkfinder))
  208. $this->find();
  209. // Will be a record object or false;
  210. $result = $this->current();
  211. // If we got an object, we'll advance to the next record.
  212. if (false !== $result)
  213. $this->next();
  214. return $result;
  215. }
  216. /**
  217. * @return string current query
  218. */
  219. public function getQuery ()
  220. {
  221. return $this->_query;
  222. }
  223. /**
  224. * @return true if there are more available.
  225. */
  226. public function hasNext ()
  227. {
  228. return $this->count() && ($this->_sparkfinder->position() < $this->count());
  229. }
  230. /**
  231. * Load some class properties with meta data about $this->_recordClass.
  232. */
  233. protected function handleClass ()
  234. {
  235. // Hack our way around some PHP fail:
  236. $rc = $this->_recordClass;
  237. // If these methods exist, we're probably dealing with a SparkRecord.
  238. // Otherwise, it's something else.
  239. if (method_exists($rc, 'getTableKey')) {
  240. $this->_tableKey = $rc::getDefaultTableKey();
  241. $this->_table = $rc::getDefaultTableName();
  242. $this->_tableRecord = $rc::getDefaults();
  243. }
  244. }
  245. /**
  246. * Set a wrapper class for results.
  247. */
  248. public function wrap ($class)
  249. {
  250. if (class_exists($class)) {
  251. $this->_wrapperClass = $class;
  252. } else {
  253. throw new Exception($class . ' does not exist');
  254. }
  255. return $this;
  256. }
  257. /**
  258. * Build the query.
  259. *
  260. * @return string sql
  261. */
  262. public function buildQuery ()
  263. {
  264. // Build and save the query
  265. $this->_query = $this->_base_query() .
  266. $this->_where .
  267. $this->_groupBy .
  268. $this->_orderBy .
  269. $this->_limit .
  270. $this->_forUpdate . "\n";
  271. return $this->_query;
  272. }
  273. /**
  274. * Build the query, execute it, and load the first object.
  275. *
  276. * @return SparkFriendFinder - $this, for method chains.
  277. */
  278. public function find ()
  279. {
  280. // Build and save the query
  281. $this->buildQuery();
  282. // clear query parts
  283. $this->_join = '';
  284. $this->_where = '';
  285. $this->_orderBy = '';
  286. $this->_limit = '';
  287. $this->_with = '';
  288. // Get a SparkFinder and stash it
  289. $this->getResults();
  290. return $this;
  291. }
  292. /**
  293. * Get a database handle.
  294. */
  295. protected function getDBH ()
  296. {
  297. return DB::getInstance();
  298. }
  299. /**
  300. * Turn the current query into a result set and stash it.
  301. */
  302. protected function getResults ()
  303. {
  304. try {
  305. $this->_sparkfinder = new SparkFinder(
  306. $this->_recordClass,
  307. $this->_query
  308. );
  309. } catch (Exception $e) {
  310. throw new Exception('Query fail: ' . $this->_query . ' -- ' . $e->getMessage());
  311. }
  312. // PDO TODO: Come up with a graceful way to request an unbuffered resultset.
  313. // (Presumably necessitates a connection per resultset.)
  314. }
  315. /**
  316. * Make the query a FOR UPDATE query.
  317. *
  318. * Essentially what this does is put a row lock on
  319. * every row that is part of the SELECT query such that
  320. * any other query (with a FOR UPDATE clause) that matches
  321. * any of the selected rows for this finder will block
  322. * until the current transaction has completed, either by
  323. * a commit or rollback.
  324. */
  325. public function forUpdate () {
  326. $this->_forUpdate = ' FOR UPDATE';
  327. return $this;
  328. }
  329. /**
  330. * Run find() and get the first result. Just a utility to shorten
  331. * things up a bit. If find() has already been called, will act
  332. * as an alias to current(), rather than re-running a bare query.
  333. *
  334. * @return SparkRecord
  335. */
  336. public function getOne ()
  337. {
  338. if (isset($this->_sparkfinder))
  339. return $this->current();
  340. return $this->find()->current();
  341. }
  342. /**
  343. * JOIN the given dinosaur by the relationship defined in records.ini
  344. */
  345. public function with ($dinosaur)
  346. {
  347. $record_class = $this->_recordClass;
  348. $relationships = $record_class::getRelationships();
  349. if (! isset($relationships[$dinosaur]))
  350. throw new Exception('Invalid relationship: ' . $dinosaur);
  351. if(in_array($relationships[$dinosaur]['type'], array('has_one', 'has_many'))) {
  352. $our_key = $relationships[$dinosaur]['pk'];
  353. $their_key = $relationships[$dinosaur]['fk'];
  354. } elseif($relationships[$dinosaur]['type'] == 'belongs_to') {
  355. $our_key = $relationships[$dinosaur]['fk'];
  356. $their_key = $relationships[$dinosaur]['pk'];
  357. }
  358. // If this is a one to many relationship, use a left join
  359. /*
  360. $has_many = $record_class::getHasManyDefaults();
  361. if($has_many[$dinosaur])
  362. $this->_join .= ' LEFT';
  363. */
  364. $classname = $relationships[$dinosaur]['class'];
  365. $this->_with .= $this->_generate_with($classname);
  366. $this->_join .= " JOIN {$classname::getDefaultTableName()} {$classname::getDefaultTableName()} ON {$this->_table}.{$our_key} = {$classname::getDefaultTableName()}.{$their_key} \n";
  367. return $this;
  368. }
  369. /**
  370. * JOIN another dinosaur by the relationship
  371. * defined in records.ini
  372. * TODO: Maybe merge this with with() or something less verbose?
  373. */
  374. public function withOn ($dinosaur, $joinon)
  375. {
  376. $record_class = $joinon . 'Saurus';
  377. $relationships = $record_class::getRelationships();
  378. if(! isset($relationships[$dinosaur]))
  379. throw new Exception('Invalid relationship: ' . $dinosaur . ' on ' . $record_class);
  380. if(in_array($relationships[$dinosaur]['type'], array('has_one', 'has_many'))) {
  381. $our_key = $relationships[$dinosaur]['pk'];
  382. $their_key = $relationships[$dinosaur]['fk'];
  383. } elseif($relationships[$dinosaur]['type'] == 'belongs_to') {
  384. $our_key = $relationships[$dinosaur]['fk'];
  385. $their_key = $relationships[$dinosaur]['pk'];
  386. }
  387. $classname = $relationships[$dinosaur]['class'];
  388. $this->_with .= $this->_generate_with($classname);
  389. $this->_join .= " JOIN {$classname::getDefaultTableName()} {$classname::getDefaultTableName()} ON {$record_class::getDefaultTableName()}.{$our_key} = {$classname::getDefaultTableName()}.{$their_key} \n";
  390. return $this;
  391. }
  392. /**
  393. * LEFT JOIN a table
  394. *
  395. * @param string $sql LEFT JOIN table on pkey = fkey
  396. */
  397. public function leftJoin ($sql)
  398. {
  399. $sql = trim($sql);
  400. if (! strlen($sql))
  401. return $this;
  402. // make sure it is left join sql
  403. if (strtoupper(substr($sql, 0, 9)) != 'LEFT JOIN')
  404. throw new Exception('Invalid LEFT JOIN clause: ' . $sql);
  405. $this->_left_join = ' ' . $sql . ' ';
  406. return $this;
  407. }
  408. /**
  409. * put some extra fields in the SELECT
  410. * for use with leftJoin() or sql functions
  411. *
  412. * BEWARE! If you use this in public-facing code with user inputs,
  413. * Brennen WILL murder you.
  414. *
  415. * @param array $fields fields or sql expressions like COUNT(*)
  416. */
  417. public function extraSelects ($fields)
  418. {
  419. if (! is_array($fields) || ! count($fields))
  420. throw new Exception('extraSelects expects a non-empty array');
  421. $sql = implode(', ', $fields) . ','; // needs trailing comma
  422. $this->_extraselects = $sql . ' ';
  423. return $this;
  424. }
  425. /**
  426. * To select only the primary table id and any extra selects
  427. * (instead of select * from all tables)
  428. *
  429. */
  430. public function selectIdOnly ()
  431. {
  432. $this->_selectIdOnly = true;
  433. return $this;
  434. }
  435. /**
  436. * FIELD table field name
  437. * **BEWARE: This function is rendered useless with a join or extraselects.
  438. */
  439. public function isValidField ($field)
  440. {
  441. if ($this->_join === '' && $this->_left_join === '' && $this->_extraselects === '') {
  442. return isset($this->_tableRecord[$field]);
  443. } else {
  444. return true;
  445. }
  446. }
  447. /**
  448. * Add to the WHERE clause for a given field.
  449. *
  450. * Be careful - this may fail if you're doing a ->with() _after_ the where().
  451. * Do with() first.
  452. *
  453. * @param $field string
  454. * @return SparkFriendFinder
  455. */
  456. public function where ($field)
  457. {
  458. if (!$this->isValidField($field))
  459. throw new Exception('Invalid field in where clause: ' . $field);
  460. $this->_where .= "\n AND " . $field;
  461. return $this;
  462. }
  463. public function whereSubstring($field, $from, $for)
  464. {
  465. if (!$this->isValidField($field))
  466. throw new Exception('Invalid field in where clause: ' . $field);
  467. if (!is_integer($from) || !is_integer($for))
  468. throw new Exception('Invalid from/for in whereSubstring clause ' . $field);
  469. $this->_where .= "\n AND substring(" . $field . " from " . $from . " for " . $for . ")";
  470. return $this;
  471. }
  472. /**
  473. * Add a WHERE lower($field)
  474. *
  475. * @param $field string
  476. * @return SparkFriendFinder
  477. */
  478. public function whereLower ($field)
  479. {
  480. if (!$this->isValidField($field))
  481. throw new Exception('Invalid field in where clause: ' . $field);
  482. $this->_where .= "\n AND lower(" . $field . ")";
  483. return $this;
  484. }
  485. public function orWhere ($field)
  486. {
  487. if (!$this->isValidField($field))
  488. throw new Exception('Invalid field in where clause: ' . $field);
  489. $this->_where .= "\n OR " . $field;
  490. return $this;
  491. }
  492. public function orWhereSubstring($field, $from, $for)
  493. {
  494. if (!$this->isValidField($field))
  495. throw new Exception('Invalid field in where clause: ' . $field);
  496. if (!is_integer($from) || !is_integer($for))
  497. throw new Exception('Invalid from/for in orWhereSubstring clause ' . $field);
  498. $this->_where .= "\n OR substring(" . $field . " from " . $from . " for " . $for . ")";
  499. return $this;
  500. }
  501. /**
  502. * Add to the WHERE clause for a given subquery evaluation.
  503. *
  504. * Like with where() be careful - this may fail if you're doing
  505. * a ->with() _after_ the where(). Do with() first.
  506. *
  507. * @param $subquery string
  508. * @return SparkFriendFinder
  509. */
  510. public function subquery ($subquery)
  511. {
  512. $this->_where .= "\n AND (\n"
  513. . $subquery . "\n"
  514. . ")";
  515. return $this;
  516. }
  517. /**
  518. * startSub - begin a sub-where clause in the query.
  519. *
  520. * example:
  521. *
  522. * $orders = OrderSaurus::finder()
  523. * $orders->startSub();
  524. * $orders->orWhere('customers_name')->like('%' . $s_query . '%');
  525. * $orders->orWhere('customers_email_address')->like('%' . $s_query . '%');
  526. * $orders->endSub();
  527. * $orders->find();
  528. *
  529. * @param $op string of either AND or OR to prepend to the outside of the subclause
  530. * @param $subop string of either AND or OR which will affect the subclause
  531. */
  532. public function startSub ($op = 'AND', $subop = 'OR')
  533. {
  534. if('AND' == strtoupper($op))
  535. $this->_where .= "\n AND ( ";
  536. if('OR' == strtoupper($op))
  537. $this->_where .= "\n OR ( ";
  538. if('AND' == strtoupper($subop))
  539. $this->_where .= ' true ';
  540. if('OR' == strtoupper($subop))
  541. $this->_where .= ' false ';
  542. return $this;
  543. }
  544. public function endSub ()
  545. {
  546. $this->_where .= "\n ) ";
  547. return $this;
  548. }
  549. public function orderBy ($field, $direction = 'ASC', $nulls = null)
  550. {
  551. if ('ASC' != $direction && 'DESC' != $direction)
  552. throw new Exception('Invalid direction in "orderBy" clause: ' . $direction);
  553. if (isset($nulls) && (('FIRST' !== $nulls) && ('LAST' !== $nulls)))
  554. throw new Exception('Invalid NULLS in "orderBy" clause: ' . $nulls);
  555. if ($field instanceof \SparkLib\DB\Literal) {
  556. // right now, this just handles Random() - might be other things, tho:
  557. $field = $field->literal();
  558. } else {
  559. // it's a string - check whether it's safe to order by
  560. // We're unable to validate fields if a FIND_IN_SET or COALESCE is present.
  561. if (!stristr($field, 'FIND_IN_SET') && !stristr($field, 'COALESCE')) {
  562. if (!$this->isValidField($field))
  563. throw new Exception('Invalid field in orderBy clause: ' . $field);
  564. }
  565. }
  566. if ('' == $this->_orderBy)
  567. $this->_orderBy = ' ORDER BY ' . $field . ' ' . $direction;
  568. else
  569. $this->_orderBy .= ', ' . $field . ' ' . $direction;
  570. if (isset($nulls))
  571. $this->_orderBy .= ' NULLS ' . $nulls;
  572. return $this;
  573. }
  574. public function groupBy ($field)
  575. {
  576. // We're unable to validate fields if a FIND_IN_SET is present.
  577. if (!stristr($field, 'FIND_IN_SET')) {
  578. if (!$this->isValidField($field) && $field !== self::RAND)
  579. throw new Exception('Invalid field in "groupBy" clause: ' . $field);
  580. }
  581. if ('' == $this->_groupBy)
  582. $this->_groupBy = ' GROUP BY ' . $field;
  583. else
  584. $this->_groupBy .= ', ' . $field;
  585. return $this;
  586. }
  587. public function distinctOn ($field)
  588. {
  589. // We're unable to validate fields if a FIND_IN_SET is present.
  590. if (!stristr($field, 'FIND_IN_SET')) {
  591. if (!$this->isValidField($field) && $field !== self::RAND)
  592. throw new Exception('Invalid field in "distinct" clause: ' . $field);
  593. }
  594. if ('' == $this->_distinctOn)
  595. $this->_distinctOn = ' DISTINCT ON (' . $field . ')';
  596. else
  597. $this->_distinctOn = str_replace(')', ',', $this->_distinctOn) . $field . ')';
  598. return $this;
  599. }
  600. /**
  601. * PDO TODO: document
  602. */
  603. public function limit ($limit, $offset = 0)
  604. {
  605. $limit = (int) $limit;
  606. $offset = (int) $offset;
  607. if ('' != $this->_limit)
  608. throw new Exception('You may only impose one "limit" clause.');
  609. if($limit > 0)
  610. $this->_limit = ' LIMIT ' . $limit;
  611. $this->_limit .= ' OFFSET ' . $offset;
  612. return $this;
  613. }
  614. /**
  615. * PDO TODO: document
  616. */
  617. public function eq ($val)
  618. {
  619. if (is_array($val)) {
  620. $this->_where .= ' IN(';
  621. foreach($val as $i => $v) {
  622. $this->_where .= $this->render($v);
  623. if($i+1 < count($val))
  624. $this->_where .= ',';
  625. }
  626. $this->_where .= ')';
  627. } else {
  628. $this->_where .= ' = ' . $this->render($val);
  629. }
  630. return $this;
  631. }
  632. /**
  633. * A hack to look for the lower-case version of a string.
  634. *
  635. * We'll let the database handle lower-casing, since there's a good
  636. * chance it's smarter than PHP.
  637. */
  638. public function eqLower ($val)
  639. {
  640. if (! is_string($val)) {
  641. throw new \Exception('lowerEq() expects a string, for now');
  642. }
  643. $this->_where .= ' = lower(' . $this->render($val) . ')';
  644. return $this;
  645. }
  646. /**
  647. * PDO TODO: document
  648. */
  649. public function ne ($val)
  650. {
  651. if (is_array($val)) {
  652. $this->_where .= ' NOT IN(';
  653. foreach($val as $i => $v) {
  654. $this->_where .= $this->render($v);
  655. if($i+1 < count($val))
  656. $this->_where .= ',';
  657. }
  658. $this->_where .= ')';
  659. } else {
  660. $this->_where .= ' <> ' . $this->render($val);
  661. }
  662. return $this;
  663. }
  664. /**
  665. * IS - use with a DB\Null, for example.
  666. */
  667. public function is ($val)
  668. {
  669. $this->_where .= ' IS ' . $this->render($val);
  670. return $this;
  671. }
  672. /**
  673. * IS NOT - use with a DB\Null, for example.
  674. */
  675. public function isnot ($val)
  676. {
  677. $this->_where .= ' IS NOT ' . $this->render($val);
  678. return $this;
  679. }
  680. /**
  681. * IN()
  682. * Added so we can use sub-selects. NOT ESCAPED.
  683. * eq/ne should be used under normal circumstances
  684. */
  685. public function in ($val)
  686. {
  687. // this isn't escaped because we need IN((query)) to be quote-free.
  688. // be seriously fucking careful.
  689. $this->_where .= ' IN(' . $val . ')';
  690. return $this;
  691. }
  692. /**
  693. * like in, but not. NOT ESCAPED.
  694. */
  695. public function nin ($val)
  696. {
  697. // this isn't escaped because we need NOT IN((query)) to be quote-free.
  698. // be really goddamned careful.
  699. $this->_where .= ' NOT IN(' . $val . ')';
  700. return $this;
  701. }
  702. public function gt ($val) { $this->_where .= ' > ' . $this->render($val); return $this; }
  703. public function lt ($val) { $this->_where .= ' < ' . $this->render($val); return $this; }
  704. public function between($val1, $val2) { $this->_where .= ' BETWEEN ' . $this->render($val1) . ' AND ' . $this->render($val2); return $this; }
  705. public function gte ($val) { $this->_where .= ' >= ' . $this->render($val); return $this; }
  706. public function lte ($val) { $this->_where .= ' <= ' . $this->render($val); return $this; }
  707. public function like ($val) { $this->_where .= ' LIKE ' . $this->render($val); return $this; }
  708. public function plus ($val) { $this->_where .= ' + ' . $this->render($val); return $this; }
  709. public function minus ($val) { $this->_where .= ' - ' . $this->render($val); return $this; }
  710. /**
  711. * Case-insensitive like.
  712. */
  713. public function ilike ($val)
  714. {
  715. // MySQL doesn't support ILIKE
  716. if ('mysql' !== constant('\DB_SERVER_TYPE')) {
  717. $this->_where .= ' ILIKE ' . $this->render($val);
  718. return $this;
  719. }
  720. return $this->like($val);
  721. }
  722. public function has($val) {
  723. return $this->like('%' . $val . '%');
  724. }
  725. public function ihas($val) {
  726. return $this->ilike('%' . $val . '%');
  727. }
  728. public function nlike ($val) { $this->_where .= ' NOT LIKE ' . $this->render($val); return $this; }
  729. /**
  730. * Case-insensitive nlike.
  731. */
  732. public function nilike ($val)
  733. {
  734. // MySQL doesn't support ILIKE
  735. if ('mysql' !== constant('\DB_SERVER_TYPE')) {
  736. $this->_where .= ' NOT ILIKE ' . $this->render($val);
  737. return $this;
  738. }
  739. return $this->nlike($val);
  740. }
  741. /**
  742. * Use buffered results.
  743. */
  744. public function buffered ()
  745. {
  746. $this->_buffered = true;
  747. return $this;
  748. }
  749. /**
  750. * Use unbuffered results.
  751. */
  752. public function unbuffered ()
  753. {
  754. // PDO TODO: it would be nice to make unbuffered resultsets available
  755. throw new \Exception('unbuffered queries are not yet implemented for SparkFriendFinder');
  756. $this->_buffered = false;
  757. return $this;
  758. }
  759. /**
  760. * Custom escape to quote and otherwise render strings.
  761. */
  762. protected function render ($value)
  763. {
  764. $value = $this->typeJuggle($value);
  765. if ($value instanceof DB\Literal) {
  766. return $value->literal();
  767. }
  768. return $this->getDBH()->quote($value);
  769. }
  770. /**
  771. * Normalize values to the scheme expected by render()
  772. */
  773. protected function typeJuggle ($val)
  774. {
  775. // promote actual PHP nulls to SQL nulls
  776. if (is_null($val))
  777. $val = new DB\Null;
  778. if ($val instanceof DB\Literal)
  779. return $val;
  780. if (is_string($val))
  781. return $val;
  782. if (is_bool($val)) {
  783. if ($val)
  784. $val = new DB\True;
  785. else
  786. $val = new DB\False;
  787. }
  788. return $val;
  789. }
  790. /**
  791. * Initial SELECT and WHERE.
  792. *
  793. * If a JOIN is present, it will be included.
  794. */
  795. protected function _base_query ()
  796. {
  797. $this->_default_query = 'SELECT ' .
  798. $this->_distinctOn . ' ' .
  799. $this->_extraselects;
  800. // if true, only primary table key and extra selects are selected
  801. if ($this->_selectIdOnly) {
  802. $this->_default_query .= $this->_table . '.' . $this->_tableKey;
  803. } else {
  804. $this->_default_query .= $this->_table . '.* ' .
  805. $this->_with;
  806. }
  807. $this->_default_query .= ' FROM ' . $this->_table .
  808. $this->_join .
  809. $this->_left_join .
  810. ' WHERE ' . (($this->_key && $this->_id) ? $this->_key . ' = ' . $this->_id : 'true ');
  811. return $this->_default_query;
  812. }
  813. /**
  814. * Generates a string which selects and aliases
  815. * each column for a given dinosaur.
  816. */
  817. protected function _generate_with ($dinosaur)
  818. {
  819. $with = '';
  820. $table = $dinosaur::getDefaultTableName();
  821. $columns = array_keys($dinosaur::getDefaults());
  822. foreach ($columns as $column) {
  823. $with .= ", " . $table . '.' . $column . ' AS "' . $dinosaur::getBaseName() . '__' . $column . '"';
  824. }
  825. return $with;
  826. }
  827. }