Zend Framework Sql not finding column - zend-framework2

I'm having an issue with Zend Framework 2's SQL class. It's saying it cannot find a certain column in the table but the column is there. I've run into this problem before and had to use actual SQL syntax instead of building the query. I rather not have to resort to putting the actual syntax in and instead figure out why this is happening so I can avoid having this issue in the future.
Here is my code:
namespace Members\Model;
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Sql\Sql;
use Zend\Db\Sql\Select;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Sql\Predicate;
use Members\Model\Interfaces\FeedInterface;
use Members\Model\Exceptions\FeedException;
class FeedModel implements FeedInterface
{
/**
* #var TableGateway
*/
public $gateway;
/**
* #var string
*/
public $user;
/**
* #var Sql
*/
public $sql;
/**
* #var Select
*/
public $select;
/**
* Constructor method for FeedModel class
* #param TableGateway $gateway
* #param string $user
*/
public function __construct(TableGateway $gateway, $user)
{
$this->gateway = $gateway instanceof TableGateway ? $gateway : null;
$this->select = new Select();
$this->sql = new Sql($this->gateway->getAdapter());
$this->user = $user;
}
/**
* {#inheritDoc}
* #see \Members\Model\Interfaces\FeedInterface::listFriendsStatus()
*/
public function listFriendsStatus()
{
// get the friend ids based on user id
// and then compare the friend id to the id in status table
$friend_query = $this->select->columns(array('*'))
->from('friends')
->where(array('user_id' => $this->getUserId()['id'])); // the issue
file_put_contents(getcwd() . '/public/query.txt', $this->sql->buildSqlString($friend_query));
$query = $this->sql->getAdapter()->query(
$this->sql->buildSqlString($friend_query),
Adapter::QUERY_MODE_EXECUTE
);
if ($query->count() > 0) {
$friend_id = array();
foreach ($query as $result) {
$friend_id[] = $result;
}
$this->select->columns(array('status'))
->from('status')
->where(array('id' => array_values($friend_id), new Predicate\IsNotNull('id')));
$status_query = $this->sql->getAdapter()->query(
$this->sql->buildSqlString($this->select),
Adapter::QUERY_MODE_EXECUTE
);
if ($status_query->count() > 0) {
// check if a image was used
$this->select->columns('username')
->from('members')
->where(array('id' => array_values($friend_id), new Predicate\IsNotNull('id')));
$image_query = $this->sql->getAdapter()->query(
$this->sql->buildSqlString($this->select),
Adapter::QUERY_MODE_EXECUTE
);
if ($image_query->count() > 0) {
$status_dir = array();
foreach ($image_query as $value) {
if (#!is_dir(getcwd() . '/public/images/profile/' . $value['username'] . '/status/')) {
continue;
}
$status_dir[] = getcwd() . '/public/images/profile/' . $value['username'] . '/status/';
}
$images = array();
// retrieve the image inside the status directory
foreach (array_diff(scandir($status_dir, 1), array('.', '..')) as $values) {
$images[] = $values;
}
} else {
throw new FeedException("The user does not exist in the user table.");
}
$status = array();
// get all the statuses
foreach ($status_query as $rows) {
$status[] = $rows;
}
return array('status' => $status, 'images' => $images);
} else {
throw new FeedException("No status was found for your friends.");
}
} else {
throw new FeedException(sprintf("Could not locate any friends for %s", $this->user));
}
}
/**
* {#inheritDoc}
* #see \Members\Model\Interfaces\FeedInterface::hideFriendsStatus()
*/
public function hideFriendsStatus($friend_id)
{
}
/**
* Grabs the user id for the user
*
* #return int|boolean
*/
public function getUserId()
{
$this->select->columns(array('*'))
->from('members')
->where(array('username' => $this->user));
$query = $this->sql->getAdapter()->query(
$this->sql->buildSqlString($this->select),
Adapter::QUERY_MODE_EXECUTE
);
if ($query->count() > 0) {
foreach ($query as $result) {
$row = $result;
}
return $row;
}
return false;
}
}
This is the exception message I am getting:
PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'user_id' in 'where clause'
But as you can see in this screenshot, the column user_id exists in the friends table I have in place:
So my question is why is it doing this and how can I in the future avoid this issue?
Thanks!

