Q:

How to read doctrine join query results without ignoring join conditions?

I have a join query with a join condition. It works fine and getResult() gives me access to the query results. The results are fine, too (as viewed on phpmyadmin).
$em = $this->getDoctrine()->getManager();
$allRows = $em->getRepository('CampaignBundle:Kpi')->createQueryBuilder('k')
            ->leftJoin('k.countryWeights', 'w', 'WITH', 'w.country = :country')
            ->setParameter('country', $country)
            ->addSelect('w')
            ->getQuery()->getResult();

The reason for the join is that I want to check if another table (or entity) has related data and I want to include it in the results if it exists. That's exactly what the left join does.
The problem arises when I use the query results. For example:
foreach ($allRows as $row) {
    $rowOut = array();
    $rowOut['weight'] = ($row->getCountryWeights()) ? $row->getCountryWeights()[0]->getWeight() : 0;
}

The getter for the property of the related entity always gives me access to all the related data as if there was no join condition in my query. $row->getCountryWeights() is always true because without the join condition, the join is not empty. How do I test if any data was joined or not? How do I access the joined columns, e.g. 'weight'? Trying the getters of the joined entity on the result does not seem to work:
Attempted to call an undefined method named "getWeight" 

Additional info:
/**
 * Kpi
 *
 * @ORM\Table(name="kpi")
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Kpi 
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     *
     * @ORM\OneToMany(targetEntity="CampaignBundle\Entity\KpiWeight", mappedBy="kpi", cascade={"ALL"})
     */
    private $countryWeights;
}

/**
 * KPIWeight
 *
 * @ORM\Table(name="kpi_weight")
 * @ORM\Entity
 */
class KpiWeight
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Kpi", inversedBy="countryWeights")
     * @ORM\JoinColumn(name="kpi", referencedColumnName="id", nullable=false)
     */
    private $kpi;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="ZoneBundle\Entity\Country", inversedBy="countryObjectives")
     * @ORM\JoinColumn(name="country", referencedColumnName="id", nullable=false)
     */
    private $country;

    /**
     * @var float
     * @ORM\Column(name="weight", type="float")
     */
    private $weight;
}

These entity listings are incomplete but all the relevant parts should be there.

A:

Since $row->getCountryWeights() will return a Collection object, you would need to test the collection.
$countryWeights = $row->getCountryWeights(); //Collection object
$rowOut['weight'] = (($firstWeight = $countryWeights->first()) ? $firstWeight->getWeight() : 0);

Using Collection::first() will return false or the first entry in the collection by calling reset() on the collection's internal array. 
Otherwise you can test if the Collection is empty by using $row->getCountryWeights()->isEmpty().

Explaination
Example 1:
Say you have the following dataset;
\CampaignBundle\Entity\Kpi
| id | 
| 1  | 
| 2  | 

\CampaignBundle\Entity\Kpi::$countryWeights (OneToMany(targetEntity="CampaignBundle\Entity\CountryWeight"))
| kpi_id | country | weight |
| 1      | A       | 1      |
| 1      | B       | 1      |
| 2      | A       | 1      |

By default doctrine will Lazy Load the CountryWeight entity associations when the getter is called.
$allRows = $em->getRepository(\CampaignBundle\Entity\Kpi::class)->findAll();
$data = [];
foreach ($allRows as $row) {
    foreach ($row->getCountryWeights() as $weight) {
        $data[$row->getId()][$weight->getCountry()] = $weight->getWeight();
    }
}
dump($data);

Will result in the Lazy Loading of the CountryWeight associations:
array:2 [▼
  1 => array:2 [▼
    "A" => 1
    "B" => 1
  ]
  2 => array:1 [▼
    "A" => 1
  ]
]

Example 2:
However when you use the QueryBuilder, the Collection is limited based on the Join:WITH criteria specified.
$allRows = $em->getRepository(\CampaignBundle\Entity\Kpi::class)->createQueryBuilder('k')
    ->leftJoin('k.countryWeights', 'w', Join::WITH, $expr->eq('w.country', ':country'))
    ->setParameter('country', 'B')//<--- NOTE B is filtered
    ->addSelect('w')
    ->getQuery()
    ->getResult();
$data = [];
foreach ($allRows as $row) {
    $firstWeight = $row->getCountryWeights()->first();
    $data[$row->getId()][$firstWeight ? $firstWeight->getCountry() : null] = $firstWeight ? $firstWeight->getWeight() : 0;
}
dump($data);

Will Result in:
array:2 [▼
  1 => array:1 [▼
    "B" => 1
  ]
  2 => array:1 [▼
    "" => 0
  ]
]

In the event that doctrine has already retrieved the CountryWeight associations, such as running example 1 followed by example 2. You will need to tell doctrine to forcibly clear it's UnitOfWork [sic] to retrieve the filtered records from the database, otherwise it will use the already loaded collections for the retrieved results. 
One method to achieve this is to issue
$em->clear(\CampaignBundle\Entity\Kpi::class);

Prior to executing your query. Which will clear all of doctrine's unit of work for the object. Where using Query::HINT_REFRESH, will not fully refresh the excluded values from Kpi::$id => 2 with a country of A.
Example output from already retrieved associations.

References: 

Doctrine allows you to walk all the associations between all the objects in your domain model. Objects that were not already loaded from the database are replaced with lazy load proxy instances. Non-loaded Collections are also replaced by lazy-load instances that fetch all the contained objects upon first access. However relying on the lazy-load mechanism leads to many small queries executed against the database, which can significantly affect the performance of your application. Fetch Joins are the solution to hydrate most or all of the entities that you need in a single SELECT query.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/dql-doctrine-query-language.html#joins

If an object is already in memory from a previous query of any kind, then then the previous object is used, even if the database may contain more recent data. Data from the database is discarded. This even happens if the previous object is still an unloaded proxy.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/dql-doctrine-query-language.html#object-hydration

