Open Journal Systems  3.0.0
 All Data Structures Namespaces Functions Variables Groups Pages
PKPMetricsDAO.inc.php
1 <?php
20 class PKPMetricsDAO extends DAO {
21 
42  function &getMetrics($metricType, $columns = array(), $filters = array(), $orderBy = array(), $range = null, $nonAdditive = true) {
43  // Return by reference.
44  $nullVar = null;
45 
46  // Canonicalize and validate parameter format.
47  if (is_scalar($metricType)) $metricType = array($metricType);
48  if (is_scalar($columns)) $columns = array($columns);
49  if (!(is_array($filters) && is_array($orderBy))) return $nullVar;
50 
51  // Validate parameter content.
52  foreach ($metricType as $metricTypeElement) {
53  if (!is_string($metricTypeElement)) return $nullVar;
54  }
55  $validColumns = array(
56  STATISTICS_DIMENSION_CONTEXT_ID,
57  STATISTICS_DIMENSION_PKP_SECTION_ID,
58  STATISTICS_DIMENSION_ASSOC_OBJECT_ID,
59  STATISTICS_DIMENSION_ASSOC_OBJECT_TYPE,
60  STATISTICS_DIMENSION_SUBMISSION_ID,
61  STATISTICS_DIMENSION_REPRESENTATION_ID,
62  STATISTICS_DIMENSION_FILE_TYPE,
63  STATISTICS_DIMENSION_ASSOC_TYPE,
64  STATISTICS_DIMENSION_ASSOC_ID,
65  STATISTICS_DIMENSION_COUNTRY,
66  STATISTICS_DIMENSION_REGION,
67  STATISTICS_DIMENSION_CITY,
68  STATISTICS_DIMENSION_MONTH,
69  STATISTICS_DIMENSION_DAY,
70  STATISTICS_DIMENSION_METRIC_TYPE
71  );
72 
73  // If the metric column was defined, remove it. We've already
74  // add that below.
75  $metricKey = array_search(STATISTICS_METRIC, $columns);
76  if ($metricKey !== false) unset($columns[$metricKey]);
77 
78  if (count(array_diff($columns, $validColumns)) > 0) return $nullVar;
79  $validColumns[] = STATISTICS_METRIC;
80  foreach ($filters as $filterColumn => $value) {
81  if (!in_array($filterColumn, $validColumns)) return $nullVar;
82  }
83  $validDirections = array(STATISTICS_ORDER_ASC, STATISTICS_ORDER_DESC);
84  foreach ($orderBy as $orderColumn => $direction) {
85  if (!in_array($orderColumn, $validColumns)) return $nullVar;
86  if (!in_array($direction, $validDirections)) return $nullVar;
87  }
88 
89  // Validate correct use of the (non-additive) metric type dimension. We
90  // either require a filter on a single metric type or the metric type
91  // must be present as a column.
92  if (empty($metricType)) return $nullVar;
93  if (count($metricType) !== 1 && $nonAdditive) {
94  if (!in_array(STATISTICS_DIMENSION_METRIC_TYPE, $columns)) {
95  array_push($columns, STATISTICS_DIMENSION_METRIC_TYPE);
96  }
97  }
98 
99  // Add the metric type as filter.
100  $filters[STATISTICS_DIMENSION_METRIC_TYPE] = $metricType;
101 
102  // Build the select and group by clauses.
103  if (empty($columns)) {
104  $selectClause = 'SELECT SUM(metric) AS metric';
105  $groupByClause = '';
106  } else {
107  $selectedColumns = implode(', ', $columns);
108  $selectClause = "SELECT $selectedColumns, SUM(metric) AS metric";
109  $groupByClause = "GROUP BY $selectedColumns";
110  }
111 
112  // Build the where and having clauses.
113  $params = array();
114  $whereClause = '';
115  $havingClause = '';
116  $isFirst = true;
117  foreach ($filters as $column => $values) {
118  // The filter array contains STATISTICS_* constants for the filtered
119  // hierarchy aggregation level as keys.
120  if ($column === STATISTICS_METRIC) {
121  $havingClause = 'HAVING ';
122  $currentClause =& $havingClause; // Reference required.
123  } else {
124  if ($isFirst && $column) {
125  $whereClause = 'WHERE ';
126  $isFirst = false;
127  } else {
128  $whereClause .= ' AND ';
129  }
130  $currentClause =& $whereClause; // Reference required.
131  }
132 
133  if (is_array($values) && isset($values['from'])) {
134  // Range filter: The value is a hashed array with from/to entries.
135  if (!isset($values['to'])) return $nullVar;
136  $currentClause .= "($column BETWEEN ? AND ?)";
137  $params[] = $values['from'];
138  $params[] = $values['to'];
139  } else {
140  // Element selection filter: The value is a scalar or an
141  // unordered array of one or more hierarchy element IDs.
142  if (is_array($values) && count($values) === 1) {
143  $values = array_pop($values);
144  }
145  if (is_scalar($values)) {
146  $currentClause .= "$column = ?";
147  $params[] = $values;
148  } else {
149  $placeholders = array_pad(array(), count($values), '?');
150  $placeholders = implode(', ', $placeholders);
151  $currentClause .= "$column IN ($placeholders)";
152  foreach ($values as $value) {
153  $params[] = $value;
154  }
155  }
156  }
157 
158  unset($currentClause);
159  }
160 
161  // Replace the current time constant by time values
162  // inside the parameters array.
163  $currentTime = array(
164  STATISTICS_YESTERDAY => date('Ymd', strtotime('-1 day', time())),
165  STATISTICS_CURRENT_MONTH => date('Ym', time()));
166  foreach ($currentTime as $constant => $time) {
167  $currentTimeKeys = array_keys($params, $constant);
168  foreach ($currentTimeKeys as $key) {
169  $params[$key] = $time;
170  }
171  }
172 
173  // Build the order-by clause.
174  $orderByClause = '';
175  if (count($orderBy) > 0) {
176  $isFirst = true;
177  foreach ($orderBy as $orderColumn => $direction) {
178  if ($isFirst) {
179  $orderByClause = 'ORDER BY ';
180  } else {
181  $orderByClause .= ', ';
182  }
183  $orderByClause .= "$orderColumn $direction";
184  $isFirst = false;
185  }
186  }
187 
188  // Build the report.
189  $sql = "$selectClause FROM metrics $whereClause $groupByClause $havingClause $orderByClause";
190  if (is_a($range, 'DBResultRange')) {
191  if ($range->getCount() > STATISTICS_MAX_ROWS) {
192  $range->setCount(STATISTICS_MAX_ROWS);
193  }
194  $result = $this->retrieveRange($sql, $params, $range);
195  } else {
196  $result = $this->retrieveLimit($sql, $params, STATISTICS_MAX_ROWS);
197  }
198 
199  // Return the report.
200  $returner = $result->GetAll();
201  return $returner;
202  }
203 
213  function getLoadId($assocType, $assocId, $metricType) {
214  $params = array($assocType, $assocId, $metricType);
215  $result = $this->retrieve('SELECT load_id FROM metrics WHERE assoc_type = ? AND assoc_id = ? AND metric_type = ? GROUP BY load_id', $params);
216 
217  $loadIds = array();
218  while (!$result->EOF) {
219  $row = $result->FetchRow();
220  $loadIds[] = $row['load_id'];
221  }
222 
223  return $loadIds;
224  }
225 
232  function hasRecord($metricType) {
233  $result = $this->retrieve('SELECT load_id FROM metrics WHERE metric_type = ? LIMIT 1', array($metricType));
234  $row = $result->GetRowAssoc();
235  if ($row) {
236  return true;
237  } else {
238  return false;
239  }
240  }
241 
247  function purgeLoadBatch($loadId) {
248  $this->update('DELETE FROM metrics WHERE load_id = ?', $loadId); // Not a number.
249  }
250 
257  function purgeRecords($metricType, $toDate) {
258  $this->update('DELETE FROM metrics WHERE metric_type = ? AND day IS NOT NULL AND day <= ?', array($metricType, $toDate));
259  }
260 
267  function insertRecord($record) {
268  $recordToStore = array();
269 
270  // Required dimensions.
271  $requiredDimensions = array('load_id', 'assoc_type', 'assoc_id', 'metric_type');
272  foreach ($requiredDimensions as $requiredDimension) {
273  if (!isset($record[$requiredDimension])) {
274  throw new Exception('Cannot load record: missing dimension "' . $requiredDimension . '".');
275  }
276  $recordToStore[$requiredDimension] = $record[$requiredDimension];
277  }
278  $assocType = $recordToStore['assoc_type'] = (int)$recordToStore['assoc_type'];
279  $assocId = $recordToStore['assoc_id'] = (int)$recordToStore['assoc_id'];
280 
281  list($contextId, $pkpSectionId, $assocObjType,
282  $assocObjId, $submissionId, $representationId) = $this->foreignKeyLookup($assocType, $assocId);
283 
284  $recordToStore['context_id'] = $contextId;
285  $recordToStore['pkp_section_id'] = $pkpSectionId;
286  $recordToStore['assoc_object_type'] = $assocObjType;
287  $recordToStore['assoc_object_id'] = $assocObjId;
288  $recordToStore['submission_id'] = $submissionId;
289  $recordToStore['representation_id'] = $representationId;
290 
291  // File type is optional.
292  if (isset($record['file_type']) && $record['file_type']) $recordToStore['file_type'] = (int)$record['file_type'];
293 
294  // We require either month or day in the time dimension.
295  if (isset($record['day'])) {
296  if (!PKPString::regexp_match('/[0-9]{8}/', $record['day'])) {
297  throw new Exception('Cannot load record: invalid date.');
298  }
299  $recordToStore['day'] = $record['day'];
300  $recordToStore['month'] = substr($record['day'], 0, 6);
301  if (isset($record['month']) && $recordToStore['month'] != $record['month']) {
302  throw new Exception('Cannot load record: invalid month.');
303  }
304  } elseif (isset($record['month'])) {
305  if (!PKPString::regexp_match('/[0-9]{6}/', $record['month'])) {
306  throw new Exception('Cannot load record: invalid month.');
307  }
308  $recordToStore['month'] = $record['month'];
309  } else {
310  throw new Exception('Cannot load record: Missing time dimension.');
311  }
312 
313  // Geolocation is optional.
314  if (isset($record['country_id'])) $recordToStore['country_id'] = (string)$record['country_id'];
315  if (isset($record['region'])) $recordToStore['region'] = (string)$record['region'];
316  if (isset($record['city'])) $recordToStore['city'] = (string)$record['city'];
317 
318  // The metric must be set. If it is 0 we ignore the record.
319  if (!isset($record['metric'])) {
320  throw new Exception('Cannot load record: metric is missing.');
321  }
322  if (!is_numeric($record['metric'])) {
323  throw new Exception('Cannot load record: invalid metric.');
324  }
325  $recordToStore['metric'] = (int) $record['metric'];
326 
327  // Save the record to the database.
328  $fields = implode(', ', array_keys($recordToStore));
329  $placeholders = implode(', ', array_pad(array(), count($recordToStore), '?'));
330  $params = array_values($recordToStore);
331  return $this->update("INSERT INTO metrics ($fields) VALUES ($placeholders)", $params);
332  }
333 
334 
335  //
336  // Protected methods.
337  //
346  protected function foreignKeyLookup($assocType, $assocId) {
347  $contextId = $sectionId = $submissionId = $assocObjectType = $assocObjectId = $representationId = null;
348 
349  $isFile = false;
350  $isRepresentation = false;
351 
352  switch($assocType) {
353  case ASSOC_TYPE_SUBMISSION_FILE:
354  $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO'); /* @var $submissionFileDao SubmissionFileDAO */
355  $submissionFile = $submissionFileDao->getLatestRevision($assocId);
356  if ($submissionFile) {
357  $isFile = true;
358  $submissionId = $submissionFile->getSubmissionId();
359  if ($submissionFile->getAssocType() == ASSOC_TYPE_REPRESENTATION) {
360  $representationId = $submissionFile->getAssocId();
361  } else {
362  throw new Exception('Cannot load record: submission file is not associated with a representation object.');
363  }
364  } else {
365  throw new Exception('Cannot load record: invalid submission file id.');
366  }
367  // Don't break but go on to retrieve the representation.
368  case ASSOC_TYPE_REPRESENTATION:
369  if (!$isFile) $representationId = $assocId;
370  $representationDao = Application::getRepresentationDAO(); /* @var $representationDao RepresentationDAO */
371  $representation = $representationDao->getById($representationId); /* @var $representation Representation */
372  if ($representation) {
373  if (!$isFile) $isRepresentation = true;
374 
375  $contextId = $representation->getContextId();
376  $submissionId = $representation->getSubmissionId();
377  } else {
378  throw new Exception('Cannot load record: invalid representation id.');
379  }
380  // Don't break but go on to retrieve the submission.
381  case ASSOC_TYPE_SUBMISSION:
382  if (!$isFile && !$isRepresentation) $submissionId = $assocId;
383  $submissionDao = Application::getSubmissionDAO(); /* @var $submissionDao SubmissionDAO */
384  $submission = $submissionDao->getById($submissionId);
385  if ($submission) {
386  $contextId = $submission->getContextId();
387  $submissionId = $submission->getId();
388  $sectionId = $submission->getSectionId();
389  } else {
390  throw new Exception('Cannot load record: invalid submission id.');
391  }
392  list($assocObjectType, $assocObjectId) = $this->getAssocObjectInfo($submissionId, $contextId);
393  break;
394  case ASSOC_TYPE_SECTION:
395  $sectionDao = Application::getSectionDAO();
396  $section = $sectionDao->getById($assocId); /* @var $section PKPSection */
397  if ($section) {
398  $sectionId = $section->getId();
399  $contextId = $section->getContextId();
400  } else {
401  throw new Exception('Cannot load record: invalid section id.');
402  }
403  break;
405  $contextDao = Application::getContextDAO(); /* @var $contextDao ContextDAO */
406  $context = $contextDao->getById($assocId);
407  if (!$context) {
408  throw new Exception('Cannot load record: invalid context id.');
409  }
410  $contextId = $assocId;
411  break;
412  }
413 
414  return array($contextId, $sectionId, $assocObjectType, $assocObjectId, $submissionId, $representationId);
415  }
416 
426  protected function getAssocObjectInfo($submissionId, $contextId) {
427  return array(null, null);
428  }
429 }
430 ?>
static & getDAO($name, $dbconn=null)
Operations for retrieving and modifying objects from a database.
Definition: DAO.inc.php:30
getAssocObjectInfo($submissionId, $contextId)
static getRepresentationDAO()
& retrieveLimit($sql, $params=false, $numRows=false, $offset=false, $callHooks=true)
Definition: DAO.inc.php:146
& getMetrics($metricType, $columns=array(), $filters=array(), $orderBy=array(), $range=null, $nonAdditive=true)
& retrieve($sql, $params=false, $callHooks=true)
Definition: DAO.inc.php:84
& retrieveRange($sql, $params=false, $dbResultRange=null, $callHooks=true)
Definition: DAO.inc.php:174
Class with basic operations for retrieving and adding statistics data.
static getSubmissionDAO()
getLoadId($assocType, $assocId, $metricType)
hasRecord($metricType)
static regexp_match($pattern, $subject)
static getContextAssocType()
foreignKeyLookup($assocType, $assocId)
update($sql, $params=false, $callHooks=true, $dieOnError=true)
Definition: DAO.inc.php:208
static getContextDAO()
static getSectionDAO()
purgeRecords($metricType, $toDate)