It looks like the select is causing the issue.
Since your code ->from('friends') is called first and then it is overridden due to this function call $this->getUserId(), which overrides the friends table to members due to ->from('members').
Try changing your code to.
$userId = $this->getUserId()['id'];
$friend_query = $this->select->columns(array('*'))
->from('friends')
->where(array('user_id' => $userId));
This should work, but if it doesn't, try to just create new select object $select = new Select(); in both the functions rather than $this->select = new Select();.

Related

How Unit Test angular material MatTableDataSource?

Some methods angular material, i need to unit test of each one, but i don't know how
/**
* Setup the filter for a table
* #param dataTable data source to setup
*/
private setupFilter(dataTable: MatTableDataSource<Element>) {
dataTable.filterPredicate = (data: any, filter: string) => {
filter = filter.toLowerCase();
return data.name.toLowerCase().includes(filter)
|| data.description.toString().includes(filter);
};
}
Some methods angular material, i need to unit test of each one, but i don't know how
/**
* Checking control validation for edit inputs
* #param value Equals to ngmodel for the input
* #param column Equals to column name
*/
public editControlHasError(value: string, column: string): void {
if (column === 'name') {
this.errorInName.required = value === '';
this.errorInName.maxlength = value.length > Constants.MAX_LENGTH_NAME;
return;
}
this.errorInDescription.required = value === '';
this.errorInDescription.maxlength = value.length > Constants.MAX_LENGTH_DESCRIPTION;
}
Some methods angular material, i need to unit test of each one, but i don't know how
/**
* Methode that apply the table filter
* #param filterValue the filter value
*/
public applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
Some methods angular material, i need to unit test of each one, but i don't know how
/**
* Clear the filters value
*/
clearFilters() {
this.filter = '';
this.dataSource.filter = '';
}
Some methods angular material, i need to unit test of each one, but i don't know how
/**
* Method that loads a list of profiles to display on the rol select
*/
private loadProfiles() {
this.loaders.dataSource = true;
this.profilesService.getAllProfiles()
.pipe(
finalize(() => {
this.loaders.dataSource = false;
this.updateDOM();
})
)
.subscribe(
data => {
this.dataSource = new MatTableDataSource<Element>(data);
// getting properties from the object to sort the column from nested objects
this.dataSource.sortingDataAccessor = (obj, property) =>
this.getProperty(obj, property);
this.dataSource.sort = this.sort;
this.setupFilter(this.dataSource);
}
);
}
Some methods angular material, i need to unit test of each one, but i don't know how
/**
* Method that create a Profile
*/
public addProfile() {
for (const key in this.profileForm.controls) {
if (this.profileForm.controls[key] &&
this.profileForm.controls[key].value.toString().trim() === '') {
this.profileForm.controls[key].setValue('');
}
}
if (this.profileForm.valid) {
this.loaders.process = true;
const profile: Profile = new Profile();
profile.idProfile = 0;
profile.name = this.profileForm.controls.name.value.trim();
profile.description = this.profileForm.controls.description.value.trim();
this.profilesService.addProfile(profile).subscribe(
data => {
this.loaders.process = false;
this.profileForm.reset();
this.loadProfiles();
this.showModalAlert(data);
}
);
}
}
Some methods angular material, i need to unit test of each one, but i don't know how
/**
* Method that loads a profile for edit from the list
*/
public loadEditProfile(element: Profile) {
if (this.dataSource.data.some((e: any) => !!e.edit)) {
this.cancelEdit();
this.initErrorCheckers();
}
this.profileElementTmp = JSON.parse(JSON.stringify(element));
this.profileElementAux = element;
element.edit = true;
}
Some methods angular material, i need to unit test of each one, but i don't know how

Laravel Nova - How to determine the view (index, detail, form) you are in for a resource's computed field?

