--- /dev/null
+<?php
+/**
+ * Copyright 2004-2017 Facebook. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @package WebDriver
+ *
+ * @author Justin Bishop <jubishop@gmail.com>
+ * @author Anthon Pang <apang@softwaredevelopment.ca>
+ * @author Fabrizio Branca <mail@fabrizio-branca.de>
+ */
+
+namespace WebDriver;
+
+use WebDriver\Exception as WebDriverException;
+
+/**
+ * Abstract WebDriver\Container class
+ *
+ * @package WebDriver
+ */
+abstract class Container extends AbstractWebDriver
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($url = 'http://localhost:4444/wd/hub')
+ {
+ parent::__construct($url);
+
+ $locatorStrategy = new \ReflectionClass('WebDriver\LocatorStrategy');
+ $this->strategies = $locatorStrategy->getConstants();
+ }
+
+ /**
+ * Find element: /session/:sessionId/element (POST)
+ * Find child element: /session/:sessionId/element/:id/element (POST)
+ * Search for element on page, starting from the document root.
+ *
+ * @param string $using the locator strategy to use
+ * @param string $value the search target
+ *
+ * @return \WebDriver\Element
+ *
+ * @throws \WebDriver\Exception if element not found, or invalid XPath
+ */
+ public function element($using = null, $value = null)
+ {
+ $locatorJson = $this->parseArgs('element', func_get_args());
+
+ try {
+ $result = $this->curl(
+ 'POST',
+ '/element',
+ $locatorJson
+ );
+ } catch (WebDriverException\NoSuchElement $e) {
+ throw WebDriverException::factory(
+ WebDriverException::NO_SUCH_ELEMENT,
+ sprintf(
+ "Element not found with %s, %s\n\n%s",
+ $locatorJson['using'],
+ $locatorJson['value'],
+ $e->getMessage()
+ ),
+ $e
+ );
+ }
+
+ $element = $this->webDriverElement($result['value']);
+
+ if ($element === null) {
+ throw WebDriverException::factory(
+ WebDriverException::NO_SUCH_ELEMENT,
+ sprintf(
+ "Element not found with %s, %s\n",
+ $locatorJson['using'],
+ $locatorJson['value']
+ )
+ );
+ }
+
+ return $element;
+ }
+
+ /**
+ * Find elements: /session/:sessionId/elements (POST)
+ * Find child elements: /session/:sessionId/element/:id/elements (POST)
+ * Search for multiple elements on page, starting from the document root.
+ *
+ * @param string $using the locator strategy to use
+ * @param string $value the search target
+ *
+ * @return array
+ *
+ * @throws \WebDriver\Exception if invalid XPath
+ */
+ public function elements($using = null, $value = null)
+ {
+ $locatorJson = $this->parseArgs('elements', func_get_args());
+
+ $result = $this->curl(
+ 'POST',
+ '/elements',
+ $locatorJson
+ );
+
+ if (!is_array($result['value'])) {
+ return array();
+ }
+
+ return array_filter(
+ array_map(
+ array($this, 'webDriverElement'),
+ $result['value']
+ )
+ );
+ }
+
+ /**
+ * Parse arguments allowing either separate $using and $value parameters, or
+ * as an array containing the JSON parameters
+ *
+ * @param string $method method name
+ * @param array $argv arguments
+ *
+ * @return array
+ *
+ * @throws \WebDriver\Exception if invalid number of arguments to the called method
+ */
+ private function parseArgs($method, $argv)
+ {
+ $argc = count($argv);
+
+ switch ($argc) {
+ case 2:
+ $using = $argv[0];
+ $value = $argv[1];
+ break;
+
+ case 1:
+ $arg = $argv[0];
+
+ if (is_array($arg)) {
+ $using = $arg['using'];
+ $value = $arg['value'];
+ break;
+ }
+
+ // fall through
+ default:
+ throw WebDriverException::factory(
+ WebDriverException::JSON_PARAMETERS_EXPECTED,
+ sprintf('Invalid arguments to %s method: %s', $method, print_r($argv, true))
+ );
+ }
+
+ return $this->locate($using, $value);
+ }
+
+ /**
+ * Return JSON parameter for element / elements command
+ *
+ * @param string $using locator strategy
+ * @param string $value search target
+ *
+ * @return array
+ *
+ * @throws \WebDriver\Exception if invalid locator strategy
+ */
+ public function locate($using, $value)
+ {
+ if (!in_array($using, $this->strategies)) {
+ throw WebDriverException::factory(
+ WebDriverException::UNKNOWN_LOCATOR_STRATEGY,
+ sprintf('Invalid locator strategy %s', $using)
+ );
+ }
+
+ return array(
+ 'using' => $using,
+ 'value' => $value,
+ );
+ }
+
+ /**
+ * Return WebDriver\Element wrapper for $value
+ *
+ * @param mixed $value
+ *
+ * @return \WebDriver\Element|null
+ */
+ protected function webDriverElement($value)
+ {
+ return array_key_exists('ELEMENT', (array) $value)
+ ? new Element(
+ $this->getElementPath($value['ELEMENT']), // url
+ $value['ELEMENT'] // id
+ )
+ : null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __call($name, $arguments)
+ {
+ if (count($arguments) === 1 && in_array(str_replace('_', ' ', $name), $this->strategies)) {
+ return $this->locate($name, $arguments[0]);
+ }
+
+ // fallback to executing WebDriver commands
+ return parent::__call($name, $arguments);
+ }
+
+ /**
+ * Get wire protocol URL for an element
+ *
+ * @param string $elementId
+ *
+ * @return string
+ */
+ abstract protected function getElementPath($elementId);
+}