I'm working on a custom Twig filter which resizes your images to a set width (and auto height).
{# TWIG CODE #}
{% set imgpath = page.header.custom.bgimage|first.path %}
<div class="thumb" style="background-image: url({{ url(imgpath|resize(240)) }})"></div>
It works great so far, but I encountered some errors when the ordering of the pages is changed. I'd like to use the Grav Debug Bar for debugging, since it's very convenient and keeps the code clean.
Inside Twig, you can simply use {{ dump(message) }}.
Unfortunately the resizing process happens inside native PHP, so I need a way to output messages from PHP to the Grav Debug Bar.
As stated inside the Docs, you can use $grav['debugger']->addMessage($this).
This throws an error when calling the resize Twig filter:
Twig_Error_Runtime
An exception has been thrown during the rendering of a template ("Undefined variable: grav").
Why is the variable $grav undefined?
<?php namespace Grav\Common;
use \Grav\Common\Grav;
use \Grav\Common\Page\Page;
use \RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use \Eventviva\ImageResize;
include_once getcwd().'/user/plugins/resizer/lib/ImageResize.php';
class TwigResizerFilters extends \Twig_Extension
{
private $grav;
public function __construct() {
$this->grav = Grav::instance();
}
public function getName() {
return 'TwigResizeFilters';
}
public function getFilters() {
return [
new \Twig_SimpleFilter( 'resize', [$this, 'resizeImage'] )
];
}
public function resizeImage($mediapath, $maxWidth = 1920) {
if (file_exists($mediapath)) {
// if file exists
if ($currImg = getimagesize($mediapath)) {
// if is image
if (preg_match('(jpg|jpeg|png)', $currImg['mime'])) {
// if file format correct
$resizedFolder = 'images/resized/';
// calculate exact img dimensions for proper naming
$maxHeight = floor(($maxWidth/$currImg[0]) * $currImg[1]);
if (!file_exists($resizedFolder)) {
// create folder if it does not exist
mkdir($resizedFolder, 0777, true);
}
// create filename
$resizedExtension = '.'.pathinfo($mediapath, PATHINFO_EXTENSION);
$resizedFilename = basename($mediapath, $resizedExtension).'#'.$maxWidth.'x'.$maxHeight.$resizedExtension;
if (file_exists($resizedFolder.$resizedFilename)) {
// if file already has been cached, just potput
return $resizedFolder.$resizedFilename;
} else {
// if not cached, resize to desired size
$image = new ImageResize($mediapath);
$image->resize($maxWidth, $maxHeight);
$image->save($resizedFolder.$resizedFilename);
return $resizedFolder.$resizedFilename;
}
} else {
$grav['debugger']->addMessage("File type of ".$mediapath." is not supported.");
}
} else {
$grav['debugger']->addMessage($mediapath." is not an image.");
}
} else {
$grav['debugger']->addMessage("File ".$mediapath." does not exist.");
}
}
private function mergeConfig( Page $page ) {
$defaults = (array) $this->grav['config']->get('plugins.resizer');
if ( isset($page->header()->resizer) ) {
$this->grav['config']->set('plugins.resizer', array_merge($defaults, $page->header()->resizer));
}
}
}
Related
I have a website where I can upload a .xlsx file which contains some rows of information for my database. I read the documentation for laravel-excel but it looks like it only works with progress bar if you use the console method; which I don't.
I currently just use a plain HTML upload form, no ajax yet.
But to create this progress bar for this I need to convert it to ajax, which is no hassle, that I can do.
But how would I create the progress bar when uploading the file and iterating through each row in the Excel file?
This is the controller and method where the upload gets done:
/**
* Import companies
*
* #param Import $request
* #return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
public function postImport(Import $request)
{
# Import using Import class
Excel::import(new CompaniesImport, $request->file('file'));
return redirect(route('dashboard.companies.index.get'))->with('success', 'Import successfull!');
}
And this is the import file:
public function model(array $row)
{
# Don't create or validate on empty rows
# Bad workaround
# TODO: better solution
if (!array_filter($row)) {
return null;
}
# Create company
$company = new Company;
$company->crn = $row['crn'];
$company->name = $row['name'];
$company->email = $row['email'];
$company->phone = $row['phone'];
$company->website = (!empty($row['website'])) ? Helper::addScheme($row['website']) : '';
$company->save();
# Everything empty.. delete address
if (!empty($row['country']) || !empty($row['state']) || !empty($row['postal']) || !empty($row['address']) || !empty($row['zip'])) {
# Create address
$address = new CompanyAddress;
$address->company_id = $company->id;
$address->country = $row['country'];
$address->state = $row['state'];
$address->postal = $row['postal'];
$address->address = $row['address'];
$address->zip = $row['zip'];
$address->save();
# Attach
$company->addresses()->save($address);
}
return $company;
}
I know this is not much at this point. I just need some help figuring out how I would create this progress bar, because I'm pretty stuck.
My thought is to create a ajax upload form though, but from there I don't know.
Just an idea, but you could use the Laravel session to store the total_row_count and processed_row_count during the import execution. Then, you could create a separate AJAX call on a setInterval() to poll those session values (e.g., once per second). This would allow you to calculate your progress as processed_row_count / total_row_count, and output to a visual progress bar. – matticustard
Putting #matticustard comment into practice. Below is just sample of how things could be implemented, and maybe there are areas to improve.
1. Routes
import route to initialize Excel import.
import-status route will be used to get latest import status
Route::post('import', [ProductController::class, 'import']);
Route::get('import-status', [ProductController::class, 'status']);
2. Controller
import action will validate uploaded file, and pass $id to ProductsImport class. As it will be queued and run in the background, there is no access to current session. We will use cache in the background. It will be good idea to generate more randomized $id if more concurrent imports will be processed, for now just unix date to keep simple.
You currently cannot queue xls imports. PhpSpreadsheet's Xls reader contains some non-utf8 characters, which makes it impossible to queue.
XLS imports could not be queued
public function import()
{
request()->validate([
'file' => ['required', 'mimes:xlsx'],
]);
$id = now()->unix()
session([ 'import' => $id ]);
Excel::queueImport(new ProductsImport($id), request()->file('file')->store('temp'));
return redirect()->back();
}
Get latest import status from cache, passing $id from session.
public function status()
{
$id = session('import');
return response([
'started' => filled(cache("start_date_$id")),
'finished' => filled(cache("end_date_$id")),
'current_row' => (int) cache("current_row_$id"),
'total_rows' => (int) cache("total_rows_$id"),
]);
}
3. Import class
Using WithEvents BeforeImport we set total rows of our excel file to the cache. Using onRow we set currently processing row to the cache. And AfterReset clear all the data.
<?php
namespace App\Imports;
use App\Models\Product;
use Maatwebsite\Excel\Row;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Events\AfterImport;
use Maatwebsite\Excel\Events\BeforeImport;
use Maatwebsite\Excel\Concerns\WithEvents;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class ProductsImport implements OnEachRow, WithEvents, WithChunkReading, ShouldQueue
{
public $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function chunkSize(): int
{
return 100;
}
public function registerEvents(): array
{
return [
BeforeImport::class => function (BeforeImport $event) {
$totalRows = $event->getReader()->getTotalRows();
if (filled($totalRows)) {
cache()->forever("total_rows_{$this->id}", array_values($totalRows)[0]);
cache()->forever("start_date_{$this->id}", now()->unix());
}
},
AfterImport::class => function (AfterImport $event) {
cache(["end_date_{$this->id}" => now()], now()->addMinute());
cache()->forget("total_rows_{$this->id}");
cache()->forget("start_date_{$this->id}");
cache()->forget("current_row_{$this->id}");
},
];
}
public function onRow(Row $row)
{
$rowIndex = $row->getIndex();
$row = array_map('trim', $row->toArray());
cache()->forever("current_row_{$this->id}", $rowIndex);
// sleep(0.2);
Product::create([ ... ]);
}
}
4. Front end
On the front-end side this is just sample how things could be handled. Here I used vuejs, ant-design-vue and lodash.
After uploading file handleChange method is called
On successful upload trackProgress method is called for the first time
trackProgress method is recursive function, calling itself on complete
with lodash _.debounce method we can prevent calling it too much
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
<template>
<a-modal
title="Upload excel"
v-model="visible"
cancel-text="Close"
ok-text="Confirm"
:closable="false"
:maskClosable="false"
destroyOnClose
>
<a-upload-dragger
name="file"
:multiple="false"
:showUploadList="false"
:action="`/import`"
#change="handleChange"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">Click to upload</p>
</a-upload-dragger>
<a-progress class="mt-5" :percent="progress" :show-info="false" />
<div class="text-right mt-1">{{ this.current_row }} / {{ this.total_rows }}</div>
<template slot="footer">
<a-button #click="close">Close</a-button>
</template>
</a-modal>
</template>
<script>
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
</script>
I am trying to overwrite a template file located in vendor/bolt/bolt/app/view/twig/editcontent/fields/_block.twig (I want to replace the "block selection" dropdown). Regarding to #1173, #1269, #5588, #3768 and #5102 this is not supported by default so I have to write a extension for this. So I tried this:
BackendBlockSelectionExtension:
namespace Bundle\Site;
use Bolt\Filesystem\Adapter\Local;
use Bolt\Filesystem\Filesystem;
use Silex\Application;
use Bolt\Extension\SimpleExtension;
class BackendBlockSelectionExtension extends SimpleExtension
{
public function getServiceProviders()
{
return [
$this,
new BackendBlockSelectionProvider(),
];
}
}
BackendBlockSelectionProvider:
namespace Bundle\Site;
use Bolt\Filesystem\Adapter\Local;
use Bolt\Filesystem\Filesystem;
use Silex\Application;
use Silex\ServiceProviderInterface;
class BackendBlockSelectionProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$side = $app['config']->getWhichEnd();
if ($side == 'backend') {
$path = __DIR__ . '/App/templates/Backend';
$filesystem = $app['filesystem'];
$filesystem->mountFilesystem('bolt', new Filesystem(new Local($path)));
$app['twig.loader.bolt_filesystem'] = $app->share(
$app->extend(
'twig.loader.bolt_filesystem',
function ($filesystem, $app) {
$path = __DIR__ . 'src/App/templates/Backend/';
$filesystem->prependPath($path, 'bolt');
return $filesystem;
}
)
);
}
}
public function boot(Application $app)
{
}
}
This seems to do the job, but I got an error I don't understand at all: The "bolt://app/theme_defaults" directory does not exist.
So my final question is: Does anyone have some example code how to overwrite/modify vendor/bolt/bolt/app/view/twig/editcontent/fields/_block.twig without touching the vendor folder?
This should be much simplier than this.
In your extension class overwrite protected function registerTwigPaths() function like this:
protected function registerTwigPaths()
{
if ($this->getEnd() == 'backend') {
return [
'view' => ['position' => 'prepend', 'namespace' => 'bolt']
];
}
return [];
}
private function getEnd()
{
$backendPrefix = $this->container['config']->get('general/branding/path');
$end = $this->container['config']->getWhichEnd();
switch ($end) {
case 'backend':
return 'backend';
case 'async':
// we have async request
// if the request begin with "/admin" (general/branding/path)
// it has been made on backend else somewhere else
$url = '/' . ltrim($_SERVER['REQUEST_URI'], $this->container['paths']['root']);
$adminUrl = '/' . trim($backendPrefix, '/');
if (strpos($url, $adminUrl) === 0) {
return 'backend';
}
default:
return $end;
}
}
Now you can crete a view directory in your extensions directory in which you can define templates in structure like in Bolt's default. I would start with copy and overwrite.
The private array $list_of_files stays uninitialized. How can I update it from the while loop?
class listOfFiles {
private $list_of_files = [];
function __construct() {
if ($handle = opendir(WEB_STORAGE_DIR)) {
while (false !== ($entry = readdir($handle))) {
$this->list_of_files[$entry] = filesize(WEB_STORAGE_DIR.DIRECTORY_SEPARATOR.$entry);
}
closedir($handle);
// Remove . and .. from the list
unset($list_of_files['.']);
unset($list_of_files['..']);
}
}
function is_empty() {
return empty($list_of_files);
}
}
$list_of_files is referring to a variable, which is not the same as a property which would be $this->list_of_files.
Variables declared/referenced in a function are only available in that function (unless you use global - but this is generally considered 'evil' and should be avoided)
Properties are available from all methods in the class (unless they are static) and persist for the life of the object.
<?php
//lets show all error so we can see if anything else is going on..
error_reporting(E_ALL & ~E_NOTICE);
class listOfFiles {
private $list_of_files = [];
function __construct() {
if ($handle = opendir(WEB_STORAGE_DIR)) {
while (false !== ($entry = readdir($handle))) {
$this->list_of_files[$entry] = filesize(WEB_STORAGE_DIR.DIRECTORY_SEPARATOR.$entry);
}
closedir($handle);
// Remove . and .. from the list
unset($this->list_of_files['.']);
unset($this->list_of_files['..']);
}
}
function is_empty() {
return empty($this->list_of_files);
}
}
Is the issue that the directory doesn't exist? It would be better to check this before trying to open, and also allow for what to do when it does exist but you cant actually read it:
<?php
//lets show all error so we can see if anything else is going on..
error_reporting(E_ALL & ~E_NOTICE);
class listOfFiles {
private $list_of_files = [];
function __construct() {
if(!is_dir(WEB_STORAGE_DIR)){
throw new Exception("Missing Web Storage Directory");
}
$handle = opendir(WEB_STORAGE_DIR);
if (!$handle) {
throw new Exception("Could not read Web Storage Directory");
}
else{
while (false !== ($entry = readdir($handle))) {
$this->list_of_files[$entry] = filesize(WEB_STORAGE_DIR.DIRECTORY_SEPARATOR.$entry);
}
closedir($handle);
// Remove . and .. from the list
unset($this->list_of_files['.']);
unset($this->list_of_files['..']);
}
}
function is_empty() {
return empty($this->list_of_files);
}
}
I have added error_reporting(E_ALL & ~E_NOTICE); to the examples as this would make sure that you see any errors and may help debug your issue. More info on this here: http://php.net/manual/en/function.error-reporting.php
The access a property, you need to use $this, otherwise you are making a local variable. You do this at one place, but e.g. not here
return empty($list_of_files);
As that variable is never set, this will always return the same thing.
return empty($this->list_of_files);
The same goes for other references to that property, making the complete code (this is untested of course, as you didn't provide anything testable) look something like this
class listOfFiles {
private $list_of_files = [];
function __construct() {
if ($handle = opendir(WEB_STORAGE_DIR)) {
while (false !== ($entry = readdir($handle))) {
$this->list_of_files[$entry] = filesize(WEB_STORAGE_DIR.DIRECTORY_SEPARATOR.$entry);
}
closedir($handle);
// Remove . and .. from the list
unset( $this->list_of_files['.']);
unset( $this->list_of_files['..']);
}
}
function is_empty() {
return empty( $this->list_of_files);
}
}
I am pretty new to Twig. I have a class called Session and a static function called get I want to return the content of Session::get('user_name'). Is it even possible without modifying anything?
I tried {{ constant('Namespace\\Session::get("user_name")') }} and {{ Session.get('user_name') }} but it doesn't seem to work.
You could use Twig Extension like below:
class CustomExtension extends \Twig_Extension {
public function getFunctions() {
return array(
new \Twig_SimpleFunction('static_call', array($this, 'staticCall')),
);
}
function staticCall($class, $function, $args = array()) {
if (class_exists($class) && method_exists($class, $function)) {
return call_user_func_array(array($class, $function), $args);
}
return null;
}
}
Usage in in twig:
{{ static_call('AppBundle\\Entity\\YourEntity', 'GetSomething', ['var1', 'var2']) }}
I found the answer. I simply added the following code right after new Twig_Enviroment($twig_loader)
$twig = new Twig_Environment($twig_loader);
$twig->addFunction('_call', new Twig_Function_Function(
function($class, $function, $arguments = array())
{
return call_user_func(array($class, $function), $arguments);
})
);
In Twig
{{ _call('Session', 'get', 'user_name')|raw }}
I create Pagination
pagination = PaginationBuilder.create().pageFactory(new Callback<Integer, Node>() {
#Override
public Node call(Integer pageIndex) {
return createChartPage(pageIndex);
}
}).build();
And i expect that total pages can be changed .
And when i setPageCount(totalPages). that change pageCountproprety which inform the pageFactory and make callback with index = 0 ;
- My Problem.
I want the page index not change when i change the PageCount.
In other word how can i removeChangeListener from pageCountProperty.
Use this workaround on the factory method until it's fixed in the JDK:
private Parent createPage(Integer pageIndex) {
// Fix for Pagination.getPageCount resetting the current index to 0
if (Math.abs(previousPageIndex - pageIndex) > 1) {
mainPagination.setCurrentPageIndex(previousPageIndex);
return root.get(previousPageIndex);
} else {
// Normal navigation
previousPageIndex = pageIndex;
return root.get(pageIndex);
}
}