The same action works with PhpBrowser but as soon as I set WebDriver in acceptance.suite.yml it throws the following error :
[PHPUnit\Framework\Exception] Invalid argument supplied for foreach() at vendor/php-webdriver/webdriver/lib/Remote/RemoteWebDriver.php:240
I followed the documentation for setting up WebDriver with Selenium. Here is what my acceptance.suite.yml looks like:
actor: AcceptanceTester
modules:
enabled:
- WebDriver:
url: '{website url here}'
browser: chrome
- \Helper\Acceptance
step_decorators: ~
Here is my acceptance test file:
<?php
class FirstAcceptanceCest
{
public function _before(AcceptanceTester $I)
{
}
public function seeLoginInFrontPage(AcceptanceTester $I)
{
$I->amOnPage('/');
$I->see('Login');
}
}
Any help would be greatly appreciated.
Issufficient error handling in php-webdriver library again.
https://github.com/php-webdriver/php-webdriver/blob/1.13.1/lib/Remote/RemoteWebDriver.php#L228-L245
$raw_elements = $this->execute(
DriverCommand::FIND_ELEMENTS,
JsonWireCompat::getUsing($by, $this->isW3cCompliant)
);
if ($raw_elements === null) {
throw new UnknownErrorException('Unexpected server response to findElements command');
}
$elements = [];
foreach ($raw_elements as $raw_element) {
$elements[] = $this->newElement(JsonWireCompat::getElement($raw_element));
}
My debugging advice is to add
if (!is_array($raw_elements) && !is_object($raw_elements)) {
var_dump($raw_elements); die;
}
before foreach and see if the output helps to understand the root cause.
Related
We have a Silverstripe 4 project which acts as a headless CMS returning a group of complex data models formatted as JSON.
Here's an example of the code:
class APIController extends ContentController
{
public function index(HTTPRequest $request)
{
$dataArray['model1'] = AccessPointController::getModel1();
$dataArray['model2'] = AccessPointController::getModel2();
$dataArray['model3'] = AccessPointController::getModel3();
$dataArray['model4'] = AccessPointController::getModel4();
$dataArray['model5'] = AccessPointController::getModel5();
$dataArray['model6'] = AccessPointController::getModel6();
$this->response->addHeader('Content-Type', 'application/json');
$this->response->addHeader('Access-Control-Allow-Origin', '*');
return json_encode($dataArray);
}
The problem we're having is the data models have got so complex the generation time for the JSON is running into seconds.
The JSON should only change when site content has been updates so ideally we'd like to cache the JSON & rather than dynamically generating it for each call.
What is the best way to cache the JSON in the above example?
Have you looked at the silverstripe docs about caching?
They do provide a programmatic way to store things in cache. And configuration options what back-ends are to be used to store the cache.
A simple example might be:
I've extended the cache live time here, but still you should note that this cache is not intended for storing generated static content, but rather to reduce load. Your application will still have to compute the api response every 86400 seconds (24 hours).
# app/_config/apiCache.yml
---
Name: apicacheconfig
---
# [... rest of your config config ...]
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.apiResponseCache:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "apiResponseCache"
defaultLifetime: 86400
<?php // app/src/FooController.php
class FooController extends \SilverStripe\Control\Controller {
public function getCache() {
return Injector::inst()->get('Psr\SimpleCache\CacheInterface.apiResponseCache');
// or your own cache (see below):
// return new MyCache();
}
protected function hasDataBeenChanged() {
// alternative to this method, you could also simply include Page::get()->max('LastEdited') or whatever in your cache key inside index(), but if you are using your own cache system, you need to handle deleting of old unused cache files. If you are using SilverStripe's cache, it will do that for you
$c = $this->getCache();
$lastCacheTime = $c->has('cacheTime') ? (int)$c->get('cacheTime') : 0;
$lastDataChangeTime = strtotime(Page::get()->max('LastEdited'));
return $lastDataChangeTime > $lastCacheTime;
}
public function index() {
$c = $this->getCache();
$cacheKey = 'indexActionResponse';
if ($c->has($cacheKey) && !$this->hasDataBeenChanged()) {
$data = $c->get($cacheKey);
} else {
$dataArray['model1'] = AccessPointController::getModel1();
$dataArray['model2'] = AccessPointController::getModel2();
$dataArray['model3'] = AccessPointController::getModel3();
$dataArray['model4'] = AccessPointController::getModel4();
$dataArray['model5'] = AccessPointController::getModel5();
$dataArray['model6'] = AccessPointController::getModel6();
$data = json_encode($dataArray);
$c->set($cacheKey, $data);
$c->set('cacheTime', time());
}
$this->response->addHeader('Content-Type', 'application/json');
$this->response->addHeader('Access-Control-Allow-Origin', '*');
return json_encode($dataArray);
}
}
If you are looking for a permanent/persistent cache, that will only ever update when data changed, I suggest you look for a different back-end or just implement a simple cache yourself and use that instead of the silverstripe cache.
class MyCache {
protected function fileName($key) {
if (strpos($key, '/') !== false || strpos($key, '\\') !== false) {
throw new \Exception("Invalid cache key '$key'");
}
return BASE_PATH . "/api-cache/$key.json";
}
public function get($key) {
if ($this->has($key)) {
return file_get_contents($this->fileName($key));
}
return null;
}
public function set($key, $val) {
file_put_contents($this->fileName($key), $val);
}
public function has($key) {
$f = $this->fileName($key);
return #file_exists($f);
}
I am writing a small service for a Symfony 4 project
and I'm just wondering if my error handling is a good idea.
My class currently looks like this:
namespace App\Service;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\;
/**
* Class XMLHelper
* #package App\Service
*/
class XMLHelper
{
public function getContent(string $path): array
{
$fileSystem = new Filesystem();
if ($fileSystem->exists($path)) {
libxml_use_internal_errors(true);
$object = simplexml_load_file($path);
if ($object === false) {
$str = 'Failed loading XML: ';
foreach(libxml_get_errors() as $error) {
$str = $error->message . ', ';
}
throw new \UnexpectedValueException($str);
}
return $object;
} else {
throw new \FileNotFoundException('File ' . $path . ' not found.');
}
}
}
My question is, do I need that at all? Will symfony not throw
out an error anyway. Is the output not superfluous? How do
you do it the best or generally correctly? If you have own
Exceptions in Services.
IMO you should never hesitate to throw an exception when appropriate. Your code looks just fine, it provides an exhaustive information about the encountered exeptions.
There is nothing to do with Symfony ifself, it up to your code to throw right exceptions in right places.
Hope this helps.
P.S. And don't forget to libxml_clear_errors();
I've been using Symfony 3.1 with new Cache Component (https://symfony.com/doc/current/components/cache.html), I'm using the redis adapter
config.yml
cache:
app: cache.adapter.redis
default_redis_provider: "redis://127.0.0.1:6379"
Basically, I save data in redis when I do a GET to a specific resource, and I remove it from redis, when I do a POST..
With symfony in dev mode data is stored/removed from cache as I expected.
But when I change it to prod, the 'deleteItem' no longer removes the item from redis cache..
I cannot find any error in logs, so I'm getting a little bit lost with it..
This is a sample of how I'm using the cache
protected function getCache(){
return $this->get('cache.app');
}
public function getAction(){
$cacheItem = $this->getCache()->getItem('example-key');
$data = ... // Check cacheItem isHit() ...
$cacheItem->expiresAfter($this->defaultCacheTime);
$cacheItem->set($data);
$this->getCache()->save($cacheItem);
}
public function postAction() {
...
$this->getCache()->deleteItem('example-key');
}
Update - I've found what might be causing this issue
This is part of the code of symfony AbstractAdapter and RedisAdapter:
public function deleteItem($key)
{
return $this->deleteItems(array($key));
}
public function deleteItems(array $keys)
{
$ids = array();
foreach ($keys as $key) {
$ids[$key] = $this->getId($key);
unset($this->deferred[$key]);
}
try {
if ($this->doDelete($ids)) {
return true;
}
} catch (\Exception $e) {
}
$ok = true;
// When bulk-delete failed, retry each item individually
foreach ($ids as $key => $id) {
try {
$e = null;
if ($this->doDelete(array($id))) {
continue;
}
} catch (\Exception $e) {
}
CacheItem::log($this->logger, 'Failed to delete key "{key}"', array('key' => $key, 'exception' => $e));
$ok = false;
}
return $ok;
}
protected function doDelete(array $ids)
{
if ($ids) {
$this->redis->del($ids);
}
return true;
}
This is part of code from Predis StreamConnection.php:
public function writeRequest(CommandInterface $command)
{
$commandID = $command->getId();
$arguments = $command->getArguments();
$cmdlen = strlen($commandID);
$reqlen = count($arguments) + 1;
$buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n";
for ($i = 0, $reqlen--; $i < $reqlen; $i++) {
$argument = $arguments[$i];
$arglen = strlen($argument);
$buffer .= "\${$arglen}\r\n{$argument}\r\n";
}
$this->write($buffer);
}
When I call deleteItem('example-key'), it then calls the deleteItems(..) to remove that key..
The thing is, deleteItems() is calling doDelete() and passing an array like
'example-key' => 'prefix_example-key'
The doDelete(), then calls the Redis client, passing that same array string => string, when I think it should be, index => string,eg: [0] => 'prefix_example-key' instead of ['example-key'] => 'prefix_example-key'
Then the redis client when processing the command to execute, receives that array as $arguments, and in the for loop, it does this:
$argument = $arguments[$i]; since the array is in string => string format, it won't work, in dev mode, it shows Notice undefined offset 0 error
This is the strange part
In 'dev' mode, it will throw an error, and so, the deleteItems() will catch it, and try to delete the item again, this time, sending the arguments properly
In 'prod' mode, the Notice undefined offset 0 don't know why, but it does not throw an exception, so deleteItems(..) won't catch it, returns right there..
I've found a way to make it work for me, if I add array_values in the doDelete method, it works:
protected function doDelete(array $ids)
{
if ($ids) {
$this->redis->del(array_values($ids));
}
return true;
}
I don't know if all of this is making sense or not, I think I'll open an issue in symfony bug tracker
My bad, this was caused by an outdated version of Predis, I tought I had the latest version of Predis, but I didn't
Everything is working fine with the latest version
I build a model side validation in Laravel 4 with the creating Model Event :
class User extends Eloquent {
public function isValid()
{
return Validator::make($this->toArray(), array('name' => 'required'))->passes();
}
public static function boot()
{
parent::boot();
static::creating(function($user)
{
echo "Hello";
if (!$user->isValid()) return false;
});
}
}
It works well but I have issues with PHPUnit. The two following tests are exactly the same but juste the first one pass :
class UserTest extends TestCase {
public function testSaveUserWithoutName()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // pass
assertEquals($count, User::all()->count()); // pass
}
public function testSaveUserWithoutNameBis()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // fail
assertEquals($count, User::all()->count()); // fail, the user is created
}
}
If I try to create a user twice in the same test, it works, but it's like if the binding event is present only in the first test of my test class. The echo "Hello"; is printed only one time, during the first test execution.
I simplify the case for my question but you can see the problem : I can't test several validation rules in different unit tests. I try almost everything since hours but I'm near to jump out the windows now ! Any idea ?
The issue is well documented in Github. See comments above that explains it further.
I've modified one of the 'solutions' in Github to automatically reset all model events during the tests. Add the following to your TestCase.php file.
app/tests/TestCase.php
public function setUp()
{
parent::setUp();
$this->resetEvents();
}
private function resetEvents()
{
// Get all models in the Model directory
$pathToModels = '/app/models'; // <- Change this to your model directory
$files = File::files($pathToModels);
// Remove the directory name and the .php from the filename
$files = str_replace($pathToModels.'/', '', $files);
$files = str_replace('.php', '', $files);
// Remove "BaseModel" as we dont want to boot that moodel
if(($key = array_search('BaseModel', $files)) !== false) {
unset($files[$key]);
}
// Reset each model event listeners.
foreach ($files as $model) {
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Reregister them.
call_user_func(array($model, 'boot'));
}
}
I have my models in subdirectories so I edited #TheShiftExchange code a bit
//Get all models in the Model directory
$pathToModels = '/path/to/app/models';
$files = File::allFiles($pathToModels);
foreach ($files as $file) {
$fileName = $file->getFileName();
if (!ends_with($fileName, 'Search.php') && !starts_with($fileName, 'Base')) {
$model = str_replace('.php', '', $fileName);
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Re-register them.
call_user_func(array($model, 'boot'));
}
}
Using the following code:
function mymodule_menu_alter(&$items) {
if (isset($items['node/add/page'])) {
$items['node/add/page']['access arguments'] = FALSE;
}
}
I get the following error:
warning: Missing argument 1 for node_access() in
/var/www/vhosts/mysite.co.uk/httpdocs/modules/node/node.module on line
2011.
The code actually works and does what I need it to do but the error concerns me and confuses my site users.
I am not sure what the issue is or how to resolve it. Can anyone offer some assistance?
access arguments needs to be an array:
function mymodule_menu_alter(&$items) {
if (isset($items['node/add/page'])) {
$items['node/add/page']['access arguments'] = array();
}
}
If you're trying to deny access to your page to absolutely anyone you should use the access callback key instead:
function mymodule_menu_alter(&$items) {
if (isset($items['node/add/page'])) {
$items['node/add/page']['access callback'] = FALSE;
}
}