TeraWurflDatabase
[ class tree: TeraWurflDatabase ] [ index: TeraWurflDatabase ] [ all elements ]

Source for file TeraWurflDatabase_MongoDB.php

Documentation is available at TeraWurflDatabase_MongoDB.php

  1. <?php
  2. /**
  3.  * Tera_WURFL - PHP MySQL driven WURFL
  4.  *
  5.  * Tera-WURFL was written by Steve Kamerman, and is based on the
  6.  * Java WURFL Evolution package by Luca Passani and WURFL PHP Tools by Andrea Trassati.
  7.  * This version uses a MySQL database to store the entire WURFL file, multiple patch
  8.  * files, and a persistent caching mechanism to provide extreme performance increases.
  9.  *
  10.  * @package TeraWurflDatabase
  11.  * @author Steve Kamerman <stevekamerman AT gmail.com>
  12.  * @author Simon Harris <tw AT pointbeing.net>
  13.  * @version Stable 2.1.3 $Date: 2010/09/18 15:43:21
  14.  * @license http://www.mozilla.org/MPL/ MPL Vesion 1.1
  15.  */
  16. /**
  17.  * Provides connectivity from Tera-WURFL to MongoDB
  18.  * @package TeraWurflDatabase
  19.  * @see TeraWurflDatabase
  20.  */
  21.     /**
  22.      * MongoDB connection options
  23.      * @link http://us.php.net/manual/en/mongo.construct.php
  24.      * @var array 
  25.      */
  26.     protected static $CONNECTION_OPTIONS array(
  27.         "persist" => "Tera-WURFL",
  28. //        "timeout" => 500,
  29. //        "replicaSet" => true,
  30.     );
  31.     
  32.     /**
  33.      * @var array 
  34.      */
  35.     public $errors;
  36.  
  37.     /**
  38.      * @var boolean 
  39.      */
  40.     public $db_implements_ris = true;
  41.     
  42.     /**
  43.      * @var boolean 
  44.      */
  45.     public $db_implements_fallback = true;
  46.  
  47.     /**
  48.      * @var boolean 
  49.      * @todo See if this can be implemented
  50.      */
  51.     public $db_implements_ld = false;
  52.  
  53.     /**
  54.      * @var int 
  55.      */
  56.     public $numQueries = 0;
  57.  
  58.     /**
  59.      * @var boolean 
  60.      */
  61.     public $connected = false;
  62.  
  63.     /**
  64.      * @var Mongo 
  65.      */
  66.     protected $mongo;
  67.  
  68.     /**
  69.      * @var MongoDB 
  70.      */
  71.     protected $dbcon;
  72.     
  73.     /**
  74.      * @var string 
  75.      */
  76.     protected static $MERGE;
  77.     
  78.     /**
  79.      * @var MongoCollection 
  80.      */
  81.     protected $mergecoll;
  82.     
  83.     /**
  84.      * @var int 
  85.      */
  86.     protected static $PREALLOC_SIZE 31457280;
  87.     
  88.     public function __construct(){
  89.         parent::__construct();
  90.         self::$MERGE TeraWurflConfig::$TABLE_PREFIX.'Merge';
  91.     }
  92.     
  93.     // Device Table Functions (device, hybrid, patch) --------------------------
  94.  
  95.  
  96.     /**
  97.      * @param string $wurflID 
  98.      * @throws Exception
  99.      * @return array The device capabilities
  100.      */
  101.     public function getDeviceFromID($wurflID{
  102.         $tofind array(
  103.                     'deviceID' => $wurflID,
  104.         );
  105.  
  106.         $device $this->mergecoll->findOne($tofind);
  107.         $this->numQueries++;
  108.  
  109.         if (is_null($device)) {
  110.             // is this really an Exception? Cloned from MySQL5 driver
  111.             throw new Exception("Tried to lookup an invalid WURFL Device ID: $wurflID");
  112.         }
  113.  
  114.         return $device['capabilities'];
  115.     }
  116.  
  117.  
  118.     /**
  119.      * @param string $tablename 
  120.      * @return array 
  121.      */
  122.     public function getFullDeviceList($tablename){
  123.         $matcher $this->getMatcherNameFromTable($tablename);
  124.         $res $this->mergecoll->find(array("matcher"=>$matcher));
  125.         $this->numQueries++;
  126.  
  127.         $data array();
  128.         foreach ($res as $device{
  129.             $data[$device['deviceID']] $device['user_agent'];
  130.         }
  131.         return $data;
  132.     }
  133.  
  134.  
  135.     /**
  136.      * Exact match by user agent string
  137.      *
  138.      * @param string $userAgent 
  139.      */
  140.     public function getDeviceFromUA($userAgent{
  141.         $tofind array(
  142.                     'user_agent' => utf8_encode($userAgent),
  143.         );
  144.         $data $this->mergecoll->findOne($tofind);
  145.         $this->numQueries++;
  146.  
  147.         if (is_null($data)) {
  148.             return false;
  149.         }
  150.  
  151.         return $data['deviceID'];
  152.     }
  153.  
  154.  
  155.     /**
  156.      * RIS == Reduction in String (reduce string one char at a time)
  157.      *
  158.      * @param string $userAgent 
  159.      * @param int $tolerance 
  160.      * @param UserAgentMatcher $matcher 
  161.      * @return string A TW Device ID
  162.      */
  163.     public function getDeviceFromUA_RIS($userAgent$toleranceUserAgentMatcher $matcher{
  164.  
  165.         $toexec 'function(ua, tolerance, matcher) { return performRis(ua, tolerance, matcher) }';
  166.         $args   array(utf8_encode($userAgent)$tolerance$matcher->tableSuffix());
  167.  
  168.         $this->numQueries++;
  169.         $response $this->dbcon->execute($toexec$args);
  170.         if !empty($response['ok']&& $response['ok'== && !empty($response['retval'])) {
  171.             return $response['retval'];
  172.         }
  173.         return WurflConstants::$GENERIC;
  174.     }
  175.     public function getDeviceFallBackTree($wurflID){
  176.         $this->numQueries++;
  177.         $response $this->dbcon->execute('function(deviceID){ return performFallback(deviceID) }',array($wurflID));
  178.         $data $response['retval'];
  179.         if($data[count($data)-1]['id'!= WurflConstants::$GENERIC){
  180.             $tw new TeraWurfl();
  181.             $tw->toLog("WURFL Error: device {$data[count($data)-1]['id']} falls back on an inexistent device: {$data[count($data)-1]['fall_back']}",LOG_ERR,__CLASS__.'::'.__FUNCTION__);
  182.         }
  183.         return $data;
  184.     }
  185.  
  186.     protected function getMatcherNameFromTable($table){
  187.         $parts explode('_'$table);
  188.         $matcher array_pop($parts);
  189.         return $matcher;
  190.     }
  191.     /**
  192.      * @param array $tables 
  193.      */
  194.     public function loadDevices(&$tables{
  195.  
  196.         $insert_errors array();
  197.         $insertcache   array();
  198.         $insertedrows  0;
  199.  
  200.         $this->createProcedures();
  201.         // insert records into a new temp table until we know everything is OK
  202.         $temptable self::$MERGE self::$DB_TEMP_EXT;
  203.         $this->_dropCollectionIfExists($temptable);
  204.         // create this collection manually since it is fixed
  205.         $this->dbcon->command(array(
  206.             "create" => $temptable,
  207.             "size" => self::$PREALLOC_SIZE,
  208. //            "capped" => false,
  209. //            "autoIndexId" => false
  210.         ));
  211.         $collection $this->dbcon->selectCollection($temptable);
  212.  
  213.         foreach ($tables as $table => $devices{
  214.             $matcher $this->getMatcherNameFromTable($table);
  215.             $matcherbatch array();
  216.             foreach ($devices as $device{
  217.                 $matcherbatch[array(
  218.                                 'deviceID'            => $device['id'],
  219.                                 'user_agent'        => $device['user_agent'],
  220.                                 'fall_back'            => $device['fall_back'],
  221.                                 'match'                => preg_match('/^DO_NOT_MATCH/',$device['user_agent'])falsetrue,
  222.                                 'actual_device_root'=> (isset($device['actual_device_root']) ) $device['actual_device_root''',
  223.                                 'matcher'            => $matcher,
  224.                                 'capabilities'        => $device,
  225.                 );
  226.                 $insertedrows++;
  227.             }
  228.             try{
  229.                 $collection->batchInsert($matcherbatch);
  230.                 $this->numQueries++;
  231.  
  232.             catch (Exception $e{
  233.                 $insert_errors['DB server reported error on id "' $device['id''": ' $e->getMessage();
  234.             }
  235.  
  236.             if (count($insert_errors0{
  237.  
  238.                 // Roll back changes, and leave the temp table in the DB for manual inspection
  239.                 $this->errors = array_merge($this->errors$insert_errors);
  240.                 return false;
  241.             }
  242.         }
  243.         // Commit changes
  244.         $this->_dropCollectionIfExists(self::$MERGE);
  245.         $this->_renameCollection($temptableself::$MERGE);
  246.         // Enforce Indecies
  247.         $this->mergecoll->ensureIndex(array('deviceID' => 1)array("unique"=>true,"dropDups"=>true,"background"=>true,"safe"=>false));
  248.         $this->mergecoll->ensureIndex(array('user_agent' => 1)array("unique"=>false,"dropDups"=>false,"background"=>true,"safe"=>false));
  249.         $this->mergecoll->ensureIndex(array('fall_back' => 1)array("unique"=>false,"dropDups"=>false,"background"=>true,"safe"=>false));
  250.         $this->mergecoll->ensureIndex(array('matcher' => 1)array("unique"=>false,"dropDups"=>false,"background"=>true,"safe"=>false));
  251.         $this->mergecoll->ensureIndex(array('match' => 1)array("unique"=>false,"dropDups"=>false,"background"=>true,"safe"=>false));
  252.         return true;
  253.     }
  254.     
  255.     public function createSettingsTable(){
  256.         $name TeraWurflConfig::$TABLE_PREFIX.'Settings';
  257.         if($this->collectionExists($name)){
  258.             $this->_createCollection($name);
  259.             $collection $this->dbcon->selectCollection($name);
  260.             //$collection->ensureIndex(array('id' => 1), array("unique"=>true,"dropDups"=>true,"background"=>false,"safe"=>true));
  261.         }
  262.     }
  263.     // Cache Table Functions ---------------------------------------------------
  264.  
  265.  
  266.     /**
  267.      * Drops, creates and indexes the cache table
  268.      */
  269.     public function createCacheTable(){
  270.         $name TeraWurflConfig::$TABLE_PREFIX.'Cache';
  271.         $this->_recreateCollection($name);
  272.         $collection $this->dbcon->selectCollection($name);
  273.         $collection->ensureIndex(array('user_agent' => 1)array("unique"=>true,"dropDups"=>true,"background"=>true,"safe"=>false));
  274.     }
  275.  
  276.  
  277.     /**
  278.      * @param string $userAgent 
  279.      * @return array Should return (bool) false or the device array
  280.      */
  281.     public function getDeviceFromCache($userAgent{
  282.  
  283.         $tofind array(
  284.                         'user_agent' => $userAgent,
  285.         );
  286.  
  287.         try {
  288.             $cachecoll $this->dbcon->selectCollection(TeraWurflConfig::$TABLE_PREFIX.'Cache');
  289.             $device $cachecoll->findOne($tofind);
  290.             $this->numQueries++;
  291.  
  292.             if !is_null($device) ) {
  293.                 return $device['cache_data'];
  294.             }
  295.  
  296.         catch(Exception $e{
  297.             $this->errors[$e->__toString(', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
  298.         }
  299.         return false;
  300.     }
  301.  
  302.  
  303.     /**
  304.      * @param string $userAgent 
  305.      * @param array $device 
  306.      * @return boolean Whether the insert was successful
  307.      */
  308.     public function saveDeviceInCache($userAgent$device{
  309.  
  310.         $toinsert array(
  311.                         'user_agent' => utf8_encode($userAgent),
  312.                         'cache_data' => $device,
  313.         );
  314.  
  315.         try {
  316.             $cachecoll $this->dbcon->selectCollection(TeraWurflConfig::$TABLE_PREFIX.'Cache');
  317.             $cachecoll->insert($toinsertarray('safe' => true));
  318.             $this->numQueries++;
  319.             return true;
  320.  
  321.         catch(Exception $e{
  322.             $this->errors[$e->__toString(', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
  323.         }
  324.         return false;
  325.     }
  326.  
  327.  
  328.     /**
  329.      * @return boolean 
  330.      */
  331.     public function rebuildCacheTable({
  332.  
  333.         // Use this instance to rebuild the cache and to facilitate logging
  334.         $rebuilder new TeraWurfl();
  335.  
  336.         $cachetable TeraWurflConfig::$TABLE_PREFIX.'Cache';
  337.         $temptable  TeraWurflConfig::$TABLE_PREFIX.'Cache' self::$DB_TEMP_EXT;
  338.  
  339.         $this->_dropCollectionIfExists($temptable);
  340.         $this->_renameCollection($cachetable$temptable);
  341.         $this->createCacheTable();
  342.  
  343.         $tempcoll  $this->dbcon->selectCollection($temptable);
  344.         $cachecoll $this->dbcon->selectCollection($cachetable);
  345.  
  346.         /* @var $fromcache MongoCursor */
  347.         $fromcache $tempcoll->find(array(),array("user_agent" => 1));
  348.         $this->numQueries++;
  349.  
  350.         // migrate cached items from old cache
  351.         if (== $fromcache->count()) {
  352.             // No records in cache table == nothing to rebuild
  353.             $this->_dropCollectionIfExists($temptable);
  354.             $rebuilder->toLog('Rebuilt cache table, existing table was empty - this is very unusual.'LOG_WARNING__FUNCTION__);
  355.             return true;
  356.         }
  357.  
  358.         foreach ($fromcache as $item{
  359.  
  360.             // Just looking the device up will force it to be cached
  361.             $rebuilder->getDeviceCapabilitiesFromAgent($item['user_agent']);
  362.  
  363.             // Reset the number of queries since we're not going to re-instantiate the object
  364.             $this->numQueries += $rebuilder->db->numQueries;
  365.             $rebuilder->db->numQueries 0;
  366.         }
  367.  
  368.         $this->_dropCollectionIfExists($temptable);
  369.         $rebuilder->toLog('Rebuilt cache table.'LOG_NOTICE__FUNCTION__);
  370.         return true;
  371.     }
  372.  
  373.  
  374.     // Supporting DB functions -------------------------------------------------
  375.     public function getServerVersion(){
  376.         $status $this->dbcon->command(array("serverStatus"=>1));
  377.         return "MongoDB ".$status['version'];
  378.     }
  379.  
  380.  
  381.     /**
  382.      * Runs any stored procs needed to set up db
  383.      */
  384.     public function createProcedures({
  385.  
  386.         // clear the db.system.js collection
  387.         $collection $this->dbcon->selectCollection('system.js');
  388.         $this->numQueries++;
  389.         $collection->remove(array("_id"=>"performRis"));
  390.         $merge self::$MERGE;
  391.         $performRis =<<<EOL
  392. function performRis(ua, tolerance, matcher) {
  393.     var curlen = ua.length;
  394.     var curua;
  395.     while (curlen >= tolerance) {
  396.         var toMatch = ua.substr(0, curlen);
  397.         toMatch     = toMatch.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\\\$&");
  398.         var matchReg   = new RegExp('^' + toMatch);
  399.         var device = db.$merge.findOne({match:true, matcher:matcher, user_agent: matchReg},{deviceID:1});
  400.  
  401.            if(device != null){
  402.                 return device.deviceID;
  403.            }
  404.         curlen--;
  405.     }
  406. }
  407. EOL;
  408.         $this->numQueries++;
  409.         $collection->save(
  410.             array('_id' => 'performRis','value' => new MongoCode($performRis)),
  411.             array('safe' => true)
  412.         );
  413.         $this->numQueries++;
  414.         $collection->remove(array("_id"=>"performFallback"));
  415.         $performFallback =<<<EOL
  416. function performFallback(deviceID){
  417.     var current_fall_back = deviceID;
  418.     var res;
  419.     var tree = [];
  420.     var i = 0;
  421.     while(current_fall_back != 'root' && i++ < 30){
  422.         res = db.$merge.findOne({deviceID:current_fall_back},{capabilities:1});
  423.         if(!res) return tree;
  424.         tree.push(res.capabilities);
  425.         current_fall_back = res.capabilities.fall_back;
  426.     }
  427.     return tree;
  428. }
  429. EOL;
  430.         $this->numQueries++;
  431.         $collection->save(
  432.             array('_id' => 'performFallback','value' => new MongoCode($performFallback)),
  433.             array('safe' => true)
  434.         );
  435.     }
  436.  
  437.  
  438.     /**
  439.      * Establishes connection to database (does not check for DB sanity)
  440.      *
  441.      * @return boolean 
  442.      */
  443.     public function connect({
  444.  
  445.         $this->numQueries++;
  446.  
  447.         try {
  448.             $this->mongo = new Mongo(TeraWurflConfig::$DB_HOST,self::$CONNECTION_OPTIONS);
  449.             $this->dbcon = $this->mongo->selectDB(TeraWurflConfig::$DB_SCHEMA);
  450.  
  451.             if (!empty(TeraWurflConfig::$DB_USER&& !empty(TeraWurflConfig::$DB_PASS)) {
  452.                 $this->dbcon->authenticate(TeraWurflConfig::$DB_USERTeraWurflConfig::$DB_PASS);
  453.             }
  454.  
  455.         catch(Exception $e{
  456.             $this->errors[$e->__toString(', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
  457.             $this->connected = $e->getCode();
  458.             return false;
  459.         }
  460.         $this->mergecoll = $this->dbcon->selectCollection(self::$MERGE);
  461.         $this->connected = true;
  462.         return true;
  463.     }
  464.     
  465.     public function updateSetting($key,$value){
  466.         $collection $this->dbcon->selectCollection(TeraWurflConfig::$TABLE_PREFIX.'Settings');
  467.         $collection->save(array('_id'=>$key,'value'=>$value));
  468.         $this->numQueries++;
  469.     }
  470.  
  471.     public function getSetting($key){
  472.         $collection $this->dbcon->selectCollection(TeraWurflConfig::$TABLE_PREFIX.'Settings');
  473.         $record $collection->findOne(array('_id'=>$key),array('value'));
  474.         if(is_null($record)) return null;
  475.         return $record['value'];
  476.         $this->numQueries++;
  477.     }
  478.  
  479.  
  480.     /**
  481.      * Prepare raw text for use in queries (adding quotes if necessary)
  482.      *
  483.      * @param mixed $value 
  484.      * @return mixed 
  485.      */
  486.     public function SQLPrep($value{
  487.         return (string) $value;
  488.     }
  489.  
  490.  
  491.     /**
  492.      * @return array 
  493.      */
  494.     public function getTableList({
  495.  
  496.         $collections $this->dbcon->listCollections();
  497.  
  498.         $output array();
  499.  
  500.         foreach ($collections as $coll{
  501.             $output[$coll->getName();
  502.         }
  503.         return $output;
  504.     }
  505.     public function collectionExists($name){
  506.         $cols $this->getTableList();
  507.         return in_array($name,$cols);
  508.     }
  509.  
  510.     /**
  511.      * @param string $table 
  512.      * @return array 
  513.      */
  514.     public function getTableStats($table{
  515.         $stats array();
  516.         if(!$this->collectionExists($table)) return $stats;
  517.         $rawstats $this->dbcon->command(array('collStats' => $table));
  518.         $stats['rows'$rawstats['count'];
  519.         if ($table TeraWurflConfig::$TABLE_PREFIX.'Merge'{
  520.             $collection $this->dbcon->selectCollection($table);
  521.             $tofind array(
  522.                         'actual_device_root' => 1,
  523.             );
  524.             $res $collection->find($tofind);
  525.             $stats['actual_devices'$res->count();
  526.         }
  527.  
  528.         $stats['bytesize'$rawstats['storageSize'];
  529.         return $stats;
  530.     }
  531.  
  532.     /**
  533.      * @return array 
  534.      */
  535.     public function getCachedUserAgents({
  536.  
  537.         $cachecoll $this->dbcon->selectCollection(TeraWurflConfig::$TABLE_PREFIX.'Cache');
  538.  
  539.         $cached $cachecoll->find(array()array('user_agent'));
  540.         $cached->sort(array('user_agent'));
  541.  
  542.         $uas array();
  543.         foreach ($cached as $device{
  544.             $uas[$device['user_agent'];
  545.         }
  546.         return $uas;
  547.     }
  548.  
  549.  
  550.     // Low-level collection management methods ---------------------------------
  551.  
  552.  
  553.     /**
  554.      * @param string $collectionname 
  555.      * @return boolean Whether the operation was successful
  556.      */
  557.     protected function _recreateCollection($collectionname{
  558.  
  559.         try {
  560.             $this->_dropCollectionIfExists($collectionname);
  561.             $this->_createCollection($collectionname);
  562.             return true;
  563.         catch (Exception $e{
  564.             $this->errors[$e->__toString(', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
  565.         }
  566.         return false;
  567.     }
  568.  
  569.  
  570.     /**
  571.      * @param string $collectionname 
  572.      * @return boolean Whether the operation was successful
  573.      */
  574.     protected function _dropCollectionIfExists($collectionname{
  575.  
  576.         try {
  577.             // NOTE: the MongoCollection::dropCollection() method leaks memory, do not use
  578.             if($this->collectionExists($collectionname)){
  579.                 $col $this->dbcon->selectCollection($collectionname);
  580.                 $col->drop();
  581.             }
  582.             $this->numQueries++;
  583.             return true;
  584.         catch (Exception $e{
  585.             $this->errors[$e->__toString(', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
  586.         }
  587.         return false;
  588.     }
  589.  
  590.  
  591.     /**
  592.      * @param string $collectionname 
  593.      * @return boolean Whether the operation was successful
  594.      */
  595.     protected function _createCollection($collectionname{
  596.  
  597.         try {
  598.             $this->dbcon->createCollection($collectionname);
  599.             $this->numQueries++;
  600.             return true;
  601.         catch (Exception $e{
  602.             $this->errors[$e->__toString(', caught in ' . __CLASS__ . '::' . __METHOD__ . '()';
  603.         }
  604.         return false;
  605.     }
  606.  
  607.  
  608.     /**
  609.      * @var string $from 
  610.      * @var string $to 
  611.      * @return boolean 
  612.      */
  613.     protected function _renameCollection($from$to{
  614.  
  615.         $admindb $this->mongo->admin;
  616.         $dbname  TeraWurflConfig::$DB_SCHEMA;
  617.  
  618.         $admindb->command(array(
  619.                 'renameCollection' => $dbname '.' $from,
  620.                 'to'               => $dbname '.' $to,
  621.         ));
  622.         $this->numQueries++;
  623.     }
  624.  
  625.     // methods pending implementation ------------------------------------------
  626.  
  627.  
  628.     // LD == Levesthein Distance
  629.     public function getDeviceFromUA_LD($userAgent$toleranceUserAgentMatcher $matcher{
  630.         throw new Exception("Error: this function (LD) is not yet implemented in MongoDB");
  631.     }
  632.  
  633.     // methods enforced by parent class, but never called ----------------------
  634.     public function getMatcherTableList(){return array();}
  635.     public function createGenericDeviceTable($tablename{}
  636.     public function getActualDeviceAncestor($wurflID{}
  637.     public function clearTable($tablename{}
  638.     public function createIndexTable(){}
  639. }

Documentation generated on Sun, 19 Sep 2010 00:15:58 +0000 by phpDocumentor 1.4.3