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