I would like to return a different result for a computed field when viewing the index view than when viewing the detail view of a resource.
Basically something like viewIs() below:
Text::make('Preview', function () {
if($this->viewIs('index'){
return \small_preview($this->image);
}
return \large_preview($this->image);
})->asHtml(),
You can check the class of the request:
Text::make('Preview', function () use ($request) {
if ($request instanceof \Laravel\Nova\Http\Requests\ResourceDetailRequest) {
return \large_preview($this->image);
}
return \small_preview($this->image);
});
Otherwise, you can create your own viewIs function:
// app/Nova/Resource.php
/**
* Check the current view.
*
* #param string $view
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #retrun bool
*/
public function viewIs($view, $request)
{
$class = '\Laravel\Nova\Http\Requests\\Resource'.ucfirst($view).'Request';
return $request instanceof $class;
}
Then you can do it like this:
Text::make('Preview', function () use ($request) {
if ($this->viewIs('detail', $request) {
return \large_preview($this->image);
}
return \small_preview($this->image);
});
Unfortunately, #Chin's answer did not work for me as for Edit view the request class is just a basic Laravel\Nova\Http\Request class.
My workaround to check if this is an index view is as follows:
/**
* Check if the current view is an index view.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #return bool
*/
public function isIndex($request)
{
return $request->resourceId === null;
}
The NovaRequest class will soon be able to help, as the isResourceIndexRequest and isResourceDetailRequest are already in master.
As the Nova repo is private I will keep you posted, when it will be usable.
In the meantime I am falling back to helper methods on the Nova Resource class (app/Nova/Resource.php):
namespace App\Nova;
use Laravel\Nova\Http\Requests\ResourceDetailRequest;
use Laravel\Nova\Http\Requests\ResourceIndexRequest;
use Laravel\Nova\Resource as NovaResource;
use Laravel\Nova\Http\Requests\NovaRequest;
abstract class Resource extends NovaResource
{
// [...]
/**
* Determine if this request is a resource index request.
*
* #return bool
*/
public function isResourceIndexRequest($request)
{
return $request instanceof ResourceIndexRequest;
}
/**
* Determine if this request is a resource detail request.
*
* #return bool
*/
public function isResourceDetailRequest($request)
{
return $request instanceof ResourceDetailRequest;
}
}
Usage:
public function fields(Request $request)
{
$fields = [
// [...]
];
if ($this->isResourceDetailRequest($request)) {
if ($this->isResourceDetailRequest($request)) {
$fields = array_merge($fields, [
// [...]
]);
}
}
return $fields;
}
I added this little helper class
namespace App\Helpers;
class CurrentResourceAction {
public static function isIndex($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\ResourceIndexRequest;
}
public static function isDetail($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\ResourceDetailRequest;
}
public static function isCreate($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\NovaRequest &&
$request->editMode === 'create';
}
public static function isUpdate($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\NovaRequest &&
$request->editMode === 'update';
}
}
you can call it anywhere you need to
A bit late but hey! You can check against the NovaRequest properties editing and editMode ('create', 'update', 'attach' etc.)
// Determine if you are creating a model.
$request->editMode == 'create';
Or as they say, "Read the source Luke" and see how they determine it. See the Laravel\Nova\Http\Requests\NovaRequest, it contains similar checks.
namespace Laravel\Nova\Http\Requests;
class NovaRequest extends FormRequest
{
/**
* Determine if this request is via a many to many relationship.
*
* #return bool
*/
public function viaManyToMany();
/**
* Determine if this request is an attach or create request.
*
* #return bool
*/
public function isCreateOrAttachRequest();
/**
* Determine if this request is an update or update-attached request.
*
* #return bool
*/
public function isUpdateOrUpdateAttachedRequest();
/**
* Determine if this request is a resource index request.
*
* #return bool
*/
public function isResourceIndexRequest();
/**
* Determine if this request is a resource detail request.
*
* #return bool
*/
public function isResourceDetailRequest();
/**
* Determine if this request is an action request.
*
* #return bool
*/
public function isActionRequest();
}
Would've been nice if you can type hint the NovaRequest instead of the regular one in the Nova resource fields() method but it is not allowed due to the parent resource being extended.
You can create two separate fields for index & details page.
// ----- For Index page
Text::make('Preview', function () {
return \small_preview($this->image);
})
->onlyOnIndex()
->asHtml(),
// ----- For Detail page
Text::make('Preview', function () {
return \large_preview($this->image);
})
->onlyOnDetail()
->asHtml(),

Symfony CMF multiple image fields using ImagineBlock

Problem
Hello, I am using Symfony CMF 1.2, liip/imagine-bundle 1.3, symfony-cmf/media-bundle 1.2. I want to add 2 additional image fields to my block that extends ImagineBlock because for every image I upload there will be a mobile and tablet version of the image which is not a simple resize, the aspect ratio or whatnot is not similar. I cannot just crop/resize without affecting the quality of the image.
Attempts
My block
namespace xx\BlockBundle\Document;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
use Symfony\Cmf\Bundle\BlockBundle\Doctrine\Phpcr\ImagineBlock;
use Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image;
use Symfony\Cmf\Bundle\MediaBundle\ImageInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Class ClickableBlock
* #package xx\BlockBundle\Document
* #PHPCR\Document(referenceable=true)
*/
class ClickableBlock extends ImagineBlock
{
/**
* #PHPCR\Child(nodeName="image-mobile", cascade={"persist"})
* #var Image
*/
protected $imageMobile;
/**
* #PHPCR\Child(nodeName="image-tablet", cascade={"persist"})
* #var Image
*/
protected $imageTablet;
public function setIsPublishable($publishable)
{
$this->setPublishable($publishable);
}
/**
* #return Image
*/
public function getImageMobile()
{
return $this->imageMobile;
}
/**
* #return Image
*/
public function getImageTablet()
{
return $this->imageTablet;
}
/**
* Set the imageMobile for this block.
*
* #param ImageInterface|UploadedFile|null $image optional the imageMobile to update
* #return $this
* #throws \InvalidArgumentException If the $image parameter can not be handled.
*/
public function setImageMobile($image = null)
{
return $this->processImage($image, 'image-mobile', $this->imageMobile);
}
/**
* Set the imageTablet for this block.
*
* #param ImageInterface|UploadedFile|null $image optional the imageTablet to update
* #return $this
* #throws \InvalidArgumentException If the $image parameter can not be handled.
*/
public function setImageTablet($image = null)
{
return $this->processImage($image, 'image-tablet', $this->imageTablet);
}
/**
* #param ImageInterface|UploadedFile|null $image
* #param string $imageName
* #param Image $imageRef
* #return $this
*/
protected function processImage($image, $imageName, $imageRef)
{
if (!$image) {
return $this;
}
if (!$image instanceof ImageInterface && !$image instanceof UploadedFile) {
$type = is_object($image) ? get_class($image) : gettype($image);
throw new \InvalidArgumentException(sprintf(
'Image is not a valid type, "%s" given.',
$type
));
}
if ($imageRef) {
// existing imageTablet, only update content
$imageRef->copyContentFromFile($image);
} elseif ($image instanceof ImageInterface) {
$image->setName($imageName); // ensure document has right name
$imageRef = $image;
} else {
$imageRef = new Image();
$imageRef->copyContentFromFile($image);
}
return $this;
}
}
Admin:
namespace xx\BlockBundle\Admin;
use xx\BlockBundle\Document\ClickableBlock;
use xx\MainBundle\Form\Common\FormMapper as CommonFormMapper;
use Cocur\Slugify\Slugify;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Symfony\Cmf\Bundle\BlockBundle\Admin\Imagine\ImagineBlockAdmin;
class ClickableBlockAdmin extends ImagineBlockAdmin
{
/**
* {#inheritdoc}
*/
public function toString($object)
{
return $object instanceof ClickableBlock && $object->getLabel()
? $object->getLabel()
: parent::toString($object);
}
/**
* {#inheritdoc}
*/
public function prePersist($document)
{
parent::prePersist($document);
$this->InitialiseDocument($document);
}
/**
* #param $document
*/
private function InitialiseDocument(&$document)
{
$documentManager = $this->getModelManager();
$parentDocument = $documentManager->find(null, '/cms/xx/block');
$document->setParentDocument($parentDocument);
$slugifier = new Slugify();
$document->setName($slugifier->slugify($document->getLabel()));
}
/**
* {#inheritdoc}
*/
public function preUpdate($document)
{
parent::preUpdate($document);
$this->InitialiseDocument($document);
}
/**
* {#inheritdoc}
*/
protected function configureFormFields(FormMapper $formMapper)
{
parent::configureFormFields($formMapper);
if (null === $this->getParentFieldDescription()) {
$imageRequired = ($this->getSubject() && $this->getSubject()->getParentDocument()) ? false : true;
$formMapper
->with('form.group_general')
->remove('parentDocument')
->remove('filter')
->add('parentDocument', 'hidden', ['required' => false, 'data' => 'filler'])
->add('name', 'hidden', ['required' => false, 'data' => 'filler'])
->add('imageMobile', 'cmf_media_image', array('required' => $imageRequired))
->add('imageTablet', 'cmf_media_image', array('required' => $imageRequired))
->end();
// Append common fields to FormMapper
$commonFormMapper = new CommonFormMapper($formMapper);
$formMapper = $commonFormMapper->getPublishingFields();
}
}
}
Note I am unable to inject service container to this class (via constructor/method), that is why am using hardcoded node path and instantiated Slugify class instead of using it's service for now. I am all ears for a solution to this also. Ref -
xx.main.admin.pageadmin.container:
class: xx\MainBundle\Admin\PageAdmin
calls:
- [setContainer,[ #service_container ]]
# arguments: ["#service_container"]
The annotations on the image fields are based on the following config I found in
\vendor\symfony-cmf\block-bundle\Resources\config\doctrine-phpcr\ImagineBlock.phpcr.xml:
<doctrine-mapping
xmlns="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping
https://github.com/doctrine/phpcr-odm/raw/master/doctrine-phpcr-odm-mapping.xsd"
>
<document
name="Symfony\Cmf\Bundle\BlockBundle\Doctrine\Phpcr\ImagineBlock"
referenceable="true"
translator="attribute"
>
<node name="node"/>
<locale name="locale"/>
<field name="label" type="string" translated="true" nullable="true"/>
<field name="linkUrl" type="string" translated="true" nullable="true"/>
<field name="filter" type="string" nullable="true"/>
<child name="image" node-name="image">
<cascade>
<cascade-persist/>
</cascade>
</child>
</document>
</doctrine-mapping>
Result
While the default "image" field persists normally, the other two added image fields are not taken into consideration since when I debug on prePersist I see that both fields are null while image field contains its uploaded file.
I tried adding a normal text field which saved and displayed normally on my page.
I use YAML in my project, so I am not sure how exactly the given XML translates, if ever it is the correct mapping to define.
Please help. :)
A colleague found the issue which was the following:
protected function processImage($image, $imageName, $imageRef)
should be
protected function processImage($image, $imageName, &$imageRef)
$imageRef was not passed by reference making it always null. Silly me. Let's hope this code at least helps other people. :)
For the admin question: phpcr-odm admins have a rootPath for exactly the purpose of what you are doing. you could add to your service definition like this:
<call method="setRootPath">
<argument>%cmf_content.persistence.phpcr.content_basepath%</argument>
</call>
and then you do $this->getRootPath()

ZF2 Accessing routes and posts via a factory

I am trying to access a route and post via a form factory. The route or the post contains an ID which I need to inject into my form so that I can build a select statement.
Currently I am injecting into the form via the controller using
$this->MyForm->get('elementName')->setOptions(array('value_options' =>$myArrayOfOptions));
My goal is to keep the business logic out of the controller hence why I am keen to use the formFactory instead however I do need access to the ID in the post or route to achieve this.
My Form Factory looks like this:
<?php
namespace MyModule\Form;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use AdminLottery\InputFilter\MyFilter;
use AdminLottery\Service\MyService;
class MyFormFactory implements FactoryInterface
{
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
* #return mixed
*/
public function createService(
ServiceLocatorInterface $serviceLocator
)
{
//$serviceLocator is FormElementManager
$realSL = $serviceLocator->getServiceLocator();
//*** I NEED TO ACCESS THE ID / POST HERE TO SEND TO MY FORM
return new MyForm(
$realSL->get('Doctrine\ORM\EntityManager'),
$realSL->get('InputFilterManager')->get(MyFilter::class),
$realSL,
$realSL->get(MyService::class)
);
}
}
Any Ideas??
You can access the request instance
MyFormFactory
//...
$request = $serviceLocator->getServiceLocator()->get('Request');
$id = $request->getPost('id', false);
if ($id) $form->setOption('id', $id);
//...
Edit: This is very similar to another question I answered
Edit 2
In your factory can access the route params via the router's Zend\Mvc\Router\RouteMatch.
$request = $serviceLocator->getServiceLocator()->get('Request');
$router = $serviceLocator->getServiceLocator()->get('Router');
$match = $router->match($request); // \Zend\Mvc\Router\RouteMatch
$id = ($match) ? $match->getParam('id', false) : false;
if ($id) $form->setOption('id', $id); //....
For anyone looking for a reference, I thought I would add the final code I used:
Semi-final code::
$router = $realSL->get('Router');
$request = $realSL->get('Request');
$routeMatch = $router->match($request);
$matchArray = $routeMatch->getParams();
if (isset($matchArray['id'])) {
$id = (int) $matchArray['id'];
} else {
$id = 0;
}
Final code::
$realSL->get('application')->getMvcEvent()->getRouteMatch()->getParam('id', 0)
Here is an example for a controller factory, that would work in the same manner for any other factory:
namespace MyModule\Controller\Factory;
use MyModule\Controller\MyController;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class MyControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
/*
* #var Zend\ServiceManager\ServiceManager
*/
$realServiceLocator = $serviceLocator->getServiceLocator();
...
$router = $realServiceLocator->get('router');
$request = $realServiceLocator->get('request');
$routerMatch = $router->match($request);
...
$test1 = $routerMatch->getParams();
$test2 = $request->getQuery();
$test3 = $request->getPost();
...
return new MyController(...);
}
}

ZF2: DB\AbstractTableGateway: How to use JOIN?

I new in ZF and have a code located below. I try get custom colums and join in sql select, but it failed.
I tried to use the search but found no results.
Tell me examples of how to do more complex queries.
Thanks.
<?php
namespace FcFlight\Model;
use Zend\Db\TableGateway\AbstractTableGateway;
use Zend\Db\Adapter\Adapter;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\Sql\Select;
use FcFlight\Filter\FlightHeaderFilter;
class FlightHeaderModel extends AbstractTableGateway
{
/**
* #var string
*/
protected $table = 'flightBaseHeaderForm';
/**
* #param \Zend\Db\Adapter\Adapter $adapter
*/
public function __construct(Adapter $adapter)
{
$this->adapter = $adapter;
$this->resultSetPrototype = new ResultSet();
$this->resultSetPrototype->setArrayObjectPrototype(new FlightHeaderFilter($this->adapter));
$this->initialize();
}
/**
* #param $id
* #return array|\ArrayObject|null
* #throws \Exception
*/
public function get($id)
{
$id = (int)$id;
$rowSet = $this->select(array('id' => $id));
$row = $rowSet->current();
if (!$row) {
throw new \Exception("Could not find row $id");
}
$row->dateOrder = date('Y-m-d', $row->dateOrder);
return $row;
}
}
Since you are using the tableGateway, you must get an instance of Sql(): $this->getSql();
With that instance you will create an instance of Select, which will then let you perform a join:
$sql = $this->getSql();
$select = $sql->select();
$select->join('TableNameToJoin', 'MainColumnA = JoinColumnA');
Then to execute the query, you call selectWith on your tableGateway:
$this->selectWith($select);
If you wish to customize the join further, you can pass two more arugments:
An array of columns you wish to select
The type of join you wish to do (LEFT JOIN for example)
You can also add an alias to the table you are joining on by passing an Array as the table name, with the key of the array being the alias and the value being the table name.
$select->join(['Alias' => 'TableNameToJoin'], 'MainColumnA = Alias.JoinColumnA', ['ColumnA', 'ColumnB'], $select::JOIN_LEFT);

Resources